#!/usr/bin/perl
###############

require 5.6.0;
use strict;
use vars qw{$SSL_SUPPORT};

use FindBin qw{$RealBin};
use lib "$RealBin/lib";
use Digest::Perl::MD5 qw{md5_hex};

use Getopt::Std;
use IO::Socket;
use IO::Select;
use POSIX;

no utf8;
no locale;

my $VERSION      = '$Revision: 1.37 $';
my $FRAMEWORK    = '2.3';
my ($REV)        = $VERSION =~ m/\$Revisio.:\s+([^\s]+)/;

my $UPDATE_HOST  = 'metasploit.com';
my $UPDATE_PATH  = '/projects/Framework/updates/msfupdate.html';
my $SSL_VERIFIED = 0;
my $SSL_ISSUER   = '/C=US/ST=Texas/L=San Antonio/O=The Metasploit Project/OU=Development/CN=Metasploit CA/emailAddress=cacert@metasploit.com';
my $SSL_CERTS    = "$RealBin/docs";

select(STDOUT); $|++;

# Check for buggy versions of perl with utf-8 locale set
BrokenUTF8();
 
# Determine if SSL support is enabled
BEGIN
{
    if (eval "require Net::SSLeay")
    {
        Net::SSLeay->import();
        Net::SSLeay::load_error_strings();
        Net::SSLeay::SSLeay_add_ssl_algorithms();
        Net::SSLeay::randomize();
        $SSL_SUPPORT++;
    }
}

my %opts;
getopts('hrvusmaxf', \%opts);

if ($opts{v}) {
    print "Msfupdate Version: $REV\n";
    print "Framework Version: $FRAMEWORK\n";
    exit(0);
}

if ($opts{h} || (! $opts{m} && ! $opts{r} && ! $opts{u} && ! $opts{U})) {
    Usage();
}

chdir($RealBin);

my ($old_data, $now_data, $new_data);
my ($old, $now, $new);
my ($mod, $upx, $upd);

print "\n";
print "+ -- --=[ msfupdate v$FRAMEWORK [revision $REV]\n\n";
print "[*] Calculating local file checksums, please wait...\n";
$now_data = ScanFiles('.');
print "\n";

if (! -r '.current') {
    WriteFile('.current', $now_data);
    $old_data = $now_data;
} 
else {
    $old_data = ReadFile('.current');
}

if ($opts{r}) {
    print "[*] The local file hash has been rebuilt\n";
    WriteFile('.current', $now_data);
    exit(0);
}

# Allow the user to disable SSL mode entirely
if ($opts{f}) {
    $SSL_SUPPORT = 0;
}

# Display a big nasty warning for non-SSL unless -x is specified
if ($opts{u} || $opts{U}) {
    if (! $opts{x} && ! $SSL_SUPPORT) {
        print "[*] WARNING: The Net::SSLeay module is not installed. The update\n";
        print "    process will fall back to plain HTTP, this may allow a \n";
        print "    malicious attacker to inject hostile code into your system.\n";
        print "\n";
        print "Continue anyways (yes or no) ";
        
        my $resp;
        while ($resp !~ /yes|no/i) {
            print "> ";
            $resp = <STDIN>;
            chomp($resp);
        }
        print "\n";
        
        if ($resp !~ /yes/i) {
            print "[*] The update process has been cancelled\n";
            exit(0);
        }
    }
    
    $new_data = DownloadFile($UPDATE_HOST, "$UPDATE_PATH/.current");
}

$now = Hashit($now_data);
$old = Hashit($old_data);
$new = Hashit($new_data);

$mod = Diff($old, $now);
$upx = Diff($old, $new);
$upd = Diff($now, $new);

if ($opts{m}) {
    if (! keys(%{$mod})) {
        print "[*] No modifications since the last update\n";
        exit(0);
    }
    
    print "[*] Modifications since the last update:\n";
    foreach (keys(%{$mod})) {
        print "\t".$mod->{$_}."\t$_\n";
    }
    print "\n";
    exit(0);
}

