[ASIS 2019]Unicorn Shop — Unicode编码绕过
题目描述
进入题目是一个独角兽商店(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 | <--Ah,really important,seriously. --> |
第一个注释 <!--Ah,really important,seriously. --> 标记在 <meta charset="utf-8"> 后面——这暗示字符编码是解题的关键。
测试表单提交
表单提交到 /charge,参数为 id 和 price。尝试各种输入:
| 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! | 不存在该商品 |
关键发现:
price字段必须且只能是一个字符id=4&price=1通过了字符检查但钱不够——说明后端对 price 做了数值比较- 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 | // 检查字符长度 |
当 PHP 执行 floatval("፼") 时,内部调用 ICU(International Components for Unicode)库,会识别该字符的 Unicode Numeric_Value 属性,返回 10000.0。
于是:
1 | floatval("፼") → 10000.0 |
漏洞本质
程序在”字符长度检查”和”数值比较”两个阶段,对字符的语义理解不一致
1 | ┌─ 长度检查 ────────────────────────┐ |
- 长度检查时:它是 1 个字符 ✅
- 数值比较时:它是 10000 ✅
解题过程
第一步:找到合适的 Unicode 字符
需要一个 Numeric_Value ≥ 1337 的 Unicode 字符。最常用的是 U+137C(埃塞俄比亚音节符号),其 Numeric_Value = 10000。
用 Python 验证:
1 | import unicodedata |
第二步: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 | 字符 ፼ (U+137C) |
注意: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 | # 用 U+137C (፼, 数值=10000) 测试 |
观察行为变化:
- “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,绕过长度/黑名单检查 |
