ECB Chosen Plaintext Attack (ABCTF2016 – Encryption Service)

While reading a blog post last week, I decided to implement an ECB chosen plaintext attack.

This was also one of the challenges during ABCTF this year, so more on that below.

The theory of the attack is that if the target is using an ECB cipher mode with a secret, and you have control over the plaintext, then you can determine the "secret" being used (similar to a salt).

From an attacking perspective, this would be useful if your session cookie (or something similar) used this encrypted value for authentication or authorization.

In this case, you would just need to determine the secret being used. Once you have that, you set the cookie's value to the encrypted value of "admin" + "secret" + padding (where necessary), or the system you are attacking's equivalent.

So, first of all, I set out to build a server similar to c0nrad's that would take user input, encrypt the plaintext using AES mode ECB, and return to the user the encrypted (and encoded) output.

The commented out "prepend" section will actually be used later for the ABCTF section, but I wanted to include it as this was the final code for my test server.

#!/usr/bin/python

from Crypto.Cipher import AES
import socket
import sys
import random
import string

blockSize = 16
encKey = "ENCRYPTIONKEY123"
secret = "mys3cretP@ssword!"
prepend = ""
#prepend = "ENCRYPT:"
chars = string.ascii_letters + string.digits + string.punctuation
secret = ''.join(random.choice(chars) for _ in range(random.randint(1,1000)))

def pad(input):
	if (len(input) % blockSize == 0):
		return input
	else:
		extra = blockSize - (len(input) % blockSize)
		output = input + "\x00" * extra
		return output

def unpad(input):
	return input.rstrip("\x00")

def encrypt(input):
	if (input is None) or (len(input) == 0):
		print "Input text cannot be null or empty"

	toEncrypt = prepend + input + secret
	toEncrypt = pad(toEncrypt)
	cipher = AES.AESCipher(encKey, AES.MODE_ECB)
	cipherText = cipher.encrypt(toEncrypt)
	return cipherText.encode("hex")

def decrypt(input):
	if (input is None) or (len(input) == 0):
		print "Input text cannot be null or empty"

	encrypted = input.decode("hex")
	cipher = AES.AESCipher(encKey, AES.MODE_ECB)
	plainText = unpad(cipher.decrypt(encrypted))
	return plainText

def main():
	print "SECRET LENGTH: " + str(len(secret))
	print "SECRET = " + secret

	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

	server_address = ('localhost', 10000)
	print "\nStarting up on %s port %s" % server_address
	s.bind(server_address)

	s.listen(1)

	while True:
	   	connection, client_address = s.accept()

	   	try:
	   		while True:
	   			data = connection.recv(2048)
	   			if data:
	   				#print "DATA: " + data
	   				input = data.rstrip()
	   				print "INPUT: " + input
	   				print "HEX: " + input.encode("hex")
	   				encrypted = encrypt(input)
	   				print "ENCRYPTED: " + encrypted
	   				connection.send(encrypted)
	   			else:
	   				break
	   	finally:
	   		connection.close()
    
if __name__ == "__main__":
    main()

To verify that my server worked, I just needed to telnet to port 10000 and send a message.

