Man in the Middle communication between the client and the server. \$ nc mitm.ctfcompetition.com 1337

This problem gave us a challenge.py script, which is run at the address and port above. According to the script, we’d be asked whether we would like to talk to the server or client. The server/client then attempts to initiate a handshake with us, thinking that we’re the other.

def Handshake(password, reader, writer):
myPrivateKey = Private()
myNonce = os.urandom(32)

WriteBin(writer, myPrivateKey.get_public().serialize())
WriteBin(writer, myNonce)

if myNonce == theirNonce:
return None
if theirPublicKey in (b'\x00'*32, b'\x01' + (b'\x00' * 31)):
return None

theirPublicKey = Public(theirPublicKey)

sharedKey = myPrivateKey.get_shared_key(theirPublicKey)
myProof = ComputeProof(sharedKey, theirNonce + password)

WriteBin(writer, myProof)

if not VerifyProof(sharedKey, myNonce + password, theirProof):
return None

return sharedKey


The server/client does the following in the handshake:

1. Generate a private key and random nonce
2. Send out its public key and nonce
3. Receive the other’s public key and nonce
4. Check and fail if their nonces are the same or the public key is one of two known weak keys
5. Calculate the shared key from the other’s public key
6. Computer and send proof using the other’s nonce and password
7. Receive the other’s proof and verify it

Once the two have each verified the other’s proof, the handshake is complete. At this point, the server has authenticated the other and is ready to receive commands.

The idea here is that we can masquerade as the “real” client to the server and use the getflag command. However, there were two things to look out for. One is that the server and client both computed and verified proofs by adding their password to it. We don’t have that password, so it means that we must rely on the client to compute the correct proof for us (which we can then pass to the server). The second problem is that giving just any public key to both the server and client could result in a large set (or order) of possible shared keys, since we don’t know their private keys.

Thankfully, there are several known weak keys for Curve25519. The server fails the handshake if the public key it receives is one of two known weak keys – 0 and 1. The list of weak keys for Curve25519 thankfully contained more than just 0 and 1, and we could use one of the other known weak keys, p+1, since (p+1) mod p is 1. p is the prime number used for Curve25519, 2^255-19.

During the handshake, we’ll pretend to be the client to the server and vice versa. We’ll make both think that the public key the other is using is the weak key we picked. Since (p+1) has an order of 1, that means there’ll be a single possible shared key regardless of the private keys used, making it very easy for us to find it. All we must do is create any private key and use our weak key to get the shared key.

Once the server has authenticated us, it thinks we are the client and will run commands we send it. We can send the getflag command to receive the flag:

🏁 CTF{kae3eebav8Ac7Mi0RKgh6eeLisuut9oP} 🏁

My solve script is below and can also be downloaded here. The curve25519 and pynacl repos were used in the solve script.

from pwn import *
from curve25519 import Private,Public
import nacl.secret
from binascii import hexlify, unhexlify
import struct
import sys

# curve25519: https://github.com/agl/curve25519-donna
# nacl: https://github.com/pyca/pynacl

# ===== HELPER =====
# Little endian conversion in python2
def to_bytes(n):
out = ''
for i in xrange(32):
n,c = divmod(n, 256)
out += chr(c)
return out

return unhexlify(remote.recvline().strip())

# Mirror WriteBin() from challenge.py
def WriteBin(remote, data):
remote.sendline(hexlify(data))
# ==================

# This is the prime used for Curve25519 (via https://en.wikipedia.org/wiki/Curve25519)
PRIME = 2**255-19

# Note: You can run client/server locally for testing with cmds below.
#   Also remember to create "password.txt" and "flag.txt" files.
#
#   ncat -lk -p 13706 -e challenge.py
#   ncat -lk -p 13707 -e challenge.py
#s = remote('127.0.0.1', 13706)					# Local server
#c = remote('127.0.0.1', 13707)					# Local client
s = remote('mitm.ctfcompetition.com',1337)  			# Challenge server
c = remote('mitm.ctfcompetition.com',1337)  			# Challenge client

# Select server/client respectively
s.sendline('s')							# [Server] Talk to server
c.sendline('c')							# [Client] Talk to client

# ===== HANDSHAKE =====
# Ensure that the server and client pass the handshake

# Get public keys and nonces
s_publickey = ReadBin(s)					# [Server] Public key
s_nonce = ReadBin(s)						# [Server] Nonce

c_publickey = ReadBin(c)					# [Client] Public key
c_nonce = ReadBin(c)						# [Client] Nonce

# Generate a weak key that's not filtered out by challenge.py
#   via https://gist.github.com/jedisct1/39f8dee6e38b12bb34f5
# Picked (p+1) for its small order
weak_key = to_bytes(PRIME+1)

# Give server and client the weak key & each others' nonces
WriteBin(s, weak_key)					# [Server] Give weak key
WriteBin(s, c_nonce)					# [Server] Give client nonce
WriteBin(c, weak_key)					# [Client] Give weak key
WriteBin(c, s_nonce)					# [Client] Give server nonce

# Receive the server and client proofs
s_proof = ReadBin(s)					# [Server] Get proof
c_proof = ReadBin(c)					# [Client] Get proof

# Send the server and client proofs to each other
WriteBin(s, c_proof)					# [Server] Give client proof
WriteBin(c, s_proof)					# [Client] Give server proof

# ===== CONFIRM AUTHENTICATED =====
# Server will give AUTHENTICATED status

s_status = ReadBin(s)					# [Server] Get AUTHENTICATED

# ===== MAKE SHARED KEY =====
# Generate the shared key in the same way the client/server would do so in
#   challenge.py, using our weak public key and a generated private key.

myPrivateKey = Private()
sharedKey = myPrivateKey.get_shared_key(Public(weak_key))
mySecretBox = nacl.secret.SecretBox(sharedKey)

print "[Server] Status: " + mySecretBox.decrypt(s_status)

# ===== SEND CMDS TO SERVER =====
# At this point, the client-server handshake succeeded so the server believes
#   it is connected to the client. We can ignore the client and send data to
#   the server as if we're the client using our private key.

WriteBin(s, mySecretBox.encrypt(b"getflag"))			# Get flage
print "[Flag]: " + mySecretBox.decrypt(ReadBin(s))		# Print flage