Đôi khi

Ngày này năm trước là một ngày rất quan trọng. Tôi không thể cứ thế mà không kỷ niệm chút gì được.

Môn Ngữ văn từng là ác mộng của nhiều người, khi họ phải phân tích những tác phẩm mà họ thấy chẳng có gì để phân tích. “Tôi muốn tắt nắng đi, cho màu đừng nhạt mất. Tôi muốn buộc gió lại, cho hương đừng bay đi”, mà cũng viết ra đến là dài. Bạn nghĩ rằng bạn đã học xong, đã ra trường, bạn không còn cái đau khổ đó nữa? Vậy thì hôm nay tôi sẽ cho bạn thấy là bạn đã nhầm. Sự giả dối, khoa trương luôn xuất hiện ở khắp nơi.

Nhắc về ngày xưa, xưa rất là xưa, có một phong trào trong giới bug bounty về những lỗi mà là sự kết hợp của nhiều lỗi nhỏ khác (ví dụ chiếm đoạt tài khoản bằng CSRF + Self XSS). Trước đó thì vẫn có nhiều bug được chain loạn cả lên rồi, nhưng ngày ấy tự nhiên bùng phát, kiểu như nếu tôi tìm được một lỗi P1 và khoe trên Twitter, sẽ có một hacker rảnh phím nhảy vào comment, rồi cuộc thảo luận diễn biến như sau:

  • (A) Congrats! How many bugs did you chain?
  • (B) Nope. I didn’t need to chain anything, it was an Unauthenticated RCE 0day via command injection! My bug has been assigned CVE-2017-9
  • (A) No need to tell me about your CVE! I really don’t care. Nowadays, all people have their own CVEs, so you should stop bragging about it. Uhm, you just said that your bug was a RCE 0day? My twitter is full of 0days and also full of RCEs, so I’m not interested in them anymore. [1/2]
  • (A) It would be better if you can chain some of your bugs to make a bigger one instead of reporting those trivial issues. You should try harder. Good luck! [2/2]
  • (B) Ok.

Đùa vậy thôi, giờ tôi sẽ đi vào nội dung chính 

Nhưng trước tiên, chúng ta sẽ cùng nhau quay về quá khứ bằng cách chỉnh giờ trên máy tính về 02 giờ 50 phút sáng ngày 02 tháng 07 năm 2017. Ở thời điểm đó, bạn sẽ thấy một chàng thanh niên khôi ngô tuấn tú đang ngồi trước màn hình máy tính, gõ ra những dòng lệnh nhảy múa đẹp mắt, hoàn toàn ra dáng một hacker. Tôi không biết đó là ai.

Trùng hợp là tôi cũng đang ngồi trước màn hình máy tính. Nhưng dù là gần 3 giờ sáng, tôi không ngồi đây để xem cái mà bạn đang nghĩ (đừng cố giải thích tôi không nghe đâu). Tôi ngồi đây là để tham gia vào một chương trình bug bounty, mang thật nhiều ngoại tệ về cho đất nước, và giúp internet trở nên an toàn hơn cho tất cả mọi người (nghe mới giả tạo làm sao ).

Chương trình này là một nhánh chức năng nhỏ của một hệ thống lớn, người dùng truy cập website, đăng ký, đăng nhập, đổi tên, upload avatar. Bên cạnh đó thì mỗi người sẽ có một trang cá nhân để chia sẻ với bạn bè. Ngay trước tầm mắt thì là như vậy.

Bạn đang nghĩ về XSS, đúng không?

Nếu thế, tôi có một lời khuyên cho bạn: XSS là lỗi vớ vẩn, ai cũng tìm được, hãy quên nó đi.

Bạn định cứ tiêu tốn cả đời cho mấy lỗi kiểu low hanging fruit ấy à? Sao bạn không thử tìm một lỗi nào đấy thật sáng tạo, thật lập dị mà không ai nghĩ ra được, rồi được cả ngàn lượt retweet, rồi trở nên nổi tiếng?

