[RMI] Study Note And Some Study Case

Peterjson
tradahacking
Published in
15 min readFeb 23, 2020

--

Hi ! Lâu rồi mình cũng không viết blog hay là write-up về CTF nữa, mà lần này mình muốn viết về một chủ đề khác, về RMI, về Java và muốn chia sẻ chính về cách mình tiếp cận, quá trình đào sâu một vấn đề mới và nhiều thứ khác nữa. Bài viết này dựa vào một phần nhỏ mình tự tìm hiểu và phần còn lại là phân tích về bài talk mới đây của anh Antp (@_tint0). Thật sự đây là một bài research rất hay và tâm huyết của anh Antp và nếu qua bài viết của mình đâu đó có làm bạn đọc cảm thấy thích thú muốn tìm hiểu về Java nhiều hơn thì blog này rất phù hợp kèm với một số resources khác mình sẽ reference thêm trong bài viết.

CTF case

Okay, một phương pháp mà mình áp dụng cho chính bản thân mình khi học một cái gì đó mới là xem thử đã có ai có tổ chức nào chuyển idea bug mà mình muốn analysis thành một bài CTF. Tại sao điều này lại có ý nghĩa với mình? Thứ nhất là mình sẽ có sẵn một context, env cụ thể, có thể giúp bản thân tiết kiệm thời gian deploy, thứ hai là tất nhiên sẽ có một bài write-up nào đó là một mớ reference cũng như resource kèm theo để mình tìm hiểu.
Như lúc mình học về Java Deserialization thì mình cũng tìm và làm những bài CTF liên quan. Nếu bạn quan tâm thì có 2 bài CTF rất hay.

Bài đầu tiên là bài WutFaces Mates CTF 2018 sẽ giúp bạn vừa hiểu được cách viết một gadget custom như thế nào, kĩ năng phân tích một 1day chưa có PoC chẳng hạn, và mở ra tiếp một case thực tế cụ thể hơn từ tác giả ( When EL Injection meets Java Deserialization ), bạn sẽ biết thêm về Expression Language là gì, có những parser nào, từng parser có những pros/cons như thế nào, recon library của target mà tác giả tìm ra thú vị như thế nào, …

Bài thứ hai là bài web03 whitehat final 2018 , đây cũng là một bài rất hay, giúp cho bạn biết thêm SSRF ở java thì khác như thế nào với những loại SSRF cổ điển khác, cũng như cũng protocol chỉ có ở Java có ích như thế nào khi target có bug SSRF. Bug thứ 2 là chain giữa Java Deserialization và SQLi, bạn sẽ một lần nữa biết cách analysis + viết gadget chain để có thể từ Java Deserialization tới Sqli trong context của bài CTF này như thế nào, …

Mình có bài phân tích cả 2 bài CTF trên, nhưng điều mình mong muốn là các bạn hãy thử bỏ thời gian ra thử sức để học để hiểu được nhưng thứ hay ho mà 2 anh tác giả mang lại

Quay lại context RMI, như vậy thì sẽ có những bài CTF như thế nào để vừa làm vừa học? Okay loanh quay internet thì mình tìm được 2 bài sau: link1 (bài này về codebase và remote class loading), link2 (bài này Realworld CTF 2018 Final — CTF kiểu mang 1day vô hay PoC mới để player build + analysis là một gì đó rất thú vị mà mình vẫn chưa được tham gia lần nào). Về chi tiết của từng bài mình sẽ phân tích sau. Đến đây thì vẫn mong bạn đọc sẽ bỏ thời gian thử sức như một player để xem bản thân stuck như thế nào, giải quyết như thế nào. Với bản thân mình thì quá trình khi giải một bài CTF giúp mình grow up rất nhanh cũng như lượng kiến thức mang lại là rất nhiều !

RMI Architecture

Điều đầu tiên khi bạn muốn tìm hiểu một loại bug mới thì trước tiên bạn phải hiểu rõ cơ chế hoạt động của target bạn nhắm đến nó như thế nào cũng như nhưng công nghệ bạn cần phải học. Quá trình này rất cần thiết và là việc cần làm đầu tiên để về sau khi đi vào phân tích bug sẽ không còn mơ hồ làm bạn mất thời gian quay lại xem.

