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}