# Strip this list down to just modifications
foreach (keys(%{$mod})) {
    if ($mod->{$_} ne 'Mod') {
        delete($mod->{$_});
    }
}

if (! $new_data) {
    print "[*] Could not obtain the current version information from the update server\n";
    exit(0);
}

my $mver = CheckVersion();

if ( ($opts{u} || $opts{U}) && ! $mver) {
    print "[*] Could not determine the current Framework version.\n";
    exit(0);
}

if ($mver && $mver ne $FRAMEWORK) {

    if (! $opts{U}) {
        print "[*] Version $mver of the Metasploit Framework is now available.\n";
        print "    - http://metasploit.com/projects/Framework/downloads.html\n\b\n";
    }
    else {
        print "[*] Starting upgrade process for version $mver...\n\n";
    }
}

if ($opts{U} && $mver && $mver eq $FRAMEWORK) {
    print "[*] No version upgrade is necessary.\n";
    exit(0);
}

# Upgrade to a new major release version...
if ($opts{U}) {
  
    my $backupdir = 'backup_'. $FRAMEWORK;
    
    print "[*] WARNING: You are about to upgrade to a new major version of the\n";
    print "    Metasploit Framework. This process involves removing the current\n";
    print "    installation and downloading the new version, one file at a time.\n";
    print "    This process is slow and somewhat prone to failure. If you encounter\n";
    print "    any problems during the download, you *should* be able to simply\n";
    print "    restart msfupdate with the -u parameter\n\n";
    
    print "    If you have modified any of the files in the Framework directory,\n";
    print "    these will be saved to $backupdir. The same applies for any modules\n";
    print "    that you have added to this Framework installation. After the update\n";
    print "    is complete, you should be able to copy these modules from the\n";
    print "    backup directory ($backupdir) into the appropriate directories\n";
    print "    in the new installation.\n\n";

    print "    If you are using FreeBSD or any other operating that maintains a\n";
    print "    package for the Framework, we recommend that you use the system\n";
    print "    package manager to upgrade\n\n";
    
    print "    Please type 'yes' to initiate the upgrade process.\n";
    print "Continue? (yes or no) ";
    
    my $resp;
    while ($resp !~ /yes|no/i) {
        print "> ";
        $resp = <STDIN>;
        chomp($resp);
    }
    if ($resp =~ /no/i) {
        exit(0);
    }
    
    delete($now->{'./msfupdate'});
    delete($new->{'./msfupdate'});
    delete($mod->{'./msfupdate'});
    delete($upd->{'./msfupdate'});

    print "[*] Backing up modified files to $backupdir...\n";
    
    # Backup framework files that the user modified
    foreach my $file (keys %{$mod} ) {
        print "    --- MOD $file\n";
        my $data = ReadFile($file);
        WriteFile($backupdir .'/'. $file , $data);
    }
    
    # Backup files that the user created
    foreach my $file (keys %{$upd} ) {
        next if $upd->{$file} ne 'Del';
        print "    --- USR $file\n";
        my $data = ReadFile($file);
        WriteFile($backupdir .'/'. $file , $data);
    }   
    
    print "[*] Removing the current installation...\n";

    # Do not remove these files since we need them to update...
    my %docs_save = ('./docs/7f8d5320.0' => 1, './docs/cacert.pem' => 1);

    # Remove all files that are part of the framework
    foreach my $file (keys %{$now}) {
        next if exists($docs_save{$file});
        unlink($file);
    }

    # Remove all directories but 'docs'
    foreach my $dir (split(/\n/, ScanDirs('.', 0))) {
        next if $dir eq './docs';
        system("rm", "-rf", $dir);
    }
    
    # Bump up the framework version
    $FRAMEWORK = $mver;
    
    # Download the file list for the new framework version
    $new_data = DownloadFile($UPDATE_HOST, "$UPDATE_PATH/.current");

    # Rebuild the tables
    $new = Hashit($new_data);
    $now = {};
    $old = {};

    $mod = Diff($old, $now);
    $upx = Diff($old, $new);
    $upd = Diff($now, $new);
    
    # Configure some options
    $opts{a}++;
}




