Mình lại hack một hệ thống bán vé

Với những ai đã theo dõi J2TEAM từ lâu thì có lẽ mọi người sẽ còn nhớ bài viết “debut” (ra mắt) đầu tiên của mình với J2TEAM đó là hacking một hệ thống X. X là một hệ thống bán vé.
Sau writeup về X thì mình có viết writeup về hệ thống Y với bug vô hạn tiền. Rất tiếc là sau Y thì có một vài bài writeup khác đã bay màu cùng với blog cũ của mình viết trên nền tảng Ghost do quên không gia hạn server =((.
Sau một khoảng thời gian mai danh ẩn tích khá lâu thì mình đấm được Z. Z - một hệ thống bán vé khác. Có lẽ bằng một cách nào đó thì mình có duyên nhìn vào “khu vé” ra bug 🤫.
Điểm bắt đầu - Starting point
Mỗi hệ thống bán vé thường họ sẽ tách nghiệp vụ ra thành các phần:
Phần app/web booking cho khách hàng gồm các tính năng như chọn show, vào hàng chờ (queue), chọn zone/chỗ, giữ chỗ, thanh toán, lấy mã vé…
Phần dashboard quản lý dành cho bên tổ chức sự kiện gồm các tính năng như tạo sự kiện, quét vé, check-in, đổi quà, xem doanh thu,…
Dashboard quản lý dành cho admin của hệ thống bán vé để quản lý sự kiện, cấp tài khoản cho ban tổ chức,…
Thường mình pentest dạo nên quan tâm tới các bug về phân quyền như IDOR, unauthorized access hơn là những bug kĩ thuật như SQL injection, XSS, RCE…Với hệ thống của Z, mình đã xem thử web nhưng chưa thấy vấn đề gì, vì vậy nên mình quyết định đấm app mobile của Z. Đây là một ngách mà mình vẫn thường làm khi research, vì các bên họ làm app mobile cũng thường lỏng lẻo hơn do ít bị pentester nhòm ngó.
Với Z thì họ không có mobile cho nghiệp vụ đặt vé, chỉ có app cho bên tổ chức sự kiện, nên mình không có lựa chọn nào khác ngoài nghiên cứu app này.

Chuẩn bị
Dịch ngược - Reverse engineering
Sau khi tải app về và mở lên thì mình được một giao diện như sau:

Như thường lệ khi nghiên cứu ap Android thì mình dùng frida để hook vào, bypass SSL pinning để xem app gửi gì lên server. Script hooking mình dùng ở đây

Thử login phát

Ta bắt được request

Và response

Lúc này thì mình đã biết app sử dụng Google Identity Toolkit và Firestore, kèm với trong request đã có API key là AIzaSyAwI5x7pxck6Eandouo_O72ft-VgJdXpf4. Tuy nhiên vì ở đây chỉ có mỗi màn hình login mà mình lại không có account nên không truy cập được thêm tính năng nào khác để nghiên cứu.
Vì vậy nên mình dùng jadx-gui để decompile apk của app.


Sau khi xem qua vài class và không thấy có gì thú vị thì mình biết đây là một con app Flutter. Với Flutter thì code của app sẽ được pack thành Dart code nằm trong thư viện native library libapp.so.
Vì gần đây cũng có đấm qua vài con app Flutter nên mình quyết định sẽ extract và decompile thư viện này ra để xem nó xử lý ra sao dưới native lib.
Mình sử dụng đoạn code sau để dump file libapp.so và libflutter.so, 2 file này bắt buộc phải có để có thể decompile được.
Java.perform(function () {
let app_path = '/data/data/<package>';
var lib = Process.findModuleByName('libapp.so');
console.log(` ${lib.base} - ${lib.base.add(ptr(lib.size))} / size: ${lib.size}`)
Memory.protect(ptr(lib.base), lib.size, 'rwx');
var dump = new File(`${app_path}/files/dump_libapp.so`, "wb")
var lib_buffer = lib.base.readByteArray(lib.size)
dump.write(lib_buffer)
dump.close()
var lib = Process.findModuleByName('libflutter.so');
console.log(` ${lib.base} - ${lib.base.add(ptr(lib.size))} / size: ${lib.size}`)
Memory.protect(ptr(lib.base), lib.size, 'rwx');
var dump = new File(`${app_path}/files/dump_libflutter.so`, "wb")
var lib_buffer = lib.base.readByteArray(lib.size)
dump.write(lib_buffer)
dump.close()
});
Sau đó mình sử dụng Blutter để decompile 2 thư viện này ra thành Dart code
python3 ./blutter.py /<path-to-package>/lib/arm64-v8a /<path-to-package>/extracted-lib --rebuild
Mình được một cấu trúc thư mục khá đồ sộ chứa toàn bộ logic xử lý của app, mỗi tội là nó ở dạng như kiểu Assembly. Tới đây nếu mà mình trình bày làm sao để reverse đống ASM này và tìm ra lỗ hổng thì có lẽ bạn đọc sẽ ngủ gật giữa chừng mất. Vì vậy nên mình sẽ trình bày một hướng giải quyết khác :v.

AI to the rescue
Tới bước này thì trong đầu mình nghĩ ra ngay một giải pháp, đấy là dùng LLM để cho nó đọc hiểu đống code này và implement lại bằng Python cho mình. Tất nhiên là mình có xem qua xem phần xử lý tính năng ở đâu rồi mới bảo AI đọc hiểu và code lại từ đó ra, đồng thời đưa thêm thông tin cho LLM trong quá trình xử lý, chứ với context giới hạn thì nó không thể nào tự đọc hết đống ASM đấy rồi tự code được. Mình sử dụng Claude 4.5 Sonnet, model đọc và code ngon nhất hiện tại.

Sau khoảng 10 phút đọc hiểu, gen code và test thì AI “nấu” ra cho mình đoạn code như sau

Tìm lỗi
Viết tới đây thì mình mới nhận ra nãy giờ vẫn chỉ là chuẩn bị và vẫn chưa bắt đầu thực sự hack cái gì :)). Tin tốt là giờ mình đã nắm trong tay tất cả API implementation của app, coi như game này checkpoint ở đây và phần hacking mới thật sự bắt đầu.
Nếu bạn để ý thì trong những tấm hình phía trên, khi mình dịch ngược từ code Dart ra Python của app, thì có một tính năng không hề xuất hiện trên giao diện, đó là tính năng “đăng ký”. Và đây cũng chính là tính năng mở đầu cho một chuỗi bug domino của app.
Đầu tiên, với tính năng đăng ký thì mình đã có thể biến bản thân từ một tác nhân bên ngoài trở thành một tác nhân bên trong hệ thống. Điều này khả năng sẽ cung cấp cho mình nhiều quyền hơn unauthenticated users.

