ASCIS 2022 QUAL WRITEUP WEB CHALLENGES - Upcoming & WAF-Deser

ASCIS 2022 QUAL WRITEUP WEB CHALLENGES - Upcoming & WAF-Deser

Contents

Upcoming

image.png

Chia sẻ thật thì câu này mình làm cả buổi sáng :), lí do thì đơn giản thôi ban đầu mình chạy dirsearch nhưng nó chỉ trả về hai endpoint này

image.png

Tới trưa mình chạy lại thì lại lòi ra /about (endpoint này đóng vai trò quan trọng trong việc solved challenge này)

image.png

😡😡😡cay thật chứ

Overview:

image.png

Check source một lúc thì các bạn sẽ thấy một file .js

image.png

Thử call hàm readFlag trên console, chỉ nhận lại một fake flag

image.png

Còn về endpoint /file thì có vẻ như dùng để "bẫy", mình chỉ có thể đọc được duy nhất file /etc/passwd

image.png

Trở lại với endpoint tìm được từ dirsearch:

image.png

Ở đây ta lại được "mở" thêm tính năng search, và sau một hồi fuzz mình nhận ra vuln sqli

image.png

Việc còn lại chỉ cần tìm tên table, column

image.png

Và flag

image.png


WAF-Deser

image.png

Source code: https://github.com/to016/CTFs/tree/main/SVATTT/2022/Qual/WAF-Deser/src

Theo mình cảm nhận thì challenge java này không quá dễ nhưng cũng không quá khó

image.png

Từ file docker-compose.yml có thể thấy có 2 service webnginx

Ở file nginx.conf, nginx server được cấu hình như sau

server {    
    listen 80;

    large_client_header_buffers 4 3000; # Limit URI length upto 3000 bytes

    location ~* H4sI {
        return 403 'Deserialization of Untrusted Data Detected. (From real WAF with <3)';
    }

    location / {
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   Host $http_host;
        proxy_pass         "http://web:8080";
    }

}
  • URI length phải <= 3000 bytes
  • Nếu có dính chuỗi H4sI thì sẽ return 403
  • Thỏa mãn các điều kiện trên thì forward request đến service web chạy ở port 8080

Tiếp đó mình extract file waf-deser-0.0.1-SNAPSHOT.jar ra và mở với Intellij IDEA

image.png

Từ thư mục lib, có thể thấy tác giả add vào commons-collections4-4.0.jar => dự đoán là deseri với chain cc4

Class User

package vcs.example.wafdeser;

public class User {
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return this.name;
    }

    public Integer getAge() {
        return this.age;
    }
}

Class UserController

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package vcs.example.wafdeser;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.GZIPInputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    public UserController() {
    }

    @GetMapping({"/"})
    public String sayHello() {
        return String.format("Hello ASCIS");
    }

    @RequestMapping(
        value = {"/info/{info}"},
        method = {RequestMethod.GET}
    )
    public String getUser(@PathVariable("info") String info, @RequestParam(name = "compress",defaultValue = "false") Boolean isCompress) throws IOException {
        String unencodedData = this.unEncode(info);
        String returnData = "";
        byte[] data = Base64.getMimeDecoder().decode(unencodedData);
        if (isCompress) {
            InputStream is = new ByteArrayInputStream(data);
            InputStream is = new GZIPInputStream(is);
            ObjectInputStream ois = new ObjectInputStream(is);

            try {
                User user = (User)ois.readObject();
                returnData = user.getName();
                ois.close();
            } catch (Exception var9) {
                returnData = "?????";
            }
        } else {
            returnData = new String(data, StandardCharsets.UTF_8);
        }

        return String.format("Hello %s", returnData);
    }

    private String unEncode(String s) {
        return s.replaceAll("-", "\\r\\n").replaceAll("%3D", "=").replaceAll("%2B", "\\+").replaceAll("_", "/");
    }
}

Nhận vào một chuỗi ở dạng base64 từ url /info/{info} và gán vào giá trị cho PathVariable info sau đó thực hiện base64 decode chuỗi này và return lại về giá trị sau đi đã decode

image.png

Đặc biệt hơn nếu GET param compress=true, thì sẽ thực hiện thêm gzip decompress và sau đó deserialize với readObject() => Có thể áp dụng chain cc4 ở đây

Nhưng không đơn giản như vậy, vì nginx đã cấu hình nếu url match H4sI -> 403, mà chuỗi H4sI này lại là magic byte của gzip sau khi đã base64 encode 🥺, thế thì có cách nào để bypass không ?

Để ý trong class UserController, tác giả dùng Base64.getMimeDecoder() để decode chuỗi, mình search wiki về dạng MIME này của Base64 và sau một hồi lướt lên lướt xuống được dẫn tới link sau https://datatracker.ietf.org/doc/html/rfc2045

image.png

=> Có thể thêm một kí tự như tab %09 vào giữa H4%09sI là xong

Một điểm nữa ở bài này đó là server không có curl nên mình chọn cách reverse shell để đọc flag, và payload reverse shell chọn từ link này (ở phía dưới có 2 link nữa, các bạn nên đọc để hiểu tại sao payload của họ lại như vậy)

image.png

Sau đó gzip compress + base64 encode dữ liệu từ file này (may là < 3000 bytes)

image.png

Ở phía server họ có xài hàm unEncode nên mình tạo thêm hàm enEncode để encode vài kí tự cho giống bên server

public static String enEncode(String s) {
     return s.replaceAll("\\r\\n", "-").replaceAll("=", "%3D").replaceAll("\\+","%2B").replaceAll("/","_");
    }

Sau đó urlencode

image.png

Và gửi payload lên server, nhớ vụ "%09" nhé

image.png

Kết quả

image.png


Lời kết

Thú thật thì mình không nghĩ sẽ giải ra câu java ở trên, bởi vì lúc solved thì cũng đã hơn 4h chiều, mình tốn khá nhiều thời gian vì gặp rắc rối với tool cũng như bị lan man vào mấy thứ vớ vẩn khác. Nhưng qua vòng thi khởi động này mình cảm thấy tự tin hơn hẵn, hiển nhiên vì lần này ngoài cái cái áo mới thì còn mang về được giải 😁. Hi vọng bản thân vẫn sẽ giữ vững phong độ cho cuộc thi chung khảo sắp tới.