CTF Writeup

2019 Layer7 CTF - Login Challenge

LittleDev0617 2020. 9. 7. 16:38

1.Login Challenge

이 문제를 새벽 1시 29분에 풀었는데, 상당히 힘들었던 부분이 많은것같다.

우선 내가 아는 것이 별로 없기에 처음에 게싱을 잘 못한것 같고, 힌트 2가 나오자마자 제대로 풀리기 시작하면서 결국 풀었다.

우선 base64로 인코딩한 쿼리를 POST로 보내면서, GET 쿼리에 debug가 있으면 각 쿼리에 따라 debug information을 주석처리해서 페이지에 담아준다. 만약 base64(login)을 보내면

이렇게 뜨는데 힌트2에 index.php 일부분이 공개되었다.

https://gist.github.com/debukuk/f4edc592eaf2b89ab4377a9c7fe8724d

$input = urldecode($input);
$input = base64_decode($input);

if (preg_match('/index|functions|config/i', $input)) $input = '';
$input = str_replace('=', '', $input);
$input = str_replace('&', '', $input);
$input = explode("//", $input);

$page  = $input[0];
$type  = $input[1];

define("USER", html_escape($input[2]));
define("PASS", html_escape($input[3]));
unset($input); // unset variable

$data  = custom_read($page, $type);

그 중 위와같은 코드를 통해 쿼리를 처리하는데, functions와 index를 필터링하는 것을 보아 저들의 php를 확인해봐야겠고, 보니까 =와 &를 없애주는데, 이를 이용해서 func&tions, ind&ex처럼 우회할 수 있다. base64(func&tions)와 base64(ind&ex)를 보내면

<?php
function custom_read($file, $type=0xdeadbeef, $dir='./', $ext='.php'){
    $file = explode(DIRECTORY_SEPARATOR, $file.$ext);
    switch($type){
        case (string)1:
            $file = basename($file[0]);
            break;
        default:
            if (count($file) > 1) {
                for($i=0; $i<count($file)-1; $i++) {
                    $dir .= $file[$i].DIRECTORY_SEPARATOR;
                }
                $file = $dir.$file[count($file)-1];
            }else
                $file = $file[0];
            break;
    }
    ob_start();
    include_once(dirname(__FILE__).DIRECTORY_SEPARATOR.$file);
    $data = ob_get_contents();
    ob_end_clean();
    return [file_get_contents($file), $data];
}

function confirm_solvable(){
    if ((int)($_SESSION[SUPER_USER.'_A'] + $_SESSION[SUPER_USER.'_B']) == 2) return true;
    return false;
}

function alert($message="no one else..", $type="text/javascript"){
    $message = addslashes($message);
    die(sprintf("<script type='%s'>alert('%s');history.back();</script>", $type, $message));
}

function is_debug(){
    if (isset($_GET['debug'])) return true;
    return false;
}

function html_escape($input=''){ # similar to real parameter with general web-site
    $input = htmlspecialchars($input);
    $input = str_replace(array('"', "'"), array('&quot;', "&#039;"), $input);
    if (is_debug() && preg_match('/Array/i', $input))
        $input = str_replace('Array', '', $input);

    $offset = stripos($input, 'Array');
    if (strstr($input, 'Array'))
        if (!$input[$offset + 5])
            if ($offset - 1 == -1)
                $input = [];
            else
                $input = substr($input, 0, $offset).[];
        else
            $input = substr($input, 0, $offset).[].substr($input, $offset + 5, strlen($input));
    if (is_numeric($input)) $input = (int)$input;

    return $input;
}

function a_solve(){
    $_SESSION[SUPER_USER.'_A'] = true;
}

function b_solve(){
    $_SESSION[SUPER_USER.'_B'] = true;
}

function verify_user($user=SUPER_USER){
    $flag = 0;
    if ($user[USER] === PASS) $flag = 1;

    return $flag;
}
<?php
session_start(); // session needed