Tóm gọn lại thì RMI Architecture ( về Registry ) được mô phỏng lại thành hình dưới đây

RMI Architecture

Ngoài Registry ra thì còn có những component khác như là DGC (Distributed Garbage Collection) những RMI service khác.

Giải thích về Architecture

  • Đầu tiên Registry khi start sẽ bind những method cơ bản như lookup,bind,list,rebind,… vào Registry để đảm bảo những method này sẽ có sẵn để client sử dụng
  • Stub và Skel được xem như là 2 proxy của client-side và server-side. RMI là một protocol sử dụng chính JRMP protocol (Java Remote Method Call). Hiểu nôm na như thế này, ví dụ client muốn gọi một method A ở server và nhận về return value thì vai trò của Stub (proxy phía client-side) sẽ process (coi như là một quá trình encode đi) để method call đó của client thành dưới dạng JRMP, Skel (proxy phía server-side) sẽ process (decode) packet được send từ client, sau đó excute method đó phía server-side, xong xuôi process return value và gửi lại về phía client

Về DGC (Distributed Garbage Collection)

Hiểu nôm na như sau, sau khi mà client thực hiện một remote method call tới server thì sau khi encode xong packet thì một component khác là DGCClient (DGC ở phía client) sẽ thực hiện một dirty() call tới DGC server để nhằm đăng kí với DGC server là remote reference mà client đang muốn call, để DGC server tạo 1 Lease với mục đích là để GC (Garbage Collection) không collect remote reference đó trong quá trình “automatic memory management”. Một khi client đã sử dụng xong remote reference đó thì DGCClient cũng sẽ call một method là clean() tới DGC server để nói với server là không cần giữ remote reference đó nữa và để GC collect memory đó.

RMI-JRMP protocol analysis

Mình sẽ giải thích dần từng field dưới đây và quá trình mình debug để hiểu hết từng field đó !

lấy từ Slide của @_tint0

Mình sẽ sử dụng context, env từ link github này để phân tích. Để tiện cho việc debug thì mình sẽ build phần server code thành 1 file jar, mình sẽ load cả proj vào IntelliJ, file jar mình sẽ run và mở remote debug ở VM. Từ IntelliJ sẽ vừa có thể remote debug server code cũng như debug client code có sẵn!

breakpoint line 16 để xem Stub sẽ encode packet ntn ?

Sau vài step thì mình nhảy tới một constructor của Class StreamRemoteCall

<init>:70, StreamRemoteCall (sun.rmi.transport)
newCall:348, UnicastRef (sun.rmi.server)
lookup:112, RegistryImpl_Stub (sun.rmi.registry)
main:16, BSidesClient (de.mogwailabs.BSidesRMIService)
80 là operation, var2 là ObjID, var3, var4 ở đây là field num và hash (Class StreamRemoteCall)
args field là var1 (Class RegistryImpl_Stub)

Như đã thấy thì JRMP protocol phần lớn dựa vào Serialization, đây cũng là sink để các researcher focus vào và exploit. Okay tiếp đến chúng ta remote debug xem phía server side xử lý như thế nào

Stacktrace

run0:729, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 310334923 (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)

Ở lớp transport thì chúng ta thấy rõ phía server-side xử lý như thế nào với các field magic,version và protocol. Như ở đây thì sẽ có 2 magic value, magic value thứ nhất sẽ nhảy vào nhánh RMI over HTTP, nhánh 2 sẽ là RMI protocol thuần. Version thì chỉ có một value là 2. Còn protocol sẽ tương ứng như hình sau

Như ở phía trên, khi client muốn thực hiện remote call thì phải tạo một StreamRemoteCall với procotol value là 80.

dispatch:298, 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, 310334923 (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)

Khi protocol value 75, như bạn sẽ thấy, chúng ta sẽ nhảy vào serviceCall() của Class Transport.

Server sẽ get Dispatcher tương ứng với các ObjID khác nhau

num và hash field cũng được read từ Stream, sau đó tương ứng với từng hash sẽ get ra Method từ một Map, tiếp đến là unmarshalParameters (deserialize) những args kèm theo với Method đó. Như vậy thì đã xong quá trình reverse lại JRMP protocol để hiểu rõ hơn. Tiếp đến sẽ tùy từng case cụ thể, context, env như thế nào mà mình đã phân tích học được.

