X đã để lộ thông tin cá nhân của 3 triệu người dùng như thế nào?
Lời mở đầu: Trong lúc trao đổi về việc công bố bài viết chi tiết thì tôi đã nhận lời với bên X là sẽ không chỉ đích danh một công ty, tổ chức nào trong bài này. Dù sao thì, những trải nghiệm thực tế trong việc phân tích và truy tìm hung thủ… à nhầm, truy tìm lỗ hổng mới là ...
Lời mở đầu:
Trong lúc trao đổi về việc công bố bài viết chi tiết thì tôi đã nhận lời với bên X là sẽ không chỉ đích danh một công ty, tổ chức nào trong bài này. Dù sao thì, những trải nghiệm thực tế trong việc phân tích và truy tìm hung thủ… à nhầm, truy tìm lỗ hổng mới là những thứ giá trị nhất mà tôi muốn chia sẻ với các bạn. Vậy nên một cái tên gọi thì đâu có gì quá quan trọng đúng không nào?!
Giới thiệu về T-Rekt
Trong bài viết này, ngoài Juno và K-20 thì còn xuất hiện một thành viên mới chưa từng xuất hiện trong các bài viết trước đây trên Juno_okyo’s Blog. Đó chính là T-Rekt, thành viên nhỏ tuổi nhất trong J2TeaM. Tuổi nhỏ nhưng lại thích nhắm vô những công ty, tập đoàn lớn. Báo cáo về lỗ hổng rò rỉ thông tin cá nhân của 3 triệu người dùng mới chỉ là màn ra mắt của cậu ấy thôi, mọi người hãy chờ thêm các công ty X, Y, Z nào đó chuẩn bị được nêu tên trong các bài phân tích tiếp theo của Juno_okyo nhé!
T-Rekt đã tìm ra lỗ hổng như thế nào?
Trước đây T-Rekt cũng biết đọc mã nguồn trang web và thường capture các truy vấn bằng công cụ Fiddler. Rồi một ngày nọ, cậu ấy nảy ra ý tưởng debug ứng dụng Android nên tìm đọc tài liệu và thay đổi proxy của điện thoại để có thể capture thông qua Fiddler.
Và cái ứng dụng Android đầu tiên mà T-Rekt chọn để debug chính là ứng dụng Android của X. Ngay từ lúc bắt đầu, T-Rekt đã thử đăng nhập rồi order các kiểu con đà điểu để capture được càng nhiều truy vấn càng tốt. Với những gì mà Fiddler “chộp” được, T-Rekt nhận ra có điều gì đó không ổn trong phần thông tin tài khoản người dùng. Cậu ấy còn tìm được một số bug nhỏ nữa như đổi avatar của người dùng bất kỳ chỉ bằng UserID.
Ngày hôm sau, cậu ấy gửi tin nhắn cho team nhờ hỗ trợ phân tích lỗ hổng mà cậu ấy tìm được. Cậu ấy nói “phát hiện ra một lỗ hổng trong API của X cho phép lấy thông tin tài khoản của tất cả người dùng (trừ mật khẩu)”. Ngay khi đọc được tin nhắn, K-20 đã pm tôi và tối hôm đó hai đứa ngồi phân tích hệ thống X dựa trên thông tin mà T-Rekt cung cấp. Kết quả là từ lỗ hổng đó có thể truy cập vào thông tin cá nhân của hơn3 TRIỆU TÀI KHOẢN. Những thông tin này bao gồm: họ và tên, giới tính, ngày/tháng/năm sinh, email, số điện thoại, số CMND, địa chỉ cá nhân,…
Dịch ngược ứng dụng Android của X
Tôi nhanh chóng Google và tìm được ứng dụng của X trên Google Play. Thay vì phân tích ứng dụng thông qua các truy vấn HTTP như T-Rekt đã làm thì tôi sẽ dịch ngược ứng dụng về mã Java để phân tích trực tiếp từ mã nguồn ban đầu. Do đó, không cần cài đặt ứng dụng vào điện thoại mà tôi sẽ tải tập tin APK của ứng dụng này về máy tính. Việc này khá đơn giản, chỉ với từ khóa “apk downloader” trên Google là xong.
APK là phần mở rộng từ JAR và ZIP nên chỉ cần dùng các phần mềm giải nén thông thường như WinRAR hoặc WinZIP là có thể xem được tài nguyên ẩn chứa bên trong. Cấu trúc cây thư mục:
- assets
- com
- fabric
- lib
- META-INF
- res
- AndroidManifest.xml
- classes.dex (tập tin quan trọng nhất)
- resources.arsc
Tôi sử dụng tiếp công cụ dex2jar để chuyển đổi classes.dex thành classes.jar rồi dịch ngược và xem cấu trúc mã nguồn bằng jd-gui.
Phân tích ứng dụng Android của X
Như vậy là chúng ta đã có mã nguồn Java của ứng dụng, nhưng có quá nhiều class trong một package, ngồi mò thì tốn thời gian. Tôi liền sử dụng chức năng tìm kiếm của jd-gui dựa theo một Endpoint mà T-Rekt đã cung cấp:
https://x-server.com/api/customer/profile/id/
… Không có kết quả nào. Thường thì một hệ thống API không chỉ có mỗi một endpoint để truy vấn nên có thể lập trình viên thường sẽ gán Base URL vào một biến hoặc hằng. Ví dụ như:
const BASE_URL = 'https://x-server.com/api/';
Rồi nối chuỗi để tạo ra các endpoint. Nghĩ vậy, tôi liền thử tìm lại với chuỗi con “customer/profile/id”:
Có kết quả liền! He he. Lưu lại hai class này ra riêng một chỗ rồi mở lại với jd-gui để phân tích tiếp:
Có quá nhiều endpoint, đúng như dự đoán thì Base URL đã được lưu ở một nơi riêng biệt nên trong class này chỉ chứa toàn chuỗi con phía sau.
Các truy vấn HTTP của ứng dụng này đều được xử lý trong class XRequest.java. Tôi nhắm vào mục tiêu nhạy cảm nhất là mật khẩu nên nhấn ngay Ctrl+F và gõ từ khóa “password”.
1 2 3 4 5 6 7 8 9 10 11 12 |
public static ResponseData changePassword(String paramString1, String paramString2, String paramString3) { new ResponseData(); String str = Common.getXHostByLanguage() + "api/customer/changePassword"; HashMap localHashMap = new HashMap(); localHashMap.put("email", paramString1); localHashMap.put("current_password", paramString2); localHashMap.put("new_password", paramString3); return HttpUtils.callPostApi(str, localHashMap); } |
Method đầu tiên hiện ra là changePassword. Áp dụng kiểu mẫu chung nên nó yêu cầu một tham số là current_password (mật khẩu hiện tại). Dù rằng không thể khai thác method này để đổi mật khẩu của người dùng bất kỳ ngay được, nhưng hacker có thể thực hiện brute force tham số current_password để nhắm vào một người dùng với một địa chỉ email được chỉ định.
1 2 3 4 5 6 7 8 9 |
public static ResponseData forgetPassword(String paramString) { String str = Common.getXHostByLanguage() + "api/customer/resetPassword"; HashMap localHashMap = new HashMap(); localHashMap.put("email", paramString); return HttpUtils.callPostApi(str, localHashMap); } |
Method tiếp theo là forgetPassword dùng để khôi phục mật khẩu, tham số duy nhất mà nó yêu cầu là email. Method này có thể khai thác được trong trường hợp hacker đã chiếm được email của nạn nhân, từ đó chiếm tài khoản thông qua tính năng khôi phục mật khẩu.
Một vài method khác chủ yếu là truy vấn thông tin của người dùng như lịch sử giao dịch, điểm tích lũy,… Và cuối cùng, method thú vị nhất là login, có thể được dùng để thực hiện brute force.
1 2 3 4 5 6 7 8 9 10 11 |
public static ResponseData login(String paramString1, String paramString2) { new ResponseData(); String str = Common.getXHostByLanguage() + "api/customer/login"; HashMap localHashMap = new HashMap(); localHashMap.put("email", paramString1); localHashMap.put("password", paramString2); return HttpUtils.callPostApi(str, localHashMap); } |
Method register cũng có thể dùng để spam tài khoản mới nhưng hình thức spam này không gây hứng thú lắm với các hacker vì nó chỉ mang tính chất phá phách, với script kiddies thì có lẽ lại là chuyện khác.
Tìm hết các method liên quan tới mật khẩu, tôi lướt tiếp và thấy ngay cái method quan trọng nhất của bài viết:
1 2 3 4 5 6 7 |
public static ResponseData getUserProfile(String paramString) { new ResponseData(); return HttpUtils.callGetApi(Common.getXHostByLanguage() + "api/customer/profile/id" + "/" + paramString); } |
Điều khó hiểu là nếu như đa số các method truy vấn thông tin khác đều có tham sốusersessionid (ID phiên làm việc của người dùng hiện tại) để xác định xem ai là người đang truy vấn thì cái method lấy hồ sơ lại chỉ yêu cầu tham số duy nhất là ID người dùng. Có nghĩa là method có thể lấy nhiều thông tin về người dùng nhất thì lại bảo mật tệ nhất. Chỉ cần thay đổi ID từ URL là có thể xem thông tin của người dùng bất kỳ.
Giả sử lập trình viên không muốn dùng session ID thì cũng có thể sử dụng access token để chứng thực. Trong method đăng nhập thì access token được trả về nhưng có vẻ như nơi duy nhất nó được tận dụng sau đó chỉ là để đăng xuất người dùng ra khỏi ứng dụng:
Một ví dụ điển hình là hệ thống API của Facebook: https://graph.facebook.com/<ID> (bạn chỉ có thể truy vấn khi truyền vào tham sốaccess_token)
Viết mã khai thác lỗ hổng tự động
Như đã phân tích, hiện tại chúng ta có thể truy vấn thông tin của người dùng bất kỳ dựa theo ID và thực hiện brute force tài khoản dựa theo địa chỉ Email. Tôi tiến hành viết mã khai thác cho chúng bằng Python.
Vét cạn thông tin người dùng của X
Chỉ việc tạo một hàm truy vấn tới https://x-server.com/api/customer/profile/id/[USER_ID] rồi sau đó dùng vòng lặp để quét từ Min tới Max. Vấn đề bây giờ là cần tìm giá trị của Min và Max. Nếu một hacker có thời gian rảnh, anh ta có thể quét từ số 1 cho tới một số ID nào đó thật lớn. Nhưng như thế thì thật lãng phí thời gian cho những truy vấn vào ID nào đó không tồn tại.
Cách tìm cũng không khó, tôi thực hiện dò giống như khi tìm số cột để khai thác lỗiSQL Injection. Đầu tiên tôi thử với ID 10000 (vì X là hệ thống lớn nên tôi đoán số người dùng cao ngay lần đầu). Kết quả trả về mã lỗi 400 với thông báo “Id param is not provided”. Chuyện gì vậy nhỉ? Rõ ràng trong URL đã có ID rồi mà.
Ngó lại mã nguồn của ứng dụng Android, trong method callGetApi có đoạn:
Hừm, mỗi truy vấn đều được thêm vào một header “X-Device”. Trong tin nhắn gửi cho tôi, T-Rekt có đề cập tới vụ này nhưng lúc code PoC tôi lại quên mất. Lập tức thêm cái header đó vào, tại thời điểm này tôi còn phát hiện ra rằng giá trị của header này không nhất thiết phải là Android hoặc iOS như T-Rekt nói mà có thể đặt giá trị nào cũng được, thậm chí là một chuỗi rỗng.
Các bạn có thể hiểu là tại server X sẽ chỉ kiểm tra xem có header nào tên như vậy hay không chứ chả quan tâm xem giá trị của nó là gì, kiểu như: if (isset($header)) {...}
Tôi thử với các số ID: 10000, 100000, 1000000,… (tăng thêm từng số 0) đều trả về thông tin người dùng. Cho tới 10 triệu thì nhận được mã lỗi 404 và thông báo “The customer does not exist” (tức là ID này không tồn tại). Suy ra giá trị nằm trong khoảng: 1 triệu < Max < 10 triệu.
Tôi lấy tiếp một giá trị ở khoảng giữa là 5 triệu, vẫn nhận được lỗi 404. Theo kinh nghiệm, tôi còn cẩn thận thay số 0 ở cuối thành 1, tránh trường hợp số chẵn 404 nhưng số lẻ lại tồn tại. Lặp đi lặp lại cho tới khi ra được Max là một số lớn hơn 3000000 người dùng.
Quá trình tìm Min cũng tương tự nhưng áp dụng với một khoảng nhỏ và ra được 1779. Sử dụng vòng lặp để quét toàn bộ:
1 2 3 4 5 6 |
#!/usr/bin/env python def main(): for uid in range(1779, 3000000): scan(uid) |
Kết quả:
Brute force tài khoản người dùng X
Đây là hình thức tấn công bằng cách tạo ra các mật khẩu ngẫu nhiên rồi đăng nhập thử, lặp đi lặp lại cho tới khi có một mật khẩu đăng nhập thành công. Với những thông tin đã phân tích, chúng ta đã biết endpoint để đăng nhập:
https://x-server.com/api/customer/login
Tham số để đăng nhập là địa chỉ email và mật khẩu. Email thì chúng ta có được từ phần vét cạn thông tin phía trên, cứ chọn ra một email bất kỳ thôi. Hoặc nếu có thời gian và một máy chủ ảo (VPS) để treo thì có thể kết hợp cả 2 hình thức tấn công cùng lúc: Dùng vòng lặp với User ID để quét ra được email > gọi tới hàm brute force với tham số là địa chỉ email đó. Thế là hacker có thể lấy đầy đủ thông tin cá nhân lẫn mật khẩu (dò ra thành công) của hơn 3 triệu tài khoản.
PoC (Proof of Concept)
Timeline
19/09/2016 2:12 PM | Lỗ hổng được báo cáo tới X. |
25/09/2016 9:07 PM | Do không nhận được phản hồi nào, tôi quyết định tạo ra trang web sử dụng API của chính X để giúp mọi người kiểm tra xem họ có nằm trong số 3 triệu người có thể bị rò rỉ dữ liệu cá nhân hay không. |
26/09/2016 11:18 AM | GenK đăng bài viết chia sẻ về vụ rò rỉ dữ liệu cá nhân, kéo theo một loạt trang báo khác cùng đưa tin. |
26/09/2016 11:03 PM | Tôi gửi báo cáo lần thứ hai. |
27/09/2016 10:31 AM | Phía X phản hồi lại và xin thông tin liên lạc để trao đổi trực tiếp với tôi. |
27/09/2016 5:25 PM | Tôi cung cấp thông tin liên lạc như đề nghị từ X. |
27/09/2016 10:09 PM | Đại diện của X liên hệ với tôi. Tôi chia sẻ toàn bộ quá trình team phát hiện và phân tích lỗ hổng. Đại diện phía X ngỏ lời muốn tặng một phần thưởng cho chúng tôi (tất nhiên là chúng tôi không từ chối). |
28/09/2016 11:10 PM | Tôi báo cáo thêm một bug nhỏ trên trang web của X. Tại thời điểm này lỗ hổng xác thực trong API của X đã được khắc phục. |
Techtalk via Juno_okyo