Còn tôi, tôi không cần nổi tiếng. Tôi cần mỗi tiền thôi, nên tôi sẽ giúp bạn tìm XSS. Bạn có sự nổi tiếng, tôi có tiền, đôi bên cùng vui vẻ, tại sao lại không?

Như một con ong chăm chỉ, tôi cần mẫn đặt payload vào tất cả những chỗ có thể đặt được, và hồi hộp chờ alert box hiện ra. Đáng buồn là chẳng có gì xuất hiện cả, input được xử lý hoàn hảo. Nhưng duyên dáng thay, sau thêm vài phút lọ mọ, tôi phát hiện ra có chút bất thường trong cơ chế hoạt động của chức năng upload avatar: Thay vì xử lý toàn bộ trên server, quá trình upload avatar lại gồm hai bước:
1. Trình duyệt upload avatar lên endpoint /XXX. Endpoint này sẽ trả về URL của file đã upload.
2. Trình duyệt gửi URL ở bước 1 lên endpoint /YYY. Sau đó, URL này được dùng làm URL của avatar.

Bạn lại nghĩ về XSS, đúng không?

Thật cạn lời. Chẳng lẽ bạn không nghĩ được gì khác? SSRF chẳng hạn?

Sau khi tìm cách thoát khỏi những suy nghĩ lối mòn, cố gắng thinking out of box, tôi quyết định đặt một payload XSS vào trường URL ở bước 2 xem sao. Ai mà chẳng có những hy vọng mong manh?

Vẫn không được. Mặc dù server chấp nhận payload của tôi gửi lên, nhưng khi in ra HTML thì payload vẫn bị escape nên mọi thứ cũng không đi đâu ra khỏi thuộc tính src của thẻ <img>. Thế là suy cho cùng, những gì tôi có cũng chỉ là một thẻ <img> với thuộc tính src bất kỳ. Tôi làm được gì với nó? Liệu tôi có thể đặt ba chữ XXX to thật to vào đấy, xong report lỗi kiểu: “Hello, your website is broken! My avatar is not displayed on my profile page. No one likes a broken website. Many of your users will leave your website! Hope that I will get a nice bounty for this critical bug!”?

Ha ha, buồn cười quá.

Không phải có ý xúc phạm gì đâu, ha ha, nhưng mấy lỗi nhảm nhí kiểu này cả thế giới chỉ có Legal Robot là trả tiền thôi, ha ha ha.

Buồn cười quá đi -_-

Ừm, tôi có một ý tưởng khác. Nếu tôi đặt vào đó một URL hợp lệ, trỏ đến server của tôi, thì tôi sẽ biết được khi nào có ai đó truy cập trang cá nhân của mình, nhỉ? Nghe có vẻ hợp lý phết đấy chứ, một lỗi về quyền riêng tư rõ mồn một: Giờ giả sử bạn vào Facebook của người yêu cũ, bạn có muốn người ta biết không? Không chứ còn gì nữa! Đấy, lỗi là đấy đấy chứ còn đâu.

Cơ mà đây cũng không phải là Facebook, tôi không chắc lắm về việc họ có muốn trả tiền hay không. $100, $300, hay Won’t Fix? Tôi không biết được. Tiền thì tôi thích, nhưng cái thích ấy không đủ lớn nếu xét đến những gì tôi có thể mất trong trường hợp này.

Lời dẫn từ đầu bài, giờ mới phát huy tác dụng.

Có một cái chain rất phổ biến, là account takeover via open redirect, chắc bạn cũng biết rồi. Ok, tôi đã có điều kiện đầu tiên: Avatar của tôi sẽ leak được URL của trang hiện tại đến server mà tôi kiểm soát (không phải open redirect nhưng chúng ta phải có thói quen suy nghĩ tổng quát ra), nên cái mà tôi cần nữa là, hừm, tôi cần một cái gì đó để leak.

