SYC · 阿基里斯追乌龟 题解:JSON 非数值与浮点比较绕过

SYC · 阿基里斯追乌龟 题解:JSON 非数值与浮点比较绕过

本文字数:3146  阅读时长:7分钟

访问量 0

SYC · 阿基里斯追乌龟 —— Writeup(JSON 非数值与浮点比较绕过)

项目内容
题名阿基里斯追乌龟(芝诺悖论)
靶场地址http://8080-8103e2bc-9125-418b-a8e4-96a8da214309.challenge.ctfplus.cn/
题型Web 接口逆向 + JSON 非标准字面量(Infinity / 溢出)+ 浮点比较缺陷
环境Werkzeug/3.1.3,Python/3.11.14,Flask 单体应用
接口POST /chase(唯一业务接口,仅接受 POST 与 OPTIONS)
最终 payload{"data": base64({"achilles_distance":1e999,"tortoise_distance":0})}
FlagSYC{Spi1t_th3_T1me_t0_the_3nd_LMAO}
解题日期2026-11-07

一、题目形态

首页是一个芝诺悖论的演示页:一个「追赶」按钮、两个距离显示、一张 gif。除此之外只有一个接口。

text
GET /              → 静态页面
GET /static/*      → index.js / style.css / 222.gif
POST /chase        → 业务接口

探测隐藏路由(/flag、/source、/app.py、/.git/HEAD 等)全部 404,说明解题点只能在这一个接口上。

二、为什么是 POST

三条证据,不靠猜:

证据内容
前端源码static/index.js 里 fetch('/chase', { method: 'POST', ... })
直接访问GET /chase 返回 405,响应头带 Allow: POST, OPTIONS
方法探测OPTIONS /chase 返回 200,同样带 Allow: POST, OPTIONS

通用手法:想快速知道某个接口接受什么方法,发一个 OPTIONS,看 Allow 响应头;或者故意用错方法,405 页面也会把它告诉你。

服务端用 POST 的设计原因:

原因说明
语义提交一次「动作」,服务端依据提交的数据做判定,不是读取资源
藏参数JSON 塞进 body 并 base64,制造「加密」的错觉;GET 会把 payload 全暴露在 URL
长度base64 膨胀 4/3,GET 的 URL 长度有限制

三、协议拆解:base64 不是加密

请求与响应是对称的:

text
请求   {"data": base64( {"achilles_distance":x, "tortoise_distance":y} )}
响应   {"data": base64( {"flag":...} 或 {"message":...} )}

抓一次真实请求并解码:

json
{"achilles_distance": 0, "tortoise_distance": 10000000000}

第一个关键发现:页面 HTML 里写的是「乌龟在 100 米处」,而实际参数是 10000000000(1e10)。页面展示是装饰性的,判定只看接口参数——从这里开始不要相信页面。

前端逻辑(index.js):

js
let achillesPos = 0;
let tortoisePos = 10000000000;          // 实际初始距离

const achillesMoveDistance = tortoisePos - achillesPos;   // 阿基里斯瞬移到乌龟位置
achillesPos = tortoisePos;
tortoisePos += achillesMoveDistance / 10;                 // 乌龟再走 1/10

四、正常路径只会拿到假 flag

1e10 / 10^k 递减,约 60 次后浮点数加法不再改变 tortoisePos(精度归零),两个数变成完全相等,触发「追上」分支。但它给的是:

text
哦天哪,可恶的浮点数问题,居然真的让你追到了。
然而这并不是预期的解法,阿基里斯可以跑得更快对吗?
SYC{this_is_fake_flag}

这段文字直接把答案说了一半:

原文读出来的信息
「可恶的浮点数问题」a == t 是靠精度归零凑出来的,不是真追上
「并不是预期的解法」相等分支是出题人埋的诱饵
「可以跑得更快对吗」真正的条件是比较方向:a 要 大于 t

五、服务端判定分支(实测刻画)

achillestortoise返回
1e101e10(相等)假 flag
1e10 + 11e10(大 1)真 flag
2e101e10真 flag
Infinity-Infinity真 flag
1e9990真 flag
NaN任意message(没追上)
-InfinityInfinitymessage(没追上)

反推的逻辑:

python
if a > t:
    return real_flag
if a == t:
    return fake_flag          # 浮点追平的诱饵分支
return {"message": "Achilles has not caught up ..."}

注意 NaN 落在 message 分支,因为 NaN 与任何值比较恒为 False,两个 if 都进不去。

六、真解推导链(四步)

第 1 步:从假 flag 的话里读出 a > t

「跑得更快」= 严格大于。理论上 10000000001 就够了。

第 2 步:为什么还要往上加码

写 payload 时并不知道判定写的是 > 还是 >=,也不知道有没有额外校验(数值上限、必须有限、必须合理)。用「刚好大 1」等于赌实现细节。

第 3 步:找一个在所有比较语义下都成立的值

值与有限数比较与自身比较能否过 a > t
大有限数 1e308正常相等能,但受上限/精度影响
NaN恒 False恒 False不能
Infinity恒 TrueTrue能,不受任何阈值影响

NaN 是陷阱:它比较恒 False,反而掉进「没追上」分支。所以要选 Infinity(或用溢出造出来的 inf)。

第 4 步:乌龟压到 -Infinity 是冗余保险

a > t 有两个变量,把 t 压到负无穷是双保险,还贴合 flag 的梗(Split the Time to the End,时间倒流)。严格说 t 取什么都不影响结果。

七、Infinity 的各种等价写法(实测)

Python 的 json 解码器只给三个固定单词开了后门,大小写敏感:

写法json.loads 结果靶场结果
Infinityfloat inf真 flag
-Infinityfloat -inf真 flag
NaNfloat nanmessage
inf / Inf / INFINITYJSONDecodeErrorHTTP 400
“Infinity”(带引号)字符串HTTP 500
1e999 / 1e309 / 1E400float inf(溢出)真 flag
truebool True(即整数 1)真 flag
超长整数Python 任意精度 int真 flag

三条要点:

  1. inf、Inf、INFINITY 都不行。json 不是调用 float() 解析标识符,而是走一张常量表精确匹配,匹配不上直接报错,请求根本没进业务逻辑。
  2. 1e999 是标准 JSON,语法合法,任何解析器都会接受,Python 内部转 float 时溢出成 inf。它比 Infinity 更好:不依赖语言扩展、不触发关键词 WAF,Java/Go 的严格解析器也照样中招。
  3. true 是最容易被忽略的:Python 里 bool 是 int 的子类,True > 0 成立,于是「必须是数字」的校验直接穿了。

本地验证(无需靶场):

python
import json
for t in ['Infinity','inf','INFINITY','1e999','"Infinity"','true']:
    try:    print(t, '->', repr(json.loads('{"a": %s}' % t)['a']))
    except Exception as e: print(t, '-> ERROR', e)

八、完整操作步骤(Burp)

步骤 1:抓真实请求

浏览器 F12 → Network → 点「追赶」→ 找到 POST /chase → 右键 Send to Repeater。

http
POST /chase HTTP/1.1
Host: 8080-8103e2bc-9125-418b-a8e4-96a8da214309.challenge.ctfplus.cn
Content-Type: application/json

{"data":"eyJhY2hpbGxlc19kaXN0YW5jZSI6MCwidG9ydG9pc2VfZGlzdGFuY2UiOjEwMDAwMDAwMDAwfQ=="}

步骤 2:解码 data

复制 data 的值 → Burp Decoder → Base64 decode:

json
{"achilles_distance": 0, "tortoise_distance": 10000000000}

(新版 Burp 的 Inspector 右侧面板也会自动显示解码结果,但要注意它只是查看器,改内容后需要手动「应用更改」。)

步骤 3:先 Send 一次建基线

不改任何东西直接发,解码响应应得到:

json
{"message": "Achilles has not caught up with the tortoise yet."}

这一步证明通道通了、参数确实被读取。基线不对就先别往下走。

步骤 4:验证方法限制

Repeater 里 Ctrl+M 切换成 GET 发送 → 405,Allow: POST, OPTIONS。坐实方法限制。

步骤 5:构造新明文并编码

在 Decoder 里输入(推荐用标准 JSON 的溢出写法):

json
{"achilles_distance": 1e999, "tortoise_distance": 0}

Encode as Base64 得到:

text
eyJhY2hpbGxlc19kaXN0YW5jZSI6IDFlOTk5LCAidG9ydG9pc2VfZGlzdGFuY2UiOiAwfQ==

备用写法(非标准常量版):

json
{"achilles_distance": Infinity, "tortoise_distance": -Infinity}
text
eyJhY2hpbGxlc19kaXN0YW5jZSI6IEluZmluaXR5LCAidG9ydG9pc2VfZGlzdGFuY2UiOiAtSW5maW5pdHl9

步骤 6:替换 body 并 Send

只替换 "data":" 与 " 之间的内容,外层双引号和花括号不动:

http
{"data":"eyJhY2hpbGxlc19kaXN0YW5jZSI6IDFlOTk5LCAidG9ydG9pc2VfZGlzdGFuY2UiOiAwfQ=="}

Content-Length 由 Burp 自动重算;若报 400 再检查它是否与新 body 长度一致。

步骤 7:解响应

响应仍是 {"data":"..."},Decoder 选 Base64 decode:

json
{"flag": "SYC{Spi1t_th3_T1me_t0_the_3nd_LMAO}"}

九、五个坑

#现象原因
1直接点按钮怎么点都只有假 flag那是浮点追平的诱饵分支,不是解法
2照页面显示的 100 构造 payload页面是装饰,实际初始距离是 1e10
3写 inf / INFINITY → 400json 常量表只认真实拼写 Infinity,大小写敏感
4给 Infinity 加引号 → 500变成字符串,比较时类型错误
5用 NaN 想「特殊绕过」NaN 比较恒 False,反而进不了任何命中分支

十、可复用的方法论

① 拿到题目先分离三件事:页面展示、前端逻辑、后端接口。本题三者互相矛盾(页面 100、代码 1e10、判定看接口),先对齐它们再动手。

② 判断接口方法:OPTIONS 看 Allow,或故意用错方法看 405。

③ 「输入必须是数字」的校验,按成本从低到高试:

顺序试什么目的
1true / false类型混淆,Python 里 bool 就是 int
21e999标准 JSON 的溢出,最不容易被拦
3Infinity / -InfinityPython/JS 系后门常量
4NaN让所有比较失效
5超长整数、负数、0精度与边界
6“123” 字符串触发类型错误,看报错细节

④ 记住「标准内」与「标准外」的分界:1e999 是标准 JSON,Infinity 是语言扩展。WAF 和严格解析器通常只堵后者,前者反而长期存活。

十一、汇总表

项目内容
接口POST /chase,Content-Type application/json,仅 POST 与 OPTIONS
协议请求与响应都是 {"data": base64(json)},base64 不是加密
前端假象页面显示乌龟 100,实际参数 1e10
正常玩法点约 60 次 → 浮点精度归零 → 相等 → 假 flag
诱饵提示「可恶的浮点数问题」「可以跑得更快对吗」
真判定a > t 真 flag;a == t 假 flag;a < t 或 NaN → 提示
最小解achilles 比 tortoise 大 1 即可
最优解1e999(标准 JSON 溢出成 inf,最不易被拦)
备用解Infinity 与 -Infinity(非标准常量)
无效写法inf、Inf、INFINITY(400)、“Infinity”(500)、NaN(message)
修复视角json 解析加 allow_nan=False;判定前用 math.isfinite 校验;浮点比较用容差
FlagSYC{Spi1t_th3_T1me_t0_the_3nd_LMAO}

一句话总结:出题人想考「浮点精度追平」是错的,真正的口子在于——JSON 里的数字不只有有限值,1e999 会溢出成无穷,而无穷在任何比较里都必胜。