Skip to content

Commit

Permalink
Add writeups for git_the_flag and ots challenges
Browse files Browse the repository at this point in the history
  • Loading branch information
msm-code committed May 16, 2020
1 parent 33abcd9 commit 27c789d
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 0 deletions.
103 changes: 103 additions & 0 deletions 2020-05-10-spam-and-flags-teaser/git_the_flag/README.md
@@ -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"}
```
118 changes: 118 additions & 0 deletions 2020-05-10-spam-and-flags-teaser/ots/README.md
@@ -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+}
```

0 comments on commit 27c789d

Please sign in to comment.