Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add writeups for git_the_flag and ots challenges
- Loading branch information
Showing
2 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
2020-05-10-spam-and-flags-teaser/git_the_flag/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# git the flag (misc, 96 pts, 58 solved) | ||
|
||
This challenge is a CGI server and served its own source code using git. | ||
The code that we're supposed to hack looks like this: | ||
|
||
```bash | ||
#!/bin/bash | ||
set -euo pipefail | ||
source /etc/config.ini | ||
no_cache(){ | ||
echo -ne "Pragma-directive: no-cache\n"; | ||
echo -ne "Cache-directive: no-cache\n"; | ||
echo -ne "Cache-control: no-cache\n"; | ||
echo -ne "Pragma: no-cache\n"; | ||
echo -ne "Expires: 0\n"; | ||
echo -ne "Content-type: text/html\n\n" | ||
} | ||
|
||
success() { | ||
rm -f /tmp/login_session.txt | ||
cp /proc/sys/kernel/random/uuid /tmp/login_session.txt 2>&1 | ||
echo -ne "Status: 302 Moved Temporarily\n" | ||
echo -ne "Set-Cookie: session=$(cat /tmp/login_session.txt)\n" | ||
echo -ne "Location: /cgi-bin/setup.cgi\n\n" | ||
exit | ||
} | ||
|
||
fail() { | ||
echo -ne "Content-type: text/html\n\n" | ||
|
||
echo "<html>" | ||
echo "<head><title>Omegalink login</title>" | ||
echo "<body><center>" | ||
echo "<h1>Login unsuccessful.</h1>" | ||
echo "<h3>Reason: $1</h3>" | ||
echo "<p><a href=/>Click here to try again</a></p>" | ||
echo "<p>This incident will be reported</p>" | ||
echo "</center></body>" | ||
echo "</html>" | ||
exit | ||
} | ||
|
||
parse_query() { | ||
saveIFS=$IFS | ||
IFS='=&' | ||
parm=($QUERY_STRING) | ||
IFS=$saveIFS | ||
|
||
declare -gA query_params | ||
for ((i=0; i<${#parm[@]}; i+=2)) | ||
do | ||
query_params[${parm[i]}]="${parm[i+1]}" | ||
done | ||
} | ||
|
||
check_name_and_password() { | ||
pw_hash=$(echo -n "${query_params[password]}" | md5sum | cut -d ' ' -f 1) | ||
if [[ "${query_params[name]}" != $USERNAME || "$pw_hash" != $PASSWORD_HASH ]]; then | ||
fail "Wrong username or password" | ||
fi | ||
} | ||
|
||
check_remote_ip() { | ||
if [[ ! "$REMOTE_ADDR" =~ $ALLOWED_REMOTES ]]; then | ||
fail "$REMOTE_ADDR is not authorized to enter this site." | ||
fi | ||
} | ||
|
||
parse_query | ||
check_name_and_password | ||
check_remote_ip | ||
success | ||
``` | ||
|
||
There are two checks - check for name and password, and remote_ip. | ||
|
||
The name and password was `admin` and `admin`. The author of this writeup | ||
wasted some time, because the random md5 database he used didn't find it | ||
for some weird reason (even though even google does)... | ||
|
||
Anyway, the second check was harder. It was implemented correctly, so we had to | ||
step back a bit. We remembered, that git clone works via ssh, we had credentials | ||
that authenticated us to the server (to clone the code), and that ssh | ||
allows any authenticated user to create a socks proxy. | ||
|
||
So we quickly create one: | ||
|
||
```bash | ||
$ ssh git@35.234.131.107 -p 22222 -D 9090 "git-receive-pack '/code.git'" | ||
``` | ||
|
||
And then two quick curls (what are webbrowsers for anyway?) are enough ftw: | ||
|
||
``` | ||
curl "http://127.0.0.1/cgi-bin/login.cgi?name=admin&password=admin" -x socks5://127.0.0.1:9090 -vvv | ||
curl "http://127.0.0.1/cgi-bin/setup.cgi" -x socks5://127.0.0.1:9090 -vvv --cookie session=fa3cc064-cf79-4691-a122-9723ae7fc79 | ||
``` | ||
|
||
And the flag is: | ||
|
||
``` | ||
SaF{lmgtfy:"how to serve git over ssh"} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# OTS (misc, 105 pts, 51 solved) | ||
|
||
This challenge is a pretty easy crypto challenge. What makes it surprising is | ||
that it's very similar to WOTS which is a real world signature scheme. | ||
|
||
We get a plaintext (something like `My favorite number is 123123123`) and a | ||
valid signature. Our goal is to forge the signature. | ||
|
||
The code (with my refactoring and some debug prints sprinkled in) looks like this: | ||
|
||
```python | ||
class OTS: | ||
def __init__(self): | ||
self.key_len = 128 | ||
self.priv_key = token_bytes(128 * 16) | ||
self.pub_key = b"".join( | ||
[self.hash_iter(chonk, 255) for chonk in chonks(self.priv_key, 16)] | ||
).hex() | ||
|
||
def hash_iter(self, msg, n): | ||
assert len(msg) == 16 | ||
for i in range(n): | ||
msg = hashlib.md5(msg).digest() | ||
return msg | ||
|
||
def wrap(self, msg): | ||
raw = msg.encode("utf-8") | ||
assert len(raw) <= self.key_len - 16 | ||
raw = raw + b"\x00" * (self.key_len - 16 - len(raw)) | ||
raw = raw + hashlib.md5(raw).digest() | ||
return raw | ||
|
||
def sign(self, msg): | ||
raw = self.wrap(msg) | ||
signature = b"".join( | ||
[ | ||
self.hash_iter(chonk, 255 - raw[i]) | ||
for i, chonk in enumerate(chonks(self.priv_key, 16)) | ||
] | ||
).hex() | ||
self.verify(msg, signature) | ||
return signature | ||
|
||
def verify(self, msg, signature): | ||
raw = self.wrap(msg) | ||
print(raw) | ||
signature = bytes.fromhex(signature) | ||
assert len(signature) == self.key_len * 16 | ||
calc_pub_key = b"".join( | ||
[ | ||
self.hash_iter(chonk, raw[i]) | ||
for i, chonk in enumerate(chonks(signature, 16)) | ||
] | ||
).hex() | ||
|
||
print("===") | ||
for r, a, b in zip(raw, chonks(self.pub_key, 32), chonks(calc_pub_key, 32)): | ||
print(a, b, chr(r if 32 < r < 128 else 0x20), a == b) | ||
|
||
assert hmac.compare_digest(self.pub_key, calc_pub_key) | ||
``` | ||
|
||
Basically, every byte B is signed by hashing privkey 255-X times, and verified by | ||
hashing the result of the above X times and comparing it with privkey hashed 255 | ||
times (aka the pubkey). | ||
|
||
After understanding what's going on, we immediately notice that we can decrease | ||
any byte in the message and still forge a valid signature (by computing a | ||
hash once). So we can take a valid signature for `My favorite number is 1299072346121938061` | ||
and change it for a signature for `My faflagte number is 1299072346121938061`. | ||
|
||
The only problem is the checksum - there is a md5 sum at the end of the input | ||
that must match the data. We bruteforced the number at the end, so that | ||
every byte of the new md5 will be smaller than the old one, and solved the | ||
challenge. | ||
|
||
Core of the exploit looks like this: | ||
|
||
```python | ||
|
||
def fixup(sign, n, fromwhat, towhat): | ||
frag = chonks(sign, 32) | ||
for i, c in enumerate(towhat): | ||
off = n + i | ||
for _ in range(fromwhat[i] - c): | ||
frag[off] = hashlib.md5(bytes.fromhex(frag[off])).hexdigest() | ||
return "".join(frag) | ||
|
||
|
||
def wrap(raw): | ||
raw = raw + b"\x00" * (128 - 16 - len(raw)) | ||
return hashlib.md5(raw).digest() | ||
|
||
origmd5 = wrap(msg) | ||
podpis = fixup(podpis, 5, b"vori", b"flag") | ||
msg = msg[:5] + b"flag" + msg[9:] | ||
|
||
N = 6 | ||
for rndchrs in itertools.product(string.ascii_uppercase, repeat=N): | ||
rndfrag = ''.join(rndchrs).encode() | ||
msg = msg[:12] + rndfrag + msg[18:] | ||
|
||
newmd5 = wrap(msg) | ||
for a, b in zip(origmd5, newmd5): | ||
if a < b: | ||
break | ||
else: | ||
break | ||
|
||
podpis = fixup(podpis, 12, b"number", rndfrag) | ||
podpis = fixup(podpis, 112, origmd5, newmd5) | ||
``` | ||
|
||
It's unnecessarily complicated, but still got the job done. | ||
|
||
``` | ||
SaF{better_stick_with_WOTS+} | ||
``` |