Hành trình lọt top 100 all time Intigriti: Từ Bypass login đến RCE
Tiếp nối bài viết trước, tranh thủ sau khi tất cả lỗ hổng đã được Accept và Fix, hôm nay mình viết blog chia sẻ lại đợt săn bug vừa rồi.
Có lẽ đây là một chain attack khá may mắn: chỉ từ một điểm mở đầu nhỏ, mình lần ra được nhiều lỗ hổng xếp hàng xuất hiện — đủ để tấn công sâu và rộng theo kiểu “đào đến đâu ra quặng đến đó”.
Hiện tại phương châm của mình là hướng tới wildcard domain scope: recon sâu, truy ra các domain liên quan và tài sản mà công ty đó sở hữu. Hướng đi này tốn thời gian và đòi hỏi kinh nghiệm để đảm bảo độ chính xác. Theo sát một mục tiêu lâu dài cũng không dễ, đôi khi rất dễ nản.
Nhưng bù lại, cách này giúp mình gặp được các lỗ hổng “đơn giản mà hiệu quả” hơn. Vì thật lòng mà nói, ở những dự án “cũ” trên sàn thì scope thường bị đóng cứng, các lỗ hổng technical đã bị khai thác gần hết, và đôi khi cả năm không có nổi một report mới. Đây không phải lời khuyên, chỉ là chia sẻ cá nhân — và chắc chắn sẽ cần thời gian để trau dồi.
Bắt đầu hành trình thôi nào!
Mở bài: một cú “giật mình” khi vừa truy cập
Trong quá trình tìm kiếm và thử nghiệm trên một số domain, lúc mình review lại thì có một domain khiến mình giật mình.
Tại sao ư? Vì khi truy cập vào, trang web có vẻ như load frontend sau đăng nhập… trước cả khi mình đăng nhập được vào. Chỉ trong nháy mắt.
Ok, đây là một “điểm báo” khá hay — và cũng là mở đầu cho chuỗi Critical/Exceptional lần này.
Ngày trước, phương châm của mình là kiểu “vượt rào”: cái gì cản thì tìm cách vượt. Nhất là màn login. Nhiều lúc mình cũng tự hỏi:
“Liệu những web này phía sau login sẽ có gì?”

Vậy tại sao lại có thể như vậy?
Hiện tại rất nhiều hệ thống chia theo kiểu Frontend/Backend với kiến trúc RESTful API: một bên lo giao diện, một bên lo logic. Nhưng khi tách đôi như vậy, đôi khi vì cấu hình thiếu, lỗi design, hệ thống sẽ xuất hiện những hành vi khá “lạ đời”.
Ví dụ điển hình ở case này là bypass login.
Thực ra lỗ hổng bypass login kiểu frontend này không quá khó và impact có thể không quá lớn, nhưng nó gợi ra một câu hỏi quan trọng:
Nếu frontend dựa hoàn toàn vào response từ backend để quyết định “cho qua hay bắt login”, thì chỉ cần sửa lại status code (ví dụ 401 → 200) là frontend có thể tự tin đi tiếp theo luồng hard-code sẵn.
Và đúng, client-side thường bị xem nhẹ vì “client thì có gì ghê”, nhưng mình vẫn luôn nhắc bản thân: server mới là nơi đáng quan ngại — và client bypass thường chỉ là “cánh cửa đầu tiên”.
Thân bài: bypass login bằng... AI
Bước ngoặt là khi màn đăng nhập bị bỏ qua, mình view được vào phía trong trang web như một người dùng bình thường, không hề bị yêu cầu login lại.
Mình làm thế nào?
Đơn giản thôi: mình đọc code Javascript — đúng đoạn liên quan login.

Vấn đề lộ rõ: trang kiểm tra status của response để quyết định cho phép load tiếp hay đá về login.
Nhưng chưa hết… mình gặp một vấn đề khác: trang liên tục báo 401 cho các request tiếp theo — liên tục, liên tục.
Lúc này mình nghĩ: nếu chỉ là 401 thì đơn giản thôi, Match and Replace là “ngon”!

Tuy nhiên, bypass được login xong thì… hơn được gì?
Không có gì cả! Chỉ là không có cái popup thông báo nổi liên tục lên thôi!
Vậy nên mình quay lại đọc sâu hơn trong file Javascript. Và như bao lần, thời đại này là thời đại AI: nhà nhà vibe code, người người vibe code… mình thử vibe hack xem sao.