Thật may mắn quá, tôi nhớ ra website này có chức năng đăng nhập qua Facebook. Ôi sao mà màu mỡ, hãy đăng xuất và thử nó ngay!

Link Facebook OAuth của website này như sau, với tham số redirect_uri được đặt là https://xxx.website.com/facebook/callback:

https://www.facebook.com/v2.5/dialog/oauth?response_type=code&client_id=123456789&redirect_uri=https%3A%2F%2Fxxx.website.com%2Ffacebook%2Fcallback&scope=email%2Cpublic_profile%2Cuser_friends

Theo như những writeup mà tôi từng đọc, tất nhiên là tôi sẽ chuyển redirect_uri thành https://xxx.website.com/profile/cutegirl1996.

Tôi nhấn enter và luồng login vẫn hoạt động bình thường. Đó là một tín hiệu đáng mừng.


Góc kỹ thuật

redirect_uri là một tham số quan trọng trong cơ chế OAuth. Hình thức tấn công thường thấy là attacker thay đổi redirect_uri thành địa chỉ server do mình kiểm soát (có thể sử dụng Open Redirect để làm trung gian) nhằm đánh cắp giá trị code/token trả về. Facebook có một tùy chọn là Use Strict Mode for Redirect URIs, khi được bật (mặc định) sẽ chỉ cho phép sử dụng các giá trị redirect_uri được quy định sẵn, còn nếu không, việc cho phép http://abc.comredirect_uri cũng đồng nghĩa với chấp nhận những giá trị được prefix bằng http://abc.com, ví dụ http://abc.com/123, http://abc.com/xyz/def, …

Tham khảo thêm tại: https://developers.facebook.com/blog/post/2017/12/18/strict-uri-matching/


Kịch bản tấn công sẽ như sau (phải viết nhanh lên, mấy lỗi nghiêm trọng mà lại đơn giản thế này dễ Duplicate lắm):

  1. Đặt URL avatar của tôi thành https://mywebsite.com/callback_for_a_p1
  2. Gửi URL này đến người tôi yêu: https://www.facebook.com/v2.5/dialog/oauth?response_type=code&client_id=123456789&redirect_uri=https://xxx.website.com/profile/cutegirl1996&scope=email,public_profile,user_friends
  3. Khi cô ấy truy cập URL, cô ấy sẽ phải xác thực đăng nhập nếu cần, sau đó, cô ấy sẽ được redirect sang trang cá nhân của tôi với OAuth code ở trên URL (?code=blahblah). Trong trang cá nhân này, avatar của tôi sẽ được load và leak cái OAuth code đó đến https://mywebsite.com/callback_for_a_p1 qua header Referer.
  4. Tôi có OAuth code của cô ấy, nên có thể dễ dàng vào, không, không vào phòng, tôi vào tài khoản của cô ấy, bằng cách truy cập https://xxx.website.com/facebook/callback?code=YYY
  5. AAAAA rewarded Account Takeover with $XXXXX.

Viết xong xuôi đâu đấy, trước khi bắt đầu quay video làm POC, tự dưng tôi lại, biết nói thế nào nhỉ, là một cảm xúc bất chợt, tôi tự dưng cảm thấy cái laptop mà mình đang dùng đã quá già. Đây là quy luật của cuộc sống, cái gì quá cũ sẽ bị thay thế bởi những cái mới hơn, tốt hơn, thương nhớ đến mấy cũng chỉ có thể giấu ở trong lòng.

Tôi trầm tư giây lát, vẫn thấy là nó đã già, liền bật thêm một tab mới, vào  và tìm mua một cái laptop.

Tôi không phải người chuộng những thứ mới nhất, nên vì năm nay là 2017, tôi sẽ tìm những dòng Macbook Pro 2016 hoặc 2015. Cấu hình khoảng 16GB Ram, 512GB SSD, 15 inch… Ừm… Cũng khá nhiều loại, nhưng hầu hết là Ram 8 với SSD 256 trở xuống…

