CVE-2022-26xx9 - Lack of Randomness

I. Giới thiệu

CVE-2022-26xx9 là một challenge trên Pentesterlab.com về review source code của một ứng dụng java thực.

II. Review source code

Đọc đoạn code pentestlab cung cấp, chúng ta có thể thấy rằng phương thức generateToken() (dòng 1350-1359) tạo một chuỗi ngẫu nhiên bằng lớp Random với seed là thời gian hiện tại.

Sau đó 2 phương thức inviteAccountToProject() và inviteUserToProject() sẽ gọi phương thức generateToken() để tạo chuỗi token với độ dài là 10 ký tự (dòng 862 và 886) để tạo lời mời cho email, account tới 1 project.

Việc sử dụng lớp Random trong Java không được coi là an toàn vì nó sử dụng một thuật toán cơ bản để sinh ra các giá trị ngẫu nhiên, gọi là thuật toán "Linear Congruential Generator" (LCG). Thuật toán LCG dễ bị dự đoán và không đảm bảo tính ngẫu nhiên thực sự. Sự dự đoán dễ dẫn đến việc lợi dụng các giá trị ngẫu nhiên được sinh ra bởi lớp Random để tấn công hệ thống. Khi một kẻ tấn công có thể dự đoán các giá trị ngẫu nhiên được sinh ra, họ có thể dễ dàng thực hiện các cuộc tấn công như tấn công từ điển, tấn công Brute Force.

III. Rủi ro khi sử dụng lớp Random

Đầu tiên hãy tìm hiểu qua về lớp Random của java. Trong java, lớp Random được sử dụng để tạo 1 chuỗi số ngẫu nhiên với giá trị seed, seed là một giá trị khởi tạo ban đầu dùng để tạo chuỗi số ngẫu nhiên. Seed xác định một trạng thái ban đầu cho generator số ngẫu nhiên. Vấn đề nằm ở chính giá trị seed, khi khởi tạo lớp Random với giá trị seed cụ thể, các số ngẫu nhiên sẽ được tạo ra theo một chuỗi nhất định dựa trên seed đó. Điều này có nghĩa là mỗi lần chạy chương trình với seed giống nhau, các số ngẫu nhiên sẽ được tạo ra theo cùng một chuỗi.

Cùng quan sát 2 đoạn mã java đơn giản dưới đây:

Cả 2 đoạn mã trên đều cùng thực hiện tạo 10 số ngẫu nhiên trong khoảng từ 0 đến 1000 bằng lớp Random với giá trị seed là 10, sau đó in từng số ra màn hình với mỗi lần lặp.

Mặc dù 2 đoạn mã này hoàn toàn chạy độc lập với nhau nhưng số ngẫu nhiên được tạo sau mỗi lần lặp của 2 đoạn mã này lại có giá trị y hệt nhau.

Xem lại đoạn mã tạo token của CVE

Đoạn mã trên thực hiện định nghĩa chuỗi ký tự charset gồm các ký tự số từ 0 đến 9 và các ký tự chữ cái từ A đến Z. Tạo một đối tượng Random để tạo số ngẫu nhiên. Tạo một đối tượng StringBuffer để xây dựng chuỗi kết quả. Trong vòng lặp, lặp ‘length’ lần:

  • Sử dụng phương thức nextInt() của đối tượng Random để sinh ra một số ngẫu nhiên từ 0 đến charset.length()-1.

  • Lấy ký tự tại vị trí ngẫu nhiên từ charset và thêm vào StringBuffer.

Kết quả là một chuỗi ngẫu nhiên có độ dài ‘length’ được tạo ra từ các ký tự trong ‘charset’.

Đối tượng Random sử dụng phương thức System.currentTimeMillis() để khai báo cho giá trị seed, đây là một phương thức trong lớp System của java, dùng để lấy thời gian hiện tại tính từ thời điểm Epoch(1/1/1970) đến thời điểm hiện tại dưới dạng số mili giây. Vì vậy khi sử dụng System.currentTimeMillis() làm giá trị seed, mỗi khi phương thức tạo token được gọi, giá trị seed sẽ luôn thay đổi và chuỗi token cũng luôn thay đổi theo. Tuy nhiên nếu chúng ta có thể đoán được thời điểm hoặc khoảng thời gian mà phương thức tạo token được gọi, chúng ta hoàn toàn có thể lấy được chuỗi token đó.

IV. Timing thời gian token được tạo

Ở đây em có build một trang web java đơn giản trên local mô phỏng lại quá trình tạo và xuất token dựa trên phương thức generateToken() của CVE.

Khi thực hiện request đến trang web này, trang web sẽ trả về phản hồi về 1 đoạn token được tạo tương tự như cách mà phương thức generateToken() tạo token.

Việc tiếp theo là khoanh vùng thời gian khi token được tạo. Để tạo lại token chính xác, chúng ta cần đoán giá trị seed, chính xác là mili giây mà token đã được tạo. Rất may, giá trị được trả về bởi System.currentTimeMillis() dựa theo chuẩn UTC, vì vậy không cần lo lắng về sự khác biệt múi giờ.

Chúng ta có thể lấy mili giây (kể từ epoch) bằng cách sử dụng lệnh date trong Kali với cờ %s và cờ %N. Cờ %s sẽ trả về số giây kể từ thời điểm epoch đế hiện tại nên chúng ta sẽ phải dùng thêm cờ %N, cờ %N sẽ đại diện cho nano giây của thời điểm hiện tại. Khi sử dụng ‘date +%s%N’, lệnh sẽ trả về một chuỗi số gồm số giây và nanosecond, được nối liền với nhau.

Tuy nhiên phương thức System.currentTimeMillis() được tính dưới dạng mili giây nên chúng ta sẽ dùng %3N để chỉ bao gồm 3 chữ số của nano giây, định dạng này sẽ khớp với đầu ra của phương thức System.currentTimeMillis().

Chúng ta sẽ sử dụng lệnh date trước và sau khi gửi request tạo token để xác định thời gian mà chuỗi token được tạo.

Dựa trên đầu ra, chuỗi token được tạo có độ dài 10 ký tự là “VODCZETI20” chúng ta có thể đoán rằng chuỗi token đã được tạo với giá trị seed trong khoảng từ 1684944393254 đến 1684944393269. Giá trị này bao gồm 15 giá trị seed có thể có. Phạm vi này thay đổi dựa trên độ trễ mạng và thời gian xử lý của máy chủ.

V. Tạo lại chuỗi token

Khi đã có được dãy các giá trị seed, việc cuối cùng là sử dụng các giá trị này để tạo lại chuỗi token đã được tạo khi ta gửi request trên. Sao chép lại phương thức generateToken() để tạo chuối token, nhưng em đã sửa đổi lại một chút để phương thức này sử dụng các giá trị date vừa tìm được thay vì System.currentTimeMillis() để làm giá trị seed.

Chạy chương trình trên và xem kết quả, ta có thể thấy rằng trong danh sách các chuỗi token được tạo, có một chuỗi có giá trị y hệt chuỗi token được tạo khi ta thực hiện chạy curl trước đó