Past Exploits

Trước đây, từ lúc Java Deserialization bắt đầu bùng nổ thì những researchers khác cũng bắt đầu research và có những exploit với RMI. Cụ thể như là các chain:

  1. java.rmi.server.useCodebaseOnly property
  • Mình không chắc đây có phải là một exploit chain hay không, property java.rmi.server.useCodebaseOnly của JDK được enable default sau JDK 7u21
  • property này cho phép server-side khi deserialize một Object nhưng không tồn tại trong classpath có thể thực hiện remote classloading. Tức có nghĩa load Java Byte Code từ site của attacker và recover lại Object đó. Và bạn biết rồi đó quá trình này có thể giúp attacker thực hiện những đoạn “malicious code” :)
  • Đây cũng là một solution cho bài CTF thứ nhất mình đề cập về RMI

2. Registry exploit của mbechler

  • Chain này exploit vào method bind() của Registry khi không validate từ client-side, dẫn đến client có thể bind một malicicous object sau đó phía server-side sẽ deserialize object này (unmarshalParameters).
  • Chain: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
  • Đây cũng là solution thứ hai của bài CTF thứ nhất mình để cập
  • Tuy nhiên attack vector bị limit từ jdk version 8u121, sau khi có JEP 290
  • JEP 290 là gì thì các bạn có thể tìm hiểu qua link sau: https://openjdk.java.net/jeps/290
  • Từ JEP 290 thì trong config của JDK cũng có sẵn một whitelist dành cho RMI Registry
JDK_PATH\jdk1.8.0_201\jre\lib\security\java.security

3. JRMPClient của mbechler

  • Chain này nhằm vào DGC service mà mình đã đề cập ban đầu, các bạn có thể trace và debug như quá trình mình phân tích method lookup() của Registry phía trên để hiểu rõ hơn ở step nào thì attacker có thể chèn một malicious Object trong lúc thực hiện JRMP call đến DGC server
  • Chain: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java
  • Chain này cũng bị filter từ jdk version 8u121
  • Từ sau JEP 290 thì jdk cũng có whitelist dành cho DGC

4. JRMPListener của mbechler

  • Chain này đại loại như giả lập một DGC server mà attacker đã control sẵn, mục tiêu là làm cách nào đó chúng ta khiến target có thể tạo một JRMP call tới Listener của chúng ta
  • Như chúng ta đã phân tích về JRMP, thì có thể thấy ở method StreamRemoteCall.excuteCall(), sau khi releaseOutputStream() (hoàn thành quá trình encode và đẩy packet lên server), sau đó sẽ read một byte, nếu value đó là 2 thì tiếp tục thực hiện readObject() -> Java Deserialize Sink
  • Thế nên ra đời một version custom gọi là JRMPListener mục tiêu là chuyển server-side thành client-side và khiến target connect tới JRMPListener và send back một malicious Object cho target để Deserialize
  • Chain: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPListener.java
  • Vậy thì làm như thế nào để chuyển server-side thành client-side và call JRMP tới attacker JRMPListener ?
  • idea sau xuất phát từ bài blog của codewhitesec: https://codewhitesec.blogspot.com/2017/04/amf.html
  • Ngoài sink là readObject() thì khi phân tích Java Deserialization thì bạn có thể còn thấy như readExternal(), về điểm này thì mình khuyên các bạn nên thử debug và phân tích internal khi readObject thì JDK xử lý như thế nào. Ở đây thì trong quá trình recover lại một Object qua ObjectInputStream.readObject() thì JDK sẽ check thử xem class mà đang recover implement Serializable (tức có thể overide method readObject()), hoặc implement Externalizable (tức có thế overide method readExternal())

như ở nhánh readSerialData thì JDK sẽ cố gắng excute method readObject() mà class đó có overide, đây cũng là điểm mà dẫn đến khái niệm gadget chain. Hoặc ở nhánh còn lại cố gắng excute method readExternal()

  • Turning readExternal() to readObject() ? (bạn đọc hãy đọc bài viết gốc của codewhitesec để hiểu rõ ý tưởng hơn)
  • Stacktrace
