0x00 背景
某商城系统(下称 X 系统)的一个前台列表接口,允许调用方通过一个 field 参数指定要查询的列,并把这个值原样交给底层 ORM 拼进 SQL 的 SELECT 列表,最终形成一个未授权、回显型(in-band)的任意读 SQL 注入。
1 | [路由] 请求命中入口 → 定位到未授权的列表方法 |
0x01 路由:一个请求是怎么进到漏洞方法的
这套框架用「多应用(multi-app)+ 路由参数」分发请求。以一个虚构的 URL 为例(真实路由已隐去):
1 | https://shop.example.com/svc.php?s=goods/listdata |
拆开看:
svc.php= 入口文件?s=goods/listdata= 这个 app 内部的路由,格式是控制器/方法:goods→ 控制器类Goods(路由小写,类名首字母大写)listdata→ 方法ListData()
请求进来后,框架按这几步分发(鉴权就在第 4 步):
1 | 1. 命中入口 svc.php → 绑定 app = 接口应用 |
用伪代码看这条链(已脱敏):
1 | // 「接口」应用的基类:构造函数里没有 IsLogin() |
所以这里的「未授权」请求直达 ListData(),顺手就把 field 喂进了查询。
对比一下:同样一个
ListData,假如挂在后台入口下,它的基类构造函数会调用IsLogin() + IsPower(),未授权请求第 4 步就被踢出去了。
0x02 Source(污点源)
这里是上面那个列表方法接收的 field 参数,经框架请求助手(input() 一类)取出:
1 | $params = input(); // ← HTTP 请求,未授权可达 |
特征:未授权可达、字符串类型、完全可控。既无需身份、又直接进查询构造,是高危污点源。
0x03 传播路径
污点从 Source 到 Sink,中间没有被清洗
1 | // 控制器:把请求参数整体透传(只补了分页/where,没动 field) |
关键:控制器的 array_merge 只覆盖了 m/n/where,没覆盖 field;服务层也没做列白名单。污点一路无损传到 field()。
0x04 Sink
这套 ORM 的 field() 方法,为了让开发者能写 field('COUNT(*) as c') 这类原始表达式,留了个分支:
1 | function field($field) { |
只要 field 值里带一个左括号,就进 fieldRaw()——它把整串包成「原始表达式」对象,生成 SQL 时逐字节原样输出。而子查询天然含 (,于是 field = 合法列,(子查询) 直接命中这条 raw 路径。
0x05 完整 payload
1 | https://shop.example.com/svc.php?s=goods/listdata&field=id,(SELECT database()) AS x |
SQL 最终执行
1 | SELECT id,(SELECT database()) AS x FROM `xxxx` WHERE `is_enable` = 1 ORDER BY `id` DESC LIMIT 0,20 |
0x06 影响与危害边界
能做到(严重): 未授权任意读全库(管理员凭证哈希与盐、令牌、用户 PII、订单、各类密钥);配合系统一处「令牌即身份」鉴权,注入读出管理员令牌后,可在能访问后台入口的部署上接管后台。

