Cyber Security Rumble 2022 Writeup

Tue Oct 11 2022

I participated Cyber Security Rumble 2022 as a member of ./Vespiary. The results were 8th/264. This is the first time I've played CTF with a Japanese team. It was very fun time to discuss in Japanese.

Here are my write-ups for all crypto-related challs.

[cry, expl] FINANCE CALCULAT0R 2022

3 solves

main.py
import ast
import hashlib
import base64
from ecdsa import BadSignatureError, SigningKey

SIGNING_KEY = SigningKey.generate(hashfunc=hashlib.md5)


WHITELIST_NODES = [
    ast.Module,
    ast.Expr,
    ast.BinOp,
    ast.Constant,
    ast.Num,
    ast.Add,
    ast.Sub,
    ast.Mult,
    ast.Div
]


EXAMPLE_PROG = "31337 * 70 + 5"


def check_code_security(code):
    # Decode for parser
    s = code.decode(errors="ignore")
    tree = ast.parse(s, mode='exec')
    for node in ast.walk(tree):
        if type(node) not in WHITELIST_NODES:
            raise ValueError("Forbidden code used in type '{}'. NOT allowed!".format(type(node)))


def run_code(code):
    # Decode for parser
    code = code.decode(errors="ignore")
    locals = {}
    exec(f"result = {code}", {}, locals)
    return locals["result"]


def sign(data):
    return SIGNING_KEY.sign(data)


def verify_signature(signature, data):
    try:
        SIGNING_KEY.verifying_key.verify(signature, data)
        return True
    except BadSignatureError:
        return False


def read_prog():
    print("~" * 52)
    print("To avoid line breaks submit your programs in base64.")

    prog_b64 = input("Program> ")
    prog = base64.b64decode(prog_b64)
    
    return prog


def read_signed_prog():
    print("~" * 52)
    print("To avoid line breaks submit your programs in base64.")

    prog_inp = input("Signed Program> ")
    signature, prog_b64 = prog_inp.split(":", 1)
    prog = base64.b64decode(prog_b64)
    signature = bytes.fromhex(signature)
    
    return signature, prog


def verify_and_sign():
    code = read_prog()

    # If program is unknown, check for safety
    try:
        check_code_security(code)
    except ValueError as ex:
        print("The program uses invalid code! Nice try hacker!")
        raise

    print("Your signed program:")
    print(sign(code).hex() + ":" + base64.b64encode(code).decode())


def run_signed_program():
    signature, code = read_signed_prog()

    if not verify_signature(signature, code):
        print("Invalid signature! This incident will be reported!")
        raise ValueError()

    print("*" * 20)
    print("Your Output:")

    print(run_code(code))

    print("*" * 20, end="\n\n")


def menu():
    print("~" * 52)
    print("What would you like to do?")
    print("  1. Verify and Sign Program")
    print("  2. Run Signed Program")
    print("~" * 52)
    choice = input("Choice >")

    try:
        choice = int(choice)
        if choice == 1:
            verify_and_sign()
        elif choice == 2:
            run_signed_program()
        else:
            raise ValueError()
    except (KeyError, ValueError) as ex:
        print("Invalid Input:", ex)


def main():
    print("~" * 52)
    print("Welcome to finance calculat0r 2021")
    print("The number 1 app for heavy number processing!")
    print("Example batch program:")
    print(EXAMPLE_PROG)
    print("~" * 52)
    
    for _ in range(10):
        menu()
    
    print("You've already run 10 programs! For more please buy our enterprise edition!")

main()

We can send any payload consisting of WHITELIST_NODES and get sign. Signed payload will be used by exec.