makeDirtyCall:377, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readExternalData:1849, ObjectInputStream (java.io)
readOrdinaryObject:1806, ObjectInputStream (java.io)
readObject0:1353, ObjectInputStream (java.io)
readObject:373, ObjectInputStream (java.io)
deserialize:37, JRMPClient (it.polictf.lamermi)
main:13, JRMPClient (it.polictf.lamermi)
  • Tóm tắt lại thì deserialize một Object UnicastRef thì class này sẽ cố gắng call dirtyCall() tới DGC server. Đến đây thì chain có thể hoàn chỉnh, có thể turn server-side thành client-side và archive Deserialization
  • Class UnicastRef này nằm trong whitelist nên có thể bypass JEP 290

Antp’s Work Analysis

Trước khi đi đến phần bài talk của Antp thì mình muốn nói một tý về quá trình mình tìm hiểu và dựng lại chain trên. Trước khi làm chain của Antp thì mình làm case chuyển server-side thành client-side trước và mình cố gắng kế hợp nhưng bug khác để hiểu rõ hơn, như là

CVE-2019–2684

Attack vector này giúp chúng ta không nhảy vào oldDispatch() nếu như var3 < 0. Mình không rõ từ jdk version nào thì RegistryImpl_Skel.dispatch() method bind check thêm là client chỉ có thể bind từ các local interface

CVE-2018–2800

Như ở phần phân tích JRMP protocol thì var6 ở đây là magic field, chúng ta có thể custom lại client và lợi dụng feature HTTP over RMI để bypass checkAcess. Một case rất cụ thể trong slide của Antp đó là Registry Rebinding.

Tuy rằng 2 cái CVE trên không liên quan nhiều đến phần phân tích chain của Antp nhưng qua đó mình muốn nói rằng, khi bạn đã có một cái view chuẩn, hiểu rõ thứ mà mình đang làm thì sẽ dễ dàng và luồng suy nghĩ bạn sẽ rộng ra rất nhiều. Chẳng hạn nếu chỉ focus vào sink readObject trong RMI thì bạn sẽ chỉ tìm cách bypass JEP 290 và tìm một chain mới, bạn sẽ không thể biết thêm JRMP protocol hoạt động như thế nào, dispatch như thế nào, không thể biết rằng JDK có enable RMI over HTTP, … Đây là một cách làm mà mình cảm giác rất hay từ Antp và chỉ khi bạn bỏ thời gian đọc slide thì mới cảm nhận được cách làm cũng như suy nghĩ của những người khác truyền đạt !

Phải thừa nhận lúc làm mình chưa hiểu thật rõ nhiều khía cạnh của chain này, mình lại suy nghĩ theo chain UnicastRef, và thực sự chain này vẫn bypass được JEP 290 vì lý do Object Registry extends class Remote (và class Remote nằm trong whitelist)

Tuy nhiên, chain này không thể read được return value bằng error base do bị catch lại ngay method DGCClient.makeDirtyCall() và không tiếp tục throw Exception. Chi tiết mình sẽ đề cập rõ hơn trong phần sau.

Antp Gadget + Error based to read return value

Tưởng tượng xem ở một môi trường restricted network, không có DNS, ICMP, không có outbound, bạn chỉ có thể connect tới Registry port 1099, classpath có Common-Collections version 3.2.1 chẳng hạn, đủ điều kiện RCE rồi. Hết ! Dù jdk version nào nữa thì bạn vẫn cần một cách để đọc return value của cmd mà bạn đã execute. Right?

Env ở đây vẫn là ví dụ ban đầu và JDK version 8u201

Vậy thì làm thế nào có thể đọc return value bằng error base, classpath có Common Collection 3.2.1 ?

Ý tưởng đầu tiên, Dùng classloader load bytecode đã craft sẵn để recover lại Class và execute một method (exec cmd và return một Exception). Idea này mình fail ban đầu định dùng DefiningClassLoader.defineClass() tuy nhiên lib rhino lại trong có trong classpath. Mình cũng tìm một vòng jdk để xem có classloader nào khác được implement lại có cả constructor và method define với acess modifier là public hay không. Tuy nhiên mình cũng dừng lại sau vài cái search vì được Jang hướng dẫn idea thứ 2. Đó là dùng ScriptEngineManager để eval Java code.

Ở đây mình edit lại chain CC6 như sau