root@kali:~/ecb# telnet localhost 10000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MY SECRET MESSAGE
93ddd26ad133d086d16df74ef2ba96417c936e46ec4cbe142d6e69b88b4d92e515e324e836c5c43065ba47ea200d046d8c88a62410ff28ed32440c8e6c52b7cabdcac9db9db7351762e5329d2539b69e3c6ff2e87de7cb2a8b0a197d12fd1ca11e4db1cf177e90522ac7aa858882b701458849fe88bf896a798f706727982a69a42445fca22d70fc0b3f4926cdcd2ffab91facea2129e8a0d6415463d8e64b5b248413efa6b666baef257e3389e4ae3e10d2b20c53d468d7af57a9beda526eed243ac36b77c34b547df8c6c8f9f90c601b309e218639c1fac8159da652f1a4b2468dfb04804aa3a2e5bd555b9b03e539f8cad7e8e4ac4c3e907eb03f959a46e5dfbd46012044f41616d81e6bf36218c1d1863360f2ef1eb445c23d8fb155b5e22b5a0ff069bb3a5305628d90789af490758ef669f0bba00f5e3b997496337f2295f62ace663fb01b99ad94226a9991fa525910a6ba65728367f2e0f3253dc8fd7594afa25c7157f8a92281222d45799406809e0b8a1c69118e4a60c3902e8fed1a529c737f6fdc24241dd901112c8e9f5b76901d5fafc99fff7a6bd3eaf901872bdf4f522ab8991e8be973f64756489dba0f4d26c1a11d11a925115ec9779b8c14607081ec3aa44e4c4a6b231b3fc14eb1eb1eaaa42e1b59ef259b79139d3c6808889243ef70245deab1899362e155d65a4030a17250d6e9ad11c3dba7f01de01c4b28ee50f20d9903040a130eb9e7013d1921b23c1d6e5e359cc54225eb68b61d4c2b913e9ae600a172c5df936f5a0b56981edf5ebbc75cac39ad1d34e02052ac8dd48336f8219641b4df30e122e298949e5921f34f4ce86b8db46f2cf34de17f8ab5e0f960374620c79d7236ee7ac2b412155c4bfc5721484608e16c28765d8a2e61121c1a3ce8b4235853a6b38f9f620ea1e081c2bc64d4437f394e394cbfc6f9e5a4e4b97b8b1ae9f6ddfccace376b8d492aa88026e34c962d4d79d14c4778a1f4889953796b40d44db9c1215a2f

On the server side of things I saw my connection, my input, as well as the encrypted output.

root@kali:~/ecb# python ecbServer.py 
SECRET LENGTH: 699
SECRET = 7o|&@8Sj:D-ayGW%YA2&lk1+XeZro.@1vjdT#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)z@|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjI

Starting up on localhost port 10000
INPUT: MY SECRET MESSAGE
HEX: 4d5920534543524554204d455353414745
ENCRYPTED: 93ddd26ad133d086d16df74ef2ba96417c936e46ec4cbe142d6e69b88b4d92e515e324e836c5c43065ba47ea200d046d8c88a62410ff28ed32440c8e6c52b7cabdcac9db9db7351762e5329d2539b69e3c6ff2e87de7cb2a8b0a197d12fd1ca11e4db1cf177e90522ac7aa858882b701458849fe88bf896a798f706727982a69a42445fca22d70fc0b3f4926cdcd2ffab91facea2129e8a0d6415463d8e64b5b248413efa6b666baef257e3389e4ae3e10d2b20c53d468d7af57a9beda526eed243ac36b77c34b547df8c6c8f9f90c601b309e218639c1fac8159da652f1a4b2468dfb04804aa3a2e5bd555b9b03e539f8cad7e8e4ac4c3e907eb03f959a46e5dfbd46012044f41616d81e6bf36218c1d1863360f2ef1eb445c23d8fb155b5e22b5a0ff069bb3a5305628d90789af490758ef669f0bba00f5e3b997496337f2295f62ace663fb01b99ad94226a9991fa525910a6ba65728367f2e0f3253dc8fd7594afa25c7157f8a92281222d45799406809e0b8a1c69118e4a60c3902e8fed1a529c737f6fdc24241dd901112c8e9f5b76901d5fafc99fff7a6bd3eaf901872bdf4f522ab8991e8be973f64756489dba0f4d26c1a11d11a925115ec9779b8c14607081ec3aa44e4c4a6b231b3fc14eb1eb1eaaa42e1b59ef259b79139d3c6808889243ef70245deab1899362e155d65a4030a17250d6e9ad11c3dba7f01de01c4b28ee50f20d9903040a130eb9e7013d1921b23c1d6e5e359cc54225eb68b61d4c2b913e9ae600a172c5df936f5a0b56981edf5ebbc75cac39ad1d34e02052ac8dd48336f8219641b4df30e122e298949e5921f34f4ce86b8db46f2cf34de17f8ab5e0f960374620c79d7236ee7ac2b412155c4bfc5721484608e16c28765d8a2e61121c1a3ce8b4235853a6b38f9f620ea1e081c2bc64d4437f394e394cbfc6f9e5a4e4b97b8b1ae9f6ddfccace376b8d492aa88026e34c962d4d79d14c4778a1f4889953796b40d44db9c1215a2f

With the server in place, it was time to write my attacking script.

This script will connect to the server, calculate the length of the secret, and go about brute forcing it. For more specifics about the attack, please see the blog linked above.