À đây rồi, 16GB Ram và 512GB SSD.

Gần 50 triệu 

Số tiền này có thể mua được hơn 1000 cốc trà sữa , đủ để tôi uống cả thanh xuân cũng không hết.

Ngoài kia, biết bao trẻ em đang không có cơm ăn áo mặc.

Ngoài kia, biết bao kiếp người đang vất vả cần mẫn mưu sinh trong sương gió.

Ngoài kia, đất nước vẫn còn nghèo.

Ngoài kia, dân tộc vẫn cần những sự đồng cảm, sẻ chia.

Còn tôi, ngồi đây, bỏ ra 50 triệu mua một cái máy tính, chỉ để chạy nano và vào Facebook.

Người liêm sao lại như thế?

Nhưng mà nghĩ đi nghĩ lại, chẳng phải cứ coi đây như quà người ta tặng là được rồi sao? Tôi có tự bỏ tiền ra đâu, chưa kể bounty lắm quá khéo lại đủ mua vài cái Macbook Pro cộng làm từ thiện ấy chứ? 

Vượt qua được chấp niệm, lòng thấy thật nhẹ nhàng…

Nhưng mà.

Cuộc sống không dễ dàng như thế. Làm xong bước 4 không giúp tôi đăng nhập được vào tài khoản của ai cả, chỉ nhận về một thông báo lỗi. Tôi không biết tại sao.

Thử thêm vài lần. Vẫn lỗi. Vẫn không biết tại sao.

Tìm kiếm một lúc, tôi thấy cái này trên HackerOne: https://hackerone.com/reports/211477, trong đó dẫn link đến https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#exchangecode.

Có vẻ là Facebook đã quá chán với việc bị blame vì kiểu tấn công này rồi, nên họ quyết định tạo ra một thay đổi mang tính đột phá: Với thay đổi này, ở bước 4, endpoint https://xxx.website.com/facebook/callback sẽ không thể đổi code lấy access token được, vì redirect_uri ở bước này là https://xxx.website.com/facebook/callback, khác với redirect_uri đã dùng để sinh ra code (mà bị tôi đổi thành https://xxx.website.com/profile/cutegirl1996). Facebook chắc không biết rằng họ đã làm khổ tôi đến nhường nào.


Góc kỹ thuật

Thông báo lỗi mà bạn nhận được khi hai giá trị redirect_uri không trùng khớp sẽ như sau:

{"error":{"message":"Error validating verification code. Please make sure your redirect_uri is identical to the one you used in the OAuth dialog request","type":"OAuthException","code":100,"fbtrace_id":"..."}}

Đến lúc này thì tôi thấy hơi mệt mỏi. Tôi đã thức gần một đêm mà không thu lại được gì. Tôi không nghĩ ra cách để bypass cơ chế check kia, và tự thấy bản thân thật ngốc nghếch: đâu phải chưa có ai tìm ra lỗi này, một lỗi quá đơn giản, chẳng qua vì nó không thể khai thác nên mới không ai buồn report đó thôi. Tôi nhớ lại mẩu chuyện cười về một đoạn code phức tạp, mà ai sau khi cố gắng tối ưu cũng chỉ thấy là phí công. Chúng ta đều từng đi những con đường khờ dại, đầy năng lượng, đầy nhiệt huyết, có biết đâu đến cuối cùng chỉ là ngõ cụt.

Hết phần 1…

Preview phần 2…

4 Responses

  1. vanDo says:

    Thật là một bài viết màu mè xuất sắc, hahaha (y) <3

  2. hồng anh says:

    Ra lẹ phần 2 đi anh ơi :))))

  3. hd67 says:

    Em hóng phần 2 quá bác sẻ ơi.

Leave a Reply to vanDoCancel reply