final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(ScriptEngineManager.class),
new InvokerTransformer("getConstructor",
new Class[] {Class[].class },
new Object[] {new Class[0] }),
new InvokerTransformer("newInstance",
new Class[] {Object[].class},
new Object[] {new Object[0]}),
new InvokerTransformer("getEngineByName",
new Class[] { String.class},
new Object[]{ "JavaScript"}),
new InvokerTransformer("eval",
new Class[] {String.class},
new Object[] {payload}),
new InvokerTransformer("toString",
new Class[] {},
new Object[] {})};

Nếu như bạn có từng phân tích chain CC thì chắc có thể sẽ từng stuck vì chưa thêm return value, như ở các chain CC khác thì return value sẽ được set

new ConstantTransformer(1)

ở đây thì mình viết một payload exec cmd, return Exception với value là output của cmd đó, khi getEngineByName eval Java code thì mình gọi method toString để dump output của cmd ra

và output cmd sẽ được map vào trong gadget CC6 dạng [“foo”: output_cmd]

Okay quay lại thì tại sao Chain của anh Antp có thể read được return value bằng error base?

Stacktrace chain

Để hiểu rõ hơn thì mogwailabs cũng có một bài phân tích về chain này

Ý tưởng chain này vẫn là chuyển server-side thành client-side, quay lại chỗ excuteCall() xem kĩ vì sao chúng ta có thể nhận được return value nhé !

Đoạn này sau khi connect đến JRMPListener, Listener sẽ send lại một malicious Object cho server (bị chuyển thành client-side) để deserialize, và nếu Object này là instance của Exception thì server (bị chuyển thành client-side) có thể đọc được stacktrace này. Như lúc bạn mở ysoserial exploit RMIRegistryExploit chẳng hạn, nếu version jdk có JEP 290 thì bạn sẽ nhận lại về một stacktrace như sau, cơ chế này giúp bạn có thể đọc được stacktrace khi có Exception xảy ra khi remote call fail

Nhưng nếu craft một object như BadAttributeValueExpException(HashMap(foo=[Transformer Chain])), thì sau quá trình deserialize thì vẫn thỏa mãn object sau khi Deserialize là instance của Exception nhưng đó chỉ là phía server-side nhận, còn để chuyển exception đó về cho attacker thì như thế nào?

Đây mới là cái hay trong chain của Antp, đó là proxy UnicastServerRef qua RemoteObjectInvocationHandler, thì trong quá trình deserialize UnicastRemoteObject, sẽ call method reexport(), … rồi đến exportObject()

Do là UnicastServerRef đã được proxy qua RemoteObjectInvocationHandler, nên lúc này method exportObject() sẽ được qua method invokeRemoteMethod()

Và từ đây sẽ JRMP call tới JRMPListener mà mình đã control sẵn bằng Java Reflection, khiến quá trình deserialize connect tới JRMPListener của attacker, để ý trong method invokeRemoteMethod này có catch lại Exception và throw ra. Và ở UnicastServerRef.dispatch() có đoạn catch như sau

Như đoạn code sau các bạn có thể thấy là Exception từ server được serialize và send về phía client để client nhận

Tóm lại flow sẽ như thế này

  • Chain bypass JEP 290
  • UnicastServerRef.dispatch()
  • UnicastRemoteObject.readObject()
  • UnicastRemoteObject.reexport()
  • RemoteObjectInvocationHandler.invokeRemoteMethod()
  • make JRMPCall to JRMPListener
  • Deserialize BadAttributeValueExpException(HashMap(foo=[Transformer Chain])), return BadAttributeValueExpException(HashMap(foo=[output cmd])) -> gọi object này là A
  • StreamRemoteCall.exceptionReceivedFromServer() -> throw Object A
  • RemoteObjectInvocationHandler.invokeRemoteMethod() catch Exception -> tiếp tục throw
  • UnicastServerRef catch Exception serialize Exception send back to server

=> full bypass JEP 290 + read output cmd via error based

Kết

Kết bằng một đoạn message mang lại rất nhiều inspired từ một anh nước ngoài và mình cũng muốn mang thông điệp này đến các người khác. Go ahead and keep working ! Have a nice weekend all !

thanks @_jsoo_

--

--