#!/usr/bin/python

import math
import socket
import sys

def chunkstring(string, length):
    return (string[0+i:length+i] for i in range(0, len(string), length))

def roundup(x, base=10):
    return int(math.ceil(x / (base + 0.0))) * base

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_address = ('localhost', 10000)
s.connect(server_address)

try:
    found = False
    secret = ""

    secretLen = 0
    #prependChars = "ENCRYPT:"
    prependChars = ""

    message = "A"
    s.sendall(message)
    data = s.recv(2048)
    output = list(chunkstring(data, 32))
    initialLen = len(output)

    curLen = 0

    while (curLen <= initialLen):
        message += "A"
        s.sendall(message)
        data = s.recv(2048)
        output = list(chunkstring(data, 32))
        curLen = len(output)

    extra = len(message) - 1

    secretLen = ((curLen - 1) * 16) - extra - len(prependChars)

    print "SECRETLEN: " + str(secretLen)

    while not found:
        initialBlock = "A" * (16 - len(prependChars))
        fullLen = roundup(secretLen, 16)
        prepend = "B" * (fullLen - len(secret) - 1)
        message1 = initialBlock + prepend

        s.sendall(message1)
        data = s.recv(8192)
        initialReturn = list(chunkstring(data, 32))
        #print "INITIAL: " + str(initialReturn)

        for i in range(33, 127):
            message2 = message1 + secret + chr(i)
            s.sendall(message2)
            data = s.recv(8192)
            oracle = list(chunkstring(data, 32))
            #print "ORACLE: " + str(oracle)
            compareBlock = (len(prependChars + message2) / 16) - 1
            #print "COMPARE = " + str(compareBlock)
            if oracle[compareBlock] == initialReturn[compareBlock]:
                secret += chr(i)
                #print "LENGTH: " + str(len(secret))
                #print "SECRET: " + secret
                #print "INITIAL: " + str(initialReturn)
                #print "ORACLE: " + str(oracle)
                if len(secret) == secretLen:
                    found = True
                    print secret
                break
    
finally:
    s.close()

With my attack script written, it was time to point it at my server. Within just a few seconds it had already brute forced my random 699 character secret!

root@kali:~/ecb# python ecbAttack.py 
SECRETLEN: 699
7o|&@8Sj:D-ayGW%YA2&lk1+XeZro.@1vjdT#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)z@|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjI

On the server side of things, I could see the brute force and subsequent ciphertext calculations in progress.