# They modified something, ask them what they want to do with it
if (! $opts{a} && keys(%{$mod})) {  
    print "[*] You have modified the following Framework components:\n\n";
    foreach (sort(keys(%{$mod}))) {
        print "\t".$_."\n";
    }
    print "\n";
    print "    Would you like to preserve these changes? If you say no, all\n";
    print "    local modifications will be overwritten by the update process.\n";
    print "\n";
    print "Preserve modifications (yes or no) ";
    my $resp;
    while ($resp !~ /yes|no/i) {
        print "> ";
        $resp = <STDIN>;
        chomp($resp);
    }
    if ($resp =~ /no/i) {
        $mod = {};
    }
    print "\n";
}

foreach (keys(%{$upd})) {
    # Do not remove user-created files
    if ($upd->{$_} eq 'Del') {
        delete($upd->{$_});
    }

    # Ignore updates we already have
    if ($now->{$_} eq $new->{$_}) {
        delete($upd->{$_});
    }    
}


# Ignore locally modified files when -a is supplied
if ($opts{a}) {
    $mod = {};
}

# After all of this stuff, this is what we have
# - $mod is a table of anything the user wants to keep
# - $upd is a table of everything online new or updated


# The plan of attack is:
# - Iterate through each item in $upd, if there is a match in
#   $mod, we print a message and ignore the download for it.
#   We will delete anything with the Del status set in this
#   table. 
# - Once we have identified an update, we try to download it.
#   If an error occurs, either because the download fails or
#   the md5 does no match.
#
# - After all files have been processed, we rebuild the local
#   .current file and get ready for the next update.
#

if (! keys(%{$upd})) {
    print "[*] No new updates are available\n";
    exit(0);
}

print "[*] Online Update Task Summary\n\n";
foreach my $entry (keys(%{$upd})) {
    next if $opts{U};
    
    if ($mod->{$entry}) {
        print "\tIgnore: $entry\n";
    }
    else {
        print "\tUpdate: $entry\n";
    }
}
print "\n";

exit(0) if $opts{s};

if (! $opts{a}) {
    print "Continue? (yes or no) ";
    my $resp;
    while ($resp !~ /yes|no/i) {
        print "> ";
        $resp = <STDIN>;
        chomp($resp);
    }
    print "\n";
    if ($resp !~ /yes/i) {
        exit(0);
    }
}

my @tasks = sort(keys(%{$upd}));

# Force msfupdate to downloaded first...
if ($opts{U}) {
    unshift(@tasks, './msfupdate');
    unshift(@tasks, './lib/Digest/Perl/MD5.pm');
}

my $upd_tot = scalar(@tasks);
my $upd_cur = 0;

print "\n";
print "[*] Starting online update of $upd_tot file(s)...\n\n";
foreach my $entry (@tasks) {
    if ($mod->{$entry}) {
        next;
    } 
    my $data = DownloadFile($UPDATE_HOST, "$UPDATE_PATH/$entry");
    if ($new->{$entry} ne md5_hex($data)) {
        print "$entry: ". $new->{$entry} ." != ". md5_hex($data) ."\n";
        my $errmsg = $data ? 'checksum mismatch' : 'download failed';
        print "[*] Failed to update $entry: $errmsg\n";
        next;
    }
    
    # overwrite the local file O_o
    WriteFile($entry, $data);
    
    # set it executable since we dont track permissions
    chmod(0755, $entry);
    
    printf("[%.4d/%.4d - 0x%.6x bytes] $entry\n", ++$upd_cur, $upd_tot, length($data));
}

print "\n";
print "[*] Regenerating local file database\n";
$now_data = ScanFiles('.');
WriteFile('.current', $now_data);


