题目描述

进入题目是一个独角兽商店(Unicorn Shop),有四种独角兽商品:

Item ID Price English Spanish German Russian
1 2.0 black and white unicorn unicornio blanco y negro Schwarzweiss-Einhorn черно-белый единорог
2 5.0 unicorn family familia unicornio Einhorn-Familie семья единорога
3 8.0 warrior unicorn guerrero unicornio Krieger Einhorn воин единорог
4 1337.0 ultra unicorn ultra unicornio ultra Einhorn ультра единорог

目标很明确:购买第 4 个商品 ultra unicorn(价格 1337.0)获取 flag。

探索过程

查看页面源码

查看 HTML 源码,发现几个关键的注释:

1
2
3
<--Ah,really important,seriously. -->
<-- Don't be frustrated by the same view,we've changed the challenge content.-->
<--We still have some surprise for admin.password-->

第一个注释 <!--Ah,really important,seriously. --> 标记在 <meta charset="utf-8"> 后面——这暗示字符编码是解题的关键

测试表单提交

表单提交到 /charge,参数为 idprice。尝试各种输入:

id price 返回信息 分析
1 2.0 Only one char(?) allowed! price 参数不允许小数点
1 2 Wrong commodity! 单字符price,但商品不对
4 1337 Only one char(?) allowed! price 超过1个字符被拒
4 1 You don’t have enough money! 单字符price可提交,但钱不够
a 1 No commodity found! 不存在该商品

关键发现:

  1. price 字段必须且只能是一个字符
  2. id=4&price=1 通过了字符检查但钱不够——说明后端对 price 做了数值比较
  3. ASCII 单字符数字最大是 9,远小于 1337

矛盾出现了:我们要用 $1337$ 买商品,但只能传 $1$ 个字符。

漏洞原理:Unicode Numeric Value 属性

Unicode 数字属性

Unicode 标准中,每个字符除了字形外,还带有元数据属性(Properties)。其中一个重要属性是 Numeric_Value(数字值),用于标记这个字符代表的数字。

比如这些字符都有 Numeric_Value 属性:

字符 Unicode 代码点 Numeric_Value 说明
4 U+0034 4 ASCII 数字
U+2163 4 罗马数字 4
U+56DB 4 中文数字
U+09EA 4 孟加拉数字
U+137C 10000 埃塞俄比亚音节(本题关键)
U+2182 10000 罗马数字 10000
U+2187 50000 罗马数字 50000
U+2188 100000 罗马数字 100000

PHP 的类型转换机制

后端 PHP 代码大概这样:

1
2
3
4
5
6
7
8
9
10
// 检查字符长度
if (mb_strlen($_POST['price'], 'UTF-8') > 1) {
die("Only one char(?) allowed!");
}
// 数值比较 — 关键漏洞点
if (floatval($_POST['price']) >= $item['price']) {
// 购买成功,返回 flag
} else {
die("You don't have enough money!");
}

当 PHP 执行 floatval("፼") 时,内部调用 ICU(International Components for Unicode)库,会识别该字符的 Unicode Numeric_Value 属性,返回 10000.0

于是:

1
2
floatval("፼") → 10000.0
10000.0 >= 1337 → ✅ 通过检查

漏洞本质

程序在”字符长度检查”和”数值比较”两个阶段,对字符的语义理解不一致

1
2
3
4
5
6
7
8
9
10
┌─ 长度检查 ────────────────────────┐
│ mb_strlen("፼", 'UTF-8') → 1个字符 │ ✅ 通过
└────────────────────────────────────┘

┌─ 数值比较 ────────────────────────┐
│ floatval("፼") → 10000.0 │
│ 10000.0 >= 1337 → true │ ✅ 通过
└────────────────────────────────────┘

🚩 获得 flag
  • 长度检查时:它是 1 个字符
  • 数值比较时:它是 10000

解题过程

第一步:找到合适的 Unicode 字符

需要一个 Numeric_Value ≥ 1337 的 Unicode 字符。最常用的是 U+137C(埃塞俄比亚音节符号),其 Numeric_Value = 10000。

用 Python 验证:

1
2
3
4
5
>>> import unicodedata
>>> b = bytes.fromhex('e18dbc')
>>> cp = b.decode('utf-8')
>>> print(f'U+{ord(cp):04X} numeric={unicodedata.numeric(cp)}')
U+137C numeric=10000.0

第二步:URL 编码提交

U+137C 的 UTF-8 编码是 E1 8D BC,URL 编码后为 %E1%8D%BC

第三步:获取 Flag

响应成功:

补充:为什么用 URL 编码而不是直接发字符?

Windows 终端默认用 GBK 编码,不支持 U+137C 这个字符的显示和输入:

1
UnicodeEncodeError: 'gbk' codec can't encode character '፼'

HTTP 的 application/x-www-form-urlencoded 格式只能用 ASCII 字符传输,所以必须将非 ASCII 字节用 %XX 编码:

1
2
3
4
5
6
7
字符 ፼ (U+137C)
↓ UTF-8 编码
字节 E1 8D BC
↓ URL 编码
%E1%8D%BC ← HTTP 请求体中安全传输
↓ 服务器解析 POST
还原为 U+137C → floatval() 识别 = 10000

注意:U+137C 是 Unicode 代码点编号(指代字符的地址),不是字符本身。URL 编码的对象是字符的 UTF-8 字节,不是编号字符串。

知识点总结

1. Unicode Numeric_Value 属性

每个 Unicode 字符可以有多个属性,Numeric_Value 是其中之一。PHP 的 floatval() / is_numeric() / 隐式类型转换在某些环境下会读取这个属性。

2. PHP 隐式类型转换

1
"፼" >= 1337  // PHP 将 "፼" 转为 float → 10000.0 → true

PHP 的 >= 运算符在比较字符串和数字时,会将字符串通过 floatval() 转为数字。

3. 不安全的设计模式

本题漏洞的核心设计缺陷:先用字符长度做安全检查(严格),再用数值做业务逻辑(宽松)。两个阶段对同一输入的理解不一致,产生了绕过。

同类题型的识别方法

🚩 特征检测清单

特征 说明
<meta charset="utf-8"> 被注释标记 比如 <!--Ah,really important,seriously. -->
数字输入但限制单字符 Only one char(?) allowed!
存在价格/数值对比逻辑 购买商品、充值、转账等
ASCII 最大值 < 目标值 9 < 1337,显然无法正常达成

🧪 检测方法

提交含 Unicode 数字属性的字符做探测:

1
2
# 用 U+137C (፼, 数值=10000) 测试
curl -s "http://target/endpoint" -X POST -d "param=%E1%8D%BC"

观察行为变化:

  • “Only one char allowed” → “Not enough money” ✅ 数值被识别,此题类型确认
  • “Only one char allowed” → “Wrong format” ❌ 仅做长度检查,无数值解析

🔗 相关攻击面

此类”字符语义在不同阶段理解不一致”的问题还有:

漏洞类型 不一致点
GBK 编码注入 转义阶段:\ 是转义符;解码阶段:与前一字节组成 GBK 双字节
MySQL utf8 vs utf8mb4 PHP 用 utf8 截断 4 字节字符,MySQL 用 utf8mb4 不截断 → SQL 注入
Unicode 大小写折叠 ß.toUpperCase()SS,绕过长度/黑名单检查

参考链接