< ... snip ... >
INPUT: AAAAAAAAAAAAAAAABBBBB7o|&@8Sj:D-ayGW%YA2&lk1+XeZro.@1vjdT#'/S)u\Y(B%c6+(/X=0V9I_3WES76#F`-[*1^D-r9+$|$AF;h9`)z@|{cZMI7t0^&]2;%cdckdN{nf{I~5i&3p:BR;87Hu;?}0UU}hV1SQC"|{wTU7RZC\Filc9SS0"^'nY5)U77J\>~_)}F~BcQ90Lus*[o22GwwJA9!5kcd~I7[U*N{ht*+RjkVFwRPX2g2QpE;8k$+'#H'F2h|aO?\(7mj-am/YM}]MliDI5~K$3)49z57r>thsUV"sR|)d/qyGs&'"hCD3#b}X%DNt5A'vJ"S#v7ZiO0HxDjHF^XiX{;H64IFYKVt24~y[bkh5c=96A~r}q=6wyENj!WGE(\5I`+D%W/WYU4g#yQf*|vf%4JFgG4-4HkZSL?fQpE?<^q4|,'UkBX9^UaD'n}*6wj.Q[)Sm[y)=JR^%1g4|\m=Xfi1u:U%$"w\JTL/S5)goXwHFpbZiEjA
HEX: 414141414141414141414141414141414242424242376f7c264038536a3a442d6179475725594132266c6b312b58655a726f2e4031766a645423272f3c3f51587b7a765026507d7e6d564067353040367e22554f7e7a24506c7e397b2629256142363b464b3c5c5824636c2c315b4056735d702a3063597067252e4f3f2b4630732662314232492b674537244f6a34554c275c2e68682b233d29602b473c6a50317061217a7a43574443445b21276b7e38605b3f5d352c312569266130746e706c2f27364b274e38693361772f3a656c66257e4055532a5737613b28713e5329755c5928422563362b3c2f4277274c33702a744b3f3d4c403e282f583d305639495f3357455337362346602d5b2a315e442d72392b247c2441463b683960297a407c7b635a4d493774305e265d323b256364636b644e7b6e667b497e35692633703a42523b383748753b3f7d3055557d685631535143227c7b77545537525a435c46696c6339535330225e276e5935295537374a5c3e7e5f297d467e42635139304c75732a5b6f32324777774a413921356b63647e49375b552a4e7b68742a2b526a6b564677525058323c743c4c6c6f3c4c7a64512759714f295c683d795075454e45494d3e67325170453b386b242b272348274632687c614f3f5c28376d6a2d616d2f594d7d5d4d6c694449357e4b24332934397a3537723e74687355562273527c29642f717947732627226843443323627d5825444e74354127764a22532376375a694f304878446a48465e5869587b3b4836344946594b567432347e795b626b6835633d3936417e727d713d367779454e6a21574745285c3549602b4425572f5759553467237951662a7c766625344a466747342d34486b5a534c3f665170453f3c5e71347c2c27556b4258395e556144276e7d2a36776a2e515b29536d5b79293d4a525e253167347c5c6d3d58666931753a55252422775c4a544c2f533529676f5877484670625a69456a41
ENCRYPTED: 86dd17028adabcea56758942b23c112e339b524f7ff0165baab9057b35d4cad049ffe7fc6eb4b58f079c8b59375b8b4538f5be89f60868ad6ce77baa6abad0bb04f8c81162be5e0a1184baa4e2a35517bce436cef1899a00ebafd8d6557d544e130ecf6f814587c52597a17d958f2474428045341152f0221db9e9fe63b2f9ae4f9d8b927dcf0190d6b6417c18daf286bd3adce952277b29dc1160b57f235064a503f3a5e960d168d35cdafcda722f63f54ae5ff4e750c9567dbe89e97051bf11741e873f96b746a0a33d4495b3ee7fbfcaf3514f917c3a8d7d53ea1b9b156c4506f37e610489271c9eaafacb8aab667f7a58a489ca7783c0833299a76943fb4d555e3bc8c21d709287b940e36c490c1ec3c55d492835b75c7f52e4251da9d1376b4ed313681e6a2b291b3c7e6cf71ead956bc7a48e9a2a8a49cb177a0677b8fe9bd960dce291c9180570382be8b88dc64d742fd0504cf570e439da27131ab20a518a1b74f00b971182d1de8f43fbe1470fec2d6fcb439131c53ed4751629c300cc07ded37b5a729bb68df1c7e3382bfe97d2ec9a7b3e82f2f3c0740d45944119f68b8c52c6242f0236c0c97717423470f035e681be08fef53abfa0687f252737a12d06ca5f01dac314494bd7878b8b75bf0bd86cdb4d64441e36277e60d384db0f2af6281f4699f08f906a50d6a6bbe693ee3149dc01b72c63d421b009d6d7566dd5b3901c0d75c7026fd1cf591c4c1f12fd0c84bc96032d65aa974d63d7812d6ada3e096b42fa24a465f700e43340675118afac4c18f7510a6fbeac6023c99478d2abdc6493a9542ce88b963021ac210df4906ef80cabcd41cf904f7d85cc29573c05ddd2def0d42eba7759546eece9b9b90e924bf01947cac5627fd138854690e00aab9372f71ab292d7dfd2e4006086b1ee27556cd235a11107477dd7b74723cbd86fb29e8c7773fd617c5ba64c1a4ed7bb26ce1616bc9fcb5c58d3dcfe63629001cbd61ddd6b5b421ab4895efce351349c4c6716a7748a393def6e9b689d89332a18f9eb58383af964864565925b15fb2598f90cacaa7b521869c73bf564d9100c92badf7c09a47585a46e08914245149c4903340519925ce4fabd53ccbba62d536fc5732123d8de0988b52e82eb33e4c8e315a17ff1457eb428b28ad0e2b6a09f37e206b04312508eb28796f55810c69ae1e01d38206ad1f31769755d829fc499f24b3213d41d43cec69a8be8c85a9fa0c79f3c6f272caa532f26b7623bb7763ecd771b03a7febf81603b5e62e432d0f70934890357de757a9101f3f5f9f1ace1190a489c9f0f5d19879b68cb6cb50704633a4b4d90d4850958db7f16e53ec9907b49267e4dfe0802fdab2e7b53a04f8a3d071d13a9730b88c171b9aa1b8af15cc80880e2b80907321c633dd9f02c1d28b64b07d80ba194bcdd556cf2c0e8e20df9ae296b9c52d3f88f2b1e130271486e048b511773717f321d312692d1b5550384a1bd5935afe9b22e45e3bfff20a989516ec20ea11243ff47534f9d600bbf3df6a8b2893d6483c80dcee7fd4ee99665de3e1bde38d02f1cfb98d3d5432df73d75c4b3b6b842c9765263e58b1179c822d38c5af6773220a92956ecd2fd948ab7f0fd9d13fde73b12a6b690d31530481cac46f7856f2125b05a55099c94a848502df173c5fe6e00b37b083cbef02329bf44d704dbf77116c8a6b576730a79bf434133d9ba322c03242ba6043a4d2bd0e95a2be067310cec1337389cd47cbe4cd3e6ddbee627f09816aca1e5edb5fd1489f07fbe5dc3c1d7998aabaf23a76116335c32f07d836375152c72594687e42618906e06dac39f2fec086ab4f488f3a41c92a61e782219453db1757d7629b1354855fb7f296355d915f28b59d406f8c2b6dfc9aca301aea77ce3c63b146a3fcbcc821c8a443a1927d1af030eb554adb2225ba47d81de19c3c931ced161f448322e22583a57a8e2975d308d86bf7ea07d858339df99cdbdd2b28291b88e5
< ... snip ... >