First I tried to find wrong usage of ECDSA, but nothing was found... But then I felt weired that hashfunc is explicitly stated as md5 (default: sha1). Since this chall contains expl tag (I don't know what this tag exactly means, exploit?), I then tried to find md5 collisions like the following:

md5("can be signed, but nothing interesting happens")
== md5("cannot be signed, but something interesting happens")

because ECDSA takes into account only md5(msg), not msg.

But how does this type of collision happen...? @Xornet_Euphoria suggests:

md5("<number># <anything without \x00, \n, \r><suffix><mallicious payload>")
== md5("<number># <anything' without \x00, \n, \r><\n or \r><suffix><mallicious payload>")

where <suffix>.decode(errors="ignore") is equivalent to a valid variable name, <mallicious payload> is like =__import__('os').system('sh') and the length of payload before <mallicious payload> is a multiple of md5 block length. The former can be signed because anything after # will be commented out and this is evaluated as a mere number, while the latter cannot be signed but is evaluated as <number>\n<variable name>=__import__('os').system('sh').

These pair of payloads can be found by clone-fastcoll. It took a few hours to find payloads satisfying the condition (without <mallicious payload>) above.

data1 = b'12345678901234567890123456789012345678901234567890123456789012 #\xfcHY\x0e:\x8f\xae\xb1\x08\xfcp\xb9\x03\xf6%\xe7\xa6*\xcf\xcdkc\xf1\xbd/\xb68\xba\x8d\xb79\x06\xce\xc3\xee\xca\xcc\x0b\x1b\xc8\x8f\x04\xcc\xf4\xc1\xe3\x9c|\x05^-\xd3\xb9\x06^\xdc\xcfH\xb3\x89/\xb8\xc8\xad\xd2>w\x02\xd1 ^\xfd\x83\x1bU\xc8\xaf\xaa\xd1\'\xb4\xe9\xf1\xda\x0f{`\x1a\xd5\x97\xf7\xc8\xd4(}\xa2\xb3N\x1f\xdb,\xb4^ek\xb0\xb0\xaf\xce\x19k\xe8\xdb\xff\x88\xdfl4\xaf\xe1\xa5"\xa8\x8d\xcdd\xe9\x88'
data2 = b'12345678901234567890123456789012345678901234567890123456789012 #\xfcHY\x0e:\x8f\xae\xb1\x08\xfcp\xb9\x03\xf6%\xe7\xa6*\xcfMkc\xf1\xbd/\xb68\xba\x8d\xb79\x06\xce\xc3\xee\xca\xcc\x0b\x1b\xc8\x8f\x04\xcc\xf4\xc1c\x9d|\x05^-\xd3\xb9\x06^\xdc\xcfH\xb3\t/\xb8\xc8\xad\xd2>w\x02\xd1 ^\xfd\x83\x1bU\xc8\xaf\xaa\xd1\'\xb4\xe9\xf1Z\x0f{`\x1a\xd5\x97\xf7\xc8\xd4(}\xa2\xb3N\x1f\xdb,\xb4^ek\xb0\xb0\xaf\xce\x99j\xe8\xdb\xff\x88\xdfl4\xaf\xe1\xa5"\xa8\r\xcdd\xe9\x88'

Using this pair, I could run sh and find a flag.

solve.py
import base64
import hashlib
from binascii import unhexlify

from Crypto.Util.number import bytes_to_long, long_to_bytes
from pwn import remote

io = remote("chall.rumble.host", 42323)


def sign(cmd: bytes):
    io.sendlineafter(b"Choice >", b"1")
    io.sendlineafter(b"Program> ", base64.b64encode(cmd))
    _ = io.recvline()
    sign_cmd = io.recvline().strip().decode()
    r_s = unhexlify(sign_cmd.split(":")[0])
    r, s = r_s[:24], r_s[24:]
    return bytes_to_long(r), bytes_to_long(s)


def exec(cmd: bytes, r: int, s: int):
    r_s = long_to_bytes(r, 24) + long_to_bytes(s, 24)
    payload = r_s.hex() + ":" + base64.b64encode(cmd).decode()
    io.sendlineafter(b"Choice >", b"2")
    io.sendlineafter(b"Signed Program> ", payload.encode())
    io.interactive()


data1 = b'12345678901234567890123456789012345678901234567890123456789012 #\xfcHY\x0e:\x8f\xae\xb1\x08\xfcp\xb9\x03\xf6%\xe7\xa6*\xcf\xcdkc\xf1\xbd/\xb68\xba\x8d\xb79\x06\xce\xc3\xee\xca\xcc\x0b\x1b\xc8\x8f\x04\xcc\xf4\xc1\xe3\x9c|\x05^-\xd3\xb9\x06^\xdc\xcfH\xb3\x89/\xb8\xc8\xad\xd2>w\x02\xd1 ^\xfd\x83\x1bU\xc8\xaf\xaa\xd1\'\xb4\xe9\xf1\xda\x0f{`\x1a\xd5\x97\xf7\xc8\xd4(}\xa2\xb3N\x1f\xdb,\xb4^ek\xb0\xb0\xaf\xce\x19k\xe8\xdb\xff\x88\xdfl4\xaf\xe1\xa5"\xa8\x8d\xcdd\xe9\x88'
data2 = b'12345678901234567890123456789012345678901234567890123456789012 #\xfcHY\x0e:\x8f\xae\xb1\x08\xfcp\xb9\x03\xf6%\xe7\xa6*\xcfMkc\xf1\xbd/\xb68\xba\x8d\xb79\x06\xce\xc3\xee\xca\xcc\x0b\x1b\xc8\x8f\x04\xcc\xf4\xc1c\x9d|\x05^-\xd3\xb9\x06^\xdc\xcfH\xb3\t/\xb8\xc8\xad\xd2>w\x02\xd1 ^\xfd\x83\x1bU\xc8\xaf\xaa\xd1\'\xb4\xe9\xf1Z\x0f{`\x1a\xd5\x97\xf7\xc8\xd4(}\xa2\xb3N\x1f\xdb,\xb4^ek\xb0\xb0\xaf\xce\x99j\xe8\xdb\xff\x88\xdfl4\xaf\xe1\xa5"\xa8\r\xcdd\xe9\x88'
assert len(data1) % 16 == 0 and len(data2) % 16 == 0
suffix = b"=__import__('os').system('sh')"
data1 += suffix
data2 += suffix
assert hashlib.md5(data1).digest() == hashlib.md5(data2).digest()
r, s = sign(data1)
exec(data2, r, s)

CSR{OhManSandiNeinNeinDasMachtManNichtAberWirklich}

[cry, misc, pwn] ENCRYPTNSTUFF

14 solves

main.c
//
// Created by rg on 9/22/22.
//
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/random.h>

#define PLAINTEXT_LEN 32
#define CIPHERTEXT_STORE_LEN 10

int stream_ref_xor_ic(unsigned char *c, const unsigned char *m,
                  unsigned long long mlen, const unsigned char *n, uint64_t ic,
                  const unsigned char *k);

void print_hex(const uint8_t *x, size_t xlen) {
    size_t i;
    for (i = 0; i < xlen; i++) {
        printf("%02x", x[i]);
    }
    printf("\n");
}

void read_hex(char *dst, int num_bytes) {
    for (size_t count = 0; count < num_bytes; count++) {
        scanf("%2hhx", &dst[count]);
    }
}

void memzero(char *dst, size_t len) {
    explicit_bzero(dst, len);
}
#define KEYLEN 32
#define NONCE_LEN 8
#define KEYMAT_LEN 4096
void encrypt_stuff(char ct[PLAINTEXT_LEN], char pt[PLAINTEXT_LEN], int print_key) {
    unsigned char keymat[KEYMAT_LEN] = {0};
    unsigned char key[KEYLEN] = {0};
    unsigned char nonce[NONCE_LEN] = {0};
    
    getrandom(keymat, KEYMAT_LEN, GRND_RANDOM);
    memcpy(key, keymat + (rand() % (KEYMAT_LEN - KEYLEN)), KEYLEN);
    memcpy(nonce, keymat + (rand() % (KEYMAT_LEN - NONCE_LEN)), NONCE_LEN);
    
    int res = stream_ref_xor_ic(ct, pt, PLAINTEXT_LEN, nonce, 0, key);

    if (print_key) {
        printf("Key: ");
        print_hex(key, KEYLEN);
        printf("Nonce: ");
        print_hex(nonce, NONCE_LEN);
        printf("Ciphertext: ");
        print_hex(ct, PLAINTEXT_LEN);
    }

     // explicitly overwrite plaintext, key and nonce
     getrandom(key, KEYLEN, GRND_RANDOM);
     getrandom(nonce, NONCE_LEN, GRND_RANDOM);
     getrandom(keymat, KEYMAT_LEN, GRND_RANDOM);
     getrandom(pt, PLAINTEXT_LEN, GRND_RANDOM);
}

int read_flag(char *flag) {
    FILE* f = fopen("flag.txt", "r");
 
    if (NULL == f) {
        printf("flag not found!\n");
        return -1;
    } 

    if(fgets(flag, PLAINTEXT_LEN, f) == NULL){
        printf("Flag could not be read.");
        return -2;
    }
 
    fclose(f);
}

int main(void) {
    char pt[PLAINTEXT_LEN] = {0};
    unsigned char ciphertext[PLAINTEXT_LEN];
    int ciphertext_store_pos = 0, choice = 1;

    char ciphertext_store[CIPHERTEXT_STORE_LEN][PLAINTEXT_LEN]; 

    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    if (read_flag(pt) < 0) {
        return EXIT_FAILURE;
    }

    encrypt_stuff(ciphertext_store[0], pt, 0);

    while (ciphertext_store_pos < CIPHERTEXT_STORE_LEN)
    {
        puts("What would you like to do?");
        puts("1: Print a ciphertext.");
        puts("2: Encrypt a plaintext.");
        scanf("%d", &choice);
        if (choice == 1) {
            puts("Which ciphertext would you like to read [0-9]?");
            scanf("%d", &choice);
            printf("Ciphertext: ");
            print_hex(ciphertext_store[choice], PLAINTEXT_LEN);
        } else if (choice == 2)
        {
            puts("Plaintext(32 bytes) pls: ");
            read(STDIN_FILENO, pt, PLAINTEXT_LEN);
            printf("Ciphertext will be stored at index %d\n", ++ciphertext_store_pos);
            encrypt_stuff(ciphertext_store[ciphertext_store_pos], pt, 1);
        } else {
            puts("Learn to type you 'tard.");
        }
        
    }
    
    printf("Plaintext(hex) pls: ");
    read_hex(pt, PLAINTEXT_LEN);

    encrypt_stuff(ciphertext, pt, 1);
    printf("Ciphertext: ");
    print_hex(ciphertext, PLAINTEXT_LEN);
}

flag and any messages can be encrypted by ChaCha20. A used key and nonce are shown except when flag is encrypted.

There exists vulnerability in the following:

            puts("Which ciphertext would you like to read [0-9]?");
            scanf("%d", &choice);
            printf("Ciphertext: ");
            print_hex(ciphertext_store[choice], PLAINTEXT_LEN);

Since choice can be negative, we can see the stack values above ciphertext_store.

This can be used to see key and nonce in encrypt_stuff... wait, key and nonce are filled with random bytes generated getrandom (generated via /dev/random).

But if key and nonce are copied and used in stream_ref_xor_ic, these values still can be seen. I checked it using gdb and found them in ciphertext_store[-143] (key) and ciphertext_store[-142] (nonce).

solve.py
from binascii import unhexlify

from Crypto.Cipher import ChaCha20


"""
❯ nc chall.rumble.host 55432
What would you like to do?
1: Print a ciphertext.
2: Encrypt a plaintext.
1
Which ciphertext would you like to read [0-9]?
-143
Ciphertext: b0a7db1109b23f401b6beb019fd326d7978326f91aec4823d314110c4f762b3b
What would you like to do?
1: Print a ciphertext.
2: Encrypt a plaintext.
1
Which ciphertext would you like to read [0-9]?
-142
Ciphertext: 910886cee6ba1a20103c238afe7f0000a04d238afe7f0000abc6009600000000
What would you like to do?
1: Print a ciphertext.
2: Encrypt a plaintext.
1
Which ciphertext would you like to read [0-9]?
0
Ciphertext: abc60096bd2d297ede8d5949c9a88e77333302e107675150a572a222f24582df
What would you like to do?
1: Print a ciphertext.
2: Encrypt a plaintext.
"""


key = unhexlify("b0a7db1109b23f401b6beb019fd326d7978326f91aec4823d314110c4f762b3b")
nonce = unhexlify("910886cee6ba1a20")
cipher = ChaCha20.new(key=key, nonce=nonce)
ct = unhexlify("abc60096bd2d297ede8d5949c9a88e77333302e107675150a572a222f24582df")
print(cipher.decrypt(ct))

The output was b'CSR{InsaneInFRAUXIPAUXIBBINGOOO\x00'... I modified it to CSR{InsaneInFRAUXIPAUXIBBINGOOO} then submitted (I didn't know why it happened).

CSR{InsaneInFRAUXIPAUXIBBINGOOO}

[cry] CMS

17 solves

main.py
from flask import Flask, request, render_template, redirect, abort, url_for, send_from_directory, make_response
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.wrappers import Request
from functools import wraps

import re
import os
import time
import traceback
import threading
import pickle
import random

from secret import key_e, key_d, key_n, flag

admin_name = 'willi_pyjCsgC'

app = Flask(__name__)
app.secret_key = os.urandom(32)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DB_URI']
app.config['SQLALCHEMY_POOL_RECYCLE'] = 500
db = SQLAlchemy(app)

### Models ###

class User(db.Model):
    name = db.Column(db.String(100), primary_key=True)
    password = db.Column(db.String(200), nullable=False)
    entries = db.relationship("Entry", back_populates="user")

    def __init__(self, name, password):
        self.name = name
        self.password = password

class Entry(db.Model):
    id = db.Column(db.BigInteger, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    hidden = db.Column(db.Boolean, nullable=False)
    user_name = db.Column(db.String(100), db.ForeignKey('user.name'),
        nullable=False)
    user = db.relationship("User", back_populates="entries")

    def __init__(self, content, hidden, user_name):
        self.content = content
        self.hidden = hidden
        self.user_name = user_name

### Setup Database ###

db.create_all()

if not User.query.filter_by(name=admin_name).first():
    willi = User(admin_name, 'notavalidpasswordhash')
    db.session.add(willi)
    db.session.commit()

e = Entry(flag, True, admin_name)
if not Entry.query.filter_by(content=e.content).first():
    db.session.add(e)
    db.session.commit()

e = Entry("""
Hello,

today i want to tell you something about crypto. It's really simple. Especially RSA.

The key consists of 2 public (e, n) and 1 private component (d).

To sign a message you just have to calculate s = m^d mod n. To verify it you have to check that m == s^e mod n.
Thats all. Really simple, right?
I even build my own RSA library which is used in this CMS app.

I use RSA signatures for my session cookies. Go ahead and try to verify your session cookie.
My public parameters are:

n = {}
e = {}
    """.format(key_n, key_e)
    , False, admin_name)
if not Entry.query.filter_by(content=e.content).first():
    db.session.add(e)
    db.session.commit()

### Helper functions ###

def get_username():
    assert len(request.cookies['username'].split('||')) == 2
    return request.cookies['username'].split('||')[0]

def rsa_sign(m, n, d):
    return pow(int.from_bytes(bytearray(m, 'utf-8'), byteorder='big'), d, n)

def rsa_verify(m, s, n, e):
    return int.from_bytes(bytearray(m, 'utf-8'), byteorder='big') == pow(int(s), e, n)

def invalidate_cookie():
    resp = make_response(render_template('cookie.html'), 403)
    resp.set_cookie('username', '', expires=0)
    return resp

### Endpoints ###

def is_logged_in(f):
    @wraps(f)
    def wrap(*args, **kwargs):
        if 'username' in request.cookies:
            return f(*args, **kwargs)
        else:
            return redirect('/login'), 403
    return wrap

@app.before_request
def security():
    if 'curl' in request.headers['User-Agent']:
        return render_template('curl.html'), 403

@app.before_request
def check_cookie():
    try:
        if request.endpoint == 'images':
            return
        if 'username' in request.cookies:
            m, s = request.cookies['username'].split('||')
            if rsa_verify(m, s, key_n, key_e):
                return
        else:
            return
    except:
        traceback.print_exc()
    
    # Failsafe defaults
    return invalidate_cookie()

@app.route('/')
def index():
    entries = Entry.query.filter_by(user_name=admin_name).filter(Entry.hidden == False).all()
    entries.extend(Entry.query.filter(Entry.user_name != admin_name).filter(Entry.hidden == False).order_by(Entry.id.desc()).limit(20))
    return render_template('index.html', login=url_for('login'), register=url_for('register'), home=url_for('home'), entries=entries)

@app.route('/images/<string:name>')
def images(name):
    return send_from_directory('images', name)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
  
    name = request.form.get('name', '')

    if not re.match("^[a-zA-Z0-9_]{1,200}$", name):
        return "The username should match this regular expression '^[a-zA-Z0-9_]{1,200}$'", 500

    user = User.query.filter_by(name=name).first()

    if not user:
        return "Invalid username", 404

    if not check_password_hash(user.password, request.form.get('passwd', '')):
        return "Invalid password", 403

    resp = make_response(redirect('home'))
    resp.set_cookie('username', f'{user.name}||{rsa_sign(user.name, key_n, key_d)}', httponly=True, samesite='Strict')
    return resp


@app.route('/logout')
@is_logged_in
def logout():
    resp = make_response(redirect('/'))
    resp.set_cookie('username', '', expires=0)
    return resp


@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')

    name = request.form.get('name', '')
    passwd = request.form.get('passwd', '')

    if not (name and passwd):
        return "You have to specify username *and* password"

    if not re.match("^[a-zA-Z0-9_]{1,200}$", name):
        return "The username should match this regular expression '^[a-zA-Z0-9_]{1,200}$'"

    user = User.query.filter_by(name=name).first()

    try:
        if user and user.name != admin_name:
            user.password = generate_password_hash(passwd)
        else:
            user = User(name, generate_password_hash(passwd))
            db.session.add(user)
        db.session.commit()
    except:
        return "Username already taken"

    return redirect('login')

@app.route('/home')
@is_logged_in
def home():
    return render_template('home.html', entries=Entry.query.filter_by(user_name=get_username()).all())

@app.route('/add', methods=['GET', 'POST'])
@is_logged_in
def add_entry():
    if request.method == 'GET':
        return render_template('add.html')
    
    username = get_username()
    content = request.form['content']
    hidden = 'hidden' in request.form

    if not User.query.filter_by(name=username).first():
        return "you are not registered"

    try:
        entry = Entry(content, hidden, username)
        db.session.add(entry)
        db.session.commit()
    except:
        traceback.print_exc()
        return "something went wrong"

    return redirect('/home')

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=6000, threaded=True)

The username is signed by RSA. Let c = bytes_to_long(b"willi_pyjCsgC"). The goal is to find cdmodnc^d \mod n.

Since RSA is homomorphic (c1dc2d=(c1c2)dc_1^d c_2^d = (c_1 c_2)^d), if (cx)d,xdmodn(cx)^d, x^d \mod n are found, then cdmodnc^d \mod n is also found, though remark that signed username has a constraint, ^[a-zA-Z0-9_]{1,200}$.

I chose x=25613+1x = 256^{13} + 1. (cx)d(cx)^d can be found by registering a user b"willi_pyjCsgC" * 2. However, xdx^d cannot be found directly, since this is invalid username. But using signs of a username b"A" * 13 and b"A" * 26, we can find xdx^d indirectly.

Using the sign cdc^d, I could see a flag.

solve.py
from Crypto.Util.number import bytes_to_long


n = 4771330883112971237939706619213599402384877940456764225888204081198779601060497681979086070114999578721419080091233367976703234544232329362591884723419024769387428773213232454098113969833287400128930396547734023781138327606819544994019879501250170998873931466416456155309743489924182569128092569329573556390884191709384091268950856005448743640839481995786235223922245026050084434032791841251275463860001265220793115744012848355102061564585012581329613537678104534071860503699821793208055435007631828644238050409789360653789413150416607515525846988146633619987077989197026930263765381707598170276275237433927420037819260032776384266766200077462410545748426945408024379859188854788073423291665396437193314169755391212230890805412235682861913844245080791592720391474573182337429659029124457866724708139451173078209732505713730392054544531845760994213178580609881571472618053214778712225445157141145547548499
e = 0x10001
c = bytes_to_long(b"willi_pyjCsgC")
# AAAAAAAAAAAAA||2551915105131639649760854943184661015500059945005963927536777915233135229421528327349217159624336186729922514885324926797630657103421096512613237864285970857665983918934491224283024754850445053557833398332193277152667482439129248350936180230576233212725068696709113962952989754766936047165050301538344419692421964491328972730652282948606608188665572687553412094232067939370533280882082144391228688017824630584610460367224453171144647299593060429766228819895938880374481196699003749473471250663835872157239515719360688678977179243710713157901712629108679249175192505113437449950956921377843731950195201202905153238826183082255286218836075973359191168881228006920889686538490751714001275783050595541666194127055329009455970515308103112481884673211907442115286251804510010179512975696770682508957200658139820020469806718175098493882550455496217518117740675394693923095486742162487979198510898685842515047320
a_d = 2551915105131639649760854943184661015500059945005963927536777915233135229421528327349217159624336186729922514885324926797630657103421096512613237864285970857665983918934491224283024754850445053557833398332193277152667482439129248350936180230576233212725068696709113962952989754766936047165050301538344419692421964491328972730652282948606608188665572687553412094232067939370533280882082144391228688017824630584610460367224453171144647299593060429766228819895938880374481196699003749473471250663835872157239515719360688678977179243710713157901712629108679249175192505113437449950956921377843731950195201202905153238826183082255286218836075973359191168881228006920889686538490751714001275783050595541666194127055329009455970515308103112481884673211907442115286251804510010179512975696770682508957200658139820020469806718175098493882550455496217518117740675394693923095486742162487979198510898685842515047320
# AAAAAAAAAAAAAAAAAAAAAAAAAA||2063988744638577153310235858104059037021860128632488891128267214851535124932575370746386996515691875344887502659309477950703134047520924309985715756143402121983774113504825177257078645718900930005178552116307215618314027750208738137380067806932941498945940050365062228942163003181815653386240790816831667657090202036031694363398525515915404201320867434524580788432148132697663461097631561548317644611612486761838270465461776672056645361104855117040363618024761206444807706612429490935548688064395939268203790005529960913280219783112480021378699082052854269918622092938465912775572701044725717287267409797836947472141866332488565811211293712275216752434627292731705359917293417281652348605802036663137735895766201641174352334605098801326177400303482671487681306698894420276135990146469115779822865820938537974234259574607386812553537931705757638593357968399296174992187586888358640549705649343862817004267
a2_d = 2063988744638577153310235858104059037021860128632488891128267214851535124932575370746386996515691875344887502659309477950703134047520924309985715756143402121983774113504825177257078645718900930005178552116307215618314027750208738137380067806932941498945940050365062228942163003181815653386240790816831667657090202036031694363398525515915404201320867434524580788432148132697663461097631561548317644611612486761838270465461776672056645361104855117040363618024761206444807706612429490935548688064395939268203790005529960913280219783112480021378699082052854269918622092938465912775572701044725717287267409797836947472141866332488565811211293712275216752434627292731705359917293417281652348605802036663137735895766201641174352334605098801326177400303482671487681306698894420276135990146469115779822865820938537974234259574607386812553537931705757638593357968399296174992187586888358640549705649343862817004267

x_d = a2_d * pow(a_d, -1, n) % n

# willi_pyjCsgCwilli_pyjCsgC||2619158213512819919811090050621961382642581271302404613800477588692803481384112871670384909757045944610328032412964434824081271790971581238739785719425383248381493485870975664082088719653321376117560043247766271818373266388494390512254988407679383094566423585462157429170255058126229378206948867722161235529327840643483918231336897783701825064566203319786167381509252927838760601246017829146249184060168842155075833463376195512294209494580810211719747862857039362569154300413987068299876611421599375771392724695389268757059587261541847199641421384648312048318602430080892862176949040225573737280580090282118653147244909974216325805890935132129722163844630303243432863522824315250477397305556356407108564466635748856312714296603380942472357615565131758353152152417490722209922911975048559698116976766593490159696849742117748330465968126045046645059278631874409975330257925444149259947360890031023033870917
c2 = 2619158213512819919811090050621961382642581271302404613800477588692803481384112871670384909757045944610328032412964434824081271790971581238739785719425383248381493485870975664082088719653321376117560043247766271818373266388494390512254988407679383094566423585462157429170255058126229378206948867722161235529327840643483918231336897783701825064566203319786167381509252927838760601246017829146249184060168842155075833463376195512294209494580810211719747862857039362569154300413987068299876611421599375771392724695389268757059587261541847199641421384648312048318602430080892862176949040225573737280580090282118653147244909974216325805890935132129722163844630303243432863522824315250477397305556356407108564466635748856312714296603380942472357615565131758353152152417490722209922911975048559698116976766593490159696849742117748330465968126045046645059278631874409975330257925444149259947360890031023033870917

print(c2 * pow(x_d, -1, n) % n)
# willi_pyjCsgC||1481746030447189447393273934642193512562998908731765730891753067534541454244212480659282459057883549812840892730647101296133227764018036068060016299575114566687819675272090700684430903699595693840037106901634931772185744912063043680984888775010239193523669334436653016327404169936650055214690837298138698176188666836310466023467789314253520520067696142795201419978648825050555278357973948807681418719199534450086998799095519234617252831060628008034950065434426615740516047869086495349671424045415859002287761670107454601946060437353059384503635624001832037220289368892474508894647043503270012558523502659031737411183707488169146908754651637374404616533082019153037229142395207019319412124490641899590325090800385162281767288388400234921212394399802907605781470735793521710138582685165006827356285319623513881519846767754402085214882461680744670157444594628024781990325638883104777528411497931399557947700

CSR{help....heeellllppppp...im blind}

[cry, expl] KRÜPTO

45 solves

Server.java
import java.security.*;
import java.util.Base64;
import java.util.Arrays;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;

class Server {
    // Message that needs to be signed
    static String MSG = "I am allowed to get a flag!";

    public static void main(String[] args) throws Throwable {
        System.out.println("Welc0me to the CSR22 Bug B0untay.");
        System.out.println("We present an extra hardend signature implementation here.");
        System.out.println("If you manage to forge a signature, you'll be rerwarded with a flag!");

        KeyPair keys = KeyPairGenerator.getInstance("EC").generateKeyPair();
        Signature sig = Signature.getInstance("SHA256WithECDSAInP1363Format");
        System.out.printf("My Public Key is:\n %s\n", keys.getPublic());

        sig.initVerify(keys.getPublic());
        sig.update(MSG.getBytes());
        
        System.out.println("Enter signature in Base64:");
        String signatureEncoded = new BufferedReader(new InputStreamReader(System.in)).readLine();

        byte[] decodedBytes = Base64.getDecoder().decode(signatureEncoded);

        if (decodedBytes.length != 64 || Arrays.equals(decodedBytes, new byte[64])) {
            System.out.println("REPELLED ATTACK OF RECENTLY FOUND VULNERABILITY!");
            System.exit(0);
        }
        if (sig.verify(decodedBytes)) {
            // this will never happen
            System.out.println("Congrats!");
            Path filePath = Path.of("/opt/flag.txt");
            String flag = Files.readString(filePath);
            System.out.println(flag);
        } else {
            System.out.println("Signature invalid!");
        }
    }
}

It seems that we need to sign without the private key... so I thought that there exists a CVE. I googled by "java SHA256WithECDSAInP1363Format cve" and then found CVE-2022-21449 (Japanese).

It said that the verification can be passed when r=0r = 0 or s=0s = 0. In this chall, the server returns System.out.println("REPELLED ATTACK OF RECENTLY FOUND VULNERABILITY!") when r=0,s=0r = 0, s = 0. Then I sent r=n,s=0r = n, s = 0 and got a flag.

CSR{FlauschigerKaefer}

[cry, web] FLAGPEDIA

66 solves

app.py
import os
from urllib.parse import parse_qs, urlencode

from flask import Flask, render_template, send_from_directory, make_response, request, flash, redirect, url_for
from werkzeug.security import safe_join
from Crypto.Cipher import AES
from Crypto.Hash import HMAC
from Crypto.Util.Padding import pad, unpad

app = Flask(__name__)
app.secret_key = os.environ["SEC_KEY"]

INFO_DIR_NAME = "infos"
PREMIUM_INFO_DIR_NAME = "premium-infos"
INSTANCE_KEY = os.environ["SEC_KEY"].encode()


def serialize_user(user):
    data = urlencode(user).encode()
    aes = AES.new(INSTANCE_KEY, AES.MODE_CBC)
    ct = aes.encrypt(pad(data, 16))
    # guarantee ciphertext integrity
    mac = HMAC.new(INSTANCE_KEY, ct).digest()
    return (aes.iv + ct + mac).hex()


def deserialize_user(ciphertext):
    ciphertext = bytes.fromhex(ciphertext)
    iv, ct, mac = ciphertext[:16], ciphertext[16:-16], ciphertext[-16:]

    # Check ciphertext integrity
    if not HMAC.new(INSTANCE_KEY, ct).digest() == mac:
        raise ValueError("Ciphertext was manipulated.")

    aes = AES.new(INSTANCE_KEY, AES.MODE_CBC, iv=iv)
    plaintext = unpad(aes.decrypt(ct), 16)
    user_obj_raw = parse_qs(plaintext.decode())
    user_obj = {k: v[0] for k, v in user_obj_raw.items()}

    return user_obj


@app.route('/')
def index():
    countries = os.listdir(INFO_DIR_NAME)
    countries.sort()
    premium_flags = os.listdir(PREMIUM_INFO_DIR_NAME)
    premium_flags.sort()
    resp = make_response(
        render_template("home.html", countries=countries, premium_flags=premium_flags)
    )

    # TODO: implement login for premium members once somebody finally buys premium
    resp.set_cookie("user", serialize_user({"user": "stduser", "role": "pleb"}))

    return resp

@app.route('/premium')
def premium():
    return render_template("premium.html")


@app.route('/info/<country>')
def flag_info(country):
    info_file = safe_join(INFO_DIR_NAME, country)
    if not info_file:
        return "Bad request", 400

    if not os.path.exists(info_file):
        flash("Country does not exist")
        return redirect(url_for("index"))

    info = open(info_file).read()
    return render_template("info.html", country=country, info=info)


@app.route('/premium-info/<country>')
def premium_info(country):
    user_cookie = request.cookies.get('user')

    if not user_cookie:
        flash("Cookie not found!")
        return redirect(url_for("index"))

    try:
        user = deserialize_user(user_cookie)
    except ValueError as ex:
        flash("Inavlid Cookie: " + str(ex))
        return redirect(url_for("index"))

    if user["role"] != "premium":
        flash("You haven't payed for premium!")
        return redirect(url_for("index"))

    info_file = safe_join(PREMIUM_INFO_DIR_NAME, country)
    info = open(info_file).read()

    return render_template("info.html", country=country, info=info)


if __name__ == '__main__':
    app.run()

The authentication is done by AES-CBC.

In deserialize_user, if there are the same key field, first one is used. Therefore, if we can modify user=stduser&role=pleb to role=premium&role=pleb, we can login as a premium user. This can be done by modifying iv like:

new_iv = xor(b"user=stduser&rol", b"role=premium&rol", iv)

Note that user is not used in this app.

solve.py
from binascii import unhexlify

from pwn import xor


cookie = unhexlify("21f8ad560643ccd13f4f093f1d3b868002e0689b7774758b5b3c4c3dae9fcdbc357f787767da1084c82d05cae66725983c09c2b6f509def46a597bd67e6be553")
iv, ct, mac = cookie[:16], cookie[16: -16], cookie[-16:]
new_iv = xor(b"user=stduser&rol", b"role=premium&rol", iv)
print((new_iv + ct + mac).hex())

CSR{ACROSSDRESSINGPIKENAMEDTRISH}