Kết quả khá ổn: nhờ AI phân tích, mình nhận ra mình đã bỏ qua một số trường quan trọng trong response body. Không chỉ status code, mà còn có dữ kiện để frontend quyết định “ai là ai” và “được load gì”.
Và đúng như dự đoán, mình bypass được trang login, đi sâu vào phía trong.
Không những vậy, mình còn phát hiện trang web đang kiểm tra username trả về có phải xxx hay không để load thêm tính năng.

Từ response AI gợi ý, mình thay thông tin username và role → thế là load được tính năng admin trong panel.
Nhưng vẫn có một vấn đề: login được rồi mà dữ liệu lại không load.
Mình quay lại kiểm tra header và thấy có Authorization.
Một ý tưởng nảy ra: xoá nó đi thì sao?
Bằng một cách kỳ diệu nào đó, sau khi xoá, response trả về status 200 kèm dữ liệu. Khá khó hiểu — có vẻ logic check Authen của trang đang “có vấn đề”.
Và mình lại… Match and Replace tiếp: chỉ cần thay nó thành một giá trị khác so với header ban đầu là được.

Trang web hoàn chỉnh lúc này trông như sau:

Đến đây, mình đã có thể report được rồi — kiểu +1 Medium hoặc High.
Nhưng mình không dừng lại. Mình tự hỏi:
“Liệu trong này có đường nào đi đến RCE không?”
Tham vọng lên chút chứ 😄
Vì thấy đây là trang đăng tải dữ liệu, mình hi vọng sẽ có chức năng upload bị Unauth. Ngồi soi một lúc thì thấy có upload thật, nhưng có vẻ không Unauth — nó yêu cầu login và quyền Admin đúng nghĩa.
Ok, vậy thử các chức năng khác. Mình thấy có tính năng chèn chữ lên hình.
Ờ… vậy tại sao không thử XSS?

Điều thú vị là ở chức năng này, hệ thống lại cho phép lưu mà không bị cản trở gì. Vậy thì lưu lại và reload trang thôi.

Và thế là +1 bug Critical:
Account take over via unauth Stored XSS

Hành trình đào sâu: khi API doc tự “lộ hint”
Mình vẫn chưa bỏ cuộc với upload. Thường khi một cụm tính năng có vấn đề, linh cảm mình nói rằng xác suất còn lỗi khác là khá cao.
Mình có thử hướng download tạo PDF → test kiểu PDF Gen lead to SSRF nhưng không may mắn, nên tạm bỏ qua.
Đây là lúc mình tận dụng những thứ mình có. Khi xem lại toàn bộ log trong Burp Suite, mình chợt nhận ra mình đã bỏ lỡ một thứ cực “ngon”: API doc.
Thật ra response đã hint cho mình từ đầu rằng hệ thống có doc cho API. Mình tự trách sao lúc đó mình bất cẩn vậy chứ.

Lại vibe hack: mình nhờ GPT phân tích doc và liệt kê các API hiện có. May mắn là mình tìm được một API rất tiềm năng: api/api_tokens.
AI phân tích và đưa ra request; mình chỉ việc thử. Quá nhàn! Và điều bất ngờ là: mình get được token mới cho người dùng chỉ với ID — hoàn toàn unauth.

Thực ra còn một API khác cho phép xem thông tin người dùng cũng bị unauth. Từ đó, mình suy đoán ra người dùng đó là Admin của hệ thống.
Vậy thì đơn giản: get token và add vào header như một người dùng “quyền lực” thôi.

Và rồi mình upload shell PHP.
Kịch bản xảy ra đúng như mong đợi: mình thành công!
+1 bug: RCE via unauth upload file unrestrict

Nhưng có nhiều “sức mạnh” trong tay vậy rồi, mình vẫn thấy chưa đủ thử thách. Trong danh sách API còn có một API cho phép đăng ký người dùng. Mình nhanh tay đăng ký luôn một user mới với quyền… ADMIN. Yeah, đăng ký user thường làm gì nữa, phải ADMIN mới lực 😄
+1 bug: Register account with admin role


Đến đây thì có vẻ mình không còn việc gì để làm thêm trong trang web này nữa.
Unauth → Admin → RCE. Chain đã quá rõ ràng.
Có lẽ đã đến lúc tìm đường đi tiếp.
Kết bài: hẹn gặp lại ở hành trình “lan rộng”
Bài đến đây cũng khá dài rồi. Mình xin hẹn các bạn ở bài tiếp theo: đó sẽ là hành trình đi rộng ra các trang web khác, bắt đầu từ “điểm tựa” là RCE.
Và cũng chính sau đợt này thì mình đạt được top 100 All time trên sàn Ingriti:

Cảm ơn mọi người đã đọc đến đây! 🙌
All rights reserved