API testing (phần 2)
IV. Server-side parameter pollution
1. Internal API routing
Một cách routing phổ biến hiện nay là sử dụng internal API: ứng dụng client gửi một yêu cầu đến server với các tham số cơ bản, sau đó server xử lý yêu cầu này và tạo một yêu cầu khác đến internal API (API nội bộ) để lấy thông tin cần thiết trả về cho client. Cụ thể các bước với ví dụ như sau:
Request từ client đến server
Khi người dùng tìm kiếm một người dùng cụ thể (ví dụ peter), trình duyệt sẽ gửi yêu cầu tới server, thường là một API public trên server:
GET /userSearch?name=peter&back=/home
Yêu cầu này chỉ chứa các tham số cần thiết cho frontend, chẳng hạn name=peter
để tìm kiếm người dùng và back=/home
để xác định vị trí người dùng sẽ quay lại sau khi hoàn tất thao tác.
Server xử lý yêu cầu và sử dụng internal API
Sau khi nhận yêu cầu từ client, server sẽ xác định và lọc các tham số cần thiết cho backend. Tiếp theo tạo một yêu cầu mới đến API nội bộ để lấy dữ liệu phù hợp với logic kinh doanh của ứng dụng:
GET /users/search?name=peter&publicProfile=true
Các tham số có thể bao gồm cả các trường thông tin mà client không yêu cầu nhưng backend cần để xử lý đúng yêu cầu. Do đó, nếu logic hoạt động không chặt chẽ, quá trình này có thể tồn tại lỗ hổng parameter pollution - attacker có thể inject và thay đổi giá trị các tham số mà internal API sẽ sử dụng. Chúng ta xem xét dạng lỗ hổng này trong một số bối cảnh:
- Query string parameter pollution
- Data formats parameter pollution
- REST paths parameter pollution
2. Query string parameter pollution
Để thấy rõ về các dấu hiệu server trả về và hướng suy luận về cách khai thác parameter pollution trong query string, chúng ta sẽ cùng phân tích bài lab Exploiting server-side parameter pollution in a query string.
Bài lab không cung cấp tài khoản bất kỳ, quan sát nhận thấy chức năng quên mật khẩu yêu cầu nhập username, có API /forgot-password
Từ API có thể dễ dàng tìm thấy tài khoản admin có username là administrator:
Trước hết, chúng ta có thể kiểm tra cách server nhận và xử lý các tham số truyền lên, thử truyền thêm một tham số không tồn tại x=y
theo hai cách.
Không mã hóa ký tự &
:
Mã hóa URL encoded ký tự &
:
Từ đây có thể suy luận rằng server có thể đã sử dụng giá trị của tham số username
và yêu cầu một internal API nào đó, chẳng hạn /private/api?username=
. API này đã nhận cả tham số x
không cần thiết nên trả về thông báo lỗi Parameter is not supported. (parameter không được supported chính là x
)
Tiếp theo, chúng ta có thể kiểm tra cách server nhận giá trị tham số nào nếu xuất hiện hai tham số y hệt nhau:
username=administrator%26username=not_exist
username=not_exist%26username=administrator
Từ hai response có thể thấy khi client gửi hai tham số y hệt nhau, server sẽ nhận giá trị của tham số đầu tiên. Như vậy, giả sử khi server gọi tới internal API và theo sau tham số username
còn có một tham số ẩn khác:
/private/api?username=<input từ người dung>&unknown_param=???
Thì chúng ta có thể ghi đè được giá trị của tham số unknow_param
này.
Thực hiện kiểm tra internal API có tham số ẩn theo sau hay không bằng cách dùng ký tự #
"cắt" chuỗi tham số. Truyền giá trị username=administrator%23
, internal API sẽ có dạng:
/private/api?username=administrator#&unknown_param=???
Ký tự #
có nhiệm vụ "cắt bỏ" toàn bộ ký tự theo sau nó, với "mong muốn" khi mất đi tham số ẩn unknown_param
server sẽ báo lỗi, thật vậy:
Với thông báo Field not specified., có thể dự đoán tham số ẩn ở đây là field
, gửi data username=administrator%26field=123
kiểm chứng cho điều đó:
Lab này thực sự phải suy luận và "đoán" khá nhiều, tới bước này chúng ta cần phải tiếp tục tìm được giá trị hợp lệ của tham số ẩn field
này. Sử dụng tính năng Intruder fuzzing với wordlist là giá trị của các param (chẳng hạn như https://github.com/whiteknight7/wordlist/blob/main/fuzz-lfi-params-list.txt)
Thu được một số giá trị hợp lệ như username
, email
:
Từ response có thể thấy response trả về dữ liệu theo tham số field
.
Nhưng hai giá trị này chưa có thông tin chúng ta cần. Một chức năng reset password thường sẽ hoạt động gồm bước sinh ra token gửi đến người dùng, server dựa vào token này xác định chủ sở hữu của tài khoản. Chúng ta mong muốn tìm kiếm được giá trị này. Ngoài cách brute force, chúng ta có thể tìm kiếm thông tin qua các file javascript. Quan sát Burpsuite HTTP History chúng ta có file forgotPassword.js
đáng chú ý:
Trong đó hàm forgotPwdReady
có chứa giá trị reset_token
và resetToken
có thể là giá trị cần tìm:
Thay vào payload nhận token dùng để đặt lại mật khẩu, chúng ta có thể chiếm đoạt tài khoản người dùng bất kỳ khi biết username của họ:
Bên cạnh phương thức tìm kiếm thông tin từ file javascript, chúng ta cũng có thể sinh ra các giá trị theo từ khóa, chẳng hạn với chương trình python sau cho phép sinh ra các giá trị dựa vào danh sách từ khóa người dùng cung cấp:
import sys
from itertools import permutations, product
def generate_keyword_combinations(keywords):
special_chars = ['', '-', '_']
combinations = set()
for perm in permutations(keywords):
for chars in product(special_chars, repeat=len(keywords) - 1):
combined = perm[0]
for word, char in zip(perm[1:], chars):
combined += char + word
combinations.add(combined)
for perm in permutations(keywords):
camel_case = perm[0].lower() + ''.join(word.capitalize() for word in perm[1:])
combinations.add(camel_case)
return sorted(combinations)
if __name__ == "__main__":
keywords = sys.argv[1:]
if not keywords:
print("Please provide keywords separated by spaces.")
else:
output = generate_keyword_combinations(keywords)
for item in output:
print(item)
Với cách thức này chúng ta cũng có thể tìm ra giá trị reset-token
, kết quả sẽ tùy thuộc vào độ phủ của chương trình với các trường hợp có thể xảy ra và danh sách từ khóa cung cấp từ người dùng.
Bài tập dành cho bạn đọc: Hãy tinh chỉnh chương trình trên để "phủ" được nhiều trường hợp hơn, từ đó sinh ra các wordlist lớn hơn.
3. Data formats parameter pollution
Data formats parameter pollution khác với query string parameter pollution khi các tham số được truyền vào internal API với định dạng sử dụng trong gói tin POST
, PATCH
, ... thường là định dạng JSON. Chúng ta cần có một cách xây dựng payload khác. Chúng ta xem xét hai dạng phổ biến.
Dạng 1: x-www-form-urlencoded json
Gói tin từ client tới server:
POST /myaccount
...
name=peter
Gói tin internal API:
PATCH /users/7312/update
...
{"name":"peter"}
Để thêm tham số ẩn access_level
, có thể xây dựng gói tin chứa payload:
POST /myaccount
...
name=peter","access_level":"administrator
Kết quả gói tin internal API trở thành:
PATCH /users/7312/update
...
{name="peter","access_level":"administrator"}
Dạng 2: json json
Gói tin từ client tới server:
POST /myaccount
...
{"name": "peter"}
Gói tin internal API:
PATCH /users/7312/update
...
{"name":"peter"}
Để thêm tham số ẩn access_level
, có thể xây dựng gói tin chứa payload:
POST /myaccount
...
{"name": "peter\",\"access_level\":\"administrator"}
Kết quả gói tin internal API trở thành:
PATCH /users/7312/update
...
{name="peter","access_level":"administrator"}
4. REST paths parameter pollution
Khi internal API sử dụng dạng RESTful API, các giá trị tham số được đưa trực tiếp vào REST path của URL thay vì sử dụng query string (tham số đi kèm sau ký tự ?
). Ví dụ, client gửi yêu cầu để xem thông tin hồ sơ người dùng có id=1
, gói tin có nội dung:
GET /profile?id=1
Host example.com
Interal RESTful API sẽ yêu cầu một gói tin khác như sau:
GET /api/profile/1/info
Host internal.example.com
Trong trường hợp tấn công trực tiếp lỗ hổng IDOR (tham khảo thêm tại https://viblo.asia/p/access-control-vulnerability-lo-hong-kiem-soat-truy-cap-phan-1-3RlL5YrzLbB) ở tham số id
thất bại, chúng ta có thể tiếp cận theo hướng khai thác REST paths parameter pollution với ý tưởng "lùi thư mục".
Chẳng hạn với payload id=1%2f..%2f2
để đọc thông tin hồ sơ người dùng có id=2
. Internal API trở thành /api/profile/1/../2/info
chính là /api/profile/2/info
.
Bạn đọc có thể luyện tập kỹ thuật khai thác này trong bài lab Exploiting server-side parameter pollution in a REST URL
Tài liệu tham khảo
All rights reserved