None
Pillow
SecuInside - crypto - 200

Challenge Text

Before sleeping, wooyaggo always read weird articles from internet. All you got is some binaries(not perfect) and network traffic from following command.

wooyaggo@genius_anncc:~$ python pillow_reader.pyc 10.0.8.24 5151 "secret" [AdminKey]

Follow instructions in the "secret" to get a flag.

Challenge code available here.

Opening up the pcap shows a partially encrypted conversation:

Welcome to Pillow Reader. Here are some articles from online.
0x5cb94b9d25716136c6b880bf791f743ecfdf5a383b3aa27093c1673599cbd8a1c160e4a06f777c5b163f0fec50405944d9764bed8c7dd9eb19abc1225b322ed
0x86e05276d3c364cd6157e23cd972e0a662dff9ebf9ade83eae022c292c767bcd6cf528c7f6ae98af2f7811d99c9e45e7e4e6f1b9841aabf89a84dd0673099b61
binascii.unhexlify('1f59856dbbbe90995cf94975afd94dcf46bf2d16d8b8a3754685d3eb3765d6918462378a2f7e948182e84a857a25be057a122e9d65e32fb01a984f442e781fa02e8a4d2728b1a4c3ea3905aa95ab9fba7b7dfa3b39116d49c10d161725aa0beeac18b7a2a9c9d55a0bf6f1a2b2ca903eb15086995b2f77d77d3f01678708ee60')
binascii.unhexlify('1b40e26d29d5e296c23591064d98817ecc81ccae71eb8f3bcd1a5874e25f333e42dbeb4f1d8ecb94697b641e4a8e8037c14d24c0fcb52b0ed4694b23395efc4ca93c9ccb97ccfbb56a3ed931f9d269cb2494df411122bb99b42fc5393fba1140916946c2b381f4bf5677f23b630ef9a5e5915029c7124e683a71f0edea149')

Not too helpful yet; let's examine the source code. Oh... compiled python. Let's import it!

>>> import pillow_reader
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: bad magic number in 'pillow_reader': b'l\x0c\r\n'

Boo. Fortunately, the code is (mostly) decompilable and Decompyle++ did a pretty good job at it (just a couple touch ups required). pillow_talk.py shows how to talk with the server, but nothing unexpected. PKI.py reveals the cryptosystem though:

def gen_keypair(bits = 512):
    p = PrimeUtils.get_prime(bits // 2)
    q = PrimeUtils.get_prime(bits // 2)
    n = p * q
    return (PrivKey(p, q, n), PubKey(n))

def encrypt(self, plain):
    while True:
        r = PrimeUtils.get_prime(int(round(math.log(self.pubkey.n, 2))))
        if r &gt; 0 and r &lt; self.pubkey.n:
            cipher = pow(self.pubkey.g, plain, self.pubkey.n_sq) * pow(r, self.pubkey.n, self.pubkey.n_sq) % self.pubkey.n_sq
            return cipher

def decrypt(self, cipher):
    if not self.privkey is not None:
        raise AssertionError('Private key must be exist')
    if not self.pubkey is not None:
        raise AssertionError('Public key must be exist')
    pubkey = self.pubkey
    privkey = self.privkey
    plain = privkey.m * ((pow(cipher, privkey.l, pubkey.n_sq) - 1) // pubkey.n) % pubkey.n
    return plain

This is a Paillier Cryptosystem. Unfortunately, no obvious vulnerabilities stand out in the implementation, but homomorphic encryption is rare enough to raise eyebrows. We now have enough source code to attempt a connection:

$ python pillow_reader.py 219.240.37.153 5151 secret
Welcome to Pillow Reader. Here are some articles from online.
Read [secret]
to get flag, just concat flag1 and flag2.
$ python pillow_reader.py 219.240.37.153 5151 flag1
Welcome to Pillow Reader. Here are some articles from online.
Read [flag1]
Permission denied
your account is guest

So we need to escalate privileges and we don't know the password to do so. The trick is that the server always uses the same key and and, while we don't know the password, remember that we do have an encrypted request from a (presumably) privileged account in the pcap. Normally, we couldn't modify the request without rendering the decrypted version nonsense, but homomorphic encryption allows additive changes to survive decryption! Specifically, E(p0) * E(p1) = E(p0 + p1 mod m^2).

>>> raw_cmd(solve.i2s(solve.s2i(binascii.unhexlify(encrypted_blob))*solve.g_pki_server.encrypt(0)))
Read [secret]
to get flag, just concat flag1 and flag2.
>>> raw_cmd(solve.i2s(solve.s2i(binascii.unhexlify(encrypted_blob))*solve.g_pki_server.encrypt(10)))
Read [secret^T]
File secret^T not exists
>>> raw_cmd(solve.i2s(solve.s2i(binascii.unhexlify(encrypted_blob))*solve.g_pki_server.encrypt((256-ord('e')+ord(' '))*256**5)))
Read [fecret]
File fecret not exists

Progress! Now it's really just a matter of brute forcing the correct offset (or solving it intelligently - I went for the former):

>>> s=raw((256-ord('e')+ord(' '))*256**5+(ord('f')-ord('c')-1)*256**4+(256-ord('r')+ord('l')-1)*256**3+(256-ord('e')+ord('a')-1)*256**2+(256-ord('t')+ord('g'))*256+(ord('1')+ord('1')-ord(';')))
Read [flag1]
The flag is "7h053 wh0 bu1ld b3n347h
>>> s=raw((256-ord('e')+ord(' '))*256**5+(ord('f')-ord('c')-1)*256**4+(256-ord('r')+ord('l')-1)*256**3+(256-ord('e')+ord('a')-1)*256**2+(256-ord('t')+ord('g'))*256+(ord('1')+ord('2')-ord(';')))
Read [flag2]
 7h3 574r5 bu1ld 700 l0w."(without quote)

Flag: 7h053 wh0 bu1ld b3n347h 7h3 574r5 bu1ld 700 l0w.


Code available on Github

- Kelson (kelson@shysecurity.com)