sub Usage { 
    print STDERR "  Usage: $0 [options]>\n";
    print STDERR "Options:\n";
    print STDERR "         -h             You're looking at me baby\n";
    print STDERR "         -v             Display version information\n";
    print STDERR "         -u             Perform an online update via metasploit.com\n";
    print STDERR "         -s             Only display update tasks, do not actually download\n";
    print STDERR "         -m             Show any files locally modified since last update\n";
    print STDERR "         -a             Do not prompt, default to overwrite all files\n";
    print STDERR "         -x             Do not require confirmation for non-SSL updates\n";
    print STDERR "         -f             Disable ssl support entirely, use with -x to avoid warnings\n";
    
    # Too buggy and dangerous to use
    # print STDERR "         -U             Upgrade to a new major revision of the Framework\n";
    # Developer options
    # print STDERR "         -r             Rebuild the local version database\n";
    print "\n";
    exit(0);
}

sub ScanFiles {
    my $dir = shift;
    my $res;
    my $hwn;
    
    opendir ($hwn, $dir) || return;
    
    while (defined(my $entry = readdir($hwn))) {
        my $path = "$dir/$entry";

        # ignore all symlinks
        next if -l $path;
        
        # ignore all leading dot files   
        next if $entry =~ /^\./;
        
        # ignore the backup directories
        next if $entry =~ /^backup_/;
        
        # recurse into directories
        if (-d $path) {
            $res .= ScanFiles($path);
        } 
        elsif (-f $path) {
            $res .= HashFile($path)."\t$path\n";;
        }
    }
    closedir($hwn);
    return $res;   
}

sub ScanDirs {
    my $dir = shift;
    my $dep = @_ ? shift() : 0;
    my $res;
    my $hwn;
    
    return if $dep == -1;
    opendir ($hwn, $dir) || return;
    
    while (defined(my $entry = readdir($hwn))) {
        my $path = "$dir/$entry";
        
        # ignore all symlinks
        next if -l $path; 
        
        # ignore the backup directories
        next if $entry =~ /^backup_/;
        
        # ignore all leading dot files   
        next if $entry =~ /^\./;
         
        # recurse into directories
        if (-d $path) {
            $res .= "$path\n";
            $res .= ScanDirs($path, $dep - 1);
        }
    }
    closedir($hwn);
    return $res;   
}



sub CheckVersion {
    my $data = DownloadFile($UPDATE_HOST, $UPDATE_PATH);
    my ($vers, $mtime) = split(/\s+/, $data);
    return $vers;
}

sub DownloadFile {
    my $host = shift;
    my $path = shift;
    my $port = $SSL_SUPPORT ? 443 : 80;
    my $data;
    
    $path = EscapeURI($path);
    
    my $sock = IO::Socket::INET->new
    (
        PeerAddr    => $host,
        PeerPort    => $port,
        Proto       => 'tcp',
    );
    
    if (! $sock) {
        print STDERR "[*] Could not connect to $host: $!\n";
        exit(0);
    }

    my $req = "GET $path HTTP/1.0\r\nUser-Agent: msfupdate/$REV $FRAMEWORK\r\n\r\n";

    if ($SSL_SUPPORT) { 
    
        # Reset the verified flag
        $SSL_VERIFIED = 0;

        # Create SSL Context
        my $ctx = Net::SSLeay::CTX_new();
       
        # Tell SSL to use the certificate in the docs directory 
        Net::SSLeay::CTX_load_verify_locations($ctx, '', $SSL_CERTS);
        
        # Configure the SSL call-back to prevent MiTM
        Net::SSLeay::CTX_set_verify($ctx, &Net::SSLeay::VERIFY_PEER, \&SSLVerify);
                
        # Configure session for maximum interoperability
        Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_ALL);
        
        # Create a new SSL object with context
        my $ssl = Net::SSLeay::new($ctx);

        # Bind the SSL descriptor to the socket
        Net::SSLeay::set_fd($ssl, $sock->fileno);
        
        # Negotiate connection
        my $sslConn = Net::SSLeay::connect($ssl);

        if ($sslConn <= 0) {
            print STDERR "[*] SSL error:". Net::SSLeay::print_errs()."\n";
            $sock->close;
            return;
        }
        
        Net::SSLeay::ssl_write_all($ssl, $req);
        $sock->shutdown(1);    
        
        my $cert = Net::SSLeay::get_peer_certificate($ssl);
        
        $data = Net::SSLeay::ssl_read_all($ssl);
        
        Net::SSLeay::free ($ssl);
        Net::SSLeay::CTX_free ($ctx);
        $sock->close;
        
        $data = ProcessHTTP($data);
        return $data;
    }
    
    $sock->send($req);
    $sock->shutdown(1);
    while (<$sock>) { $data .= $_ }
    close ($sock);
    $data = ProcessHTTP($data);    
    return $data;
}