require(dirname(__FILE__).DIRECTORY_SEPARATOR."config.php");
require(dirname(__FILE__).DIRECTORY_SEPARATOR."functions.php");

define("MAIN_PAGE", true);
define("SUPER_USER", "panghoddari");

$input = file_get_contents('php://input');

$header = custom_read("header");
$footer = custom_read("footer");

$format = $header[1]."%s".$footer[1];
unset($header, $footer); // unset variables

$contents = '';
if ($input) {
    $input = urldecode($input);
    $input = base64_decode($input);

    if (preg_match('/index|functions|config/i', $input)) $input = '';
    $input = str_replace('=', '', $input);
    $input = str_replace('&', '', $input);
    $input = explode("//", $input);

    $page  = $input[0];
    $type  = $input[1];

    define("USER", html_escape($input[2]));
    define("PASS", html_escape($input[3]));
    unset($input); // unset variable

    $data  = custom_read($page, $type);
    if (is_debug() && $data)
        $contents .= sprintf("<!--".PHP_EOL."- Debug Information: ".PHP_EOL."%s".PHP_EOL."-->", $data[0]);
    $contents .= $data[1];
}else {
    $data  = custom_read("main");
    $contents .= $data[1];
}

printf($format, $contents);

이렇게 뜬다.

<?php
$user = file_get_contents(dirname(__FILE__).DIRECTORY_SEPARATOR."userinfo.json");
$user = json_decode($user, true);

if (hash('sha256', PASS) != hash('sha256', null))
    if ($user[USER] == PASS && USER !== PASS) {
        $verify_user = verify_user($user);
        if ($verify_user)
            if (USER == SUPER_USER) {
                printf("Welcome, <b style='font-size: 18pt;'>%s</b>!! I know you are the administrator of this site~", USER);
                a_solve();
            }else
                printf("Welcome, <b>%s</b>! Now your mission is to login as the super user!", USER);
        else if (USER != SUPER_USER)
            printf("You have tried to login with account <b>%s</b>, but the credentials for yours was wrong!", USER);
        else {
            printf("Hello, Hacker?? How did you do it?!");
            b_solve();
        }
    }else
        printf("You have tried to login with account <b>%s</b>, but the credentials for yours was wrong!", USER);
?>

로그인하는 부분은 여긴데, 우선 solve_b를 먼저 푸는데, solve_b는 php trick을 이용해서 풀면 된다.

  • SUPER_USER == "panghoddari" == USER
  • PASS ≠ null
  • $user[USER] == PASS
  • USER ≠= PASS

위 4조건을 만족해야하는데, USER == panghoddari , PASS == 0을 넣어주면 "

~

" == 0 ⇒true 가되어 solve_b가 풀립니다. PASS가 숫자 0이되는 이유는 index.php에서 $input들을 html_escape()해주기 때문

이후 solve_a는 userinfo.json을 봐야하는데, 이는 custom_read를 우회해서 볼 수 있다.

custom_read는 확장자가 .php로 고정되어있고, /를 기준으로 explode한다음 type이 1이면 그 배열의 첫번째 값의 basename을 구한다.

$file = explode(DIRECTORY_SEPARATOR, $file.$ext);
    switch($type){
        case (string)1:
            $file = basename($file[0]);

그래서 $file.$ext가 userinfo.json/php 가 되면 되고, 즉 $page가 userinfo.json/, $type이 1이어야하는데, 이는 base64(userinfo.json/ //1) 이런식으로 우회하면 성공한다.

{"panghoddari":"ezezpasswd~you_probably_dunno_the_passw0rd!_hahaha!","guest":"guest123"}

위와 같이 admin 계정을 얻었고, 이를 통해 solve_a 하였다!

나중에 panghoddari 님께 들어보니 solve_a 부분이 unintended solution이라고 하는데, 암튼 기분이 좋다!