This was actually a challenge in this year's ABCTF, so I want to touch a bit more on it.

The challenge was an AES-ECB service listening on a remote server, and the flag was the secret key being used.

While the only modifications that I needed to make to my client were regarding the prepended "Encrypt:" text, I already included them in the above code samples.

The code for the server was as follows.

# ORIGINAL - http://pastebin.com/UTkSDn4H

#/usr/bin/env python
from Crypto.Cipher.AES import AESCipher
 
import SocketServer,threading,os,time
import signal
 
from secret2 import FLAG, KEY
 
PORT = 7765
 
def pad(s):
  l = len(s)
  needed = 16 - (l % 16)
  return s + (chr(needed) * needed)
 
def encrypt(s):
  return AESCipher(KEY).encrypt(pad('ENCRYPT:' + s.decode('hex') + FLAG))
 
class incoming(SocketServer.BaseRequestHandler):
    def handle(self):
        atfork()
        req = self.request
 
        def recvline():
            buf = ""
            while not buf.endswith("\n"):
                buf += req.recv(1)
            return buf
        signal.alarm(5)
 
        req.sendall("Send me some hex-encoded data to encrypt:\n")
        data = recvline()
        req.sendall("Here you go:")
        req.sendall(encrypt(data).encode('hex') + '\n')
        req.close()
 
class ReusableTCPServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
  pass
 
SocketServer.TCPServer.allow_reuse_address = True
server = ReusableTCPServer(("0.0.0.0", PORT), incoming)
 
print "Server listening on port %d" % PORT
server.serve_forever()

After running my modified client a short time, I recovered the secret key in use!

secret = ABCTF{p4dding_4_fun}

I was able to submit this flag and receive my 140 points. This was definitely a fun challenge, and I'm glad that it came out right around the time that I started this blog post.

The code and updates can be found in my GitHub repository.

doyler on Githubdoyler on Twitter
doyler
Ray Doyle is an avid pentester/security enthusiast/beer connoisseur who has worked in IT for almost 16 years now. From building machines and the software on them, to breaking into them and tearing it all down; he's done it all. To show for it, he has obtained an OSCP, eCPPT, eWPT, eWPTX, eMAPT, Security+, ICAgile CP, ITIL v3 Foundation, and even a sabermetrics certification!

He currently serves as a Senior Penetration Testing Consultant for Secureworks. His previous position was a Senior Penetration Tester for a major financial institution.

When he's not figuring out what cert to get next (currently GXPN) or side project to work on, he enjoys playing video games, traveling, and watching sports.

Leave a Comment

Filed under Security Not Included

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.