Sau đó, mình lại tiếp tục nhận ra rằng user này có thể truy cập toàn bộ thông tin users khác trong database (không bao gồm password). Có vẻ như trong hệ thống có 3 role chính đó là: staff, agency, Z (Z là tên hệ thống). Trong đó role Z có quyền cao nhất.

Và cả thông tin sự kiện

Thông tin checkin vào show, ngày giờ, hạng vé, tên người mua, email, sđt

Và cuối cùng, ảo ma nhất, mình phát hiện ra rằng mình có thể tự sửa role của mình trên database, cũng như bất cứ thông tin nào trên database mà mình nhìn thấy.
Kết quả đó là mình có thể thực hiện những hành động sau trên hệ thống:
Lấy thông tin (email) của tất cả các users trên hệ thống
Lấy thông tin show, checkins (trong đó bao gồm thông tin người mua, ngày giờ, trạng thái checkin) trên hẹ thống
Tự thay đổi quyền của mình, tự cấp quyền truy cập vào app để quét vé
Tự tạo ra thông tin vé để tham dự sự kiện, thay đổi được trạng thái vé đã quét thành chưa quét
PoC
Báo cáo
Sau khi phát hiện lỗ hổng thì mình đã liên hệ được với Security Team Leader của Z. Sau khi mình đưa một vài mô tả sơ lược về lỗ hổng thì anh Team Lead báo mình rằng bug này đã được phát hiện từ 2 tháng trước đó và deploy fix từ hôm trước. Tuy nhiên trong hôm đó mình vẫn truy cập và thay đổi được dữ liệu.
Trong buổi chiều hôm đó thì bug phân quyền khi register account mới đã được fix, tuy nhiên account được tạo trước đó với quyền Z thì vẫn có full quyền.
Lỗ hổng chỉ được fix vài hôm sau đó khi mà account admin mình tạo ra trong hệ thống của Z được xoá đi.
Mình thấy tiếc cho Z vì biết lỗ hổng từ sớm nhưng không fix nên mới dẫn tới việc bị khai thác. Có lẽ Z chỉ biết về bug register account chứ không biết về bug phân quyền nghiêm trọng. Security Team Leader cũng có trao đổi lại với mình là vụ register thì không patch được vì register là tính năng bắt buộc của Google Identity Toolkit/Firestore (cái này mình không rõ lắm). Theo mình thì nếu như register không tắt đi được thì mình nên xử lý nó ở backend để kiểm soát và chỉ cho user login qua API mình expose ra thôi.
Sau khi Z fix xong thì mình có trao đổi là muốn viết writeup cho lỗ hổng này và sẽ che hết các thông tin nhận diện của Z đi.
Timeline
25/11/2025: Phát hiện lỗ hổng
4/12/2025: Report lỗ hổng tới Z
5/12/2025: Lỗ hổng được fix
24/12/2025: Writeup



