Challenge chia flag thành 3 phần và giấu chúng vào trang web, mình dirsearch và tìm kiếm trong request burp 1 lúc thì tìm được:
Part 1 nằm ở /robots.txt
Part 2 nằm tại file js
Part 3 ở file css
Tại schema.sql mình thấy flag được đưa vào bảng giấu tên REDACTED_TABLE và cột giấu tên REDACTED_COLUMN, gợi ý để solve challenge cần dump database:
Ngoài file app.py xử lý 3 route của trang web, file config không có cấu hình gì đặc biệt nên mình tập trung đi vào đọc file app.py. Điều đặc biệt đập vào mắt mình là route check-ip
được truyền vào tham số URL ip dính SQL Injection:
ip
nếu không được truyền vào sẽ mặc định là null, còn không thì được đưa vào hàm ipaddress.ip_address() để validate xem đây có phải là một IP hợp lệ hay không.ip
thỏa mãn sau khi hàm check ip_address thì sẽ được đưa vào câu lệnh SQL dính SQLi, còn không trả về status 400Mục đích của challenge đã khá rõ ràng, việc mình cần làm là truyền vào IP thỏa mãn hàm ipaddress.ip_address(), đồng thời vẫn phải exploit SQL Injection.
Điều này không dễ tí nào khi hàm này check khá chặt, thêm 1 dấu nháy hàm sẽ trả về exception ngay:
Mình mất kha khá thời gian cho việc chèn vào được một địa chỉ IP hợp lệ mà chứa được cả dấu nháy đơn. Đi tìm kiếm các vuln của thư viện nhưng cũng không có gì sử dụng được cả
Đọc lại source code, tự nhiên mình thấy địa chỉ IPv6 được insert hàng cuối khá thú vị, vì nó vẫn có thể chứa các ký tự, mình tiếp fuzzing tiếp một số địa chỉ IPv6 như abcd::dead:beef:abcd
, nhưng vẫn chưa thể chèn được dấu nháy vào vì chỉ có thể chèn vào các ký tự biểu diễn hex mà thôi
Lượn lờ các doc thì mình thấy có phần so sánh tại đây khá thú vị khi có thể chèn giá trị đằng sau địa chỉ IPv6 bằng dấu %
Không hiểu lắm nó có tác dụng gì, mình thử chèn nhiều hơn là số 1 vào đằng sau dấu %
thì thấy hoàn toàn valid, mình có thể truyền vào gần như bất cứ thứ gì, trong đó có cả dấu nháy:
Lý do cho việc này thì ta cần phải đi đoạn code xử lý hàm ipaddress.ip_address()
, hàm xét 2 trường hợp địa chỉ IP truyền vào hoặc là IPv4 hay IPv6. Sau đó tiến hành check bằng việc gán địa chỉ đó vào 2 class, việc kiểm tra sẽ được tiếp tục diễn ra tại phương thức __init__
của 2 class đó:
__init__
của class IPv6Address, ta sẽ thấy địa chỉ IP được kiểm tra xem được truyền vào theo kiểu số nguyên, hay kiểu hỗn hợp, và cuối cùng là kiểu chuỗi -> kiểu mà ta truyền vào:_split_scope_id
, ta sẽ thấy địa chỉ IPv6 được chia thành 3 phần thông qua method partition('%')
addr
là phần đằng trước dấu %sep
là dấu %scope_id
là phần đằng sau dấu %sep
(hay dấu %
) không tồn tại, mặc định phần đằng sau cũng không tồn tại, hay scope_id
là None. Còn nếu như không tồn tại scope_id
mà lại xuất hiện dấu %
thì quá trình parse sẽ dính lỗi mà raise exceptionscope_id
thì return 2 giá trị addr -> địa chỉ IPv6 và scope_id
%
addr_str
, được đưa vào hàm _ip_int_from_string
để đưa về dạng địa chỉ, sau đó gán vào thuộc tính _ip
của object hiện tại. Còn _scope_id
tạm thời không được sử dụng tiếp trong method __init__
Như vậy về cơ bản thì mình có thể bypass hàm check ipaddress.ip_address()
bằng dấu %
đằng sau 1 địa chỉ IPv6 valid:
Việc khó đã làm được, giờ mình chỉ cần exploit SQLi để dump db là sol được challenge rồi.
Lợi dụng việc route hiển thị tất cả các row của câu lệnh select thông qua vòng for, mình dùng union based để lấy thông tin luôn:
Tại local thì có select thẳng luôn vì mình đã biết tên bảng tên cột chứa flag:
Còn lên server thì mình sẽ lấy thông tin về tên bảng trước bằng tên db malicip
:
Được tên bảng là ______________________________________________m4LiC10u5_T413Le
, mình tiếp tục lấy cột và lấy flag:
Đây là một challenge Java Spring boot mà động đến kiến thức mà mình chưa từng tìm hiểu, cho nên quá trình searching để tìm ra đúng lỗ hổng là rất lâu, sau đó mình cũng ngốn tiếp hơn 2 tiếng để build file exploit nên mình solve challenge này khi cuộc thi chỉ còn 1 tiếng, ngần đấy không đủ thời gian để tìm ra hướng cho bài web cuối cùng.
Đầu tiên xem xét Dockerfile, ta thấy flag được move với ký hiệu ngẫu nhiên, gợi ý cho việc để solve challenge ta cần phải RCE:
Tiếp tục đến docker compose, service spring boot đứng sau 1 con reversed proxy nginx, với việc service backend phía sau để no-internet khẳng định không có outbound ra ngoài, loại bỏ trường hợp sử dụng curl/wget hay reverse shell khi RCE:
FIle pom và config cũng không có gì đặc biệt nên mình chuyển qua đọc web service controller luôn.
Website chủ yếu phục vụ 3 route chính, và được code bên trong class FileController, chúng đều được bắt đầu bằng prefix path: /file/
Route /file/testUI
khi GET đến sẽ render file upload.html hiển thị form upload file:
Form sẽ gửi một POST request đến route /file/uploadResource
, tại đây file được xử lý và black list chặn một số prefix file nhất định:
Flow code của route này như sau:
MultipartFile.getBytes()
, đồng thời là filename bằng method MultipartFile.getOriginalFilename()
-> method này sẽ lấy toàn bộ tên file, bao gồm cả path nên tại đây ta xác định lỗ hổng upload file + path traversalNgoài ra còn có route /file/downloadResource
dùng để download file từ thư mục uploads:
fileName
, từ đó expose lỗ hổng read file + path traversalChương trình chỉ có 2 route này là đáng chú ý, vì user chạy web service là root nên tổng hợp lại mình đang có:
Tưởng ngon ăn nhưng lại không hề ngon, ta không thể upload web shell jsp vì Spring Boot không code để phục vụ việc hiển thị thư mục uploads ra web, mà chỉ có thể truy cập đến thông qua route /file/downloadResource
Từ đó, mình đã thử kha khá cách, một số có thể nói đến là:
Searching tài liệu Trung Hoa về Spring boot upload file to RCE thì mình tìm được lỗ hổng Spring boot Fat Jar tại csdn, từ đó dẫn tới blog https://landgrey.me/blog/22/, đã nói chi tiết về quá trình tìm và khai thác lỗ hổng, kèm lab đi kèm + file jar ghi đè.
Về cơ bản thì lỗ hổng này cho phép ta đưa từ ghi file bất kỳ lên RCE thông qua việc ghi đè các file jar có trong lib của JDK, tác giả lựa chọn file charsets.jar vì nó có thể trigger thông qua header Accept tại HTTP Request bất kỳ
Điều kiện để khai thác lỗ hổng là:
openjdk:8-jdk-alpine
thì nó sẽ nằm tại: /usr/lib/jvm/java-1.8-openjdk/jre/lib/
Cụ thể về file jar cũng như các tấn công thì nằm tại: https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks/. Mình git clone về để lấy file jar của họ test thử cũng như tự build thành 1 project.
File jar của họ được build từ 2 file java class ExtendedCharsets
dùng để khai báo các kiểu charset, còn IBM33722
dùng để xử lý với các kiểu charset đó.
Tại IBM33722.class
ta sẽ thấy khi kiểu charset nào được kích hoạt ở bên ExtendedCharsets
đều sẽ thực thi ở file này, và khi khởi tạo constructor của class IBM33722
sẽ trigger method fun() thực thi câu lệnh. Đây là câu lệnh được thực thi bên trong file jar mẫu của họ:
Upload file jar ghi đè file jar hiện tại:
Trigger bằng request kèm header: Accept: text/html;charset=GBK
Kiểm tra tại docker thì ta thấy 2 file log đã được khởi tạo, chứng tỏ ta đã RCE thành công:
Giờ công việc của mình là tự build lại 1 file jar để thay vì tạo file tmp mình sẽ ghi flag vào /tmp/flag.txt và rồi dùng route downloadResource để đọc flag
Vì chưa build file jar từ artifacts bao giờ mà mình hay dùng maven để package nên mình tốn rất nhiều thời gian cho việc này (cụ thể là 2 tiếng rưỡi)
Có một số vấn đề gặp phải mà mình lưu ý:
Bắt đầu từ việc khởi tạo project mới, mình để tên là charsets luôn cho tiện, đồng thời dùng jdk 1.8
Tại thư mục java mình chọn new package, và nhập vào package như của author là sun.nio.cs.ext
:
Tạo 2 file Java clss bên trong package vừa tạo và copy code vô :))), riêng tại IBM33722.java thì mình sẽ sửa câu lệnh tạo file log, đồng thời thêm main method vào (không có gì không sao):
Thêm folder META-INF và file MANIFEST.MF
vào bên trong thư mục resource, copy nội dung của file jar kia vào:
Tiến hành compile project này bằng cách chạy nó thôi, kết quả file class sẽ nằm tại folder target:
Tiến hành đóng gói thành file JAR tại tab Project Structure, chọn mục Artifacts và chọn tạo file JAR:
Tại đây thì mình không chọn gì, vì main method có cũng không quan trọng lắm:
Lúc này file charsets.jar sẽ chứa kết quả output của compile, để chắc chắn thì mình thêm nội dung bên trong folder resource là folder META-INF để chắc cú:
Tại tab Build chọn Build Artifacts, file jar sẽ được compile
Kiểm tra file jar khởi tạo thì nó đã đầy đủ các thành phần như của file jar mẫu: thư mục chứa MANIFEST và 2 file class
Nếu như nhét file java vào file jar thì file jar sẽ nặng khoảng 5KB, còn khi compile xong thì nó sẽ là 11KB
Mình deploy lại local để upload file jar mới, sau khi lặp lại các bước trên thì file flag.txt đã được khởi tạo tại /tmp/flag.txt
Kết quả trên instance challenge:
Đây là challenge mà mình không kịp làm trong 1 tiếng cuối trước khi cuộc thi kết thúc, nên mình có đi hỏi về hướng làm, từ đó reproduce lại để hiểu hơn
Khi truy cập vào website, ta sẽ được ghi hero name và quirk code:
Giá trị username được ghi vào trong jwt, còn quirk code thì điền bao nhiều thì quirk vẫn có giá trị là civilian thôi.
Website thì có file robots.txt có một chút gợi ý về việc mã hóa ES256 diễn ra như thế nào:
Đi theo gợi ý, mình tìm cách crack jwt để đưa giá trị của quirk về hero nhưng không kịp.
Sau đó thì mình đã xin hint của người anh em C4t-f4t về hướng giải và nhận ra có script của 1 bài gần tương tự tại NahamCon 2021 dùng để bypass jwt sử dụng hệ mật đường cong Eliptic. Lỗ hổng xảy ra khi sử dụng thuật toán ECDSA mà không thay đổi nonce, attacker có thể crack ra được private key. Để hiểu hơn thì mình có thể tham khảo thêm tại: https://asecuritysite.com/encryption/ecd5
Với 2 mẫu thông điệp thì mình có thể crack được secret key và forge ra được token của riêng mình
Link write up đó tại: https://github.com/milliesolem/writeups/blob/main/NahamCon 2021/Elliptical/solve.py
Mình chỉnh sửa 1 chút cho phù hợp với bài và chèn vào phần jwt payload
Nhưng có vẻ quirk hero là không đúng, quirk là tên gọi chung của các siêu năng lực của mấy nhân vật trong website, nên mình nảy ra ý định là thử với tất cả các quirk được giới thiệu trên trang web, bao gồm:
Thử đến cái quirk cuối cùng thì mình mới thấy route dashboard có sự khác biệt, và mình cũng để ý là tác giả đã hint để thành quirk này khi trang web đề cập đến người sở hữu quirk HACKING nên liên lạc với họ:
Forge một jwt mới, và giao diện lúc này đã thay đổi, cho phép ta được upload file docx:
Đầu tiên mình gửi một file docx valid thì chương trình sẽ đếm số lượng word có trong doc để hiển thị ra ngoài:
Đi mò mẫm các file xml có dùng để khai thác thì em tìm được write up này: https://ctftime.org/writeup/24895, trong 1 file word sẽ tồn tại các file xml và thư mục sau:
Như vậy ta có thể thấy file word thực chất là tổng hợp của nhiều file xml, nên mình có thể chèn file xml vào trong file docx để thực thi XXE.
Trong writeup mình tham khảo thì họ sử dụng file docProps/app.xml chứa các thông số về file word, cụ thể như số dòng, số chữ, số trang,… của file docx đó để inject vào số mà website show ra -> đó là số chữ (words)
Tiến hành zip file xml này vào file docx bằng command:
Nội dung file đã hiện ra ở website
Mình sẽ đọc flag bằng file:///flag.txt
Upload file docx và mình đã có được flag: