I was taking part in the OverTheWire Advent Bonanza 2019. All in all, the CTF was very pleasant because the challenges were interesting to solve, there was quite some time available as well (I spent about two weeks on and off on it) and I also managed to solve a few challenges as well.

The following write-up is about the challenge from day 19: santa's signature

Description

Can you forge Santa's signature?

Along with the IP address/port there was also the Python code running on the server given which made the challenge even easier to solve.

#!/usr/bin/env python3
import Crypto
from Crypto.PublicKey import RSA
import sys

try:
    with open("key",'r') as f:
        key = RSA.importKey(f.read())
except:
    rng = Crypto.Random.new().read
    key = RSA.generate(4096, rng)
    with open("key",'w') as f:
        f.write(key.exportKey().decode("utf-8"))

def h2i(h):
    try:
        return int(h,16)
    except Exception:
        print("Couldn't hex decode",flush=True)
        sys.exit()

header = \
"""Dear Santa,
Last christmas you gave me your public key,
to confirm it really is you please sign three
different messages with your private key.

Here is the public key you gave me:"""
print(header,flush=True)
print(key.publickey().exportKey().decode("utf-8"),flush=True)
ms = []

for i in range(1,4):
    m = h2i(input("Message %d you signed (hex encoded):" % i))
    if m in ms:
        print("I said different messages!",flush=True)
        sys.exit()
    s = [h2i(input("Signature %d:" % i))]
    if not key.verify(m,s):
        print("Looks like you aren't Santa after all!",flush=True)
        sys.exit()
    ms.append(m)

print("Hello Santa, here is your flag:",flush=True)
with open("flag",'r') as flag:
    print(flag.read(),flush=True)

The public key that is presented by the server is as follows:

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu1jb39GZlWh9XPpOHmaa
ZEYrbNqa6KMf4E211NYklZytuRBQy71zOI8V9O7i12C4hjhoAzj8JnXkpFur7w3z
PLimVB/B+KfQq3fo5YqWzVhi06LuuCvyGwkWSO3K2sMyH3ISKlPKlyVzhr/9qeHR
2gbFXK6so8rHXpZGgSJk5TimuY4yb+TNjpfi4srQIyepVPCjECs4n+Ii941c+7KW
2wScAUk7MuMExuKuNvvKeTZdhQeq/ZCd0otascBXk9GmsBx0eVBzG8/94Hm+9egl
eQu1DqLn/HZovaAcyIbqjunuB/KoM76DISjhmcaRyipafEJm+u9/jPHAG+8dLUuc
Wr+04D9iAFBEt5XBA2u3WaaL4/eP7hR5mR9QOxH8YilpttfQJY/78AXo+GJtECTF
LJ7zRyP1Jq89qdySJVumxwZmKP3sE7mojTb2030TDF/27v+vMVVtczyAQdybDHzj
8ainHn2SP3yTTOnjNTuGWvIcs3Qa4bv78ezTmLofpsZLoRbcx5FV2YXuiao8ezD/
WBuIOlDZhqRiods3LN8x7gNQo8zDmwY0Z54oac2dPgIUr1AvnDbEGdqyCyJRIrnW
kLFMXdy2GJLSFMk+rswORKEtojCmqQIydW7+5M4J4FhVNyVtNuPcLfUjF+e5+V5E
+piEcAsCnQ1k9QHGZAuVxX8CAwEAAQ==
-----END PUBLIC KEY-----

Analysis

My first guess for this challenge was that this involves some "RSA exponent three attack" (https://www.johndcook.com/blog/2019/03/06/rsa-exponent-3/) but in fact it was actually even easier. RSA is prone to a lot of attacks if implemented and used "raw", i. e. textbook style. The crypto library used is https://github.com/dlitz/pycrypto, so I downloaded that library and looked at the verify function.

def verify(self, M, signature):
   """Verify the validity of an RSA signature.
   :attention: this function performs the plain, primitive RSA encryption
    (*textbook*). In real applications, you always need to use proper
    cryptographic padding, and you should not directly verify data with
    this method. Failure to do so may lead to security vulnerabilities.
    It is recommended to use modules
    `Crypto.Signature.PKCS1_PSS` or `Crypto.Signature.PKCS1_v1_5` instead.
    """

The function description confirms the suspicion that it is vulnerable which means that we are on the right track.

Verification in case of RSA works as follows:

If s^e mod n is equal to m mod n then the signature is valid (s is the signature, e is the public exponent, n is is the public modulus).

Solution

It is obvious that the equation is true for s = m = 0 and s = m = 1. However, we have to give three forged signatures and messages. But there is another special case s = -1 because the (-1)^e = -1 if e is odd. Since e = 65537, this is the case. However, the server does not accept -1 as a message:

Message 1 you signed (hex encoded):-1
Signature 1:-1
Looks like you aren't Santa after all!

This can be solved by calculating -1 mod n so that the message is positive. This can be calculated easily with a bit of Python:

>>> hex(pow(-1, e, n))
bb58dbdfd19995687d5cfa4e1e669a64462b6cda9ae8a31fe04db5d4d624959cadb91050cbbd73388f15f4eee2d760b88638680338fc2675e4a45babef0df33cb8a6541fc1f8a7d0ab77e8e58a96cd5862d3a2eeb82bf21b091648edcadac3321f72122a53ca97257386bffda9e1d1da06c55caeaca3cac75e9646812264e538a6b98e326fe4cd8e97e2e2cad02327a954f0a3102b389fe222f78d5cfbb296db049c01493b32e304c6e2ae36fbca79365d8507aafd909dd28b5ab1c05793d1a6b01c747950731bcffde079bef5e825790bb50ea2e7fc7668bda01cc886ea8ee9ee07f2a833be832128e199c691ca2a5a7c4266faef7f8cf1c01bef1d2d4b9c5abfb4e03f62005044b795c1036bb759a68be3f78fee1479991f503b11fc622969b6d7d0258ffbf005e8f8626d1024c52c9ef34723f526af3da9dc92255ba6c7066628fdec13b9a88d36f6d37d130c5ff6eeffaf31556d733c8041dc9b0c7ce3f1a8a71e7d923f7c934ce9e3353b865af21cb3741ae1bbfbf1ecd398ba1fa6c64ba116dcc79155d985ee89aa3c7b30ff581b883a50d986a462a1db372cdf31ee0350a3ccc39b0634679e2869cd9d3e0214af502f9c36c419dab20b225122b9d690b14c5ddcb61892d214c93eaecc0e44a12da230a6a90232756efee4ce09e0585537256d36e3dc2df52317e7b9f95e44fa9884700b029d0d64f501c6640b95c57e

Now that we have all three messages and signatures, the server gives us the flag: AOTW{RSA_3dg3_c4s3s_ftw}