# Prevent MiTM attacks on the update downloads when run over SSL
sub SSLVerify {
    my ($ok, $x509_store_ctx) = @_;
    my $cert = Net::SSLeay::X509_STORE_CTX_get_current_cert($x509_store_ctx);
    
    if ($cert) {
        my $x509_issuer =  Net::SSLeay::X509_get_issuer_name($cert);
        my $issuer = Net::SSLeay::X509_NAME_oneline($x509_issuer);
        
        # differences between openssl 0.9.6 vs 0.9.7 (thanks par!)
        $issuer =~ s/Email/emailAddress/g;
        
        if ($ok && $issuer eq $SSL_ISSUER) {
            $SSL_VERIFIED++;
            return 1;
        }
    }
   
    return;
}

sub ProcessHTTP {
    my $http = shift;
    my $idx = index($http, "\r\n\r\n");
    return if -1 == $idx;
    my $head = substr($http, 0, $idx);
    my $body = substr($http, $idx + 4);
    return if $head !~ /HTTP\/1..\s+2/;
    return $body;  
}

sub WriteFile {
    my $file = shift;
    my $data = shift;

    my @path = split(/\//, $file);
    pop(@path);
    
    my $cdir = ".";
    foreach (@path) {
        $cdir .= "/".$_;
        if (! -d $cdir) {
            mkdir($cdir, 0755);
        }
    }
    
    open (my $out, ">$file") || return;
    print $out $data;
    close ($out);
}

sub ReadFile {
    my $path = shift;
    my $data;
    my $inp;
    
    open($inp, "<$path") || return;
    while (<$inp>) { $data .= $_ }
    close($inp);
    
    return $data;
}

sub HashFile {
    my $path = shift;
    my $data = ReadFile($path);
    return md5_hex($data);
}

sub Hashit {
    my $data = shift;
    my $hash = {};
    foreach my $line (split(/\n/, $data)) {
        my ($md5, $path) = $line =~ m/^([^\s]+)\s+(.*)/g;
        $hash->{$path} = $md5;
    }
    return $hash;
}

sub Diff {
    my $setA = shift;
    my $setB = shift;
    my $res;
    
    foreach (keys(%{$setA})) {
        if (exists($setB->{$_})) {
            if ($setA->{$_} ne $setB->{$_}) {
                $res->{$_} = 'Mod';
            }
        } 
        else {
            $res->{$_} = 'Del';
        }
    }
    
    foreach (keys(%{$setB})) {
        if (! exists($setA->{$_})) {
            $res->{$_} = 'New';
        }
    }
    
    return $res;
}

sub EscapeURI {
    my $path = shift;
	my %escapes = ();
	for (0..255) { $escapes{chr($_)} = sprintf("%%%02X", $_) }
	$path =~ s/([^A-Za-z0-9\-_.!~*'()\/])/$escapes{$1}/eg;
    return $path;
}

sub BrokenUTF8 {
	if ( $] >= 5.008 && $] < 5.008002 )
	 {
	 	my $badver;
		
		# Check LANG first
		$badver = ($ENV{'LANG'} =~ /utf/i) ? 1 : 0;
		
		# LC_ALL overrides LANG if its set
		if (defined($ENV{'LC_ALL'})) {
			$badver = ($ENV{'LC_ALL'} =~ /utf/i) ? 1 : 0;
		}
		
		return if ! $badver;
	 
		print STDERR qq|
[*] This version of Perl ($]) contains a buggy utf-8 implementation. If you
    would like to use this version with the Metasploit Framework, you must
    set the LC_ALL environment variable to 'C'. For example:

    \$ export LC_ALL=C; ./msfconsole
	
|;
	exit(0);
	}
}
