From 7e9892b94aa27725ff272af28536f0da326a8156 Mon Sep 17 00:00:00 2001 From: Sam Redmond Date: Sat, 27 Dec 2025 00:20:42 +1300 Subject: [PATCH 01/12] feat: add fingerprintx and naabu --- .gitignore | 174 +- .vscode/settings.json | 5 - README.md | 84 +- docker-compose.yml | 170 - docs/overview_diagram.png | Bin 28587 -> 0 bytes rigour/addons/minecraft/Dockerfile | 13 - rigour/addons/minecraft/main.py | 57 - rigour/addons/minecraft/requirements.txt | 1 - rigour/api/Dockerfile | 15 - rigour/api/README.md | 165 - rigour/api/main.py | 166 - rigour/api/requirements.txt | 3 - rigour/api/utils.py | 88 - rigour/banners/Dockerfile | 40 - rigour/banners/main.py | 177 - rigour/banners/requirements.txt | 2 - rigour/banners/zgrab.py | 79 - rigour/common/common/config.py | 27 - rigour/common/common/database/mongodb.py | 12 - rigour/common/common/queue/rabbitmq.py | 76 - .../common/common/queue/rabbitmq_asyncio.py | 73 - rigour/common/common/subprocess.py | 144 - rigour/common/common/types.py | 57 - rigour/common/common/utils.py | 62 - rigour/common/pyproject.toml | 34 - rigour/ports/.gitignore | 1 - rigour/ports/Dockerfile | 81 - rigour/ports/download_geodb.sh | 1 - rigour/ports/main.py | 72 - rigour/ports/requirements.txt | 1 - rigour/ports/zmap.py | 40 - rigour/vuln/.gitignore | 1 - rigour/vuln/Dockerfile | 16 - rigour/vuln/README.md | 3 - rigour/vuln/download_vuln_dbs.sh | 14 - rigour/vuln/main.py | 78 - rigour/vuln/manifest.json | 50 - rigour/vuln/requirements.txt | 0 rigour/vuln/vuln_detector.py | 64 - scanner/cmd/rigour/main.go | 145 + scanner/go.mod | 161 + scanner/go.sum | 757 +++ scanner/pkg/discovery/config.go | 10 + scanner/pkg/discovery/naabu/naabu.go | 93 + scanner/pkg/fingerprint/config.go | 19 + scanner/pkg/fingerprint/plugin_list.go | 43 + scanner/pkg/fingerprint/plugins/plugins.go | 54 + .../fingerprint/plugins/pluginutils/error.go | 134 + .../plugins/pluginutils/requests.go | 60 + .../fingerprint/plugins/services/dhcp/dhcp.go | 368 ++ .../plugins/services/dhcp/dhcp_test.go | 43 + .../plugins/services/dhcp/dhcpd.conf | 47 + .../fingerprint/plugins/services/dns/dns.go | 131 + .../plugins/services/dns/dns_test.go | 40 + .../fingerprint/plugins/services/echo/echo.go | 62 + .../plugins/services/echo/echo_test.go | 41 + .../fingerprint/plugins/services/ftp/ftp.go | 57 + .../plugins/services/ftp/ftp_test.go | 38 + .../fingerprint/plugins/services/http/http.go | 224 + .../plugins/services/http/http_test.go | 45 + .../plugins/services/http/https_test.go | 45 + .../fingerprint/plugins/services/imap/imap.go | 186 + .../plugins/services/imap/imap_test.go | 38 + .../fingerprint/plugins/services/ipmi/ipmi.go | 128 + .../plugins/services/ipmi/ipmi_test.go | 39 + .../plugins/services/ipsec/ipsec.go | 120 + .../plugins/services/ipsec/ipsec_test.go | 43 + .../fingerprint/plugins/services/jdwp/jdwp.go | 177 + .../plugins/services/jdwp/jdwp_test.go | 23 + .../services/kafka/kafkaNew/kafkaNew.go | 196 + .../services/kafka/kafkaNew/kafkaNew_test.go | 38 + .../services/kafka/kafkaOld/kafkaOld.go | 233 + .../services/kafka/kafkaOld/kafkaOld_test.go | 38 + .../fingerprint/plugins/services/ldap/ldap.go | 205 + .../plugins/services/ldap/ldap_test.go | 38 + .../plugins/services/linuxrpc/linuxrpc.go | 167 + .../services/linuxrpc/linuxrpc_test.go | 40 + .../services/minecraft/java/minecraft.go | 229 + .../services/minecraft/java/minecraft_test.go | 168 + .../plugins/services/modbus/modbus.go | 113 + .../plugins/services/modbus/modbus_test.go | 38 + .../plugins/services/mqtt/mqtt3/mqtt3.go | 126 + .../plugins/services/mqtt/mqtt3/mqtt3_test.go | 38 + .../plugins/services/mqtt/mqtt5/mqtt5.go | 128 + .../plugins/services/mqtt/mqtt5/mqtt5_test.go | 38 + .../plugins/services/mssql/mssql.go | 316 ++ .../plugins/services/mssql/mssql_test.go | 43 + .../plugins/services/mysql/mysql.go | 277 + .../plugins/services/mysql/mysql_test.go | 42 + .../plugins/services/netbios/netbiosns.go | 72 + .../services/netbios/netbiosns_test.go | 40 + .../fingerprint/plugins/services/ntp/ntp.go | 67 + .../plugins/services/ntp/ntp_test.go | 37 + .../plugins/services/openvpn/openvpn.go | 85 + .../plugins/services/openvpn/openvpn_test.go | 38 + .../plugins/services/oracledb/oracle.go | 253 + .../plugins/services/oracledb/oracle_test.go | 39 + .../fingerprint/plugins/services/pop3/pop3.go | 127 + .../plugins/services/pop3/pop3_test.go | 39 + .../plugins/services/postgresql/postgresql.go | 133 + .../services/postgresql/postgresql_test.go | 44 + .../fingerprint/plugins/services/rdp/rdp.go | 372 ++ .../plugins/services/rdp/rdp_test.go | 38 + .../plugins/services/redis/redis.go | 144 + .../plugins/services/redis/redis_test.go | 38 + .../plugins/services/rsync/rsync.go | 77 + .../plugins/services/rsync/rsync_test.go | 38 + .../fingerprint/plugins/services/rtsp/rtsp.go | 113 + .../plugins/services/rtsp/rtsp_test.go | 39 + .../fingerprint/plugins/services/smb/smb.go | 361 ++ .../plugins/services/smb/smb_test.go | 39 + .../fingerprint/plugins/services/smtp/smtp.go | 188 + .../plugins/services/smtp/smtp_test.go | 38 + .../fingerprint/plugins/services/snmp/snmp.go | 75 + .../plugins/services/snmp/snmp_test.go | 39 + .../fingerprint/plugins/services/ssh/ssh.go | 392 ++ .../plugins/services/ssh/ssh_test.go | 38 + .../fingerprint/plugins/services/stun/stun.go | 182 + .../plugins/services/stun/stun_test.go | 38 + .../plugins/services/telnet/telnet.go | 285 + .../plugins/services/telnet/telnet_test.go | 38 + .../fingerprint/plugins/services/vnc/vnc.go | 89 + .../plugins/services/vnc/vnc_test.go | 38 + scanner/pkg/fingerprint/plugins/types.go | 514 ++ scanner/pkg/fingerprint/scan.go | 240 + scanner/pkg/fingerprint/util.go | 57 + scanner/pkg/scan/event.go | 16 + scanner/pkg/scan/scan.go | 95 + scanner/pkg/test/testutil.go | 92 + scanner/third_party/cryptolib/.gitattributes | 10 + scanner/third_party/cryptolib/.gitignore | 2 + scanner/third_party/cryptolib/CONTRIBUTING.md | 26 + scanner/third_party/cryptolib/LICENSE | 27 + scanner/third_party/cryptolib/PATENTS | 22 + scanner/third_party/cryptolib/README.md | 23 + scanner/third_party/cryptolib/codereview.cfg | 1 + .../third_party/cryptolib/ssh/agent/client.go | 847 +++ .../cryptolib/ssh/agent/client_test.go | 533 ++ .../cryptolib/ssh/agent/example_test.go | 41 + .../cryptolib/ssh/agent/forward.go | 103 + .../cryptolib/ssh/agent/keyring.go | 241 + .../cryptolib/ssh/agent/keyring_test.go | 76 + .../third_party/cryptolib/ssh/agent/server.go | 570 ++ .../cryptolib/ssh/agent/server_test.go | 259 + .../cryptolib/ssh/agent/testdata_test.go | 64 + .../cryptolib/ssh/benchmark_test.go | 126 + scanner/third_party/cryptolib/ssh/buffer.go | 97 + .../third_party/cryptolib/ssh/buffer_test.go | 87 + scanner/third_party/cryptolib/ssh/certs.go | 589 ++ .../third_party/cryptolib/ssh/certs_test.go | 320 ++ scanner/third_party/cryptolib/ssh/channel.go | 633 +++ scanner/third_party/cryptolib/ssh/cipher.go | 789 +++ .../third_party/cryptolib/ssh/cipher_test.go | 231 + scanner/third_party/cryptolib/ssh/client.go | 282 + .../third_party/cryptolib/ssh/client_auth.go | 725 +++ .../cryptolib/ssh/client_auth_test.go | 959 ++++ .../third_party/cryptolib/ssh/client_test.go | 256 + scanner/third_party/cryptolib/ssh/common.go | 430 ++ .../third_party/cryptolib/ssh/common_test.go | 176 + .../third_party/cryptolib/ssh/connection.go | 143 + scanner/third_party/cryptolib/ssh/doc.go | 22 + .../third_party/cryptolib/ssh/example_test.go | 321 ++ scanner/third_party/cryptolib/ssh/export.go | 131 + .../third_party/cryptolib/ssh/handshake.go | 704 +++ .../cryptolib/ssh/handshake_test.go | 620 ++ .../ssh/internal/bcrypt_pbkdf/bcrypt_pbkdf.go | 93 + .../bcrypt_pbkdf/bcrypt_pbkdf_test.go | 97 + scanner/third_party/cryptolib/ssh/kex.go | 774 +++ scanner/third_party/cryptolib/ssh/kex_test.go | 65 + scanner/third_party/cryptolib/ssh/keys.go | 1448 +++++ .../third_party/cryptolib/ssh/keys_test.go | 618 ++ .../cryptolib/ssh/knownhosts/knownhosts.go | 540 ++ .../ssh/knownhosts/knownhosts_test.go | 356 ++ scanner/third_party/cryptolib/ssh/mac.go | 61 + .../third_party/cryptolib/ssh/mempipe_test.go | 110 + scanner/third_party/cryptolib/ssh/messages.go | 877 +++ .../cryptolib/ssh/messages_test.go | 288 + scanner/third_party/cryptolib/ssh/mux.go | 351 ++ scanner/third_party/cryptolib/ssh/mux_test.go | 715 +++ scanner/third_party/cryptolib/ssh/server.go | 752 +++ scanner/third_party/cryptolib/ssh/session.go | 648 +++ .../third_party/cryptolib/ssh/session_test.go | 782 +++ scanner/third_party/cryptolib/ssh/ssh_gss.go | 139 + .../third_party/cryptolib/ssh/ssh_gss_test.go | 109 + .../third_party/cryptolib/ssh/streamlocal.go | 116 + scanner/third_party/cryptolib/ssh/tcpip.go | 474 ++ .../third_party/cryptolib/ssh/tcpip_test.go | 20 + .../cryptolib/ssh/terminal/terminal.go | 76 + .../cryptolib/ssh/test/agent_unix_test.go | 60 + .../cryptolib/ssh/test/banner_test.go | 33 + .../cryptolib/ssh/test/cert_test.go | 78 + .../cryptolib/ssh/test/dial_unix_test.go | 129 + scanner/third_party/cryptolib/ssh/test/doc.go | 7 + .../cryptolib/ssh/test/forward_unix_test.go | 206 + .../cryptolib/ssh/test/multi_auth_test.go | 145 + .../cryptolib/ssh/test/session_test.go | 449 ++ .../cryptolib/ssh/test/sshd_test_pw.c | 173 + .../cryptolib/ssh/test/test_unix_test.go | 375 ++ .../cryptolib/ssh/test/testdata_test.go | 64 + .../third_party/cryptolib/ssh/testdata/doc.go | 8 + .../cryptolib/ssh/testdata/keys.go | 314 ++ .../cryptolib/ssh/testdata_test.go | 63 + .../third_party/cryptolib/ssh/transport.go | 357 ++ .../cryptolib/ssh/transport_test.go | 113 + ui/.eslintignore | 13 - ui/.eslintrc.cjs | 31 - ui/.gitignore | 10 - ui/.npmrc | 1 - ui/.prettierignore | 4 - ui/.prettierrc | 8 - ui/.vscode/settings.json | 120 - ui/Dockerfile | 11 - ui/README.md | 38 - ui/package-lock.json | 4980 ----------------- ui/package.json | 40 - ui/postcss.config.cjs | 6 - ui/src/app.d.ts | 9 - ui/src/app.html | 12 - ui/src/app.postcss | 9 - ui/src/lib/api.ts | 78 - ui/src/lib/components/HostItem.svelte | 44 - ui/src/lib/components/HostSidebar.svelte | 32 - ui/src/lib/types.ts | 82 - ui/src/routes/+layout.svelte | 22 - ui/src/routes/+page.server.ts | 17 - ui/src/routes/+page.svelte | 19 - ui/src/routes/admin/+page.svelte | 2 - ui/src/routes/host/[ip]/+page.server.ts | 6 - ui/src/routes/host/[ip]/+page.svelte | 9 - ui/static/favicon.png | Bin 15086 -> 0 bytes ui/svelte.config.js | 19 - ui/tailwind.config.ts | 23 - ui/tsconfig.json | 18 - ui/vite.config.ts | 7 - 234 files changed, 33159 insertions(+), 7795 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 docker-compose.yml delete mode 100644 docs/overview_diagram.png delete mode 100644 rigour/addons/minecraft/Dockerfile delete mode 100644 rigour/addons/minecraft/main.py delete mode 100644 rigour/addons/minecraft/requirements.txt delete mode 100644 rigour/api/Dockerfile delete mode 100644 rigour/api/README.md delete mode 100644 rigour/api/main.py delete mode 100644 rigour/api/requirements.txt delete mode 100644 rigour/api/utils.py delete mode 100644 rigour/banners/Dockerfile delete mode 100644 rigour/banners/main.py delete mode 100644 rigour/banners/requirements.txt delete mode 100644 rigour/banners/zgrab.py delete mode 100644 rigour/common/common/config.py delete mode 100644 rigour/common/common/database/mongodb.py delete mode 100644 rigour/common/common/queue/rabbitmq.py delete mode 100644 rigour/common/common/queue/rabbitmq_asyncio.py delete mode 100644 rigour/common/common/subprocess.py delete mode 100644 rigour/common/common/types.py delete mode 100644 rigour/common/common/utils.py delete mode 100644 rigour/common/pyproject.toml delete mode 100644 rigour/ports/.gitignore delete mode 100644 rigour/ports/Dockerfile delete mode 100755 rigour/ports/download_geodb.sh delete mode 100644 rigour/ports/main.py delete mode 100644 rigour/ports/requirements.txt delete mode 100644 rigour/ports/zmap.py delete mode 100644 rigour/vuln/.gitignore delete mode 100644 rigour/vuln/Dockerfile delete mode 100644 rigour/vuln/README.md delete mode 100755 rigour/vuln/download_vuln_dbs.sh delete mode 100644 rigour/vuln/main.py delete mode 100644 rigour/vuln/manifest.json delete mode 100644 rigour/vuln/requirements.txt delete mode 100644 rigour/vuln/vuln_detector.py create mode 100644 scanner/cmd/rigour/main.go create mode 100644 scanner/go.mod create mode 100644 scanner/go.sum create mode 100644 scanner/pkg/discovery/config.go create mode 100644 scanner/pkg/discovery/naabu/naabu.go create mode 100644 scanner/pkg/fingerprint/config.go create mode 100644 scanner/pkg/fingerprint/plugin_list.go create mode 100644 scanner/pkg/fingerprint/plugins/plugins.go create mode 100644 scanner/pkg/fingerprint/plugins/pluginutils/error.go create mode 100644 scanner/pkg/fingerprint/plugins/pluginutils/requests.go create mode 100644 scanner/pkg/fingerprint/plugins/services/dhcp/dhcp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/dhcp/dhcp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/dhcp/dhcpd.conf create mode 100644 scanner/pkg/fingerprint/plugins/services/dns/dns.go create mode 100644 scanner/pkg/fingerprint/plugins/services/dns/dns_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/echo/echo.go create mode 100644 scanner/pkg/fingerprint/plugins/services/echo/echo_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ftp/ftp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ftp/ftp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/http/http.go create mode 100644 scanner/pkg/fingerprint/plugins/services/http/http_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/http/https_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/imap/imap.go create mode 100644 scanner/pkg/fingerprint/plugins/services/imap/imap_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ipmi/ipmi.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ipmi/ipmi_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ipsec/ipsec.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ipsec/ipsec_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/jdwp/jdwp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/jdwp/jdwp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go create mode 100644 scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go create mode 100644 scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ldap/ldap.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ldap/ldap_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc.go create mode 100644 scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft.go create mode 100644 scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/modbus/modbus.go create mode 100644 scanner/pkg/fingerprint/plugins/services/modbus/modbus_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mssql/mssql.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mssql/mssql_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mysql/mysql.go create mode 100644 scanner/pkg/fingerprint/plugins/services/mysql/mysql_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/netbios/netbiosns.go create mode 100644 scanner/pkg/fingerprint/plugins/services/netbios/netbiosns_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ntp/ntp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ntp/ntp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/openvpn/openvpn.go create mode 100644 scanner/pkg/fingerprint/plugins/services/openvpn/openvpn_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/oracledb/oracle.go create mode 100644 scanner/pkg/fingerprint/plugins/services/oracledb/oracle_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/pop3/pop3.go create mode 100644 scanner/pkg/fingerprint/plugins/services/pop3/pop3_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/postgresql/postgresql.go create mode 100644 scanner/pkg/fingerprint/plugins/services/postgresql/postgresql_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rdp/rdp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rdp/rdp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/redis/redis.go create mode 100644 scanner/pkg/fingerprint/plugins/services/redis/redis_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rsync/rsync.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rsync/rsync_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rtsp/rtsp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/rtsp/rtsp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/smb/smb.go create mode 100644 scanner/pkg/fingerprint/plugins/services/smb/smb_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/smtp/smtp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/smtp/smtp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/snmp/snmp.go create mode 100644 scanner/pkg/fingerprint/plugins/services/snmp/snmp_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ssh/ssh.go create mode 100644 scanner/pkg/fingerprint/plugins/services/ssh/ssh_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/stun/stun.go create mode 100644 scanner/pkg/fingerprint/plugins/services/stun/stun_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/telnet/telnet.go create mode 100644 scanner/pkg/fingerprint/plugins/services/telnet/telnet_test.go create mode 100644 scanner/pkg/fingerprint/plugins/services/vnc/vnc.go create mode 100644 scanner/pkg/fingerprint/plugins/services/vnc/vnc_test.go create mode 100644 scanner/pkg/fingerprint/plugins/types.go create mode 100644 scanner/pkg/fingerprint/scan.go create mode 100644 scanner/pkg/fingerprint/util.go create mode 100644 scanner/pkg/scan/event.go create mode 100644 scanner/pkg/scan/scan.go create mode 100644 scanner/pkg/test/testutil.go create mode 100644 scanner/third_party/cryptolib/.gitattributes create mode 100644 scanner/third_party/cryptolib/.gitignore create mode 100644 scanner/third_party/cryptolib/CONTRIBUTING.md create mode 100644 scanner/third_party/cryptolib/LICENSE create mode 100644 scanner/third_party/cryptolib/PATENTS create mode 100644 scanner/third_party/cryptolib/README.md create mode 100644 scanner/third_party/cryptolib/codereview.cfg create mode 100644 scanner/third_party/cryptolib/ssh/agent/client.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/client_test.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/example_test.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/forward.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/keyring.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/keyring_test.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/server.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/server_test.go create mode 100644 scanner/third_party/cryptolib/ssh/agent/testdata_test.go create mode 100644 scanner/third_party/cryptolib/ssh/benchmark_test.go create mode 100644 scanner/third_party/cryptolib/ssh/buffer.go create mode 100644 scanner/third_party/cryptolib/ssh/buffer_test.go create mode 100644 scanner/third_party/cryptolib/ssh/certs.go create mode 100644 scanner/third_party/cryptolib/ssh/certs_test.go create mode 100644 scanner/third_party/cryptolib/ssh/channel.go create mode 100644 scanner/third_party/cryptolib/ssh/cipher.go create mode 100644 scanner/third_party/cryptolib/ssh/cipher_test.go create mode 100644 scanner/third_party/cryptolib/ssh/client.go create mode 100644 scanner/third_party/cryptolib/ssh/client_auth.go create mode 100644 scanner/third_party/cryptolib/ssh/client_auth_test.go create mode 100644 scanner/third_party/cryptolib/ssh/client_test.go create mode 100644 scanner/third_party/cryptolib/ssh/common.go create mode 100644 scanner/third_party/cryptolib/ssh/common_test.go create mode 100644 scanner/third_party/cryptolib/ssh/connection.go create mode 100644 scanner/third_party/cryptolib/ssh/doc.go create mode 100644 scanner/third_party/cryptolib/ssh/example_test.go create mode 100644 scanner/third_party/cryptolib/ssh/export.go create mode 100644 scanner/third_party/cryptolib/ssh/handshake.go create mode 100644 scanner/third_party/cryptolib/ssh/handshake_test.go create mode 100644 scanner/third_party/cryptolib/ssh/internal/bcrypt_pbkdf/bcrypt_pbkdf.go create mode 100644 scanner/third_party/cryptolib/ssh/internal/bcrypt_pbkdf/bcrypt_pbkdf_test.go create mode 100644 scanner/third_party/cryptolib/ssh/kex.go create mode 100644 scanner/third_party/cryptolib/ssh/kex_test.go create mode 100644 scanner/third_party/cryptolib/ssh/keys.go create mode 100644 scanner/third_party/cryptolib/ssh/keys_test.go create mode 100644 scanner/third_party/cryptolib/ssh/knownhosts/knownhosts.go create mode 100644 scanner/third_party/cryptolib/ssh/knownhosts/knownhosts_test.go create mode 100644 scanner/third_party/cryptolib/ssh/mac.go create mode 100644 scanner/third_party/cryptolib/ssh/mempipe_test.go create mode 100644 scanner/third_party/cryptolib/ssh/messages.go create mode 100644 scanner/third_party/cryptolib/ssh/messages_test.go create mode 100644 scanner/third_party/cryptolib/ssh/mux.go create mode 100644 scanner/third_party/cryptolib/ssh/mux_test.go create mode 100644 scanner/third_party/cryptolib/ssh/server.go create mode 100644 scanner/third_party/cryptolib/ssh/session.go create mode 100644 scanner/third_party/cryptolib/ssh/session_test.go create mode 100644 scanner/third_party/cryptolib/ssh/ssh_gss.go create mode 100644 scanner/third_party/cryptolib/ssh/ssh_gss_test.go create mode 100644 scanner/third_party/cryptolib/ssh/streamlocal.go create mode 100644 scanner/third_party/cryptolib/ssh/tcpip.go create mode 100644 scanner/third_party/cryptolib/ssh/tcpip_test.go create mode 100644 scanner/third_party/cryptolib/ssh/terminal/terminal.go create mode 100644 scanner/third_party/cryptolib/ssh/test/agent_unix_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/banner_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/cert_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/dial_unix_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/doc.go create mode 100644 scanner/third_party/cryptolib/ssh/test/forward_unix_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/multi_auth_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/session_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/sshd_test_pw.c create mode 100644 scanner/third_party/cryptolib/ssh/test/test_unix_test.go create mode 100644 scanner/third_party/cryptolib/ssh/test/testdata_test.go create mode 100644 scanner/third_party/cryptolib/ssh/testdata/doc.go create mode 100644 scanner/third_party/cryptolib/ssh/testdata/keys.go create mode 100644 scanner/third_party/cryptolib/ssh/testdata_test.go create mode 100644 scanner/third_party/cryptolib/ssh/transport.go create mode 100644 scanner/third_party/cryptolib/ssh/transport_test.go delete mode 100644 ui/.eslintignore delete mode 100644 ui/.eslintrc.cjs delete mode 100644 ui/.gitignore delete mode 100644 ui/.npmrc delete mode 100644 ui/.prettierignore delete mode 100644 ui/.prettierrc delete mode 100644 ui/.vscode/settings.json delete mode 100644 ui/Dockerfile delete mode 100644 ui/README.md delete mode 100644 ui/package-lock.json delete mode 100644 ui/package.json delete mode 100644 ui/postcss.config.cjs delete mode 100644 ui/src/app.d.ts delete mode 100644 ui/src/app.html delete mode 100644 ui/src/app.postcss delete mode 100644 ui/src/lib/api.ts delete mode 100644 ui/src/lib/components/HostItem.svelte delete mode 100644 ui/src/lib/components/HostSidebar.svelte delete mode 100644 ui/src/lib/types.ts delete mode 100644 ui/src/routes/+layout.svelte delete mode 100644 ui/src/routes/+page.server.ts delete mode 100644 ui/src/routes/+page.svelte delete mode 100644 ui/src/routes/admin/+page.svelte delete mode 100644 ui/src/routes/host/[ip]/+page.server.ts delete mode 100644 ui/src/routes/host/[ip]/+page.svelte delete mode 100644 ui/static/favicon.png delete mode 100644 ui/svelte.config.js delete mode 100644 ui/tailwind.config.ts delete mode 100644 ui/tsconfig.json delete mode 100644 ui/vite.config.ts diff --git a/.gitignore b/.gitignore index 82f9275..4eee9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,162 +1,28 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# MacOS filesysem files +.DS_Store -# C extensions +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll *.so +*.dylib -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST +# Test binary, built with `go test -c` +*.test -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# Dependency directories (remove the comment below to include it) +# vendor/ -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ +# Go workspace file +go.work -# Translations -*.mo -*.pot +# Dev folders +.vscode +.idea -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# rigour binary +scanner/rigour \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d9c88d4..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "python.analysis.extraPaths": [ - "./rigour/common" - ] -} diff --git a/README.md b/README.md index 914ae27..05cd6aa 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,3 @@ # Rigour: An IoT Scanner Inspired by Shodan.io -Rigour is a comprehensive Internet of Things (IoT) scanning tool designed to discover, analyze, and report on devices connected to the internet. By leveraging powerful tools like ZMap and ZGrab, Rigour performs large-scale network scans to identify active hosts, retrieve service banners, and detect potential vulnerabilities. It offers both REST and streaming APIs for data access, and includes a user interface for data visualization. - -## Get Started - -To quickly set up Rigour and its services, use Docker Compose: - -```bash -docker compose up -``` - -This command initializes all components required for scanning, data processing, and data access. - -## Architecture Overview - -![DiagramOverview](./docs/overview_diagram.png) - -Rigour's architecture comprises several interconnected components that work in harmony to perform comprehensive network scanning and analysis. - -### Components - -#### Port Scanner - -The **Port Scanner** uses [ZMap](https://github.com/zmap/zmap) to scan specified port ranges and identify active hosts on the internet. Discovered hosts are published to a RabbitMQ queue in the format `{country}.{port}.{ip}.port`. This allows other services to consume live data for further processing. Additionally, scan results are stored in the database for persistent access. - -#### Banner Grabber - -The **Banner Grabber** employs [ZGrab](https://github.com/zmap/zgrab2) to retrieve service banners from the identified hosts, providing detailed information about running services (e.g., SSH, HTTP). This service subscribes to the `{country}.{port}.{ip}.port` queue and publishes the collected banners to `{country}.{port}.{ip}.banners`. The database entries for each host are updated with this new information. - -#### Vulnerability Scanner - -The **Vulnerability Scanner** analyzes the collected banner data to detect vulnerable servers by cross-referencing with CVE databases. It examines identifiers such as HTTP server headers against known vulnerabilities. This service subscribes to the `{country}.{port}.{ip}.banners` queue and publishes its findings to `{country}.{port}.{ip}.vulns`. Host documents in the database are updated with vulnerability details. - -### Data Access Interfaces - -#### REST API - -Rigour provides a RESTful API to access the scanned host data programmatically. The API allows for querying hosts, services, and vulnerabilities. - -- **Documentation**: Detailed API documentation is available [here](./api/README.md). - -#### Streaming API - -For real-time data processing, Rigour offers a streaming API via RabbitMQ queues. - -##### Queue Structure - -The RabbitMQ queues follow this naming convention: - -```bash -{country}.{port}.{ip}.{data_type} -``` - -##### Message Components - -- **`{country}`**: Two-letter country code (e.g., `US` for the United States) -- **`{port}`**: Port number being scanned (e.g., `443`) -- **`{ip}`**: IP address of the host (e.g., `192.168.1.1`) -- **`{data_type}`**: Type of data (`port`, `banners`, or `vulns`) - -##### Example - -```bash -US.443.192.168.1.1.port -``` - -This structure facilitates easy identification and routing of data based on geographic location, port, host, and data type. - -### User Interface - -A web-based **User Interface** is available for visualizing scan results and interacting with the data. - -> **Note**: The user interface is in early development stages and requires manual startup. - -## Considerations - -1. **Network Capacity**: High scanning rates set in ZMap may consume significant network bandwidth, potentially causing other services to experience latency or connectivity issues. -2. **Process Resilience**: Current processes do not automatically resume after a crash. If a service like the Banner Grabber fails, it will not pick up where it left off upon restarting. Enhancements to address this limitation are planned for future releases. - -## Future Enhancements - -- **ISP/Organization Mapping**: Incorporate mapping of IP addresses to Internet Service Providers or organizations to provide more context. -- **DNS Mapping**: Implement DNS resolution to associate hostnames with IP addresses. -- **Campaign Configurability**: Introduce configurable scanning campaigns, allowing users to specify ports, protocols, and data types to capture. +Rigour is a comprehensive Internet of Things (IoT) scanning tool designed to discover, analyze, and report on devices connected to the internet. Rigour performs large-scale network scans to identify active hosts, retrieve service banners, and detect potential vulnerabilities. diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3af29e4..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,170 +0,0 @@ -services: - port-scanner: - build: - context: . - dockerfile: rigour/ports/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - PORTS: "80,443,22,21,25565,27017,143,6379" - NETWORKS: "10.0.0.0/8 192.168.0.0/16" - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-http-80: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: http - PORT: 80 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-http-443: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: http - PORT: 443 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-ssh-22: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: ssh - PORT: 22 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-ftp-21: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: ftp - PORT: 21 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-imap-143: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: imap - PORT: 143 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-redis-6379: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: redis - PORT: 6379 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-mongodb-27017: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: mongodb - PORT: 27017 - depends_on: - rabbitmq: - condition: service_healthy - - banner-scanner-jarm-443: - build: - context: . - dockerfile: rigour/banners/Dockerfile - restart: unless-stopped - network_mode: "host" - environment: - SERVICE: jarm - PORT: 443 - depends_on: - rabbitmq: - condition: service_healthy - - addon-minecraft-scanner: - build: - context: . - dockerfile: rigour/addons/minecraft/Dockerfile - restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - - vuln-scanner: - build: - context: . - dockerfile: rigour/vuln/Dockerfile - restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - - api: - build: - context: . - dockerfile: rigour/api/Dockerfile - restart: unless-stopped - network_mode: "host" - depends_on: - rabbitmq: - condition: service_healthy - - mongodb: - image: mongo - restart: always - ports: - - "27017:27017" - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] - interval: 10s - timeout: 5s - retries: 5 - - rabbitmq: - image: rabbitmq:3-management - restart: unless-stopped - ports: - - "5672:5672" - - "15672:15672" - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] - interval: 10s - timeout: 5s - retries: 5 diff --git a/docs/overview_diagram.png b/docs/overview_diagram.png deleted file mode 100644 index 3fe97346d225fef73c2ae2e3f6d750c0e1ebace7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28587 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߯X~-uS!mZ-f1(on-zQmPSLUGN$C1lS?S(xX(bo-M67xrV9lfOdm@^ zSe3w-^VmJiPkhXQz160o>Iy86oTd>FG-_`IeKZB@j=ZXq)|`ralJ$PYuQT<#BiBox zdJDEfLjq!T0#PM*oHF2o4gXTru>EB9mO@II!ZzUcM9dO$r#x(6)hOmztbB^ji5$vU zhCF-GpNtB#Z0g(~p#@*4=+){PxYhqh^3{h!KBVOYO-5z-R6A=G)fj0vME+PvF7b?| z)^1;`)}&ynYuS0OIFVxVuuHn5%f3bsXyBZa6ov1Sk8dML$61&L$0m#9@;l)Bl&>cr zw#IfJXv7Rqoa5B2NLqXPmwK#5mj2v}p5Ki{``)h5HR(Cotn% z39QD{5+CP^=`wwlfT*yqoiE2Gkk{z1xx;CqXN|pJ717Jn+l*1fc6Q)6!<9OL&n$-_ zKVWB^O$W}rTgkgX3HJ<`+U(2=7@{a6OD~5RR5`}-0^>DRvtK#y5mA$aveKc!9{QQ6 zu+l+|XAN!_X6K$uUT@*Wm(wcY9eIu}@&QqW>n71vF@@pWfDC9TVZd?I{J^1XJ8aqn zE6_x5INBBRDB1YKjABX1y=s-DXX|ibN5tHB+jI3yfUd!RBhdGl9jRl5{Tlh+e^I-y zBga7}(9N7~GhH7g{9Yosq$ON8+l!~5>tPpcb#H&y_y^m=*?6*!K?Bp5d1$-_)$Uy@ zIT+S9dr7-tCwM<(M>-dku>Q*abiKGU@9sOMI3qg)@O3v`?I~3 z=T}*c?t?}d&Z5_@bO#CG$df;Mx-t$`NM@s>ds6Q_CWZ8+&wfl>>DJY1qmH zv&xZkI9O({&wH@sIh2k`m^$IF9&vC7cJch~bYKMj^94b3S2D}f zO9l26A~`4L0EhFvUver-UOr^jAE`+RqtlBOZ=J_eq}iVCHrUq( z-+_P-^p2WoVykZnX6ajz5Xbm%(qh2d;C4Anb{g0n>gYnPSURvo*A*=`e0wup09ku6 z3bY)IlH zCU1RzNAT?1BM510{KewTA(x_$6P3gxE|Dc3FqU4BoqzcDE7IhcOA&efe7gvsu{nBF kp*QjkDEt5MWADx6R>SdBq9g%C1pMgf7@jFTWq None: - logger.debug(f"Received RabbitMQ message: {port_message}") - message = from_dict(data_class=HostMessage, data=port_message) - - banner = self.get_mc_banner(message.ip, message.port) - if banner is None: - logger.debug("Skipping as no banner found") - return - - logger.info(f"Found banner: {banner.raw}") - message.host.banner = Banner( - service="minecraft", port=message.port, data=dict(banner.raw) - ) - - route_key = utils.route_key_from_host_message(message, "banner") - await self.queue.publish(route_key, asdict(message)) - utils.save_banner(self.db, message) - - def get_mc_banner( - self, ip: str, port: int - ) -> status_response.JavaStatusResponse | None: - try: - server = JavaServer(ip, port) - status = server.status() - except: - logger.debug(f"Failed to get status of: {ip}:{port}") - else: - return status - - -if __name__ == "__main__": - grabber = MinecraftBannerGrabber() - loop = asyncio.get_event_loop() - loop.run_until_complete(grabber.listen()) - loop.run_forever() diff --git a/rigour/addons/minecraft/requirements.txt b/rigour/addons/minecraft/requirements.txt deleted file mode 100644 index d063b90..0000000 --- a/rigour/addons/minecraft/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mcstatus diff --git a/rigour/api/Dockerfile b/rigour/api/Dockerfile deleted file mode 100644 index 206d4ec..0000000 --- a/rigour/api/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.12-alpine3.20 - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/common/ -RUN pip install --no-cache-dir -e ./common - -# Install ports dependencies -COPY rigour/api /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -CMD ["python", "-m", "uvicorn", "main:app", \ - "--host", "0.0.0.0", \ - "--port", "1234"] diff --git a/rigour/api/README.md b/rigour/api/README.md deleted file mode 100644 index df43ae5..0000000 --- a/rigour/api/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# REST API Documentation - -This documentation provides an overview of the REST API endpoints available for searching and retrieving host information. -The API allows users to perform searches with advanced query syntax, retrieve counts, and get detailed host data by IP address. - -## Base URL - -All endpoints are accessible under the following base URL: - -``` -http://:1234/api/v1 -``` - ---- - -## Endpoints - -### 1. Search Hosts - -**Endpoint:** - -``` -GET /host/search -``` - -**Description:** - -Search for hosts using a query syntax with optional filters and retrieve summary information through facets. - -**Query Parameters:** - -- `query` (string, optional): Search query with optional filters in the `filter:value` format. For example, to find servers in Germany: `location.country_code:DE`. - -- `skip` (integer, optional): Number of records to skip. Default is `0`. Must be greater than or equal to `0`. - -- `limit` (integer, optional): Maximum number of records to return. Default is `10`. Must be between `1` and `100`. - -**Response:** - -- **Status Code:** `200 OK` -- **Body:** A list of host objects matching the search criteria. - -**Response Model:** - -A list of `Host` objects. The structure of `Host` includes host details such as IP address, port information, and metadata. - -**Example Request:** - -``` -GET /api/v1/host/search?query=apache%20country:DE&skip=0&limit=10 -``` - ---- - -### 2. Get Hosts Count - -**Endpoint:** - -``` -GET /host/count -``` - -**Description:** - -Retrieve the total number of hosts that match the search query, along with any requested facet information. This endpoint does not return host details. - -**Query Parameters:** - -- `query` (string, optional): Search query with optional filters in the `filter:value` format. For example: `location.country_code:DE`. - -- `facet` (string, optional): Comma-separated list of properties for faceted search, optionally with counts. Example: `country:100`. - -**Response:** - -- **Status Code:** `200 OK` -- **Body:** A JSON object containing the total count and facet information. - -**Response Format:** - -```json -{ - "total": , - "facets": { - "": [ - { - "_id": "", - "count": - }, - ... - ], - ... - } -} -``` - -**Example Request:** - -``` -GET /api/v1/host/count?query=apache%20country:DE&facet=country:10 -``` - -**Example Response:** - -```json -{ - "total": 500, - "facets": { - "country": [ - { "_id": "DE", "count": 300 }, - { "_id": "US", "count": 100 }, - { "_id": "FR", "count": 50 } - ] - } -} -``` - ---- - -### 3. Get Host by IP - -**Endpoint:** - -``` -GET /host/{ip} -``` - -**Description:** - -Retrieve detailed host information by specifying the IP address. - -**Path Parameters:** - -- `ip` (string, required): The IP address of the host. - -**Response:** - -- **Status Code:** `200 OK` - - **Body:** A `Host` object containing host details. -- **Status Code:** `404 Not Found` - - **Body:** `{ "detail": "Host not found" }` -- **Status Code:** `500 Internal Server Error` - - **Body:** `{ "detail": "Invalid host data" }` - -**Example Request:** - -``` -GET /api/v1/host/192.168.1.1 -``` - ---- - -## Models - -### Host - -The `Host` model represents the structure of a host object returned by the API. It includes fields such as: - -- `ip` (string): The IP address of the host. -- `port` (integer): The port number. -- `data` (string): Banner data or service information. -- `timestamp` (string): The timestamp when the data was collected. -- `location` (object): Geolocation data including country, city, latitude, and longitude. -- Additional metadata fields as defined in the model. - -_Note: The exact structure may vary based on the data stored in the database._ diff --git a/rigour/api/main.py b/rigour/api/main.py deleted file mode 100644 index e6b96d2..0000000 --- a/rigour/api/main.py +++ /dev/null @@ -1,166 +0,0 @@ -from common.database.mongodb import Database -from common.types import DBHost -from fastapi import APIRouter, Depends, FastAPI, HTTPException, Query -from pydantic import ValidationError -from utils import build_facet_stages, parse_query_filters, process_host_document - -db = Database() - -router = APIRouter() - - -class PaginationParams: - def __init__( - self, skip: int = Query(0, ge=0), limit: int = Query(10, ge=1, le=100) - ): - self.skip = skip - self.limit = limit - - -@router.get("/host/search", response_model=list[DBHost]) -async def get_hosts( - query: str | None = Query( - default=None, - description="Search query with optional filters in 'filter:value' format", - ), - facet: str | None = Query( - default=None, - description="Comma-separated list of properties for faceted search, optionally\ - with counts (e.g., 'country:100')", - ), - pagination: PaginationParams = Depends(), -): - """ - Search using query syntax and use facets to get summary information for different - properties. - - Args: - query: Shodan search query. The provided string is used to search the database - of banners in Shodan, with the additional option to provide filters inside - the search query using a "filter:value" format. For example, the following - search query would find Apache Web servers located in Germany: - "apache country:DE". - - facet: A comma-separated list of properties to get summary information on. - Property names can also be in the format of "property:count", where "count" - is the number of facets that will be returned for a property (i.e. - "country:100" to get the top 100 countries for a search query). - """ - pipeline = [] - - # Build match stage from query if provided - if query: - match_conditions = parse_query_filters(query) - pipeline.append({"$match": match_conditions}) - - # if facet: - # TODO: Add facet stages to pipeline - - else: - # Without facets, we can use a simpler pipeline - pipeline.extend([{"$skip": pagination.skip}, {"$limit": pagination.limit}]) - - # Execute aggregation - return list(db.scans.aggregate(pipeline)) - - -@router.get("/host/count") -async def get_hosts_count( - query: str | None = Query( - default=None, - description="Search query with optional filters in 'filter:value' format", - ), - facet: str | None = Query( - default=None, - description="Comma-separated list of properties for faceted search, optionally\ - with counts (e.g., 'country:100')", - ), -) -> dict: - """ - This method behaves identical to "/host/search" with the only difference - that this method does not return any host results, it only returns the total - number of results that matched the query and any facet information that was - requested. - - Args: - query: The provided string is used to search the database of banners, with - the additional option to provide filters inside the search query using - a "filter:value" format. For example, the following search query would - find Apache Web servers located in Germany: "location.country_name:DE". - - facet: A comma-separated list of properties to get summary information on. - Property names can also be in the format of "property:count", where "count" - is the number of facets that will be returned for a property (i.e. - "country:100" to get the top 100 countries for a search query). - - db: AsyncIOMotorDatabase instance for MongoDB access - - Returns: - dict: Contains total count and facet information if requested - """ - pipeline = [] - - # Build match stage from query if provided - if query: - match_conditions = parse_query_filters(query) - pipeline.append({"$match": match_conditions}) - - # Initialize response structure - response = {"total": 0, "facets": {}} - - # Add facet stages if facets are requested - if facet: - pipeline.append({"$facet": build_facet_stages(facet)}) - else: - # If no facets requested, just get the count - pipeline.append({"$count": "total"}) - - # Execute aggregation - result = list(db.scans.aggregate(pipeline)) - - if result: - if facet: - # Extract results from faceted query - response["total"] = ( - result[0]["total"][0]["count"] if result[0]["total"] else 0 - ) - # Remove the total from facets before returning - result[0].pop("total", None) - response["facets"] = result[0] - else: - # Extract results from simple count query - response["total"] = result[0]["total"] - - return response - - -@router.get("/host/{ip}", response_model=DBHost) -async def get_host_by_ip(ip: str): - """ - Get host information by IP address - - Args: - ip: IP address of the host to retrieve - """ - host = db.scans.find_one({"ip": ip}) - - if not host: - raise HTTPException(status_code=404, detail="Host not found") - - host = process_host_document(host) - - try: - host_model = DBHost(**host) - except ValidationError: - raise HTTPException(status_code=500, detail="Invalid host data") - - return host_model - - -app = FastAPI(root_path="/api") -app.include_router(router, prefix="/v1") - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=1234) diff --git a/rigour/api/requirements.txt b/rigour/api/requirements.txt deleted file mode 100644 index f54f004..0000000 --- a/rigour/api/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pymongo -uvicorn -fastapi diff --git a/rigour/api/utils.py b/rigour/api/utils.py deleted file mode 100644 index ebdd094..0000000 --- a/rigour/api/utils.py +++ /dev/null @@ -1,88 +0,0 @@ -def process_host_document(doc) -> dict: - """ - Process a MongoDB document to remove the _id field and return a dictionary. - """ - doc = dict(doc) - doc.pop("_id", None) # Remove MongoDB's _id field - return doc - - -def parse_query_filters(query: str) -> dict: - """ - Parse the query string to extract filters and build MongoDB match conditions. - - Args: - query: Query string with optional filters in 'filter:value' format - - Returns: - dict: MongoDB match conditions - """ - conditions = {} - - # Split query into parts by whitespace, preserving quoted strings - import shlex - - query_parts = shlex.split(query) - - for part in query_parts: - if ":" in part: - field, value = part.split(":", 1) - # Handle special cases and type conversion as needed - if value.lower() in ("true", "false"): - value = value.lower() == "true" - elif value.isdigit(): - value = int(value) - conditions[field] = value - else: - # Add text search condition for non-filter parts - if "text" not in conditions: - conditions["text"] = {"$search": part} - else: - # Append to existing text search - conditions["text"]["$search"] += f" {part}" - - return conditions - - -def build_facet_stages(facet: str | None = None) -> dict: - """ - Build MongoDB facet stages from facet parameter. - - Args: - facet: Comma-separated list of properties for faceted search - - Returns: - dict: Facet stages for MongoDB aggregation pipeline - """ - if not facet: - return {} - - facet_stages = {"total": [{"$count": "count"}]} - - for facet_item in facet.split(","): - field_parts = facet_item.strip().split(":") - field_name = field_parts[0] - limit = int(field_parts[1]) if len(field_parts) > 1 else 10 - - # Handle nested fields using $getField - group_field = field_name - if "." in field_name: - field_parts = field_name.split(".") - group_field = { - "$getField": { - "field": field_parts[-1], - "input": { - "$getField": {"field": field_parts[0], "input": "$$ROOT"} - }, - } - } - else: - group_field = f"${field_name}" - - facet_stages[field_name.replace(".", "_")] = [ # type: ignore - {"$group": {"_id": group_field, "count": {"$sum": 1}}}, - {"$sort": {"count": -1}}, - {"$limit": limit}, - ] - - return facet_stages diff --git a/rigour/banners/Dockerfile b/rigour/banners/Dockerfile deleted file mode 100644 index d1f5ffb..0000000 --- a/rigour/banners/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# ============================================== -# Stage 1: Build ZGrab from source -# ============================================== -ARG GO_VERSION=1.20 -ARG PYTHON_VERSION=3.12 -FROM golang:${GO_VERSION}-alpine3.16 as build - -# System dependencies -RUN apk add --no-cache make git - -WORKDIR /usr/src/zgrab2 - -# Clone the ZMap repository -RUN git clone https://github.com/zmap/zgrab2.git . - -# Copy and cache deps -RUN go mod download && go mod verify - -# Build the actual app -RUN make all - -# =========================================================== -# Stage 2: Create the final image with runtime dependencies -# =========================================================== -FROM python:${PYTHON_VERSION}-alpine3.20 as run - -COPY --from=build /usr/src/zgrab2/cmd/zgrab2/zgrab2 /usr/bin/zgrab2 -ENV PATH="/usr/bin/zgrab2:${PATH}" - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/lib/common/ -RUN pip install --no-cache-dir -e /app/lib/common - -# Install ports dependencies -COPY rigour/banners /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -ENTRYPOINT ["python3", "main.py"] diff --git a/rigour/banners/main.py b/rigour/banners/main.py deleted file mode 100644 index 72aa2de..0000000 --- a/rigour/banners/main.py +++ /dev/null @@ -1,177 +0,0 @@ -import asyncio -import os -from dataclasses import asdict -from datetime import datetime, timedelta - -from common import utils -from common.database.mongodb import Database -from common.queue.rabbitmq_asyncio import AsyncRabbitMQQueueManager -from common.types import Banner, HostMessage -from dacite import from_dict -from loguru import logger -from zgrab import ZGrab, ZGrabCommand, ZGrabResult - - -class PendingMessage: - def __init__(self, message: HostMessage, timestamp: datetime): - self.message = message - self.timestamp = timestamp - - -class BannerGrabber: - def __init__(self, command: ZGrabCommand, message_timeout: int = 300): - self.command = command - self.zgrab = ZGrab(command) - self.db = Database() - self.queue = AsyncRabbitMQQueueManager() - self.run_task = None - self.cleanup_task = None - self.pending_messages: dict[str, PendingMessage] = {} - self.message_timeout = message_timeout # seconds - self.running = True - self.tasks = set() - - def _create_task(self, coro): - task = asyncio.create_task(coro) - self.tasks.add(task) - task.add_done_callback(self.tasks.discard) - return task - - async def shutdown(self, signal=None): - """Cleanup tasks tied to the service's shutdown.""" - if signal: - logger.info(f"Received exit signal {signal.name}") - - logger.info("Shutting down gracefully...") - - self.running = False - - # Cancel our cleanup task if it's still running - if self.cleanup_task and not self.cleanup_task.done(): - self.cleanup_task.cancel() - - # Cancel ZGrab run task if it's still running - if self.run_task and not self.run_task.done(): - self.run_task.cancel() - - # Cancel all remaining tasks - tasks = [t for t in self.tasks if not t.done()] - if tasks: - logger.info(f"Cancelling {len(tasks)} outstanding tasks") - for task in tasks: - task.cancel() - await asyncio.gather(*tasks, return_exceptions=True) - - logger.info("Shutdown complete.") - - async def listen(self, port: int | None = None): - self.run_task = self._create_task( - self.zgrab.run(callback=self.process_zmap_result) - ) - self.cleanup_task = self._create_task(self.cleanup_stale_messages()) - - logger.info(f"Starting consumer for port: {port}") - routing_key = f"#.{port if port else '#'}.#.port" - - await self.queue.consume( - routing_key=routing_key, callback=self.process_incoming - ) # Blocking - - async def process_incoming(self, port_message: dict): - logger.debug( - f"Received RabbitMQ message: {port_message}, now {str(len(self.pending_messages) + 1)} messages in queue" - ) - try: - message = from_dict(data_class=HostMessage, data=port_message) - self.pending_messages[message.ip] = PendingMessage( - message=message, timestamp=datetime.now() - ) - await self.zgrab.pipe(message.ip) - except Exception as e: - logger.error(f"Error processing incoming message: {e}") - - async def process_zmap_result(self, result: ZGrabResult): - logger.info(f"Received ZMap result: {result}") - try: - pending = self.pending_messages.pop(result.ip, None) - if pending is None: - logger.warning(f"No pending message found for IP: {result.ip}") - return - - message = pending.message - message.host.banner = Banner( - service=self.command.service, - port=self.command.port, - data=asdict(result.data)[self.command.service], - ) - - await self.publish(message) - utils.save_banner(self.db, message) - - except Exception as e: - logger.error(f"Error processing ZMap result: {e}") - - async def cleanup_stale_messages(self): - while self.running: - try: - current_time = datetime.now() - stale_ips = [ - ip - for ip, pending in self.pending_messages.items() - if (current_time - pending.timestamp) - > timedelta(seconds=self.message_timeout) - ] - - for ip in stale_ips: - self.pending_messages.pop(ip) - logger.warning( - f"Removed stale message for IP {ip} after {self.message_timeout} seconds" - ) - - await asyncio.sleep(60) # Check every minute - - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"Error in cleanup task: {e}") - await asyncio.sleep(60) # Continue cleanup even if there's an error - - async def publish(self, message: HostMessage): - routing_key = ( - f"{message.host.location.country_code}.{message.port}.{message.ip}.banner" - ) - try: - await self.queue.publish(routing_key, asdict(message)) - except Exception as e: - logger.error(f"Error publishing message: {e}") - - -def main(): - service = os.environ.get("SERVICE") - if not service: - logger.error("SERVICE environment variable is required") - return - - port = os.environ.get("PORT") - port = int(port) if port else None - - message_timeout = int(os.environ.get("MESSAGE_TIMEOUT", "300")) - - logger.info(f"Starting banner grabber for service: {service}, port: {port}") - - command = ZGrabCommand(service=service, port=port) - grabber = BannerGrabber(command, message_timeout=message_timeout) - - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(grabber.listen(port)) - loop.run_forever() - except KeyboardInterrupt: - logger.info("Received keyboard interrupt.") - finally: - logger.info("Exiting...") - loop.run_until_complete(grabber.shutdown()) - - -if __name__ == "__main__": - main() diff --git a/rigour/banners/requirements.txt b/rigour/banners/requirements.txt deleted file mode 100644 index 98c7fdd..0000000 --- a/rigour/banners/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pika -pymongo diff --git a/rigour/banners/zgrab.py b/rigour/banners/zgrab.py deleted file mode 100644 index 526d02a..0000000 --- a/rigour/banners/zgrab.py +++ /dev/null @@ -1,79 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from common.subprocess import AsyncSubprocessBase -from dacite import from_dict - - -@dataclass -class ZGrabCommand: - service: str - port: int | None = None - - def build(self) -> list[str]: - args = ["zgrab2", self.service] - # args = ["docker", "run", "--rm", "-i", "ghcr.io/zmap/zgrab2", self.service] - if self.port: - args += ["--port", str(self.port)] - - args.append("--flush") - - return args - - -@dataclass -class ZGrabService: - status: str - protocol: str - timestamp: datetime - error: str | None = None - result: dict | None = None - - -@dataclass -class ZGrabData: - amqp091: ZGrabService | None = None - bacnet: ZGrabService | None = None - banner: ZGrabService | None = None - dnp3: ZGrabService | None = None - fox: ZGrabService | None = None - ftp: ZGrabService | None = None - http: ZGrabService | None = None - imap: ZGrabService | None = None - ipp: ZGrabService | None = None - jarm: ZGrabService | None = None - modbus: ZGrabService | None = None - mongodb: ZGrabService | None = None - mssql: ZGrabService | None = None - multiple: ZGrabService | None = None - mysql: ZGrabService | None = None - ntp: ZGrabService | None = None - oracle: ZGrabService | None = None - pop3: ZGrabService | None = None - postgres: ZGrabService | None = None - redis: ZGrabService | None = None - siemens: ZGrabService | None = None - smb: ZGrabService | None = None - smtp: ZGrabService | None = None - ssh: ZGrabService | None = None - telnet: ZGrabService | None = None - tls: ZGrabService | None = None - - -@dataclass -class ZGrabResult: - ip: str - data: ZGrabData - - -class ZGrab(AsyncSubprocessBase[ZGrabResult]): - def __init__(self, command: ZGrabCommand): - super().__init__(command, enable_piping=True) - - async def _parse_result(self, result: dict) -> ZGrabResult: - """Parse the ZGrab result into ZGrabResult dataclass.""" - result["data"][self.command.service]["timestamp"] = datetime.fromisoformat( - result["data"][self.command.service]["timestamp"] - ) - - return from_dict(data_class=ZGrabResult, data=result) diff --git a/rigour/common/common/config.py b/rigour/common/common/config.py deleted file mode 100644 index e779e8c..0000000 --- a/rigour/common/common/config.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - - -class Config: - @staticmethod - def get_mongo_uri(default: str = "mongodb://localhost:27017") -> str: - return os.environ.get("MONGO_URL", default) - - @staticmethod - def get_mongo_db(default: str = "rigour") -> str: - return os.environ.get("MONGO_DB", default) - - @staticmethod - def get_rabbitmq_uri(default: str = "amqp://localhost:5672/") -> str: - return os.environ.get("RABBITMQ_URL", default) - - @staticmethod - def get_networks(default: str = "10.0.0.0/8") -> str: - return os.environ.get("NETWORKS", default) - - @staticmethod - def get_ports(default: str = "80") -> str: - return os.environ.get("PORTS", default) - - @staticmethod - def get_scan_collection() -> str: - return "scans" diff --git a/rigour/common/common/database/mongodb.py b/rigour/common/common/database/mongodb.py deleted file mode 100644 index b0fcff8..0000000 --- a/rigour/common/common/database/mongodb.py +++ /dev/null @@ -1,12 +0,0 @@ -from common.config import Config -from pymongo import MongoClient - - -class Database: - def __init__(self, uri: str | None = None, db: str | None = None): - self.client = MongoClient(uri or Config.get_mongo_uri()) - self.db = self.client[db or Config.get_mongo_db()] - self.scans = self.db[Config.get_scan_collection()] - - def close(self): - self.client.close() diff --git a/rigour/common/common/queue/rabbitmq.py b/rigour/common/common/queue/rabbitmq.py deleted file mode 100644 index 62e9dee..0000000 --- a/rigour/common/common/queue/rabbitmq.py +++ /dev/null @@ -1,76 +0,0 @@ -import json -import threading -from typing import Callable - -import pika -from common.config import Config -from common.utils import DateTimeEncoder -from loguru import logger - - -class RabbitMQQueueManager: - def __init__(self, uri: str | None = None, exchange="data_exchange"): - self.uri = uri or Config.get_rabbitmq_uri() - self.exchange = exchange - - # Create a thread-local storage for the connection and channel - # This prevents thread errors when using the same connection in multiple threads - self.local = threading.local() - - def connect(self): - self.local.connection = pika.BlockingConnection(pika.URLParameters(self.uri)) - self.local.channel = self.local.connection.channel() - self.local.channel.exchange_declare( - exchange=self.exchange, exchange_type="topic" - ) - logger.info( - f"Thread {threading.current_thread().name}: Connected to RabbitMQ on\ - {self.uri}, exchange '{self.exchange}' declared." - ) - - def _get_channel(self): - """Get or create a channel for the current thread.""" - if not hasattr(self.local, "channel"): - self.connect() - return self.local.channel - - def publish(self, routing_key: str, message: dict): - """Publish a message to the specified routing key.""" - channel = self._get_channel() - channel.basic_publish( - exchange=self.exchange, - routing_key=routing_key, - body=json.dumps(message, cls=DateTimeEncoder), - ) - logger.debug(f"Published message to '{routing_key}': {message}") - - def consume(self, routing_key: str, callback: Callable): - """Consume messages from the specified routing key pattern.""" - channel = self._get_channel() - result = channel.queue_declare("", exclusive=True) - queue_name = result.method.queue - - channel.queue_bind( - exchange=self.exchange, queue=queue_name, routing_key=routing_key - ) - logger.info(f"Subscribed to '{routing_key}' with queue '{queue_name}'.") - - def on_message(channel, method, properties, body): - message = json.loads(body) - logger.debug(f"Received message from '{method.routing_key}': {message}") - callback(message) - channel.basic_ack(delivery_tag=method.delivery_tag) - - channel.basic_consume(queue=queue_name, on_message_callback=on_message) - logger.info("Starting consumption...") - try: - channel.start_consuming() - except Exception as e: - logger.exception(f"Error consuming messages: {e}") - self.close() - - def close(self): - """Close the connection to the RabbitMQ server.""" - if hasattr(self.local, "connection") and not self.local.connection.is_closed: - self.local.connection.close() - logger.info("Connection to RabbitMQ closed.") diff --git a/rigour/common/common/queue/rabbitmq_asyncio.py b/rigour/common/common/queue/rabbitmq_asyncio.py deleted file mode 100644 index 9fb08a5..0000000 --- a/rigour/common/common/queue/rabbitmq_asyncio.py +++ /dev/null @@ -1,73 +0,0 @@ -from datetime import datetime -from typing import Callable - -import aiormq -import aiormq.types -import msgpack -from common.config import Config -from loguru import logger - - -def decode_datetime(obj): - if "__datetime__" in obj: - obj = datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f") - return obj - - -def encode_datetime(obj): - if isinstance(obj, datetime): - return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")} - return obj - - -class AsyncRabbitMQQueueManager: - def __init__(self, uri: str | None = None, exchange: str = "data_exchange"): - self.uri = uri or Config.get_rabbitmq_uri() - self.exchange = exchange - self.connection = None - self.channel = None - - async def connect(self): - self.connection = await aiormq.connect(self.uri) - self.channel = await self.connection.channel() - await self.channel.exchange_declare( - exchange=self.exchange, exchange_type="topic" - ) - - async def get_channel(self) -> aiormq.types.AbstractChannel: - """Get or create a channel for the current thread.""" - if not self.channel: - await self.connect() - assert self.channel, "Connection not established." - return self.channel - - async def publish(self, routing_key: str, message: dict) -> None: - """Publish a message to the specified routing key.""" - channel = await self.get_channel() - - logger.debug(f"Publishing message to '{routing_key}': {message}") - body: bytes = msgpack.packb(message, default=encode_datetime) - await channel.basic_publish( - exchange=self.exchange, - routing_key=routing_key, - body=body, - ) - - async def consume(self, routing_key: str, callback: Callable) -> None: - """Consume messages from the specified routing key pattern.""" - channel = await self.get_channel() - - result = await channel.queue_declare("", exclusive=True) - queue_name = result.queue - - assert queue_name, "Queue name not found." - logger.info(f"Subscribing to '{routing_key}' with queue '{queue_name}'.") - await channel.queue_bind( - exchange=self.exchange, queue=queue_name, routing_key=routing_key - ) - - async def on_message(message: aiormq.types.DeliveredMessage): - message = msgpack.unpackb(message.body, object_hook=decode_datetime) - await callback(message) - - await self.channel.basic_consume(queue=queue_name, consumer_callback=on_message) diff --git a/rigour/common/common/subprocess.py b/rigour/common/common/subprocess.py deleted file mode 100644 index 6dab715..0000000 --- a/rigour/common/common/subprocess.py +++ /dev/null @@ -1,144 +0,0 @@ -import asyncio -import json -from typing import Generic, TypeVar - -from loguru import logger - -T = TypeVar("T") - - -class AsyncSubprocessBase(Generic[T]): - def __init__(self, command, enable_piping: bool = False): - self.command = command - self.process: asyncio.subprocess.Process | None = None - self._stdin_lock = asyncio.Lock() - self._enable_piping = enable_piping - self._stdout_task = None - self._stderr_task = None - - async def run(self, callback: callable): - """Run the subprocess asynchronously and process output line-by-line.""" - args = self.command.build() - - # Start the subprocess with appropriate pipes - logger.debug(f"Starting subprocess with args: {' '.join(args)}") - self.process = await self._create_subprocess(args) - - # Create asynchronous tasks for reading stdout and stderr - self._stdout_task = asyncio.create_task(self._read_stdout(callback)) - self._stderr_task = asyncio.create_task(self._read_stderr()) - - if not self._enable_piping: - # If piping is not enabled, wait for process to complete - await self.process.wait() - await asyncio.gather(self._stdout_task, self._stderr_task) - - return_code = self.process.returncode - if return_code != 0: - logger.warning(f"Subprocess exited with return code {return_code}") - - async def _create_subprocess(self, args): - """Create subprocess with appropriate pipes.""" - stdin_pipe = asyncio.subprocess.PIPE if self._enable_piping else None - return await asyncio.create_subprocess_exec( - *args, - stdin=stdin_pipe, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - async def _read_stderr(self): - """Asynchronously reads lines from the subprocess's stderr.""" - if not self.process or not self.process.stderr: - return - - try: - async for line in self.process.stderr: - line = line.strip() - if line: - logger.error(f"Subprocess stderr: {line.decode()}") - except Exception as e: - logger.error(f"Error reading stderr: {e}") - - async def _read_stdout(self, callback: callable): - """Reads the output from the subprocess asynchronously, handling large lines.""" - if not self.process or not self.process.stdout: - return - - buffer = b"" - try: - while True: - chunk = await self.process.stdout.read(4096) # Read in 4KB chunks - if not chunk: - break - buffer += chunk - while b"\n" in buffer: - line, buffer = buffer.split(b"\n", 1) - await self._process_line(line, callback) - - # Process any remaining data in the buffer - if buffer: - await self._process_line(buffer, callback) - except Exception as e: - logger.exception(f"Error reading stdout: {e}") - - async def _process_line(self, line: bytes, callback: callable): - """Processes a single line from stdout.""" - line = line.strip() - if not line: - return - - try: - result = json.loads(line) - parsed_result = await self._parse_result(result) - if parsed_result: - await callback(parsed_result) - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON: {line}") - except Exception as e: - logger.exception(f"Error processing line: {e}") - - async def _parse_result(self, result: dict) -> T: - """Parse the raw result into appropriate type. Must be implemented by subclasses.""" - raise NotImplementedError - - async def pipe(self, data: str): - """Pipe input data into the subprocess asynchronously.""" - if not self._enable_piping: - logger.error("Piping is not enabled for this subprocess") - return - - if not self.process or not self.process.stdin: - logger.error("Cannot pipe: process or stdin not available") - return - - try: - async with self._stdin_lock: - self.process.stdin.write(f"{data}\n".encode()) - await self.process.stdin.drain() - logger.debug(f"Successfully piped data to subprocess: {data}") - except Exception as e: - logger.error(f"Error piping data to subprocess: {e}") - - async def close(self): - """Close the process and clean up resources.""" - if self.process: - try: - # Close stdin first if piping was enabled - if self._enable_piping and self.process.stdin: - self.process.stdin.close() - await self.process.stdin.wait_closed() - - # Cancel reading tasks - if self._stdout_task: - self._stdout_task.cancel() - if self._stderr_task: - self._stderr_task.cancel() - - # Terminate the process - self.process.terminate() - await self.process.wait() - - logger.info("Subprocess and tasks terminated.") - except Exception as e: - logger.error(f"Error closing subprocess: {e}") diff --git a/rigour/common/common/types.py b/rigour/common/common/types.py deleted file mode 100644 index b4ddc5f..0000000 --- a/rigour/common/common/types.py +++ /dev/null @@ -1,57 +0,0 @@ -import datetime -from dataclasses import dataclass - -from pydantic import BaseModel - - -@dataclass -class Vulnerability: - name: str - title: str - version: str - link: str - # TODO: This should be linked to the service and port - - -@dataclass -class Location: - country_code: str | None = None - continent_name: str | None = None - country_name: str | None = None - accuracy_radius: int | None = None - latitude: float | None = None - longitude: float | None = None - - -@dataclass -class Banner: - service: str - port: int | None - data: dict - - -@dataclass -class Host: - location: Location - banner: Banner | None = None - vulnerabilities: list[Vulnerability] | None = None - - -@dataclass -class HostMessage: - ip: str - port: int - host: Host - - -class DBHost(BaseModel): - ip: str - location: Location - banners: dict[str, Banner] = {} - vulnerabilities: list[Vulnerability] = [] - updated_at: datetime.datetime - first_seen: datetime.datetime - - class Config: - from_attributes = True # Allows compatibility with ORM objects - extra = "ignore" # Ignores extra fields not defined in the model diff --git a/rigour/common/common/utils.py b/rigour/common/common/utils.py deleted file mode 100644 index 96bf879..0000000 --- a/rigour/common/common/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -import json -from dataclasses import asdict -from datetime import datetime - -from common.database.mongodb import Database -from common.types import HostMessage -from loguru import logger - - -class DateTimeEncoder(json.JSONEncoder): - def default(self, obj): # type: ignore - if isinstance(obj, datetime): - return obj.isoformat() - return super().default(obj) - - -def route_key_from_host_message(message: HostMessage, data_type: str) -> str: - return ( - f"{message.host.location.country_code}.{message.port}.{message.ip}.{data_type}" - ) - - -# TODO: move this elsewhere -def save_banner(db: Database, message: HostMessage) -> None: - assert message.host.banner is not None - logger.debug( - f"Saving banner to database for IP: {message.ip}, Port: {message.port}" - ) - - now = datetime.now() - - db.scans.update_one( - {"ip": message.ip}, - { - "$set": { - f"banners.{message.host.banner.service}": asdict(message.host.banner), - "updated_at": now, - } - }, - upsert=True, - ) - - -# TODO: move this elsewhere -def save_vulnerability(db: Database, message: HostMessage) -> None: - assert message.host.vulnerabilities is not None - logger.debug( - f"Saving vulnerabilities to database for IP: {message.ip}, Port: {message.port}" - ) - - now = datetime.now() - - db.scans.update_one( - {"ip": message.ip}, - { - "$set": { - "vulnerabilities": [asdict(v) for v in message.host.vulnerabilities], - "updated_at": now, - } - }, - upsert=True, - ) diff --git a/rigour/common/pyproject.toml b/rigour/common/pyproject.toml deleted file mode 100644 index 2a4fd31..0000000 --- a/rigour/common/pyproject.toml +++ /dev/null @@ -1,34 +0,0 @@ -[project] -name = "common" -version = "0.0.1" -description = "Common code for the Rigour project" -readme = "README.md" -classifiers = [ - "License :: Other/Proprietary License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", -] -requires-python = "~=3.11" -dependencies = [ - "pymongo", - "loguru", - "pika", - "dacite", - "pydantic", - "aiormq", - "msgpack" -] - -[project.optional-dependencies] -dev = [ - -] - -[tool.setuptools.packages.find] -include = ["common*"] - -[build-system] -requires = ["setuptools", "setuptools-scm"] -build-backend = "setuptools.build_meta" diff --git a/rigour/ports/.gitignore b/rigour/ports/.gitignore deleted file mode 100644 index dca10b3..0000000 --- a/rigour/ports/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.mmdb diff --git a/rigour/ports/Dockerfile b/rigour/ports/Dockerfile deleted file mode 100644 index 8a87afa..0000000 --- a/rigour/ports/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# ============================================== -# Stage 1: Build ZMap and ZTee from source -# ============================================== -FROM ubuntu:24.04 as builder - -ENV DEBIAN_FRONTEND=noninteractive -ENV TZ=Etc/UTC - -# Install build dependencies -RUN apt-get update \ - && apt-get install -y \ - build-essential \ - cmake \ - libgmp3-dev \ - gengetopt \ - libpcap-dev \ - flex \ - byacc \ - libjson-c-dev \ - libjudy-dev \ - pkg-config \ - libunistring-dev \ - git \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /usr/local/src - -# Clone the ZMap repository -RUN git clone https://github.com/zmap/zmap.git - -WORKDIR /usr/local/src/zmap - -# Build and install ZMap and ZTee -RUN mkdir -p /opt/zmap \ - && cmake . -DRESPECT_INSTALL_PREFIX_CONFIG=ON \ - && cmake --build . --parallel "$(nproc)" \ - && cmake --install . --prefix "/opt/zmap" - -# =========================================================== -# Stage 2: Create the final image with runtime dependencies -# =========================================================== -FROM ubuntu:24.04 - -LABEL org.opencontainers.image.source="https://github.com/zmap/zmap" - -# Install runtime dependencies and Python -RUN apt-get update \ - && apt-get install -y \ - libpcap0.8 \ - libjson-c5 \ - libjudydebian1 \ - libgmp10 \ - dumb-init \ - python3 \ - python3-pip \ - wget \ - && rm -rf /var/lib/apt/lists/* - -# Copy ZMap and ZTee binaries from the builder stage -COPY --from=builder /opt/zmap /opt/zmap - -# Set the PATH environment variable -ENV PATH="/opt/zmap/sbin:${PATH}" - -WORKDIR /app - -ENV PIP_BREAK_SYSTEM_PACKAGES=1 - -# Install the common module -COPY rigour/common/ /app/lib/common/ -RUN pip install --no-cache-dir -e /app/lib/common/ - -# Install ports dependencies -COPY rigour/ports /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -# Download GEO IP database -RUN chmod +x ./download_geodb.sh && ./download_geodb.sh - -# Set the entry point to run the Python script using dumb-init -ENTRYPOINT ["dumb-init", "python3", "main.py"] diff --git a/rigour/ports/download_geodb.sh b/rigour/ports/download_geodb.sh deleted file mode 100755 index c087352..0000000 --- a/rigour/ports/download_geodb.sh +++ /dev/null @@ -1 +0,0 @@ -wget https://github.com/john-doherty/offline-geo-from-ip/raw/refs/heads/master/database/geolite2-city.mmdb diff --git a/rigour/ports/main.py b/rigour/ports/main.py deleted file mode 100644 index e62a36a..0000000 --- a/rigour/ports/main.py +++ /dev/null @@ -1,72 +0,0 @@ -import asyncio -from dataclasses import asdict -from datetime import datetime - -import geoip2.database -import geoip2.errors -from common import utils -from common.config import Config -from common.database.mongodb import Database -from common.queue.rabbitmq_asyncio import AsyncRabbitMQQueueManager -from common.types import Host, HostMessage, Location -from loguru import logger -from zmap import ZMap, ZMapCommand, ZMapResult - - -def get_location(ip: str, reader: geoip2.database.Reader) -> Location: - try: - geoip = reader.city(ip) - return Location( - country_code=geoip.continent.code, # type: ignore - continent_name=geoip.continent.names.get("en"), - country_name=geoip.country.names.get("en"), - accuracy_radius=geoip.location.accuracy_radius, - latitude=geoip.location.latitude, - longitude=geoip.location.longitude, - ) - except geoip2.errors.AddressNotFoundError: - logger.warning(f"IP {ip} not found in database") - return Location("?") - - -def save(db: Database, message: HostMessage) -> None: - logger.debug(f"Saving host to database for IP: {message.ip}, Port: {message.port}") - now = datetime.now() - db.scans.update_one( - {"ip": message.ip}, - { - "$set": {"location": asdict(message.host.location), "updated_at": now}, - "$setOnInsert": {"first_seen": now}, - }, - upsert=True, - ) - - -def main(): - db = Database() - queue = AsyncRabbitMQQueueManager() - reader = geoip2.database.Reader("geolite2-city.mmdb") - ports = Config.get_ports() - networks = Config.get_networks() - - logger.info(f"Starting port scanner for port/s: {ports}") - - async def callback(result: ZMapResult) -> None: - print(f"Received ZMap result: {result}") - location = get_location(result.saddr, reader) - host = HostMessage(result.saddr, result.sport, Host(location=location)) - - # {country}.{port}.{ip}.port - route_key = utils.route_key_from_host_message(host, "port") - await queue.publish(route_key, asdict(host)) - save(db, host) - - command = ZMapCommand(ports, networks) - zmap = ZMap(command) - loop = asyncio.get_event_loop() - loop.run_until_complete(zmap.run(callback)) - loop.close() - - -if __name__ == "__main__": - main() diff --git a/rigour/ports/requirements.txt b/rigour/ports/requirements.txt deleted file mode 100644 index 3df7f05..0000000 --- a/rigour/ports/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -geoip2 diff --git a/rigour/ports/zmap.py b/rigour/ports/zmap.py deleted file mode 100644 index ddfcaea..0000000 --- a/rigour/ports/zmap.py +++ /dev/null @@ -1,40 +0,0 @@ -from dataclasses import dataclass - -from common.subprocess import AsyncSubprocessBase - - -class ZMapCommand: - def __init__(self, ports: str, networks: str): - self.ports = ports - self.networks = networks - - def build(self): - return [ - "zmap", - "-p", - self.ports, - "--output-module=json", # Output in JSON format - "--quiet", # Suppress status updates - "--rate=200", # Send 100 packets per second - '--output-filter="success = 1"', # Filter successful results - self.networks, - ] - - -@dataclass -class ZMapResult: - saddr: str - sport: int - - -class ZMap(AsyncSubprocessBase[ZMapResult]): - def __init__(self, command: ZMapCommand): - super().__init__(command, enable_piping=False) - - async def _parse_result(self, result: dict) -> ZMapResult: - # Source port is not added when only one port is scanned - # so it's added manually here - if "," not in self.command.ports and "-" not in self.command.ports: - result["sport"] = int(self.command.ports) - - return ZMapResult(**result) diff --git a/rigour/vuln/.gitignore b/rigour/vuln/.gitignore deleted file mode 100644 index 66a0c09..0000000 --- a/rigour/vuln/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dbs/* diff --git a/rigour/vuln/Dockerfile b/rigour/vuln/Dockerfile deleted file mode 100644 index 35f17c4..0000000 --- a/rigour/vuln/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3.12-alpine3.20 - -WORKDIR /app - -# Install the common module -COPY rigour/common/ /app/lib/common/ -RUN pip install --no-cache-dir -e /app/lib/common/ - -# Install ports dependencies -COPY rigour/vuln /app/ -RUN pip install --no-cache-dir --upgrade -r ./requirements.txt - -# Download vulnerability databases -RUN chmod +x ./download_vuln_dbs.sh && ./download_vuln_dbs.sh - -ENTRYPOINT ["python3", "main.py"] diff --git a/rigour/vuln/README.md b/rigour/vuln/README.md deleted file mode 100644 index 768d572..0000000 --- a/rigour/vuln/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Vuln Finder - -Finds vulnerable hosts by checking response header against vulnerability databases diff --git a/rigour/vuln/download_vuln_dbs.sh b/rigour/vuln/download_vuln_dbs.sh deleted file mode 100755 index 5ea7c61..0000000 --- a/rigour/vuln/download_vuln_dbs.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -data_dir="dbs" -databases="cve exploitdb openvas osvdb scipvuldb securityfocus securitytracker xforce" - -mkdir -p $data_dir - -for DB in $databases; do - wget https://www.computec.ch/projekte/vulscan/download/${DB}.csv -O ${data_dir}/${DB}.csv - - if [ -f ${DB}.csv.1 ]; then - mv ./${data_dir}/${DB}.csv.1 ./${data_dir}/${DB}.csv - fi -done diff --git a/rigour/vuln/main.py b/rigour/vuln/main.py deleted file mode 100644 index 2c18809..0000000 --- a/rigour/vuln/main.py +++ /dev/null @@ -1,78 +0,0 @@ -import asyncio -import re -from dataclasses import asdict - -from common import utils -from common.database.mongodb import Database -from common.queue.rabbitmq_asyncio import AsyncRabbitMQQueueManager -from common.types import HostMessage -from dacite import from_dict -from loguru import logger -from vuln_detector import VulnerabilityDetector - - -class VulnScanner: - def __init__(self) -> None: - self.db = Database() - self.queue = AsyncRabbitMQQueueManager() - self.detector = VulnerabilityDetector() - self.software_version_pattern = re.compile(r"(\w+)[/ ]([\d.]+)") - - async def listen(self) -> None: - # {country}.{port}.{ip}.banner - routing_key = "#.#.#.banner" - await self.queue.consume(routing_key=routing_key, callback=self.process_banners) - - async def process_banners(self, port_message: dict) -> None: - logger.debug(f"Received RabbitMQ message: {port_message}") - message = from_dict(data_class=HostMessage, data=port_message) - - assert message.host.banner is not None - if message.host.banner.service != "http": - logger.debug(f"Skipping non-HTTP service: {message.host.banner.service}") - return - - server_header = self.get_server_header(message) - if server_header is None: - logger.debug("Skipping as server header not in HTTP response") - return - - software = self.get_software_version(server_header) - if not software: - logger.debug("Skipping as software version not found in server header") - return - - vulnerabilities = self.detector.scan(*software) - if not vulnerabilities: - logger.debug("No vulnerabilities found") - return - - message.host.vulnerabilities = vulnerabilities - - await self.publish(message) - utils.save_vulnerability(self.db, message) - - def get_server_header(self, message: HostMessage) -> str | None: - assert message.host.banner is not None - try: - return message.host.banner.data["result"]["response"]["headers"]["server"][ - 0 - ] - except (KeyError, TypeError, IndexError): - return None - - def get_software_version(self, server_header: str) -> tuple[str, str] | None: - match = self.software_version_pattern.search(server_header) - if match is None: - return None - return match.groups() # type: ignore - - async def publish(self, message: HostMessage) -> None: - route_key = utils.route_key_from_host_message(message, "vuln") - await self.queue.publish(route_key, asdict(message)) - - -if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(VulnScanner().listen()) - loop.run_forever() diff --git a/rigour/vuln/manifest.json b/rigour/vuln/manifest.json deleted file mode 100644 index 6c319e0..0000000 --- a/rigour/vuln/manifest.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "name": "VulDB", - "file": "scipvuldb.csv", - "url": "https://vuldb.com", - "link": "https://vuldb.com/id.{id}" - }, - { - "name": "MITRE CVE", - "file": "cve.csv", - "url": "https://cve.mitre.org", - "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name={id}" - }, - { - "name": "SecurityFocus", - "file": "securityfocus.csv", - "url": "https://www.securityfocus.com/bid/", - "link": "https://www.securityfocus.com/bid/{id}" - }, - { - "name": "IBM X-Force", - "file": "xforce.csv", - "url": "https://exchange.xforce.ibmcloud.com", - "link": "https://exchange.xforce.ibmcloud.com/vulnerabilities/{id}" - }, - { - "name": "Exploit-DB", - "file": "exploitdb.csv", - "url": "https://www.exploit-db.com", - "link": "https://www.exploit-db.com/exploits/{id}" - }, - { - "name": "OpenVAS (Nessus)", - "file": "openvas.csv", - "url": "http://www.openvas.org", - "link": "https://www.tenable.com/plugins/nessus/{id}" - }, - { - "name": "SecurityTracker", - "file": "securitytracker.csv", - "url": "https://www.securitytracker.com", - "link": "https://www.securitytracker.com/id/{id}" - }, - { - "name": "OSVDB", - "file": "osvdb.csv", - "url": "http://www.osvdb.org", - "link": "http://www.osvdb.org/{id}" - } -] diff --git a/rigour/vuln/requirements.txt b/rigour/vuln/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/rigour/vuln/vuln_detector.py b/rigour/vuln/vuln_detector.py deleted file mode 100644 index 734dd5f..0000000 --- a/rigour/vuln/vuln_detector.py +++ /dev/null @@ -1,64 +0,0 @@ -import csv -import json -import os -import re -from typing import TypedDict - -from common.types import Vulnerability - - -class ManifestDB(TypedDict): - name: str - file: str - url: str - link: str - - -class VulnerabilityDetector: - def __init__( - self, database_dir: str = "dbs", manifest_file: str = "manifest.json" - ) -> None: - self.database_dir = database_dir - self.databases = [ManifestDB(**db) for db in json.load(open(manifest_file))] - - def scan(self, product: str, version: str) -> list[Vulnerability]: - results = [] - for db in self.databases: - results += self.find_vulnerabilities(product, version, db) - - return results - - def find_vulnerabilities( - self, product: str, version: str, db: ManifestDB - ) -> list[Vulnerability]: - vulnerabilities = [] - db_path = os.path.join(self.database_dir, db["file"]) - - with open(db_path, encoding="ISO-8859-1") as file: - reader = csv.reader(file, delimiter=";") - for row in reader: - vuln_id, vuln_title = row[0], row[1] - if self.match_product(vuln_title, product) and self.match_version( - vuln_title, version - ): - vulnerabilities.append( - Vulnerability( - name=vuln_id, - title=vuln_title, - version=version, - link=db["link"].replace("{id}", vuln_id), - ) - ) - - return vulnerabilities - - def match_product(self, vuln_title: str, product: str) -> bool: - product_keywords = product.lower().split() - for keyword in product_keywords: - if keyword in vuln_title.lower(): - return True - return False - - def match_version(self, vuln_title: str, version: str) -> bool: - version_pattern = re.compile(rf"\b{re.escape(version)}\b") - return bool(re.search(version_pattern, vuln_title)) diff --git a/scanner/cmd/rigour/main.go b/scanner/cmd/rigour/main.go new file mode 100644 index 0000000..96def61 --- /dev/null +++ b/scanner/cmd/rigour/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "encoding/json" + "fmt" + "net" + "os" + "os/user" + "runtime" + "time" + + "github.com/ctrlsam/rigour/pkg/discovery" + "github.com/ctrlsam/rigour/pkg/fingerprint" + "github.com/ctrlsam/rigour/pkg/scan" + "github.com/spf13/cobra" +) + +type cliConfig struct { + fastMode bool + timeout int + useUDP bool + verbose bool + stream bool + + // Discovery Naabu settings + scanType string + ports string + topPorts string + retries int + rate int +} + +var ( + config cliConfig + rootCmd = &cobra.Command{ + Use: "rigour [flags]\nTARGET SPECIFICATION:\n\tRequires an ip address or CIDR range\n" + + "EXAMPLES:\n\trigour 192.168.1.0/24\n", + RunE: func(cmd *cobra.Command, args []string) error { + configErr := checkConfig(config) + if configErr != nil { + return configErr + } + + cidrRange := args[0] + + // Quick estimate of number of IPs in the range. + _, ipnet, _ := net.ParseCIDR(cidrRange) + ones, bits := ipnet.Mask.Size() + numIPs := 1 << (bits - ones) + fmt.Printf("[+] Scanning %d IPs in range %s\n", numIPs, cidrRange) + + if config.stream { + onEvent := func(ev scan.ScanEvent) { + b, err := json.MarshalIndent(ev, "", " ") + if err != nil { + // Streaming should never abort the whole scan due to a single marshal failure. + fmt.Fprintf(os.Stderr, "failed to marshal event: %v\n", err) + return + } + // Print one pretty JSON object at a time. + _, _ = os.Stdout.Write(append(b, '\n')) + } + + err := scan.ScanTargetWithDiscoveryStream(cidrRange, createDiscoveryConfig(config), createScanConfig(config), onEvent) + if err != nil { + return fmt.Errorf("Failed running discovery+scan stream (%w)", err) + } + return nil + } + + results, err := scan.ScanTargetWithDiscovery(cidrRange, createDiscoveryConfig(config), createScanConfig(config)) + if err != nil { + return fmt.Errorf("Failed running discovery+scan (%w)", err) + } + + b, err := json.MarshalIndent(results, "", " ") + if err != nil { + return fmt.Errorf("Failed to marshal results (%w)", err) + } + fmt.Println(string(b)) + + return nil + }, + } +) + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + + rootCmd.PersistentFlags().BoolVarP(&config.fastMode, "fast", "f", false, "fast mode") + rootCmd.PersistentFlags(). + BoolVarP(&config.useUDP, "udp", "U", false, "run UDP plugins") + + rootCmd.PersistentFlags().BoolVarP(&config.verbose, "verbose", "v", false, "verbose mode") + rootCmd.PersistentFlags().BoolVar(&config.stream, "stream", true, "stream results as NDJSON as services are identified") + rootCmd.PersistentFlags(). + IntVarP(&config.timeout, "timeout", "w", 2000, "timeout (milliseconds)") + + // Discovery flags (Naabu). These control how rigour discovers open ports. + rootCmd.PersistentFlags().StringVar(&config.scanType, "scan-type", "c", "discovery scan type (naabu; e.g. c=connect)") + rootCmd.PersistentFlags().StringVar(&config.ports, "ports", "", "ports list (e.g. 80,443). If set, overrides top ports") + rootCmd.PersistentFlags().StringVar(&config.topPorts, "top-ports", "100", "top ports (e.g. 100, 1000, full)") // full + rootCmd.PersistentFlags().IntVar(&config.retries, "retries", 3, "discovery retries") + rootCmd.PersistentFlags().IntVar(&config.rate, "rate", 50_000, "discovery rate (packets per second)") +} + +func checkConfig(config cliConfig) error { + if config.useUDP && config.verbose { + user, err := user.Current() + if err != nil { + return fmt.Errorf("Failed to retrieve current user (error: %w)", err) + } + if !((runtime.GOOS == "linux" || runtime.GOOS == "darwin") && user.Uid == "0") { + fmt.Fprintln(os.Stderr, "Note: UDP Scan may require root privileges") + } + } + + return nil +} + +func createScanConfig(config cliConfig) fingerprint.FingerprintConfig { + return fingerprint.FingerprintConfig{ + DefaultTimeout: time.Duration(config.timeout) * time.Millisecond, + FastMode: config.fastMode, + UDP: config.useUDP, + Verbose: config.verbose, + } +} + +func createDiscoveryConfig(config cliConfig) discovery.DiscoveryConfig { + return discovery.DiscoveryConfig{ + ScanType: config.scanType, + Ports: config.ports, + TopPorts: config.topPorts, + Retries: config.retries, + Rate: config.rate, + } +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/scanner/go.mod b/scanner/go.mod new file mode 100644 index 0000000..d572274 --- /dev/null +++ b/scanner/go.mod @@ -0,0 +1,161 @@ +module github.com/ctrlsam/rigour + +go 1.24.0 + +require ( + github.com/ory/dockertest/v3 v3.9.1 + github.com/projectdiscovery/goflags v0.1.74 + github.com/projectdiscovery/naabu/v2 v2.3.7 + github.com/projectdiscovery/wappalyzergo v0.2.17 + github.com/spf13/cobra v1.5.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.45.0 + golang.org/x/term v0.37.0 +) + +require ( + aead.dev/minisign v0.2.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect + github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/Ullaakut/nmap/v3 v3.0.6 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/akrylysov/pogreb v0.10.1 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/charmbracelet/glamour v0.8.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.3.2 // indirect + github.com/cheggaaa/pb/v3 v3.1.4 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/cli v20.10.17+incompatible // indirect + github.com/docker/docker v20.10.17+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/gaissmai/bart v0.26.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-github/v30 v30.1.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gopacket/gopacket v1.2.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mholt/archives v0.1.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/miekg/dns v1.1.62 // indirect + github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.3 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/projectdiscovery/asnmap v1.1.1 // indirect + github.com/projectdiscovery/blackrock v0.0.1 // indirect + github.com/projectdiscovery/cdncheck v1.2.10 // indirect + github.com/projectdiscovery/clistats v0.1.1 // indirect + github.com/projectdiscovery/dnsx v1.2.2 // indirect + github.com/projectdiscovery/fastdialer v0.4.16 // indirect + github.com/projectdiscovery/freeport v0.0.7 // indirect + github.com/projectdiscovery/gologger v1.1.60 // indirect + github.com/projectdiscovery/hmap v0.0.95 // indirect + github.com/projectdiscovery/ipranger v0.0.53 // indirect + github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect + github.com/projectdiscovery/mapcidr v1.1.97 // indirect + github.com/projectdiscovery/networkpolicy v0.1.28 // indirect + github.com/projectdiscovery/ratelimit v0.0.82 // indirect + github.com/projectdiscovery/retryabledns v1.0.108 // indirect + github.com/projectdiscovery/retryablehttp-go v1.0.131 // indirect + github.com/projectdiscovery/uncover v1.1.0 // indirect + github.com/projectdiscovery/utils v0.7.0 // indirect + github.com/refraction-networking/utls v1.7.1 // indirect + github.com/remeh/sizedwaitgroup v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/shirou/gopsutil/v3 v3.23.7 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/btree v1.6.0 // indirect + github.com/tidwall/buntdb v1.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/grect v0.1.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/rtred v0.1.2 // indirect + github.com/tidwall/tinyqueue v0.1.1 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/yl2chen/cidranger v1.0.2 // indirect + github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark-emoji v1.0.3 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zcalusic/sysinfo v1.0.2 // indirect + github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect + github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518 // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gopkg.in/djherbis/times.v1 v1.3.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/scanner/go.sum b/scanner/go.sum new file mode 100644 index 0000000..1c479d3 --- /dev/null +++ b/scanner/go.sum @@ -0,0 +1,757 @@ +aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= +aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= +github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= +github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/Ullaakut/nmap/v3 v3.0.6 h1:ZCQ70TQp97f/YqIFhlzFMDi5xVDeA0CwMbNeJZGA//A= +github.com/Ullaakut/nmap/v3 v3.0.6/go.mod h1:dd5K68P7LHc5nKrFwQx6EdTt61O9UN5x3zn1R4SLcco= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= +github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= +github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= +github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= +github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= +github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= +github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= +github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gaissmai/bart v0.26.0 h1:xOZ57E9hJLBiQaSyeZa9wgWhGuzfGACgqp4BE77OkO0= +github.com/gaissmai/bart v0.26.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= +github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= +github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= +github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= +github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kID2iwsDqI= +github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60= +github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= +github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= +github.com/projectdiscovery/cdncheck v1.2.10 h1:Ox86LS8RFjq6pYNTP3Eqdawlor/h+bnb7BTEKBpzFyM= +github.com/projectdiscovery/cdncheck v1.2.10/go.mod h1:ibL9HoZs2JYTEUBOZo4f+W+XEzQifFLOf4bpgFStgj4= +github.com/projectdiscovery/clistats v0.1.1 h1:8mwbdbwTU4aT88TJvwIzTpiNeow3XnAB72JIg66c8wE= +github.com/projectdiscovery/clistats v0.1.1/go.mod h1:4LtTC9Oy//RiuT1+76MfTg8Hqs7FQp1JIGBM3nHK6a0= +github.com/projectdiscovery/dnsx v1.2.2 h1:ZjUov0GOyrS8ERlKAAhk+AOkqzaYHBzCP0qZfO+6Ihg= +github.com/projectdiscovery/dnsx v1.2.2/go.mod h1:3iYm86OEqo0WxeGDkVl5WZNmG0qYE5TYNx8fBg6wX1I= +github.com/projectdiscovery/fastdialer v0.4.16 h1:rmCNr5N/9KTm0nSYjSuQ5j3aXmNIPf6HhJlAhN/7NRI= +github.com/projectdiscovery/fastdialer v0.4.16/go.mod h1:X0l4+KqOE/aIL00pyTnBj4pWQDPYnCGL7cwZsJu6SCQ= +github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk= +github.com/projectdiscovery/freeport v0.0.7/go.mod h1:cOhWKvNBe9xM6dFJ3RrrLvJ5vXx2NQ36SecuwjenV2k= +github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= +github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= +github.com/projectdiscovery/gologger v1.1.60 h1:N2Zyu4WA2RgUeqSAdfhv/CLS4de8lDDc2+IdLKcAd5U= +github.com/projectdiscovery/gologger v1.1.60/go.mod h1:8FJFKmo0N4ITIH3n1Jy4ze6ijr+mA3t78g+VpN8uBRU= +github.com/projectdiscovery/hmap v0.0.95 h1:OO6MCySlK2xMzvJmsYUwdaI7YWv/U437OtsN0Ovw72k= +github.com/projectdiscovery/hmap v0.0.95/go.mod h1:KiTRdGd/GzX7uaoFWPrPBxPf4X/uZ9HTQ9dQ8x7x1bo= +github.com/projectdiscovery/ipranger v0.0.53 h1:gb4yEqtC2MJl1tSdx/ycao1A1wl7sHqjHeifZidO3Z4= +github.com/projectdiscovery/ipranger v0.0.53/go.mod h1:r6R0DFKQRo4QR2zjZXqLRCp0ovbco8F/NmOI+pK4db8= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 h1:ZScLodGSezQVwsQDtBSMFp72WDq0nNN+KE/5DHKY5QE= +github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI= +github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8QuWs2puwy+2w= +github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA= +github.com/projectdiscovery/naabu/v2 v2.3.7 h1:DFADMDWgaSDRBEOVZtZZ8DTb7ugewjSqptDlij43uBs= +github.com/projectdiscovery/naabu/v2 v2.3.7/go.mod h1:GsQWK3EzLla0+a+sMczqyswK4tb0ya7l5DIbZbRtV4c= +github.com/projectdiscovery/networkpolicy v0.1.28 h1:Rwg8iZmM4n+CRWyUClthaSrTqDAW8zBI2HULRO1CF3k= +github.com/projectdiscovery/networkpolicy v0.1.28/go.mod h1:/3XfgnxKNuxaTZc6wZ/Pq6fiKvK8N4OQyLmfcUeDk2E= +github.com/projectdiscovery/ratelimit v0.0.82 h1:rtO5SQf5uQFu5zTahTaTcO06OxmG8EIF1qhdFPIyTak= +github.com/projectdiscovery/ratelimit v0.0.82/go.mod h1:z076BrLkBb5yS7uhHNoCTf8X/BvFSGRxwQ8EzEL9afM= +github.com/projectdiscovery/retryabledns v1.0.108 h1:47LYRW2LY/0cDnZQfUhoOHNxe9rNc9NQ9ZfNrV/GbyM= +github.com/projectdiscovery/retryabledns v1.0.108/go.mod h1:j7H7K6JZePh9PeNleeRUtDSrkUKMpwDhZw3Ogewzio8= +github.com/projectdiscovery/retryablehttp-go v1.0.131 h1:OU2x9fVDIWnDoKvT8tKbaCONTL1gHnTOIFQFXmnEOE0= +github.com/projectdiscovery/retryablehttp-go v1.0.131/go.mod h1:ttW+Zka1L8IwEUhJ4zArbC+pKZum7b47fzV+4VGN6cA= +github.com/projectdiscovery/uncover v1.1.0 h1:UDp/qLZn78YZb6VPoOrfyP1vz+ojEx8VrTTyjjRt9UU= +github.com/projectdiscovery/uncover v1.1.0/go.mod h1:2rXINmMe/lmVAt2jn9CpAOs9An57/JEeLZobY3Z9kUs= +github.com/projectdiscovery/utils v0.7.0 h1:akjTW9tt2QTv3BCiXlqSf7OfodrqrcaIQowuF7H1BWw= +github.com/projectdiscovery/utils v0.7.0/go.mod h1:j4Fb6PDir9PcTxLOL9cpSVDPVKtLTZwdVxxMAeG0JjA= +github.com/projectdiscovery/wappalyzergo v0.2.17 h1:pYRBRJhR0Wuvx6O08DuicDZVltq1kbSlI5xr8nHXq/0= +github.com/projectdiscovery/wappalyzergo v0.2.17/go.mod h1:F8X79ljvmvrG+EIxdxWS9VbdkVTsQupHYz+kXlp8O0o= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/refraction-networking/utls v1.7.1 h1:dxg+jla3uocgN8HtX+ccwDr68uCBBO3qLrkZUbqkcw0= +github.com/refraction-networking/utls v1.7.1/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ= +github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= +github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= +github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= +github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= +github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39 h1:Bz/zVM/LoGZ9IztGBHrq2zlFQQbEG8dBYnxb4hamIHM= +github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39/go.mod h1:2oFzEwGYI7lhiqG0YkkcKa6VcpjVinQbWxaPzytDmLA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= +github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc= +github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= +github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518 h1:O8GHQBxrphDuNhJQdKBHwP3JQUtZUyi3b+jjPYmF7oA= +github.com/zmap/zcrypto v0.0.0-20230814193918-dbe676986518/go.mod h1:Z2SNNuFhO+AAsezbGEHTWeW30hHv5niUYT3fwJ61Nl0= +github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= +gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/scanner/pkg/discovery/config.go b/scanner/pkg/discovery/config.go new file mode 100644 index 0000000..acde66e --- /dev/null +++ b/scanner/pkg/discovery/config.go @@ -0,0 +1,10 @@ +package discovery + +type DiscoveryConfig struct { + ScanType string + Ports string + TopPorts string + Retries int + Rate int + Silent bool +} diff --git a/scanner/pkg/discovery/naabu/naabu.go b/scanner/pkg/discovery/naabu/naabu.go new file mode 100644 index 0000000..7399eaa --- /dev/null +++ b/scanner/pkg/discovery/naabu/naabu.go @@ -0,0 +1,93 @@ +package naabu + +import ( + "context" + "fmt" + "strings" + + "github.com/projectdiscovery/goflags" + naabuResult "github.com/projectdiscovery/naabu/v2/pkg/result" + naabuRunner "github.com/projectdiscovery/naabu/v2/pkg/runner" +) + +type Options struct { + ScanType string + Ports string + TopPorts string + Interface string + Retries int + Rate int + Stream bool +} + +type OSFingerprint struct { + Target string + DeviceType string + Running string + OSCPE string + OSDetails string +} + +type Result struct { + Host string + Port int + Protocol string + Confidence int + OSFingerprint *OSFingerprint + MacAddress string +} + +// Run executes Naabu discovery for a single input target and invokes onResult +// for each open port found. +func Run(ctx context.Context, ipRange string, opts Options, onResult func(Result)) error { + if strings.TrimSpace(ipRange) == "" { + return fmt.Errorf("naabu discovery input is empty") + } + + naabuOpts := &naabuRunner.Options{ + Host: goflags.StringSlice{ipRange}, + // caller-configurable + ScanType: opts.ScanType, + Ports: opts.Ports, + TopPorts: opts.TopPorts, + Rate: opts.Rate, + Retries: opts.Retries, + //Silent: true, + } + + naabuOpts.OnReceive = func(hr *naabuResult.HostResult) { + for _, p := range hr.Ports { + //fmt.Println("[DISCOVERY] Open port found:", hr.IP, p.Port) + onResult(Result{ + Host: hr.IP, + Port: p.Port, + Protocol: "tcp", + Confidence: int(hr.Confidence), + OSFingerprint: (*OSFingerprint)(hr.OS), + MacAddress: hr.MacAddress, + }) + } + } + + r, err := naabuRunner.NewRunner(naabuOpts) + if err != nil { + return fmt.Errorf("naabu.NewRunner failed: %w", err) + } + defer r.Close() + + // Naabu runner is not fully context-aware; honour ctx by stopping early if canceled. + done := make(chan error, 1) + go func() { + done <- r.RunEnumeration(ctx) + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-done: + if err != nil { + return fmt.Errorf("naabu enumeration failed: %w", err) + } + return nil + } +} diff --git a/scanner/pkg/fingerprint/config.go b/scanner/pkg/fingerprint/config.go new file mode 100644 index 0000000..76c64f7 --- /dev/null +++ b/scanner/pkg/fingerprint/config.go @@ -0,0 +1,19 @@ +package fingerprint + +import ( + "time" +) + +type FingerprintConfig struct { + // UDP scan + UDP bool + + FastMode bool + + // The timeout specifies how long certain tasks should wait during the scanning process. + // This may include the timeouts set on the handshake process and the time to wait for a response to return. + DefaultTimeout time.Duration + + // Prints logging messages to stderr + Verbose bool +} diff --git a/scanner/pkg/fingerprint/plugin_list.go b/scanner/pkg/fingerprint/plugin_list.go new file mode 100644 index 0000000..7bb11a8 --- /dev/null +++ b/scanner/pkg/fingerprint/plugin_list.go @@ -0,0 +1,43 @@ +package fingerprint + +// These import statements ensure that the init functions run in each plugin. +// When a new plugin is added, this list should be updated. + +import ( + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/dhcp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/dns" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/echo" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ftp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/http" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/imap" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ipmi" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ipsec" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/jdwp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/kafka/kafkaNew" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/kafka/kafkaOld" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ldap" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/linuxrpc" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/minecraft/java" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/modbus" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/mqtt/mqtt3" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/mqtt/mqtt5" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/mssql" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/mysql" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/netbios" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ntp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/openvpn" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/oracledb" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/pop3" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/postgresql" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/rdp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/redis" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/rsync" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/rtsp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/smb" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/smtp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/snmp" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/ssh" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/stun" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/telnet" + _ "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/services/vnc" +) diff --git a/scanner/pkg/fingerprint/plugins/plugins.go b/scanner/pkg/fingerprint/plugins/plugins.go new file mode 100644 index 0000000..9c7a221 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/plugins.go @@ -0,0 +1,54 @@ +package plugins + +import "fmt" + +var Plugins = make(map[Protocol][]Plugin) +var pluginIDs = make(map[PluginID]bool) + +// This function must not be run concurrently. +// This function should only be run once per plugin. +func RegisterPlugin(p Plugin) { + id := CreatePluginID(p) + if pluginIDs[id] { + panic(fmt.Sprintf("plugin: Register called twice for driver %+v\n", id)) + } + + pluginIDs[id] = true + + var pluginList []Plugin + if list, exists := Plugins[p.Type()]; exists { + pluginList = list + } else { + pluginList = make([]Plugin, 0) + } + + Plugins[p.Type()] = append(pluginList, p) +} + +func (p Protocol) String() (s string) { + switch p { + case IP: + s = "IP" + case TCP: + s = "TCP" + case TCPTLS: + s = "TCPTLS" + case UDP: + s = "UDP" + default: + panic("No string name for protocol %d.") + } + + return +} + +func CreatePluginID(p Plugin) PluginID { + return PluginID{ + name: p.Name(), + protocol: p.Type(), + } +} + +func (p PluginID) String() string { + return fmt.Sprintf("%s/%v", p.protocol, p.name) +} diff --git a/scanner/pkg/fingerprint/plugins/pluginutils/error.go b/scanner/pkg/fingerprint/plugins/pluginutils/error.go new file mode 100644 index 0000000..de66f75 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/pluginutils/error.go @@ -0,0 +1,134 @@ +package pluginutils + +import "fmt" + +type RandomizeError struct { + Message string +} + +type InvalidResponseError struct { + Service string +} + +type InvalidResponseErrorInfo struct { + Service string + Info string +} + +type WriteTimeoutError struct { + WrappedError error +} + +type ReadTimeoutError struct { + WrappedError error +} + +type WriteError struct { + WrappedError error +} + +type ReadError struct { + Info string + WrappedError error +} + +type CreateDialError struct { + Message string +} + +type CloseDialError struct { +} + +type RequestError struct { + Message string +} + +type ServerNotEnable struct { +} + +type InvalidAddrProvided struct { + Service string +} + +func (e *RandomizeError) Error() string { + return fmt.Sprintf("failed to generate random bytes [%s]", e.Message) +} + +func (e *InvalidResponseError) Error() string { + return fmt.Sprintf("invalid %s response", e.Service) +} + +func (e *InvalidResponseErrorInfo) Error() string { + return fmt.Sprintf("invalid %s response, %s", e.Service, e.Info) +} + +func (e *WriteTimeoutError) Error() string { + errString := "failed to set timeout value for write" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *WriteTimeoutError) Unwrap() error { + return e.WrappedError +} + +func (e *ReadTimeoutError) Error() string { + errString := "failed to set timeout value for read" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *ReadTimeoutError) Unwrap() error { + return e.WrappedError +} + +func (e *WriteError) Error() string { + errString := "failed to send out packet" + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *WriteError) Unwrap() error { + return e.WrappedError +} + +func (e *ReadError) Error() string { + errString := "failed to receive packet" + if len(e.Info) > 0 { + errString = fmt.Sprintf("%s (Info: %s)", errString, e.Info) + } + if e.WrappedError != nil { + errString = fmt.Sprintf("%s (Error: %s)", errString, e.WrappedError.Error()) + } + return errString +} + +func (e *ReadError) Unwrap() error { + return e.WrappedError +} + +func (e *CreateDialError) Error() string { + return fmt.Sprintf("failed to create connection: %s", e.Message) +} + +func (e *CloseDialError) Error() string { + return "failed to close connection" +} + +func (e *RequestError) Error() string { + return fmt.Sprintf("failed to send request, %s", e.Message) +} + +func (e *ServerNotEnable) Error() string { + return "server is not enabled" +} + +func (e *InvalidAddrProvided) Error() string { + return fmt.Sprintf("a valid address is required for %s service", e.Service) +} diff --git a/scanner/pkg/fingerprint/plugins/pluginutils/requests.go b/scanner/pkg/fingerprint/plugins/pluginutils/requests.go new file mode 100644 index 0000000..d78079d --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/pluginutils/requests.go @@ -0,0 +1,60 @@ +package pluginutils + +import ( + "encoding/hex" + "errors" + "fmt" + "net" + "syscall" + "time" +) + +func Send(conn net.Conn, data []byte, timeout time.Duration) error { + err := conn.SetWriteDeadline(time.Now().Add(timeout)) + if err != nil { + return &WriteTimeoutError{WrappedError: err} + } + length, err := conn.Write(data) + if err != nil { + return &WriteError{WrappedError: err} + } + if length < len(data) { + return &WriteError{ + WrappedError: fmt.Errorf( + "Failed to write all bytes (%d bytes written, %d bytes expected)", + length, + len(data), + ), + } + } + return nil +} + +func Recv(conn net.Conn, timeout time.Duration) ([]byte, error) { + response := make([]byte, 4096) + err := conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return []byte{}, &ReadTimeoutError{WrappedError: err} + } + length, err := conn.Read(response) + if err != nil { + var netErr net.Error + if (errors.As(err, &netErr) && netErr.Timeout()) || + errors.Is(err, syscall.ECONNREFUSED) { // timeout error or connection refused + return []byte{}, nil + } + return response[:length], &ReadError{ + Info: hex.EncodeToString(response[:length]), + WrappedError: err, + } + } + return response[:length], nil +} + +func SendRecv(conn net.Conn, data []byte, timeout time.Duration) ([]byte, error) { + err := Send(conn, data, timeout) + if err != nil { + return []byte{}, err + } + return Recv(conn, timeout) +} diff --git a/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp.go b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp.go new file mode 100644 index 0000000..69ab031 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp.go @@ -0,0 +1,368 @@ +package dhcp + +import ( + "bytes" + "crypto/rand" + "fmt" + "math/big" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const DHCP = "dhcp" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func getSignatures() map[int]string { + signature := map[int]string{ + 0: "Pad", + 1: "Subnet Mask", + 2: "Time Offset", + 3: "Router", + 4: "Time Server", + 5: "Name Server", + 6: "Domain Server", + 7: "Log Server", + 8: "Quotes Server", + 9: "LPR Server", + 10: "Impress Server", + 11: "RLP Server", + 12: "Hostname", + 13: "Boot File Size", + 14: "Merit Dump File", + 15: "Domain Name", + 16: "Swap Server", + 17: "Root Path", + 18: "Extension File", + 19: "Forward On/Off", + 20: "SrcRte On/Off", + 21: "Policy Filter", + 22: "Max DG Assembly", + 23: "Default IP TTL", + 24: "MTU Timeout", + 25: "MTU Plateau", + 26: "MTU Interface", + 27: "MTU Subnet", + 28: "Broadcast Address", + 29: "Mask Discovery", + 30: "Mask Supplier", + 31: "Router Discovery", + 32: "Router Request", + 33: "Static Route", + 34: "Trailers", + 35: "ARP Timeout", + 36: "Ethernet", + 37: "Default TCP TTL", + 38: "Keepalive Time", + 39: "Keepalive Data", + 40: "NIS Domain", + 41: "NIS Servers", + 42: "NTP Servers", + 43: "Vendor Specific", + 44: "NETBIOS Name Srv", + 45: "NETBIOS Dist Srv", + 46: "NETBIOS Node Type", + 47: "NETBIOS Scope", + 48: "X Window Font", + 49: "X Window Manager", + 50: "Address Request", + 51: "Address Time", + 52: "Overload", + 53: "DHCP Msg Type", + 54: "DHCP Server Id", + 55: "Parameter List", + 56: "DHCP Message", + 57: "DHCP Max Msg Size", + 58: "Renewal Time", + 59: "Rebinding Time", + 60: "Class Id", + 61: "Client Id", + 62: "NetWare/IP Domain", + 63: "NetWare/IP Option", + 64: "NIS-Domain-Name", + 65: "NIS-Server-Addr", + 66: "Server-Name", + 67: "Bootfile-Name", + 68: "Home-Agent-Addrs", + 69: "SMTP-Server", + 70: "POP3-Server", + 71: "NNTP-Server", + 72: "WWW-Server", + 73: "Finger-Server", + 74: "IRC-Server", + 75: "StreetTalk-Server", + 76: "STDA-Server", + 77: "User-Class", + 78: "Directory Agent", + 79: "Service Scope", + 80: "Rapid Commit", + 81: "Client FQDN", + 82: "Relay Agent Information", + 83: "iSNS", + 85: "NDS Servers", + 86: "NDS Tree Name", + 87: "NDS Context", + 88: "BCMCS Controller Domain Name list", + 89: "BCMCS Controller IPv4 address option", + 90: "Authentication", + 91: "client-last-transaction-time option", + 92: "associated-ip option", + 93: "Client System", + 94: "Client NDI", + 95: "LDAP", + 97: "UUID/GUID", + 98: "User-Auth", + 99: "GEOCONF_CIVIC", + 100: "PCode", + 101: "TCode", + 109: "OPTION_DHCP4O6_S46_SADDR", + 112: "Netinfo Address", + 113: "Netinfo Tag", + 114: "URL", + 116: "Auto-Config", + 117: "Name Service Search", + 118: "Subnet Selection Option", + 119: "Domain Search", + 120: "SIP Servers DHCP Option", + 121: "Classless", + 122: "CCC", + 123: "GeoConf Option", + 124: "V-I Vendor", + 125: "V-I Vendor-Specific Information", + 131: "Remote statistics server IP address", + 132: "IEEE 802.1Q VLAN ID", + 133: "IEEE 802.1D/p Layer", + 134: "Diffserv Code Point", + 135: "HTTP Proxy for phone-specific applications", + 136: "OPTION_PANA_AGENT", + 137: "OPTION_V4_LOST", + 138: "OPTION_CAPWAP_AC_V4", + 139: "OPTION-IPv4_Address-MoS", + 140: "OPTION-IPv4_FQDN-MoS", + 141: "SIP UA Configuration Service Domains", + 142: "OPTION-IPv4_Address-ANDSF", + 143: "OPTION_V4_SZTP_REDIRECT", + 144: "GeoLoc", + 145: "FORCERENEW_NONCE_CAPABLE", + 146: "RDNSS Selection", + 151: "status-code", + 152: "base-time", + 153: "start-time-of-state", + 154: "query-start-time", + 155: "query-end-time", + 156: "dhcp-state", + 157: "data-source", + 158: "OPTION_V4_PCP_SERVER", + 160: "DHCP Captive-Portal", + 161: "OPTION_MUD_URL_V4", + 175: "Etherboot", + 176: "IP Telephone", + 209: "Configuration File", + 210: "Path Prefix", + 211: "Reboot Time", + 212: "OPTION_6RD", + 213: "OPTION_V4_ACCESS_DOMAIN", + 220: "Subnet Allocation Option", + 221: "Virtual Subnet Selection", + 255: "End", + } + return signature +} + +func hostnameParse(options []byte) []string { + var ret string + var retList []string + wholePacket := options[2 : 2+int(options[1])] + packet := wholePacket + for len(packet) != 0 { + length := int(packet[0]) + if len(packet) < length+1 { + return retList + } + if length == 0 { + retList = append(retList, ret) + ret = "" + packet = packet[1:] + } else { + ret += string(packet[1 : 1+length]) + packet = packet[1+length:] + if len(packet) == 0 { + break + } + if packet[0] != 0 { + ret += "." + } + if packet[0] == 0xc0 && len(packet) == 2 { + wholePacket = wholePacket[int(packet[1]) : len(wholePacket)-(4+length)] + packet = wholePacket + } + } + } + retList = append(retList, ret) + return retList +} + +func ipParse(options []byte) []string { + ipLen := int(options[1]) / 4 + ipList := options[2 : 2+int(options[1])] + var ipStrList []string + for ipLen != 0 { + ip := fmt.Sprintf("%d.%d.%d.%d", int(ipList[0]), int(ipList[1]), int(ipList[2]), int(ipList[3])) + ipStrList = append(ipStrList, ip) + ipLen-- + ipList = ipList[4:] + } + return ipStrList +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + sliceIP := net.ParseIP("127.0.0.1") + if sliceIP == nil { + return nil, &utils.InvalidAddrProvided{Service: DHCP} + } + + LocalIP := []byte{sliceIP[12], sliceIP[13], sliceIP[14], sliceIP[15]} + InitialConnectionPackage := []byte{ + 0x01, // Message type: Boot Request (1) + 0x01, // Hardware type: Ethernet (0x01) + 0x06, // Hardware address length: 6 + 0x01, // Hops: 1 + } + transactionID := make([]byte, 4) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + InitialConnectionPackage = append(InitialConnectionPackage, transactionID...) + SecondPartConnectionPackage := []byte{ + 0x00, 0x00, // Seconds elapsed: 0 + 0x00, 0x00, // Bootp flags: 0x0000 (Unicast) + } + InitialConnectionPackage = append(InitialConnectionPackage, SecondPartConnectionPackage...) + IPConnectionPackage := []byte{ + 0x00, 0x00, 0x00, 0x00, // Client IP address: 0.0.0.0 + 0x00, 0x00, 0x00, 0x00, // Your (client) IP address: 0.0.0.0 + 0x00, 0x00, 0x00, 0x00, // Next server IP address: 0.0.0.0 + } + InitialConnectionPackage = append(InitialConnectionPackage, IPConnectionPackage...) + InitialConnectionPackage = append(InitialConnectionPackage, LocalIP...) // Relay server IP address: LocalIP + ClientMAC := make([]byte, 6) + _, err = rand.Read(ClientMAC) + if err != nil { + return nil, &utils.RandomizeError{Message: "ClientMAC"} + } + InitialConnectionPackage = append(InitialConnectionPackage, ClientMAC...) + ThirdPartConnectionPackage := []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client hardware address padding + // Server host name + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Boot File name + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x63, 0x82, 0x53, 0x63, // Magic cookie: DHCP + + 0x35, 0x01, 0x01, // Option: (53) DHCP Message Type (Discover) + } + InitialConnectionPackage = append(InitialConnectionPackage, ThirdPartConnectionPackage...) + InitialConnectionPackage = append(InitialConnectionPackage, 0xff) // Option: (255) End + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) < 8 { + return nil, nil + } + // https://ecanet.ir/dhcp-option-list/ + if bytes.Equal(transactionID, response[4:8]) { + if len(response) <= 240 && response[len(response)-1] != 255 { + return nil, nil + } + + signature := getSignatures() + options := response[240:] + + optionList := map[string]any{} + for int(options[0]) != 255 { + if len(options) < int(options[1])+2 { + // packet corruption + payload := plugins.ServiceDHCP{ + Option: fmt.Sprintf("%s", optionList), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + c := int(options[0]) + switch c { + case 51, 58, 59: + optionList[signature[c]] = big.NewInt(0).SetBytes(options[2 : 2+int(options[1])]).Uint64() + case 119: + outList := hostnameParse(options) + optionList[signature[c]] = outList + case 15: + optionList[signature[c]] = string(options[2 : 2+int(options[1])]) + case 1: + ipStrList := ipParse(options) + optionList[signature[c]] = ipStrList + if len(ipStrList) == 1 { + optionList[signature[c]] = ipStrList[0] + } + case 3, 6, 28, 42, 44, 54: + ipStrList := ipParse(options) + optionList[signature[c]] = ipStrList + default: + if int(options[1]) == 1 { + optionList[signature[c]] = int(options[2]) + } else { + optionList[signature[c]] = options[2 : 2+int(options[1])] + } + } + options = options[2+int(options[1]):] + } + payload := plugins.ServiceDHCP{ + Option: fmt.Sprintf("%s", optionList), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 67 +} + +func (p *Plugin) Name() string { + return DHCP +} + +func (p *Plugin) PortReject(u uint16) bool { + return u != 67 +} + +func (p *Plugin) SrcPort() uint16 { + return 67 +} + +func (p *Plugin) Priority() int { + return 100 +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} diff --git a/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp_test.go b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp_test.go new file mode 100644 index 0000000..0b56160 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcp_test.go @@ -0,0 +1,43 @@ +package dhcp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/test" +) + +func TestDHCP(t *testing.T) { + // cwd, err := os.Getwd() + // if err != nil { + // t.Fatalf("failed to get current directory") + // } + // TODO more work is required to get this test working locally + testcases := []test.Testcase{ + // { + // Description: "dhcp", + // Port: 67, + // Protocol: plugins.UDP, + // Expected: func(res *plugins.PluginResults) bool { + // return res != nil + // }, + // RunConfig: dockertest.RunOptions{ + // Repository: "wastrachan/dhcpd", + // Mounts: []string{fmt.Sprintf("%s/dhcpd.conf:/config/dhcpd.conf", cwd)}, + // ExposedPorts: []string{"67/udp"}, + // }, + // }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/dhcp/dhcpd.conf b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcpd.conf new file mode 100644 index 0000000..f6aa36c --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/dhcp/dhcpd.conf @@ -0,0 +1,47 @@ +# dhcpd.conf +# +# Sample configuration file for ISC dhcpd +# + +option domain-name "example.org"; +option domain-name-servers ns1.example.org, ns2.example.org; +default-lease-time 600; +max-lease-time 7200; +#ddns-update-style none; +authoritative; +log-facility local7; + +# No service will be given on this subnet, but declaring it allows the dhcp +# server to listen on this network +subnet 0.0.0.0 netmask 0.0.0.0 { + range 172.17.0.0 172.17.0.255; +} + +# This is a very basic subnet declaration. +subnet 10.254.239.0 netmask 255.255.255.224 { + range 10.254.239.10 10.254.239.20; + option routers rtr-239-0-1.example.org, rtr-239-0-2.example.org; +} + +# A slightly different configuration for an internal subnet. +subnet 10.5.5.0 netmask 255.255.255.224 { + range 10.5.5.26 10.5.5.30; + option domain-name-servers ns1.internal.example.org; + option domain-name "internal.example.org"; + option routers 10.5.5.1; + option broadcast-address 10.5.5.31; + default-lease-time 600; + max-lease-time 7200; +} + +# Fixed IP addresses can also be specified for hosts. These addresses +# should not also be listed as being available for dynamic assignment. +# Hosts for which fixed IP addresses have been specified can boot using +# BOOTP or DHCP. Hosts for which no fixed address is specified can only +# be booted with DHCP, unless there is an address range on the subnet +# to which a BOOTP client is connected which has the dynamic-bootp flag +# set. +host fantasia { + hardware ethernet 08:00:07:26:c0:a5; + fixed-address fantasia.example.com; +} diff --git a/scanner/pkg/fingerprint/plugins/services/dns/dns.go b/scanner/pkg/fingerprint/plugins/services/dns/dns.go new file mode 100644 index 0000000..dc574e6 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/dns/dns.go @@ -0,0 +1,131 @@ +package dns + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const DNS = "dns" + +type UDPPlugin struct{} +type TCPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&UDPPlugin{}) + plugins.RegisterPlugin(&TCPPlugin{}) +} + +func CheckDNS(conn net.Conn, timeout time.Duration) (bool, error) { + for attempts := 0; attempts < 3; attempts++ { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return false, &utils.RandomizeError{Message: "Transaction ID"} + } + + InitialConnectionPackage := append(transactionID, []byte{ //nolint:gocritic + // Transaction ID + 0x01, 0x00, // Flags: 0x0100 Standard query + 0x00, 0x01, // Questions: 1 + 0x00, 0x00, // Answer RRs: 0 + 0x00, 0x00, // Authority RRs: 0 + 0x00, 0x00, // Additional RRs: 0 + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x04, 0x62, 0x69, 0x6e, 0x64, 0x00, // Name: version.bind + 0x00, 0x10, // Type: TXT (Text strings) (16) + 0x00, 0x03, // Class: CH (0x0003) + }...) + + if conn.RemoteAddr().Network() == "tcp" { + InitialConnectionPackage = append([]byte{0x00, 0x1e}, InitialConnectionPackage...) + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return false, err + } + + if len(response) == 0 { + return false, nil + } + + if conn.RemoteAddr().Network() == "udp" { + if !bytes.Equal(transactionID[0:1], response[0:1]) { + return false, nil + } + } + + if conn.RemoteAddr().Network() == "tcp" { + if !bytes.Equal(transactionID[0:1], response[2:3]) { + return false, nil + } + } + } + + return true, nil +} + +func (p *UDPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isDNS, err := CheckDNS(conn, timeout) + if err != nil { + return nil, err + } + + if isDNS { + payload := plugins.ServiceDNS{} + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + + return nil, nil +} + +func (p *UDPPlugin) PortPriority(i uint16) bool { + return i == 53 +} + +func (p UDPPlugin) Name() string { + return DNS +} + +func (p *UDPPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p TCPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isDNS, err := CheckDNS(conn, timeout) + if err != nil { + return nil, err + } + + if isDNS { + payload := plugins.ServiceDNS{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + return nil, nil +} + +func (p TCPPlugin) PortPriority(i uint16) bool { + return i == 53 +} + +func (p TCPPlugin) Name() string { + return DNS +} + +func (p *TCPPlugin) Priority() int { + return 50 +} + +func (p *UDPPlugin) Priority() int { + return 50 +} + +func (p TCPPlugin) Type() plugins.Protocol { + return plugins.TCP +} diff --git a/scanner/pkg/fingerprint/plugins/services/dns/dns_test.go b/scanner/pkg/fingerprint/plugins/services/dns/dns_test.go new file mode 100644 index 0000000..1e1a84e --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/dns/dns_test.go @@ -0,0 +1,40 @@ +package dns + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestDNS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "dns", + Port: 53, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "ruudud/devdns", + Mounts: []string{"/var/run/docker.sock:/var/run/docker.sock:ro"}, + Privileged: true, + }, + }, + } + + var p *UDPPlugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/echo/echo.go b/scanner/pkg/fingerprint/plugins/services/echo/echo.go new file mode 100644 index 0000000..1106e8f --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/echo/echo.go @@ -0,0 +1,62 @@ +package echo + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type EchoPlugin struct{} + +const ECHO = "echo" + +func isEcho(conn net.Conn, timeout time.Duration) (bool, error) { + // Generate a random 64 byte payload + payload := make([]byte, 64) + if _, err := rand.Read(payload); err != nil { + return false, err + } + + response, err := pluginutils.SendRecv(conn, payload, timeout) + if err != nil { + return false, err + } + + // Check if the response matches the payload + isEchoService := bytes.Equal(payload, response) + + return isEchoService, nil +} + +func init() { + plugins.RegisterPlugin(&EchoPlugin{}) +} + +func (p *EchoPlugin) PortPriority(port uint16) bool { + return port == 7 +} + +func (p *EchoPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + if isEcho, err := isEcho(conn, timeout); !isEcho || err != nil { + return nil, nil + } + payload := plugins.ServiceEcho{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *EchoPlugin) Name() string { + return ECHO +} + +func (p *EchoPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *EchoPlugin) Priority() int { + return 1 +} diff --git a/scanner/pkg/fingerprint/plugins/services/echo/echo_test.go b/scanner/pkg/fingerprint/plugins/services/echo/echo_test.go new file mode 100644 index 0000000..17fc968 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/echo/echo_test.go @@ -0,0 +1,41 @@ +package echo + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestEcho(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "echo", + Port: 7, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "itsthenetwork/alpine-ncat", + Cmd: []string{"-e", "/bin/cat", "-k", "-l", "-p", "7"}, + Entrypoint: []string{"/usr/bin/ncat"}, + ExposedPorts: []string{"7"}, + }, + }, + } + + p := &EchoPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ftp/ftp.go b/scanner/pkg/fingerprint/plugins/services/ftp/ftp.go new file mode 100644 index 0000000..7fab1c5 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ftp/ftp.go @@ -0,0 +1,57 @@ +package ftp + +import ( + "net" + "regexp" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +var ftpResponse = regexp.MustCompile(`^\d{3}[- ](.*)\r`) + +const FTP = "ftp" + +type FTPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&FTPPlugin{}) +} + +func (p *FTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + matches := ftpResponse.FindStringSubmatch(string(response)) + if matches == nil { + return nil, nil + } + + payload := plugins.ServiceFTP{ + Banner: string(response), + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *FTPPlugin) PortPriority(i uint16) bool { + return i == 21 +} + +func (p *FTPPlugin) Name() string { + return FTP +} + +func (p *FTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *FTPPlugin) Priority() int { + return 10 +} diff --git a/scanner/pkg/fingerprint/plugins/services/ftp/ftp_test.go b/scanner/pkg/fingerprint/plugins/services/ftp/ftp_test.go new file mode 100644 index 0000000..fe2a48d --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ftp/ftp_test.go @@ -0,0 +1,38 @@ +package ftp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestFTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ftp", + Port: 21, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "panubo/vsftpd", + }, + }, + } + + p := &FTPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/http/http.go b/scanner/pkg/fingerprint/plugins/services/http/http.go new file mode 100644 index 0000000..af86be6 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/http/http.go @@ -0,0 +1,224 @@ +package http + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "syscall" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +type HTTPPlugin struct { + analyzer *wappalyzer.Wappalyze +} +type HTTPSPlugin struct { + analyzer *wappalyzer.Wappalyze +} + +const HTTP = "http" +const HTTPS = "https" +const USERAGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" + +func init() { + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + plugins.RegisterPlugin(&HTTPPlugin{analyzer: wappalyzerClient}) + plugins.RegisterPlugin(&HTTPSPlugin{analyzer: wappalyzerClient}) +} + +var ( + commonHTTPPorts = map[int]struct{}{ + 80: {}, + 3000: {}, + 4567: {}, + 5000: {}, + 8000: {}, + 8001: {}, + 8080: {}, + 8081: {}, + 8888: {}, + 9001: {}, + 9080: {}, + 9090: {}, + 9100: {}, + } + + commonHTTPSPorts = map[int]struct{}{ + 443: {}, + 8443: {}, + 9443: {}, + } +) + +func (p *HTTPPlugin) PortPriority(port uint16) bool { + _, ok := commonHTTPPorts[int(port)] + return ok +} + +func (p *HTTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s", conn.RemoteAddr().String()), nil) + if err != nil { + if errors.Is(err, syscall.ECONNREFUSED) { + return nil, nil + } + return nil, &utils.RequestError{Message: err.Error()} + } + + if target.Host != "" { + req.Host = target.Host + } + + // http client with custom dialier to use the provided net.Conn + client := http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return conn, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req.Header.Set("User-Agent", USERAGENT) + + resp, err := client.Do(req) + if err != nil { + return nil, &utils.RequestError{Message: err.Error()} + } + defer resp.Body.Close() + + technologies, cpes, _ := p.FingerprintResponse(resp) + + payload := plugins.ServiceHTTP{ + Status: resp.Status, + StatusCode: resp.StatusCode, + ResponseHeaders: resp.Header, + } + if len(technologies) > 0 { + payload.Technologies = technologies + } + if len(cpes) > 0 { + payload.CPEs = cpes + } + + return plugins.CreateServiceFrom(target, payload, false, resp.Header.Get("Server"), plugins.TCP), nil +} + +func (p *HTTPSPlugin) PortPriority(port uint16) bool { + _, ok := commonHTTPSPorts[int(port)] + return ok +} + +func (p *HTTPSPlugin) Run( + conn net.Conn, + timeout time.Duration, + target plugins.Target, +) (*plugins.Service, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("https://%s", conn.RemoteAddr().String()), nil) + if err != nil { + if errors.Is(err, syscall.ECONNREFUSED) { + return nil, nil + } + return nil, &utils.RequestError{Message: err.Error()} + } + + if target.Host != "" { + req.Host = target.Host + } + + // https client with custom dialer to use the provided net.Conn + client := http.Client{ + Timeout: timeout, + Transport: &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return conn, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req.Header.Set("User-Agent", USERAGENT) + + resp, err := client.Do(req) + if err != nil { + return nil, &utils.RequestError{Message: err.Error()} + } + defer resp.Body.Close() + + technologies, cpes, _ := p.FingerprintResponse(resp) + + payload := plugins.ServiceHTTPS{ + Status: resp.Status, + StatusCode: resp.StatusCode, + ResponseHeaders: resp.Header, + } + if len(technologies) > 0 { + payload.Technologies = technologies + } + if len(cpes) > 0 { + payload.CPEs = cpes + } + + return plugins.CreateServiceFrom(target, payload, true, resp.Header.Get("Server"), plugins.TCP), nil +} + +func (p *HTTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *HTTPSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *HTTPPlugin) Priority() int { + return 0 +} + +func (p *HTTPSPlugin) Priority() int { + return 1 +} + +func (p *HTTPPlugin) Name() string { + return HTTP +} + +func (p *HTTPSPlugin) Name() string { + return HTTPS +} + +func (p *HTTPPlugin) FingerprintResponse(resp *http.Response) ([]string, []string, error) { + return fingerprint(resp, p.analyzer) +} + +func (p *HTTPSPlugin) FingerprintResponse(resp *http.Response) ([]string, []string, error) { + return fingerprint(resp, p.analyzer) +} + +func fingerprint(resp *http.Response, analyzer *wappalyzer.Wappalyze) ([]string, []string, error) { + var technologies, cpes []string + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + fingerprint := analyzer.FingerprintWithInfo(resp.Header, data) + for tech, appInfo := range fingerprint { + technologies = append(technologies, tech) + if cpe := appInfo.CPE; cpe != "" { + cpes = append(cpes, cpe) + } + } + + return technologies, cpes, nil +} diff --git a/scanner/pkg/fingerprint/plugins/services/http/http_test.go b/scanner/pkg/fingerprint/plugins/services/http/http_test.go new file mode 100644 index 0000000..446a38a --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/http/http_test.go @@ -0,0 +1,45 @@ +package http + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +func TestHTTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "http", + Port: 8080, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mendhak/http-https-echo", + Tag: "24", + }, + }, + } + + p := HTTPPlugin{} + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + p.analyzer = wappalyzerClient + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, &p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/http/https_test.go b/scanner/pkg/fingerprint/plugins/services/http/https_test.go new file mode 100644 index 0000000..5087560 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/http/https_test.go @@ -0,0 +1,45 @@ +package http + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +func TestHTTPS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "https", + Port: 8443, + Protocol: plugins.TCPTLS, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mendhak/http-https-echo", + Tag: "24", + }, + }, + } + + p := HTTPSPlugin{} + wappalyzerClient, err := wappalyzer.New() + if err != nil { + panic("unable to initialize wappalyzer library") + } + p.analyzer = wappalyzerClient + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, &p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/imap/imap.go b/scanner/pkg/fingerprint/plugins/services/imap/imap.go new file mode 100644 index 0000000..3b8f12b --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/imap/imap.go @@ -0,0 +1,186 @@ +package imap + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type IMAPPlugin struct{} +type TLSPlugin struct{} + +const IMAP = "imap" +const IMAPS = "imaps" + +func init() { + plugins.RegisterPlugin(&IMAPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +/* + checkGreeting - verifies server greeting. + +/* When a client initiates a TCP handshake with an IMAP server, the server will +/* send one of three greetings immediately following the last ACK of the +/* handshake: +/* S: * OK +/* S: * PREAUTH +/* S: * BYE +*/ +func checkGreeting(response []byte) bool { + srvGreet := string(response) + srvGreetUpper := strings.ToUpper(srvGreet) + + // As per page 85 of RFC 3501, there are 3 possible greetings + greetings := []string{"* OK", "* PREAUTH", "* BYE"} + for _, greeting := range greetings { + if strings.HasPrefix(srvGreetUpper, greeting) { + return true + } + } + + return false +} + +/* + checkCapability - sends CAPABILITY command and verifies response data. + +/* CAPABILITY is an unauthenticated IMAP command that allows the client to view +/* what other commands are supported by the server. If an IP:port is running +/* IMAP, it will return data like so: +/* C: 1234 CAPABILITY\r\n +/* S: * CAPABILITY \r\n +/* S: 1234 OK \r\n +*/ +func checkCapability(conn net.Conn, timeout time.Duration) (bool, error) { + /* The tag will always be reflected in the server output. Using a random- + /* looking/nonsensical tag decreases the possibility of false positive */ + tag := "7FYWU8I4" + msg := []byte(tag + " CAPABILITY\r\n") + + response, err := utils.SendRecv(conn, msg, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + /* Sometimes servers send all the data in one packet + /* If so, parse into two strings */ + srvResponses := strings.Split(string(response), "\r\n") + + if len(srvResponses) < 2 { + return true, &utils.InvalidResponseError{Service: IMAP} + } + + capData := strings.ToUpper(srvResponses[0]) + status := strings.ToUpper(srvResponses[1]) + + // If we only got 1 IMAP response, there is probably another on the way + if status == "" { + response, err := utils.Recv(conn, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + status = string(response) + } + + /* Make sure server response matches RFC 3501, pages 68 (capability) and 88 + /* (response-tagged) */ + if !strings.HasPrefix(capData, "* CAPABILITY") || !strings.HasPrefix(status, tag) { + return true, &utils.InvalidResponseErrorInfo{Service: IMAP, Info: "missing capability info"} + } + + // imap + return false, nil +} + +func DetectIMAP(conn net.Conn, timeout time.Duration) (string, bool, error) { + /* Server has to specify a greeting upon completing the TCP handshake as + /* per RFC 3501 (page 14). If we don't get a greeting, this ain't IMAP. */ + response, err := utils.Recv(conn, timeout) + if err != nil { + return "", false, err + } + if len(response) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + if !checkGreeting(response) { + return "", true, &utils.InvalidResponseErrorInfo{ + Service: IMAP, + Info: "did not receive expected imap greeting banner", + } + } + check, err := checkCapability(conn, timeout) + return string(response[5:]), check, err +} + +func (p *IMAPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectIMAP(conn, timeout) + if err != nil && check { // service is not running IMAP + return nil, nil + } else if err != nil && !check { // plugin error + return nil, err + } + + // service is running IMAP + payload := plugins.ServiceIMAPS{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *IMAPPlugin) PortPriority(i uint16) bool { + return i == 143 +} + +func (p *IMAPPlugin) Name() string { + return IMAP +} + +func (p *IMAPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectIMAP(conn, timeout) + if err != nil && check { // service is not running IMAP + return nil, nil + } else if err != nil && !check { // plugin error + return nil, err + } + + // service is running IMAPS + payload := plugins.ServiceIMAPS{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 993 +} + +func (p *TLSPlugin) Name() string { + return IMAPS +} + +func (p *IMAPPlugin) Priority() int { + return 191 +} + +func (p *TLSPlugin) Priority() int { + return 190 +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} diff --git a/scanner/pkg/fingerprint/plugins/services/imap/imap_test.go b/scanner/pkg/fingerprint/plugins/services/imap/imap_test.go new file mode 100644 index 0000000..2142534 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/imap/imap_test.go @@ -0,0 +1,38 @@ +package imap + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestIMAP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "imap", + Port: 143, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "instrumentisto/dovecot", + }, + }, + } + + p := &IMAPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi.go b/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi.go new file mode 100644 index 0000000..4be809c --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi.go @@ -0,0 +1,128 @@ +package ipmi + +import ( + "io" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" +) + +// http://72.47.221.139/sites/default/files/standards/documents/DSP0114.pdf + +var ipmiInitialPacket = [23]byte{ + + // + // Remote Management Control Protocol, Class: IPMI + // Version: 0x06 + // Reserved: 0x00 + // Sequence: 0xFF + // Type: 0x07 + // + + 0x06, 0x00, 0xFF, 0x07, + + // + // IPMI v1.5 Session Wrapper, Session ID 0x00 + // Authentication Type: NONE (0x00) + // Session ID: 0x00 0x00 0x00 0x00 + // Session Sequence number: 0x00 0x00 0x00 0x00 + // Message Length: 9 + // + + 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x09, + + // + // Intelligent Platform Management Bus + // Bus Command Data: 20 18 C8 81 00 38 8E 04 B5 + // + + 0x20, 0x18, 0xC8, 0x81, 0x00, 0x38, 0x8E, 0x04, 0xB5, +} + +var ipmiExpectedResponse = [13]byte{ + + /* + * Remote Management Control Protocol, Class: IPMI + * Version: 0x06 + * Reserved: 0x00 + * Sequence: 0xFF + * Type: 0x07 + */ + + 0x06, 0x00, 0xFF, 0x07, + + // + // IPMI v1.5 Session Wrapper, Session ID 0x00 + // Authentication Type: NONE (0x00) + // Session ID: 0x00 0x00 0x00 0x00 + // Session Sequence number: 0x00 0x00 0x00 0x00 + // + + 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +} + +type IPMIPlugin struct{} + +const IPMI = "ipmi" + +func isIPMI(conn net.Conn, timeout time.Duration) (bool, error) { + _, err := conn.Write(ipmiInitialPacket[:]) + if err != nil { + return false, err + } + + response := make([]byte, len(ipmiExpectedResponse)) + + err = conn.SetReadDeadline(time.Now().Add(timeout)) + if err != nil { + return false, err + } + + _, err = io.ReadFull(conn, response) + if err != nil { + return false, err + } + + for i, b := range ipmiExpectedResponse { + if response[i] != b { + return false, nil + } + } + + return true, nil +} + +func init() { + plugins.RegisterPlugin(&IPMIPlugin{}) +} + +func (p *IPMIPlugin) PortPriority(port uint16) bool { + return port == 623 +} + +func (p *IPMIPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + if isIPMI, err := isIPMI(conn, timeout); !isIPMI || err != nil { + return nil, nil + } + payload := plugins.ServiceIPMI{} + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func (p *IPMIPlugin) Name() string { + return IPMI +} + +func (p *IPMIPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *IPMIPlugin) Priority() int { + return 80 +} diff --git a/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi_test.go b/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi_test.go new file mode 100644 index 0000000..b94638d --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ipmi/ipmi_test.go @@ -0,0 +1,39 @@ +package ipmi + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestIPMI(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ipmi", + Port: 623, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "vaporio/ipmi-simulator", + ExposedPorts: []string{"623/udp"}, + }, + }, + } + + p := &IPMIPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec.go b/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec.go new file mode 100644 index 0000000..bc792aa --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec.go @@ -0,0 +1,120 @@ +package ipsec + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const IPSEC = "IPsec" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (f *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + initiator := make([]byte, 8) + _, err := rand.Read(initiator) + if err != nil { + return nil, &utils.RandomizeError{Message: "initiator SPI"} + } + InitialConnectionPackage := append(initiator, []byte{ //nolint:gocritic + // 8 bit Initiator SPI + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Responder SPI + 0x01, 0x10, // Version: 1.0 + 0x02, // Exchange type + 0x00, + 0x00, 0x00, 0x00, 0x00, // ID + 0x00, 0x00, 0x01, 0x50, // Message Length + 0x00, 0x00, 0x01, 0x34, // Payload Length + 0x00, 0x00, 0x00, 0x01, // Domain of interpretation: IPSEC (1) + 0x00, 0x00, 0x00, 0x01, // Situation: identity only + 0x00, 0x00, 0x01, 0x28, // Payload Length + 0x01, // Proposal number: 1 + 0x01, // Protocol ID: ISAKMP (1) + 0x00, 0x08, // Proposal transforms: 8 + + // SHA 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x01, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x02, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x03, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x04, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x02, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA 3DES-CBC 768 bit + 0x03, 0x00, 0x00, 0x24, 0x05, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 768 bit + 0x03, 0x00, 0x00, 0x24, 0x06, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x05, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // SHA DES-CBC 1024 bit + 0x03, 0x00, 0x00, 0x24, 0x07, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x02, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + + // MD5 3DES-CBC 1024 bit + 0x00, 0x00, 0x00, 0x24, 0x08, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x01, 0x80, 0x02, 0x00, 0x01, + 0x80, 0x03, 0x00, 0x01, 0x80, 0x04, 0x00, 0x01, 0x80, 0x0b, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x04, + 0x00, 0x00, 0x70, 0x80, + }...) + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + responderISP := hex.EncodeToString(response[8:16]) + messageID := hex.EncodeToString(response[20:24]) + if bytes.Equal(initiator, response[0:8]) { + payload := plugins.ServiceIPSEC{ + ResponderISP: responderISP, + MessageID: messageID, + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (f *Plugin) PortPriority(i uint16) bool { + return i == 500 +} + +func (f *Plugin) Name() string { + return IPSEC +} + +func (f *Plugin) Priority() int { + return 198 +} + +func (f *Plugin) Type() plugins.Protocol { + return plugins.UDP +} diff --git a/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec_test.go b/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec_test.go new file mode 100644 index 0000000..59330ab --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ipsec/ipsec_test.go @@ -0,0 +1,43 @@ +package ipsec + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestIPSEC(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ipsec", + Port: 500, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "hwdsl2/ipsec-vpn-server", + Mounts: []string{ + "ikev2-vpn-data:/etc/ipsec.d", + "/lib/modules:/lib/modules:ro", + }, + Privileged: true, + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp.go b/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp.go new file mode 100644 index 0000000..327258a --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp.go @@ -0,0 +1,177 @@ +package jdwp + +import ( + "bytes" + "encoding/binary" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type JDWPPlugin struct{} + +const JDWP = "jdwp" + +var ( + commonJDWPPorts = map[int]struct{}{ + 3999: {}, + 5000: {}, + 5005: {}, + 8000: {}, + 8453: {}, + 8787: {}, + 8788: {}, + 9001: {}, + 18000: {}, + } +) + +type JDWPPacket struct { + Length uint32 + ID uint32 + Flags byte + CommandSet byte + Command byte +} + +func init() { + plugins.RegisterPlugin(&JDWPPlugin{}) +} + +func DetectJDWPVersion(conn net.Conn, timeout time.Duration) (*plugins.ServiceJDWP, error) { + info := plugins.ServiceJDWP{} + + versionRequest := JDWPPacket{ + Length: 0x0B, + ID: 0x01, + Flags: 0x00, + CommandSet: 0x01, + Command: 0x01, + } + + versionBuf := new(bytes.Buffer) + err := binary.Write(versionBuf, binary.BigEndian, versionRequest) + if err != nil { + return nil, err + } + + response, err := utils.SendRecv(conn, versionBuf.Bytes(), timeout) + if err != nil { + return nil, err + } + if len(response) < 11 { + return nil, nil + } + + var versionResponse JDWPPacket + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.BigEndian, &versionResponse) + if err != nil { + return nil, err + } + + if versionResponse.Length != (uint32(len((response)))) { + return nil, err + } + + var descriptionLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &descriptionLength) + if err != nil { + return nil, err + } + description := make([]byte, descriptionLength) + err = binary.Read(responseBuf, binary.BigEndian, &description) + if err != nil { + return nil, err + } + + var jdwpMajor int32 + err = binary.Read(responseBuf, binary.BigEndian, &jdwpMajor) + if err != nil { + return nil, err + } + var jdwpMinor int32 + err = binary.Read(responseBuf, binary.BigEndian, &jdwpMinor) + if err != nil { + return nil, err + } + + var vmVersionLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &vmVersionLength) + if err != nil { + return nil, err + } + vmVersion := make([]byte, vmVersionLength) + err = binary.Read(responseBuf, binary.BigEndian, &vmVersion) + if err != nil { + return nil, err + } + + var vmNameLength uint32 + err = binary.Read(responseBuf, binary.BigEndian, &vmNameLength) + if err != nil { + return nil, err + } + vmName := make([]byte, vmNameLength) + err = binary.Read(responseBuf, binary.BigEndian, &vmName) + if err != nil { + return nil, err + } + + info.Description = string(description) + info.JdwpMajor = jdwpMajor + info.JdwpMinor = jdwpMinor + info.VMVersion = string(vmVersion) + info.VMName = string(vmName) + + return &info, nil +} + +func (p *JDWPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + requestBytes := []byte{ + // ascii "JDWP-Handshake" + 0x4a, 0x44, 0x57, 0x50, 0x2d, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, + } + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if !bytes.Equal(requestBytes, response) { + return nil, nil + } + + info, err := DetectJDWPVersion(conn, timeout) + if err != nil { + return nil, err + } + + if info == nil { + return plugins.CreateServiceFrom(target, nil, false, "", plugins.TCP), nil + } + + return plugins.CreateServiceFrom(target, info, false, info.VMVersion, plugins.TCP), nil +} + +func (p *JDWPPlugin) PortPriority(port uint16) bool { + _, ok := commonJDWPPorts[int(port)] + return ok +} + +func (p *JDWPPlugin) Name() string { + return JDWP +} + +func (p *JDWPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *JDWPPlugin) Priority() int { + return 500 +} diff --git a/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp_test.go b/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp_test.go new file mode 100644 index 0000000..c06b485 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/jdwp/jdwp_test.go @@ -0,0 +1,23 @@ +// Copyright 2022 Praetorian Security, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jdwp + +import ( + "testing" +) + +// tested locally against a Java process with JDWP enabled +func TestJDWP(_ *testing.T) { +} diff --git a/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go new file mode 100644 index 0000000..b1ad052 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew.go @@ -0,0 +1,196 @@ +package kafkanew + +import ( + "encoding/binary" + "math" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type Plugin struct{} +type TLSPlugin struct{} + +const KAFKA = "kafkaNew" +const KAFKATLS = "KafkaNewTLS" + +func init() { + plugins.RegisterPlugin(&Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, false, timeout, target) + return result, err +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 9092 +} + +func (p *Plugin) Name() string { + return KAFKA +} + +func (p *Plugin) Priority() int { + return 200 +} + +func (p *TLSPlugin) Priority() int { + return 200 +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, true, timeout, target) + return result, err +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 9093 +} + +func (p *TLSPlugin) Name() string { + return KAFKATLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +/* +Run Kafka scanner plugins. + +Primary Sources: + - https://kafka.apache.org/protocol.html (Gold mine) + - https://kafka.apache.org/documentation.html + - https://kafka.apache.org/downloads + +Methodology: +Scanning for Kafka is a bit tricky, so I've outlined my methodology here. Kafka +is harder to detect reliably for a few reasons: + - Kafka brokers may optionally require authentication via SASL before most + commands can be issued. + - There are many different versions of Kafka, and most API calls work slightly + different on each versions (especially for pre-0.9.0.X releases) + +Fortunately, Kafka versions 0.10.0.0 and later support the ApiVersions request, +which can be sent by an unauthenticated user to check which API requests are +supported by the broker. Also versions prior to 0.9.0.0 do not offer any form of +authentication. And, all versions of Kafka are compatible with any older client. +This means that: + 1. If Kafka version 0.10.0.0 or higher is running, we can confirm with the + ApiVersions request regardless of if authentication is required This + includes any version of Kafka released since May, 2016. + 2. If Kafka version 0.8.0.X or earlier is running, we can confirm with a simple + data query using API version 0. + 3. If Kafka version 0.9.0.X is running and does not require authentication, we + can also confirm with a simple v0 data query. + +I'm not sure if Kafka brokers running version 0.9.0.X that do require +authentication will be detected by any of the above methods. It's possible that +strategy 3 will still work in this situation, but I was not able to confirm due +to the difficulty of setting up a testing environment for an older version. +*/ +func Run(conn net.Conn, tls bool, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /* Initiate first TCP connection with target. If the target is running an + /* older version of Kafka, the connection will be terminated after sending + /* ApiVersions, and we will need to make a new one. */ + /* Make first notReportError - this will catch any broker running Kafka 0.10.0.0 or + /* later. */ + notReportError, err := checkAPIVersions(conn, timeout) + if err != nil { + if !notReportError { + return nil, err + } + return nil, nil + } + if !notReportError { + return nil, nil + } + + return plugins.CreateServiceFrom(target, plugins.ServiceKafka{}, tls, ">=0.10.0.0", plugins.TCP), nil +} + +/* Helper function to generate a correlation_id */ +/* Might update to be random later */ +func genCorrelationID() []byte { + cid := []byte{0x1e, 0x33, 0xf4, 0x81} + return cid +} + +/* + checkApiVersions - sends an ApiVersions request and validates the output. + +/* +/* Note that if the broker does not support ApiVersions, it might terminate the +/* TCP connection (source: https://kafka.apache.org/protocol.html#api_versions). +/* +/* The function sends an ApiVersions request because this is widely supported, +/* and does not require authentication. All Kafka responses start with the +/* packet length followed by the "correlation ID", which is a value specified by +/* the client and included in their request. So we check to make sure the first +/* four bytes (length) are equivalent to the size of the response data and the +/* next four bytes (correlation ID) match the ID included in the request. +/* Further reading: https://kafka.apache.org/protocol.html#protocol_messages +*/ +func checkAPIVersions(conn net.Conn, timeout time.Duration) (bool, error) { + cid := genCorrelationID() + apiVersionsRequest := []byte{ + /* length */ + 0x00, 0x00, 0x00, 0x43, + /* request_api_key */ + 0x00, 0x12, + /* request_api_version */ + 0x00, 0x00, + /* correlation_id */ + cid[0], cid[1], cid[2], cid[3], + /* client_id */ + 0x00, 0x1f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, + 0x65, 0x72, 0x2d, 0x4f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x20, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, + 0x65, 0x72, 0x20, 0x32, 0x2e, 0x32, 0x2d, 0x31, + 0x38, + /* TAG_BUFFER */ + 0x00, + /* client_software_name */ + 0x12, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, + 0x6b, 0x61, 0x66, 0x6b, 0x61, 0x2d, 0x6a, 0x61, + 0x76, 0x61, + /* client_software_version */ + 0x06, 0x32, 0x2e, 0x34, 0x2e, 0x30, + /* _tagged_fields */ + 0x00, + } + + response, err := utils.SendRecv(conn, apiVersionsRequest, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + responseLength := binary.BigEndian.Uint32(response[0:4]) + expectedLength := uint32(math.Max(float64(len(response)-4), 0)) + correlationID := response[4:8] + + // First, check to see if the response length makes sense + if responseLength != expectedLength { + return false, nil + } + + // Next, make sure the correlation IDs match up + for i := 0; i < len(cid); i++ { + if cid[i] != correlationID[i] { + return false, nil + } + } + + return true, nil +} diff --git a/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go new file mode 100644 index 0000000..54a1260 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaNew/kafkaNew_test.go @@ -0,0 +1,38 @@ +package kafkanew + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestKafkaNew(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "kafkanew", + Port: 9092, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "spotify/kafka", + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go new file mode 100644 index 0000000..a51d586 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld.go @@ -0,0 +1,233 @@ +package kafkaold + +import ( + "crypto/rand" + "encoding/binary" + "math" + "math/big" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type Plugin struct{} +type TLSPlugin struct{} + +const KAFKA = "kafkaOld" +const KAFKATLS = "KafkaOldTLS" + +func init() { + plugins.RegisterPlugin(&Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, false, timeout, target) + return result, err +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 9092 +} + +func (p *Plugin) Priority() int { + return 201 +} + +func (p *TLSPlugin) Priority() int { + return 201 +} + +func (p *Plugin) Name() string { + return KAFKA +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, err := Run(conn, true, timeout, target) + return result, err +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 9093 +} + +func (p *TLSPlugin) Name() string { + return KAFKATLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +/* +Run Kafka scanner plugins. + +Primary Sources: + - https://kafka.apache.org/protocol.html (Gold mine) + - https://kafka.apache.org/documentation.html + - https://kafka.apache.org/downloads + +Methodology: +Scanning for Kafka is a bit tricky, so I've outlined my methodology here. Kafka +is harder to detect reliably for a few reasons: + - Kafka brokers may optionally require authentication via SASL before most + commands can be issued. + - There are many different versions of Kafka, and most API calls work slightly + different on each versions (especially for pre-0.9.0.X releases) + +Fortunately, Kafka versions 0.10.0.0 and later support the ApiVersions request, +which can be sent by an unauthenticated user to check which API requests are +supported by the broker. Also versions prior to 0.9.0.0 do not offer any form of +authentication. And, all versions of Kafka are compatible with any older client. +This means that: + 1. If Kafka version 0.10.0.0 or higher is running, we can confirm with the + ApiVersions request regardless of if authentication is required This + includes any version of Kafka released since May, 2016. + 2. If Kafka version 0.8.0.X or earlier is running, we can confirm with a simple + data query using API version 0. + 3. If Kafka version 0.9.0.X is running and does not require authentication, we + can also confirm with a simple v0 data query. + +I'm not sure if Kafka brokers running version 0.9.0.X that do require +authentication will be detected by any of the above methods. It's possible that +strategy 3 will still work in this situation, but I was not able to confirm due +to the difficulty of setting up a testing environment for an older version. +*/ +func Run(conn net.Conn, tls bool, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /* Initiate first TCP connection with target. If the target is running an + /* older version of Kafka, the connection will be terminated after sending + /* ApiVersions, and we will need to make a new one. */ + /* Make first notReportError - this will catch any broker running Kafka 0.10.0.0 or + /* later. */ + notReportError, err := checkMetadataQuery(conn, timeout) + if err != nil { + if !notReportError { + return nil, err + } + return nil, nil + } + if !notReportError { + return nil, nil + } + return plugins.CreateServiceFrom(target, plugins.ServiceKafka{}, tls, "<=0.9.0.X", plugins.TCP), nil +} + +/* Helper function to generate a correlation_id */ +/* Might update to be random later */ +func genCorrelationID() []byte { + cid := []byte{0x1e, 0x33, 0xf4, 0x81} + return cid +} + +/* Helper function for generating a random alphanumeric string */ +func genRandomString(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + str := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", &utils.RandomizeError{Message: "KafkaRandomString"} + } + str[i] = charset[num.Int64()] + } + + return string(str), nil +} + +func checkMetadataQuery(conn net.Conn, timeout time.Duration) (bool, error) { + cid := genCorrelationID() + topicName, err := genRandomString(6) + if err != nil { + return false, err + } + metadataRequest := []byte{ + // length + 0x00, 0x00, 0x00, 0x00, + // request_api_key + 0x00, 0x03, + // request_api_version + 0x00, 0x00, + // correlation_id + cid[0], cid[1], cid[2], cid[3], + // client_id + 0x00, 0x0d, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x35, + // topic_count + 0x00, 0x00, 0x00, 0x01, + // topics + 0x00, 0x06, topicName[0], topicName[1], topicName[2], topicName[3], + topicName[4], topicName[5], + } + + /* Correct the length field - not necessary for final script, but makes + /* debugging easier */ + packetLength := make([]byte, 4) + binary.BigEndian.PutUint32(packetLength, uint32(len(metadataRequest)-4)) + for i := 0; i < 4; i++ { + metadataRequest[i] = packetLength[i] + } + + response, err := utils.SendRecv(conn, metadataRequest, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + // Similar to checkApiVersions, first we test the length + responseLength := binary.BigEndian.Uint32(response[0:4]) + expectedLength := uint32(math.Max(float64(len(response)-4), 0)) + if responseLength != expectedLength { + return false, nil + } + + // Next, verify correlation_id + correlationID := response[4:8] + for i := 0; i < 4; i++ { + if cid[i] != correlationID[i] { + return false, nil + } + } + + /* Finally, check to make sure the topic name is in the expected location. + /* Our server's response data begins at index 8 (4 bytes for length and 4 + /* bytes for correlation_id). For metadataRequest, this is information about + /* the available brokers, which is a variable-sized array. So we must run + /* through the array to accurately skip over the brokers section. */ + brokerIndex := uint16(8) + brokerCount := binary.BigEndian.Uint32(response[brokerIndex : brokerIndex+4]) + + index := brokerIndex + 4 + for i := uint32(0); i < brokerCount; i++ { + /* Each version 0 broker object looks like the following: + /* node_id (INT32, 4 bytes) + /* host (STRING, First the length N is given as an INT16. Then N bytes follow. So 2 + N bytes total) + /* port (INT32, 4 bytes) */ + hostLength := binary.BigEndian.Uint16(response[index+4 : index+6]) + index += 4 + 2 + hostLength + 4 + } + + topicsIndex := index + + /* Topic objects are similar to brokers, but we only requested one in our + /* metadataRequest. So there should only be one, and the topic name is + /* always the second field. */ + topicsIndex += 4 // for topics_count (INT32) + topicsIndex += 2 // for status code (INT16) + topicNameLength := binary.BigEndian.Uint16(response[topicsIndex : topicsIndex+2]) + topicsIndex += 2 // for string length (INT16) + tName := string(response[topicsIndex : topicsIndex+topicNameLength]) + + if tName != topicName { + return false, nil + } + + return true, nil +} diff --git a/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go new file mode 100644 index 0000000..2c38671 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/kafka/kafkaOld/kafkaOld_test.go @@ -0,0 +1,38 @@ +package kafkaold + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestKafkaOld(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "kafkaold", + Port: 9092, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "spotify/kafka", + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ldap/ldap.go b/scanner/pkg/fingerprint/plugins/services/ldap/ldap.go new file mode 100644 index 0000000..f77f286 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ldap/ldap.go @@ -0,0 +1,205 @@ +package ldap + +import ( + "bytes" + "encoding/binary" + "math/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type LDAPPlugin struct{} +type TLSPlugin struct{} + +const LDAP = "ldap" +const LDAPS = "ldaps" + +func init() { + plugins.RegisterPlugin(&LDAPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +/* + +Data is BER encoded (Basic Encoding Rules) - Format: Type-Length-Value + +Type: + Format of Types: Bits 8-7 6 5-1 + Purpose Class Prim/Constructed Tag Number + + For example 00 11 00 00 represents the Class (Universal) Constructed Sequence (Sequence is tag number 1 00 00) + Ie: The sequence tag +Length: + Single Byte - Length is a single byte containing the number of bytes in the message up to 127 + Multi Bsyte - The most significant bit is set to 1. The remaining 7 bytes are used to indicate how + many bytes are needed to represent the length, followed by that many bytes + + + +Example Bind Request +0000 30 2b 02 01 01 60 26 02 01 03 04 1a 63 6e 3d 61 +0010 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c +0020 64 63 3d 6f 72 67 80 05 61 64 6d 69 6e + +Notes: + +The messageId MUST be non-zero and different from any other request in the session + +30 2b ... Represents a universal sequence containing 43 bytes + +02 01 01 Represents an Integer (02) type, length 1 byte, and value of 1 + (denoting a message Id of 1) - this number is reflected back in responses + +60 26 denotes a bind request of 38 bytes + +02 01 03 represents an integer of 1 byte with a value of 3 (the protocol version) + +04 1a 63 6e 3d 61 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 +Represents a universal string (04) of length 26 bytes containing the value 'cn=admin,dc=example,dc=org' + +80 05 61 64 6d 69 6e Represents a context specific 5 length string holding the simple auth password of 'admin' + +*/ + +func generateRandomString(length int) []byte { + charset := "abcdefghijklmnopqrstuvwxyz" + result := make([]byte, length) + + for i := range result { + result[i] = charset[rand.Intn(len(charset))] //nolint:gosec + } + return result +} + +func generateBindRequestAndID() [2][]byte { + rand.Seed(time.Now().UnixNano()) + sequenceBERHeader := [2]byte{0x30, 0x3a} + messageID := uint32(rand.Int31()) //nolint:gosec + messageIDBytes := [4]byte{} + binary.BigEndian.PutUint32(messageIDBytes[:], messageID) + messageIDBERHeader := [2]byte{0x02, 0x04} + finalMessageIDBER := make([]byte, 6) + copy(finalMessageIDBER[:2], messageIDBERHeader[:]) + copy(finalMessageIDBER[2:], messageIDBytes[:]) + bindRequestHeader := [2]byte{0x60, 0x32} + versionBER := [3]byte{0x02, 0x01, 0x03} + stringBERHeader := [2]byte{0x04, 0x17} + stringContextBERHeader := [2]byte{0x80, 0x14} + // We attempt to auth with a random distinguished name and password (generated below) + randomAlphaString := generateRandomString(20) + dePrefix := []byte("cn=") + distinguishedName := append(dePrefix, randomAlphaString...) //nolint:gocritic + passwordBER := randomAlphaString + combine := [][]byte{ + sequenceBERHeader[:], + finalMessageIDBER, + bindRequestHeader[:], + versionBER[:], + stringBERHeader[:], + distinguishedName, + stringContextBERHeader[:], + passwordBER, + } + fullBindRequest := make([]byte, 60) + index := 0 + for _, s := range combine { + index += copy(fullBindRequest[index:], s) + } + + return [2][]byte{fullBindRequest, finalMessageIDBER} +} + +func DetectLDAP(conn net.Conn, timeout time.Duration) (bool, error) { + requestAndID := generateBindRequestAndID() + + response, err := utils.SendRecv(conn, requestAndID[0], timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return false, nil + } + + expectedSequenceByte := byte(0x30) + expectedMessageLengthByte := byte(len(response) - 2) + // The LDAP header should have the right message ID bytes, the right sequence byte (the first byte), + // and the right length byte + expectedLDAPHeader := append( + []byte{expectedSequenceByte, expectedMessageLengthByte}, + requestAndID[1]...) + + // We might be able to try to look at the specific response message in responseBuff to try to fingerprint the specific + // vendor, but didn't attempt to do that in this current version (might be more time than value added currently) + + // In other versions, bytes at response[1:5] may differ so we remove these bytes and + // perform the expected header check against this 'otherVersionResponse' as well + if len(response) < 7 { + return false, nil + } + otherVersionResponse := append([]byte{response[0]}, response[5]+4) + otherVersionResponse = append(otherVersionResponse, response[6:]...) + + if bytes.HasPrefix(response, expectedLDAPHeader) || bytes.HasPrefix(otherVersionResponse, expectedLDAPHeader) { + return true, nil + } + return false, nil +} + +func (p *LDAPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isLDAP, err := DetectLDAP(conn, timeout) + if err != nil { + return nil, err + } + + if isLDAP { + return plugins.CreateServiceFrom(target, plugins.ServiceLDAP{}, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *LDAPPlugin) PortPriority(i uint16) bool { + return i == 389 +} + +func (p *LDAPPlugin) Name() string { + return LDAP +} + +func (p *LDAPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + isLDAPS, err := DetectLDAP(conn, timeout) + if err != nil { + return nil, err + } + + if isLDAPS { + return plugins.CreateServiceFrom(target, plugins.ServiceLDAPS{}, true, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 636 +} + +func (p *LDAPPlugin) Priority() int { + return 175 +} + +func (p *TLSPlugin) Priority() int { + return 175 +} + +func (p *TLSPlugin) Name() string { + return LDAPS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} diff --git a/scanner/pkg/fingerprint/plugins/services/ldap/ldap_test.go b/scanner/pkg/fingerprint/plugins/services/ldap/ldap_test.go new file mode 100644 index 0000000..e3046fb --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ldap/ldap_test.go @@ -0,0 +1,38 @@ +package ldap + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestLDAP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ldap", + Port: 1389, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "bitnami/openldap", + }, + }, + } + + p := &LDAPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc.go b/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc.go new file mode 100644 index 0000000..f9c8587 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc.go @@ -0,0 +1,167 @@ +package linuxrpc + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +/* +The RPC service takes two main operations we care about: call and dump. + +Call verify that the service is running and will return the version of rpc running +Dump dumps a list of all registered rpc endpoints in a list, with each entry having the following structure: + +RPCB +Program: Portmap (100000) +Version: 4 +Network Id: tcp6 + length: 4 + contents: tcp6 +Universal Address: ::.0.111 + length: 8 + contents: ::.0.111 +Owner of this Service: superuser + length: 9 + contents: superuser + fill bytes: opaque data +Value follows: Yes + +Bytes are padded to 4 bytes +*/ + +type RPCPlugin struct{} + +const RPC = "RPC" + +func init() { + plugins.RegisterPlugin(&RPCPlugin{}) +} + +func (p *RPCPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + rpcService := plugins.ServiceRPC{} + + check, err := DetectRPCInfoService(conn, &rpcService, timeout) + if check && err != nil { + return nil, nil + } + if err == nil { + return plugins.CreateServiceFrom(target, rpcService, false, "", plugins.TCP), nil + } + return nil, err +} + +func DetectRPCInfoService(conn net.Conn, lookupResponse *plugins.ServiceRPC, timeout time.Duration) (bool, error) { + callPacket := []byte{ + 0x80, 0x00, 0x00, 0x28, 0x72, 0xfe, 0x1d, 0x13, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x86, 0xa0, 0x00, 0x01, 0x97, 0x7c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + callResponseSignature := []byte{ + 0x72, 0xfe, 0x1d, 0x13, 0x00, 0x00, 0x00, 0x01, + } + + dumpPacket := []byte{ + 0x80, 0x00, 0x00, 0x28, 0x3d, 0xd3, 0x77, 0x29, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, callPacket, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if !bytes.Contains(response, callResponseSignature) { + return true, &utils.InvalidResponseError{Service: RPC} + } + + response, err = utils.SendRecv(conn, dumpPacket, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + return true, parseRPCInfo(response, lookupResponse) +} + +func parseRPCInfo(response []byte, lookupResponse *plugins.ServiceRPC) error { + if len(response) < 0x20 { + return fmt.Errorf("invalid rpc length") + } + response = response[0x20:] + valueFollows := 1 + + for valueFollows == 1 { + tmp := plugins.RPCB{} + if len(response) < 0x20 { + return nil + } + + tmp.Program = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + tmp.Version = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + networkIDLen := int(binary.BigEndian.Uint32(response[0:4])) + for networkIDLen%4 != 0 { + networkIDLen++ + } + response = response[4:] + tmp.Protocol = string(response[0:networkIDLen]) + response = response[networkIDLen:] + addressLen := int(binary.BigEndian.Uint32(response[0:4])) + for addressLen%4 != 0 { + addressLen++ + } + response = response[4:] + tmp.Address = string(response[0:addressLen]) + response = response[addressLen:] + ownerLen := int(binary.BigEndian.Uint32(response[0:4])) + for ownerLen%4 != 0 { + ownerLen++ + } + response = response[4:] + tmp.Owner = string(response[0:ownerLen]) + response = response[ownerLen:] + + valueFollows = int(binary.BigEndian.Uint32(response[0:4])) + response = response[4:] + + lookupResponse.Entries = append(lookupResponse.Entries, tmp) + } + + return nil +} + +func (p *RPCPlugin) PortPriority(i uint16) bool { + return i == 111 +} + +func (p *RPCPlugin) Name() string { + return RPC +} + +func (p *RPCPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *RPCPlugin) Priority() int { + return 300 +} diff --git a/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go b/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go new file mode 100644 index 0000000..69b5444 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/linuxrpc/linuxrpc_test.go @@ -0,0 +1,40 @@ +package linuxrpc + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestRPC(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "alpine-nfs", + Port: 111, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "woahbase/alpine-nfs", + Tag: "x86_64", + Privileged: true, + }, + }, + } + + p := &RPCPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft.go b/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft.go new file mode 100644 index 0000000..09c9198 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft.go @@ -0,0 +1,229 @@ +package minecraftjava + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type MinecraftPlugin struct{} + +const MinecraftJava = "minecraft-java" +const DefaultPort = uint16(25565) + +func init() { + plugins.RegisterPlugin(&MinecraftPlugin{}) +} + +func (p *MinecraftPlugin) PortPriority(port uint16) bool { + return port == DefaultPort +} + +func (p *MinecraftPlugin) Name() string { + return MinecraftJava +} + +func (p *MinecraftPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MinecraftPlugin) Priority() int { + return 1 +} + +type mcStatusResponse struct { + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` + Players struct { + Max int `json:"max"` + Online int `json:"online"` + } `json:"players"` + Description any `json:"description"` + Favicon string `json:"favicon"` + SecureChat bool `json:"enforcesSecureChat"` +} + +func writeVarInt(w io.Writer, v int32) error { + uv := uint32(v) + for { + if (uv & ^uint32(0x7F)) == 0 { + _, err := w.Write([]byte{byte(uv)}) + return err + } + b := byte(uv&0x7F) | 0x80 + if _, err := w.Write([]byte{b}); err != nil { + return err + } + uv >>= 7 + } +} + +func readVarInt(r io.ByteReader) (int32, error) { + var numRead int + var result int32 + for { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + value := int32(b & 0x7F) + result |= value << (7 * numRead) + numRead++ + if numRead > 5 { + return 0, fmt.Errorf("varint too big") + } + if (b & 0x80) == 0 { + break + } + } + return result, nil +} + +func writeString(w io.Writer, s string) error { + if err := writeVarInt(w, int32(len(s))); err != nil { + return err + } + _, err := w.Write([]byte(s)) + return err +} + +func encodeHandshake(host string, port uint16) ([]byte, error) { + // Handshake packet: id=0x00 + // protocol version: use 754 (1.16.5) as a broadly accepted value for status. + // server address, server port, next state=1 (status) + body := &bytes.Buffer{} + if err := writeVarInt(body, 0); err != nil { // packet id + return nil, err + } + if err := writeVarInt(body, 754); err != nil { + return nil, err + } + if err := writeString(body, host); err != nil { + return nil, err + } + if err := binary.Write(body, binary.BigEndian, port); err != nil { + return nil, err + } + if err := writeVarInt(body, 1); err != nil { // next state: status + return nil, err + } + + pkt := &bytes.Buffer{} + if err := writeVarInt(pkt, int32(body.Len())); err != nil { + return nil, err + } + pkt.Write(body.Bytes()) + return pkt.Bytes(), nil +} + +func encodeStatusRequest() ([]byte, error) { + body := &bytes.Buffer{} + if err := writeVarInt(body, 0); err != nil { // packet id + return nil, err + } + pkt := &bytes.Buffer{} + if err := writeVarInt(pkt, int32(body.Len())); err != nil { + return nil, err + } + pkt.Write(body.Bytes()) + return pkt.Bytes(), nil +} + +func decodeStatusResponse(frame []byte) (mcStatusResponse, error) { + // Frame is: packet length varint + packet data. + // We'll parse using a bytes.Reader with ByteReader. + r := bytes.NewReader(frame) + length, err := readVarInt(r) + if err != nil { + return mcStatusResponse{}, err + } + if length <= 0 { + return mcStatusResponse{}, fmt.Errorf("invalid packet length") + } + + // Read exactly length bytes as packet payload + payload := make([]byte, length) + if _, err := io.ReadFull(r, payload); err != nil { + return mcStatusResponse{}, err + } + pr := bytes.NewReader(payload) + packetID, err := readVarInt(pr) + if err != nil { + return mcStatusResponse{}, err + } + if packetID != 0 { + return mcStatusResponse{}, fmt.Errorf("unexpected packet id %d", packetID) + } + + jsonLen, err := readVarInt(pr) + if err != nil { + return mcStatusResponse{}, err + } + if jsonLen < 0 || int(jsonLen) > pr.Len() { + return mcStatusResponse{}, fmt.Errorf("invalid json length") + } + + jsonBytes := make([]byte, jsonLen) + if _, err := io.ReadFull(pr, jsonBytes); err != nil { + return mcStatusResponse{}, err + } + + var resp mcStatusResponse + if err := json.Unmarshal(jsonBytes, &resp); err != nil { + return mcStatusResponse{}, err + } + return resp, nil +} + +func (p *MinecraftPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + host := target.Host + if host == "" { + host = target.Address.Addr().String() + } + + handshake, err := encodeHandshake(host, uint16(target.Address.Port())) + if err != nil { + return nil, err + } + if err := utils.Send(conn, handshake, timeout); err != nil { + return nil, err + } + + statusReq, err := encodeStatusRequest() + if err != nil { + return nil, err + } + respFrame, err := utils.SendRecv(conn, statusReq, timeout) + if err != nil { + return nil, err + } + if len(respFrame) == 0 { + return nil, nil + } + + resp, err := decodeStatusResponse(respFrame) + if err != nil { + return nil, nil + } + + payload := plugins.ServiceMinecraftJava{ + VersionName: resp.Version.Name, + ProtocolVersion: resp.Version.Protocol, + PlayersOnline: resp.Players.Online, + PlayersMax: resp.Players.Max, + Description: resp.Description, + Favicon: resp.Favicon, + EnforcesSecure: resp.SecureChat, + } + + return plugins.CreateServiceFrom(target, payload, false, resp.Version.Name, plugins.TCP), nil +} diff --git a/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft_test.go b/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft_test.go new file mode 100644 index 0000000..bda01d1 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/minecraft/java/minecraft_test.go @@ -0,0 +1,168 @@ +package minecraftjava + +import ( + "bytes" + "encoding/json" + "net" + "net/netip" + "testing" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" +) + +type byteReader struct{ *bytes.Reader } + +func (b byteReader) ReadByte() (byte, error) { return b.Reader.ReadByte() } + +func mustEncodeHandshake(t *testing.T, host string, port uint16) []byte { + t.Helper() + b, err := encodeHandshake(host, port) + if err != nil { + t.Fatalf("encodeHandshake: %v", err) + } + return b +} + +func mustEncodeStatusReq(t *testing.T) []byte { + t.Helper() + b, err := encodeStatusRequest() + if err != nil { + t.Fatalf("encodeStatusRequest: %v", err) + } + return b +} + +func mustFrameStatusJSON(t *testing.T, status any) []byte { + t.Helper() + j, err := json.Marshal(status) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + + // Packet payload: packetID=0 + jsonString + payload := &bytes.Buffer{} + if err := writeVarInt(payload, 0); err != nil { + t.Fatalf("writeVarInt(packetID): %v", err) + } + if err := writeVarInt(payload, int32(len(j))); err != nil { + t.Fatalf("writeVarInt(jsonLen): %v", err) + } + payload.Write(j) + + // Frame: length varint + payload + frame := &bytes.Buffer{} + if err := writeVarInt(frame, int32(payload.Len())); err != nil { + t.Fatalf("writeVarInt(frameLen): %v", err) + } + frame.Write(payload.Bytes()) + return frame.Bytes() +} + +func TestVarIntRoundTrip(t *testing.T) { + vals := []int32{0, 1, 2, 127, 128, 255, 2097151} + for _, v := range vals { + buf := &bytes.Buffer{} + if err := writeVarInt(buf, v); err != nil { + t.Fatalf("writeVarInt(%d): %v", v, err) + } + got, err := readVarInt(byteReader{bytes.NewReader(buf.Bytes())}) + if err != nil { + t.Fatalf("readVarInt(%d): %v", v, err) + } + if got != v { + t.Fatalf("varint mismatch: want %d got %d", v, got) + } + } +} + +func TestMinecraftJavaRunDetectsServer(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + // lightweight fake MC status server on the other end of the pipe + go func() { + defer server.Close() + buf := make([]byte, 4096) + // read handshake + _, _ = server.Read(buf) + // read status request + _, _ = server.Read(buf) + + status := map[string]any{ + "version": map[string]any{"name": "1.20.4", "protocol": 765}, + "players": map[string]any{"max": 20, "online": 3}, + "description": map[string]any{"text": "hello"}, + "favicon": "data:image/png;base64,AAAA", + "enforcesSecureChat": true, + } + frame := mustFrameStatusJSON(t, status) + _, _ = server.Write(frame) + }() + + p := &MinecraftPlugin{} + target := plugins.Target{ + Host: "example.org", + Address: netip.MustParseAddrPort("127.0.0.1:25565"), + } + + service, err := p.Run(client, 2*time.Second, target) + if err != nil { + t.Fatalf("Run returned error: %v", err) + } + if service == nil { + t.Fatalf("expected service, got nil") + } + if service.Protocol != plugins.ProtoMinecraftJava { + t.Fatalf("expected protocol %q got %q", plugins.ProtoMinecraftJava, service.Protocol) + } + if service.Port != 25565 { + t.Fatalf("expected port 25565 got %d", service.Port) + } + + meta := service.Metadata().(plugins.ServiceMinecraftJava) + if meta.VersionName != "1.20.4" { + t.Fatalf("expected versionName 1.20.4 got %q", meta.VersionName) + } + if meta.PlayersOnline != 3 || meta.PlayersMax != 20 { + t.Fatalf("expected players 3/20 got %d/%d", meta.PlayersOnline, meta.PlayersMax) + } +} + +func TestMinecraftJavaRunReturnsNilOnNonMC(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + go func() { + defer server.Close() + // read whatever client writes and then respond with junk + buf := make([]byte, 4096) + _, _ = server.Read(buf) + _, _ = server.Read(buf) + _, _ = server.Write([]byte("not minecraft")) + }() + + p := &MinecraftPlugin{} + target := plugins.Target{Address: netip.MustParseAddrPort("127.0.0.1:25565")} + + svc, err := p.Run(client, 2*time.Second, target) + if err != nil { + t.Fatalf("Run returned error: %v", err) + } + if svc != nil { + t.Fatalf("expected nil service for non-mc response") + } +} + +func TestEncodersBuildPackets(t *testing.T) { + h := mustEncodeHandshake(t, "localhost", 25565) + if len(h) == 0 { + t.Fatalf("handshake packet empty") + } + s := mustEncodeStatusReq(t) + if len(s) == 0 { + t.Fatalf("status request packet empty") + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/modbus/modbus.go b/scanner/pkg/fingerprint/plugins/services/modbus/modbus.go new file mode 100644 index 0000000..ab1a201 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/modbus/modbus.go @@ -0,0 +1,113 @@ +package modbus + +import ( + "bytes" + "crypto/rand" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const ( + ModbusHeaderLength = 7 + ModbusDiscreteInputCode = 0x2 + ModbusErrorAddend = 0x80 +) + +type MODBUSPlugin struct{} + +func init() { + plugins.RegisterPlugin(&MODBUSPlugin{}) +} + +const MODBUS = "modbus" + +func (p *MODBUSPlugin) PortPriority(port uint16) bool { + return port == 502 +} + +// Run +/* + modbus is a communications standard for connecting industrial devices. + modbus can be carried over a number of frame formats; this program identifies + modbus over TCP. + + modbus supports diagnostic functions that could be used for fingerprinting, + however, not all implementations will support the use of these functions. + Therefore, this program utilizes a read primitive and validates both the success + response and the error response conditions. + + modbus supports reading and writing to specified memory addresses using a number + of different primitives. This program utilizes the "Read Discrete Input" primitive, + which requests the value of a read-only boolean. This is the least likely primitive to + be disruptive. + + Additionally, all modbus messages begin with a 7-byte header. The first two bytes are a + client-controlled transaction ID. This program generates a random transaction ID and validates + that the server echos the correct response. + + Initial testing done with `docker run -it -p 502:5020 oitc/modbus-server:latest` + The default TCP port is 502, but this is unofficial. +*/ +func (p *MODBUSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + + // Read Discrete Input request + requestBytes := []byte{ + // transaction ID bytes were generated above + // protocol ID (0) + 0x00, 0x00, + // following byte length + 0x00, 0x06, + // remote slave (variable, but fixed to 1 here) + 0x01, + // function code + 0x02, + // starting address of 0x0000 + 0x00, 0x00, + // read one bit. this will cause a successful request to return 1 byte, with the + // 7 high bits set to zero and the low bit set to the response value + 0x00, 0x01, + } + + requestBytes = append(transactionID, requestBytes...) + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // transaction ID was echoed correctly + if bytes.Equal(response[:2], transactionID) { + // successful request, validate contents + if response[ModbusHeaderLength] == ModbusDiscreteInputCode { + if response[ModbusHeaderLength+1] == 1 && (response[ModbusHeaderLength+2]>>1) == 0x00 { + return plugins.CreateServiceFrom(target, plugins.ServiceModbus{}, false, "", plugins.TCP), nil + } + } else if response[ModbusHeaderLength] == ModbusDiscreteInputCode+ModbusErrorAddend { + return plugins.CreateServiceFrom(target, plugins.ServiceModbus{}, false, "", plugins.TCP), nil + } + } + return nil, nil +} + +func (p *MODBUSPlugin) Name() string { + return MODBUS +} + +func (p *MODBUSPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MODBUSPlugin) Priority() int { + return 400 +} diff --git a/scanner/pkg/fingerprint/plugins/services/modbus/modbus_test.go b/scanner/pkg/fingerprint/plugins/services/modbus/modbus_test.go new file mode 100644 index 0000000..c9d5097 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/modbus/modbus_test.go @@ -0,0 +1,38 @@ +package modbus + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestModbus(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "modbus", + Port: 5020, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "oitc/modbus-server", + }, + }, + } + + p := &MODBUSPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go new file mode 100644 index 0000000..2c55c6e --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3.go @@ -0,0 +1,126 @@ +package mqtt3 + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type MQTT3Plugin struct{} +type TLSPlugin struct{} + +const MQTT = "mqtt3" +const MQTTTLS = "mqtt3tls" + +func init() { + plugins.RegisterPlugin(&MQTT3Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func testConnectRequest(conn net.Conn, requestBytes []byte, timeout time.Duration) (bool, error) { + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if response[0] == 0x20 { + // MQTT server + return true, nil + } + return true, &utils.InvalidResponseError{Service: MQTT} +} + +func (p *MQTT3Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, false, target) +} + +func (p *MQTT3Plugin) PortPriority(i uint16) bool { + return i == 1883 +} + +func (p *MQTT3Plugin) Name() string { + return MQTT +} + +func (p *MQTT3Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, true, target) +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 8883 +} + +func (p *TLSPlugin) Name() string { + return MQTTTLS +} + +func (p *MQTT3Plugin) Priority() int { + return 500 +} + +func (p *TLSPlugin) Priority() int { + return 501 +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +// Run +/* + MQTT is a publish-subscribe protocol designed to be used as + a lightweight messaging protocol. An MQTT connection begins with + a CONNECT request and a CONNACK response. A well-behaved MQTT server + will simply close the connection if an invalid request is sent. Connect + packets are formatted slightly differently between v3 and v5, so two requests + are sent. + + CONNECT requests are composed of a fixed header that indicates the message type and + length, and then a variable length header that specifies the connection details, + including the protocol version. The v5 header also includes a properties section, while the + v3 header does not. + + The CONNACK response will begin with a 0x20 byte that indicates the message type. The + presence/absence of this byte is used to determine if MQTT is present. +*/ + +func Run(conn net.Conn, timeout time.Duration, tls bool, target plugins.Target) (*plugins.Service, error) { + // version 3.1.x connect command + mqttConnect3 := []byte{ + // message type 1 + 4 bits reserved + 0x10, + // message length of 17 (the number of following bytes) + 0x11, + // protocol name length (4) + 0x00, 0x04, + // protocol name (MQTT) + 0x4d, 0x51, 0x54, 0x54, + // protocol version (3) + 0x03, + // flags (all unset except for Clean Session) + 0x02, + // keep alive + 0x00, 0x3c, + // client ID length of 5 + 0x00, 0x05, + // client ID AAAA + 0x41, 0x41, 0x41, 0x41, 0x41, + } + + check, err := testConnectRequest(conn, mqttConnect3, timeout) + if check && err == nil { + return plugins.CreateServiceFrom(target, plugins.ServiceMQTT{}, tls, "3.1.x", plugins.TCP), nil + } else if check && err != nil { + return nil, nil + } + return nil, err +} diff --git a/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go new file mode 100644 index 0000000..e57efd1 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt3/mqtt3_test.go @@ -0,0 +1,38 @@ +package mqtt3 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestMqtt3(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mqtt", + Port: 1883, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "efrecon/mosquitto", + }, + }, + } + + p := &MQTT3Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go new file mode 100644 index 0000000..6b4d173 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5.go @@ -0,0 +1,128 @@ +package mqtt5 + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type MQTT5Plugin struct{} +type TLSPlugin struct{} + +const MQTT = "mqtt5" +const MQTTTLS = "mqtt5tls" + +func init() { + plugins.RegisterPlugin(&MQTT5Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func testConnectRequest(conn net.Conn, requestBytes []byte, timeout time.Duration) (bool, error) { + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return false, err + } + if len(response) == 0 { + return true, &utils.ServerNotEnable{} + } + + if response[0] == 0x20 { + // MQTT server + return true, nil + } + return true, &utils.InvalidResponseError{Service: MQTT} +} + +func (p *MQTT5Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, false, target) +} + +func (p *MQTT5Plugin) PortPriority(i uint16) bool { + return i == 1883 +} + +func (p *MQTT5Plugin) Priority() int { + return 505 +} + +func (p *TLSPlugin) Priority() int { + return 506 +} + +func (p *MQTT5Plugin) Name() string { + return MQTT +} + +func (p *MQTT5Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return Run(conn, timeout, true, target) +} + +func (p *TLSPlugin) PortPriority(i uint16) bool { + return i == 8883 +} + +func (p *TLSPlugin) Name() string { + return MQTTTLS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +// Run +/* + MQTT is a publish-subscribe protocol designed to be used as + a lightweight messaging protocol. An MQTT connection begins with + a CONNECT request and a CONNACK response. A well-behaved MQTT server + will simply close the connection if an invalid request is sent. Connect + packets are formatted slightly differently between v3 and v5, so two requests + are sent. + + CONNECT requests are composed of a fixed header that indicates the message type and + length, and then a variable length header that specifies the connection details, + including the protocol version. The v5 header also includes a properties section, while the + v3 header does not. + + The CONNACK response will begin with a 0x20 byte that indicates the message type. The + presence/absence of this byte is used to determine if MQTT is present. +*/ + +func Run(conn net.Conn, timeout time.Duration, tls bool, target plugins.Target) (*plugins.Service, error) { + // version 3.1.1 connect command + mqttConnect5 := []byte{ + // message type 1 + 4 bits reserved + 0x10, + // message length of 18 (the number of following bytes) + 0x12, + // protocol name length (4) + 0x00, 0x04, + // protocol name (MQTT) + 0x4d, 0x51, 0x54, 0x54, + // protocol version (5) + 0x05, + // flags (all unset except for Clean Session) + 0x02, + // keep alive + 0x00, 0x3c, + // properties length of 0 + 0x00, + // client ID length of 5 + 0x00, 0x05, + // client ID AAAA + 0x41, 0x41, 0x41, 0x41, 0x41, + } + + check, err := testConnectRequest(conn, mqttConnect5, timeout) + if check && err == nil { + return plugins.CreateServiceFrom(target, plugins.ServiceMQTT{}, tls, "5.0", plugins.TCP), nil + } else if check && err != nil { + return nil, nil + } + return nil, err +} diff --git a/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go new file mode 100644 index 0000000..77b36d6 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mqtt/mqtt5/mqtt5_test.go @@ -0,0 +1,38 @@ +package mqtt5 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestMqtt5(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mqtt", + Port: 1883, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "efrecon/mosquitto", + }, + }, + } + + p := &MQTT5Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/mssql/mssql.go b/scanner/pkg/fingerprint/plugins/services/mssql/mssql.go new file mode 100644 index 0000000..4f4a784 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mssql/mssql.go @@ -0,0 +1,316 @@ +package mssql + +import ( + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +// Potential values for PLOptionToken +const ( + VERSION int = 0 + ENCRYPTION int = 1 + INSTOPT int = 2 + THREADID int = 3 + MARS int = 4 + TRACEID int = 5 + FEDAUTHREQUIRED int = 6 + NONCEOPT int = 7 + TERMINATOR byte = 0xFF +) + +type OptionToken struct { + PLOptionToken uint32 + PLOffset uint32 + PLOptionLength uint32 + PLOptionData []byte // the raw data associated with the option +} + +type MSSQLPlugin struct{} + +type Data struct { + Version string +} + +const MSSQL = "mssql" + +func init() { + plugins.RegisterPlugin(&MSSQLPlugin{}) +} + +func (p *MSSQLPlugin) PortPriority(port uint16) bool { + return port == 1433 +} + +func DetectMSSQL(conn net.Conn, timeout time.Duration) (Data, bool, error) { + // Below is a TDS prelogin packet sent by the client to begin the + // initial handshake with the server + preLoginPacket := []byte{ + + // Pre-Login Request Header + 0x12, // Type + 0x01, // Status + 0x00, 0x58, // Length + 0x00, 0x00, // SPID + 0x01, // PacketID + 0x00, // Window + + // We configure the following options within the pre-login request body: + // + // VERSION: 11 09 00 01 00 00 + // ENCRYPTION: 00 + // INSTOPT: 00 + // THREADID: 00 00 00 00 + // MARS: 00 + // TRACEID: f9 b8 cb 5c 94 6b 89 1f + // d9 aa 3c 13 4b d0 7b 88 + // 03 5c 32 21 24 a2 81 86 + // 37 cf 62 39 4a 46 2c c6 + // 00 00 00 00 + + // Pre-Login Request Payload + 0x00, // PLOptionToken (VERSION) + 0x00, 0x1F, // PLOffset + 0x00, 0x06, // PLOptionLength + + 0x01, // PLOptionToken (ENCRYPTION) + 0x00, 0x25, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x02, // PLOptionToken (INSTOPT) + 0x00, 0x26, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x03, // PLOptionToken (THREADID) + 0x00, 0x27, // PLOffset + 0x00, 0x04, // PLOptionLength + + 0x04, // PLOptionToken (MARS) + 0x00, 0x2B, // PLOffset + 0x00, 0x01, // PLOptionLength + + 0x05, // PLOptionToken (TRACEID) + 0x00, 0x2C, // PLOffset + 0x00, 0x24, // PLOptionLength + + 0xFF, // TERMINATOR + + // PLOptionData + 0x11, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF9, 0xB8, 0xCB, + 0x5C, 0x94, 0x6B, 0x89, 0x1F, 0xD9, 0xAA, 0x3C, + 0x13, 0x4B, 0xD0, 0x7B, 0x88, 0x03, 0x5C, 0x32, + 0x21, 0x24, 0xA2, 0x81, 0x86, 0x37, 0xCF, 0x62, + 0x39, 0x4A, 0x46, 0x2C, 0xC6, 0x00, 0x00, 0x00, + 0x00, + } + + response, err := utils.SendRecv(conn, preLoginPacket, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + /* + Below is an example pre-login response (tabular response) packet + returned by the client to the server: + + Pre-Login Response (Tabular Response) Header: + + Type: 0x04 + Status: 0x01 + Length: 0x00 0x30 + SPID: 0x00 0x00 + PacketId: 0x01 + Window: 0x00 + + Pre-Login Response Body: + + PLOptionToken: 0x00 (VERSION) + PLOffset: 0x00 0x1F + PLOptionLength: 0x00 0x06 + + PLOptionToken: 0x01 (ENCRYPTION) + PLOffset: 0x00 0x25 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x02 (INSTOPT) + PLOffset: 0x00 0x26 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x03 (THREADID) + PLOffset: 0x00 0x27 + PLOptionLength: 0x00 0x00 + + PLOptionToken: 0x04 (MARS) + PLOffset: 0x00 0x27 + PLOptionLength: 0x00 0x01 + + PLOptionToken: 0x05 (TRACEID) + PLOffset: 0x00 0x28 + PLOptionLength: 0x00 0x00 + + PLOptionToken: 0xFF + + PLOptionData: 0f 00 07 d0 00 00 00 00 00 + + VERSION: 0f 00 07 d0 00 00 + ENCRYPTION: 00 + INSTOPT 00 + MARS: 00 + */ + + // The TDS header is eight bytes so any response less than this can be safely classified + // as invalid (i.e. not MSSQL/TDS) + if len(response) < 8 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "response is too short to be a valid TDS packet header", + } + } + + if response[0] != 0x04 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "type should be set to tabular result for a valid TDS packet", + } + } + + if response[1] != 0x01 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "expect a status of one (end of message) for tabular result packet", + } + } + + packetLength := int(uint32(response[3]) | uint32(response[2])<<8) + if len(response) != packetLength { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "packet length does not match length read", + } + } + + if response[4] != 0x00 || response[5] != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for SPID should always be zero", + } + } + + if response[6] != 0x01 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for packet id should always be one", + } + } + + if response[7] != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "value for window should always be zero", + } + } + + // The body of the pre-login response message is a list of PL_OPTION tokens + // that index into the PLOptionData segment and the list is + // terminated by a PLOptionToken with TERMINATOR (0xFF) as the value. + + position := 8 // set to the position to just after the TDS packet header + + var optionTokens []OptionToken + for response[position] != TERMINATOR && position < len(response) { + plOptionToken := uint32(response[position+0]) + plOffset := uint32(response[position+2]) | uint32(response[position+1])<<8 + plOptionLength := uint32(response[position+4]) | uint32(response[position+3])<<8 + + plOptionData := []byte{} + if plOptionLength != 0 { + if plOffset+plOptionLength < uint32(len(response)) { + plOptionData = response[plOffset+8 : plOffset+8+plOptionLength] + } else { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "server returned an invalid PLOffset or PLOptionLength"} + } + } + + position += 5 + optionTokenStruct := OptionToken{ + PLOptionToken: plOptionToken, + PLOffset: plOffset, + PLOptionLength: plOptionLength, + PLOptionData: plOptionData, + } + + optionTokens = append(optionTokens, optionTokenStruct) + } + + if response[position] != 0xFF { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "list of option tokens should be terminated by 0xff", + } + } + + if len(optionTokens) < 1 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "there should be at least one option token since VERSION is required", + } + } + + if optionTokens[0].PLOptionToken != 0x00 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "TDS requires VERSION to be the first PLOptionToken value", + } + } + + if optionTokens[0].PLOptionLength != 0x06 { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: MSSQL, + Info: "version field should be fixed bytes", + } + } + + MajorVersion := optionTokens[0].PLOptionData[0] + MinorVersion := optionTokens[0].PLOptionData[1] + BuildNumber := uint32( + (uint32(optionTokens[0].PLOptionData[2]) * 256) + uint32( + optionTokens[0].PLOptionData[3], + ), + ) + + version := fmt.Sprintf("%d.%d.%d\n", MajorVersion, MinorVersion, BuildNumber) + + return Data{Version: version}, true, nil +} + +func (p *MSSQLPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectMSSQL(conn, timeout) + if check && err != nil { + return nil, nil + } else if !check && err != nil { + return nil, err + } + + return plugins.CreateServiceFrom(target, plugins.ServiceMSSQL{}, false, data.Version, plugins.TCP), nil +} + +func (p *MSSQLPlugin) Name() string { + return MSSQL +} + +func (p *MSSQLPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MSSQLPlugin) Priority() int { + return 143 +} diff --git a/scanner/pkg/fingerprint/plugins/services/mssql/mssql_test.go b/scanner/pkg/fingerprint/plugins/services/mssql/mssql_test.go new file mode 100644 index 0000000..56fc675 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mssql/mssql_test.go @@ -0,0 +1,43 @@ +package mssql + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestMSSQL(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mssql", + Port: 1433, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mcr.microsoft.com/mssql/server", + Tag: "2019-latest", + Env: []string{ + "ACCEPT_EULA=Y", + "SA_PASSWORD=yourStrong(!)Password", + }, + }, + }, + } + + p := &MSSQLPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/mysql/mysql.go b/scanner/pkg/fingerprint/plugins/services/mysql/mysql.go new file mode 100644 index 0000000..ab280eb --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mysql/mysql.go @@ -0,0 +1,277 @@ +package mysql + +import ( + "fmt" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +/* +When we perform fingerprinting of the MySQL service, we can expect to get one +of two packets back from the server on the initial connection. The first would +be an initial handshake packet indicating we can authenticate to the server. + +The second potential response would be an error message returned by the server +telling us why we can't authenticate. For example, the server may respond with +an error message stating the client IP is not allowed to authenticate to the +server. + + Example MySQL Initial Handshake Packet: + Length: 4a 00 00 00 + Version: 0a + Server Version: 38 2e 30 2e 32 38 00 (null terminated string "8.0.28") + Connection Id: 0b 00 00 00 + Auth-Plugin-Data-Part-1: 15 05 6c 51 28 32 48 15 + Filler: 00 + Capability Flags: ff ff + Character Set: ff + Status Flags: 02 00 + Capability Flags: ff df + Length of Auth Plugin Data: 15 + Reserved (all 00): 00 00 00 00 00 00 00 00 00 00 + Auth-Plugin-Data-Part-2 (len 13 base 10): 26 68 15 1e 2e 7f 69 38 52 6b 6c 5c 00 + Auth Plugin Name: null terminated string "caching_sha2_password" + + Example MySQL Error Packet on Initial Connection: + Packet Length: 45 00 00 00 + Header: ff + Error Code: 6a 04 + Human Readable Error Message: Host '50.82.91.234' is not allowed to connect to this MySQL server +*/ + +type MYSQLPlugin struct{} + +const ( + // protocolVersion = 10 + // maxPacketLength = 1<<24 - 1 + MYSQL = "MySQL" +) + +func init() { + plugins.RegisterPlugin(&MYSQLPlugin{}) +} + +// Run checks if the identified service is a MySQL (or MariaDB) server using +// two methods. Upon the connection of a client to a MySQL server it can return +// one of two responses. Either the server returns an initial handshake packet +// or an error message packet. +func (p *MYSQLPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + mysqlVersionStr, err := CheckInitialHandshakePacket(response) + if err == nil { + payload := plugins.ServiceMySQL{ + PacketType: "handshake", + ErrorMessage: "", + ErrorCode: 0, + } + return plugins.CreateServiceFrom(target, payload, false, mysqlVersionStr, plugins.TCP), nil + } + + errorStr, errorCode, err := CheckErrorMessagePacket(response) + if err == nil { + payload := plugins.ServiceMySQL{ + PacketType: "error", + ErrorMessage: errorStr, + ErrorCode: errorCode, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *MYSQLPlugin) PortPriority(port uint16) bool { + return port == 3306 +} + +func (p *MYSQLPlugin) Name() string { + return MYSQL +} + +func (p *MYSQLPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *MYSQLPlugin) Priority() int { + return 133 +} + +// CheckErrorMessagePacket checks the response packet error message +func CheckErrorMessagePacket(response []byte) (string, int, error) { + // My brief research suggests that its not possible to get a compliant + // error message packet that is less than eight bytes + if len(response) < 8 { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet is too small for an error message packet", + } + } + + packetLength := int( + uint32( + response[0], + ) | uint32( + response[1], + )<<8 | uint32( + response[2], + )<<16 | uint32( + response[3], + )<<24, + ) + actualResponseLength := len(response) - 4 + + if packetLength != actualResponseLength { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length does not match length of the response from the server", + } + } + + header := int(response[4]) + if header != 0xff { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid header for an error message packet", + } + } + + errorCode := int(uint32(response[5]) | uint32(response[6])<<8) + if errorCode < 1000 || errorCode > 2000 { + return "", errorCode, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid error code", + } + } + + errorStr, err := readEOFTerminatedASCIIString(response, 7) + if err != nil { + return "", errorCode, &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: err.Error()} + } + + return errorStr, errorCode, nil +} + +// CheckInitialHandshakePacket checks if the response received from the server +// matches the expected response for the MySQL service +func CheckInitialHandshakePacket(response []byte) (string, error) { + // My brief research suggests that its not possible to get a compliant + // initial handshake packet that is less than roughly 35 bytes + if len(response) < 35 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length is too small for an initial handshake packet", + } + } + + packetLength := int( + uint32( + response[0], + ) | uint32( + response[1], + )<<8 | uint32( + response[2], + )<<16 | uint32( + response[3], + )<<24, + ) + version := int(response[4]) + + if packetLength < 25 || packetLength > 4096 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet length doesn't make sense for the MySQL handshake packet", + } + } + + if version != 10 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "packet has an invalid version", + } + } + + mysqlVersionStr, position, err := readNullTerminatedASCIIString(response, 5) + if err != nil { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "unable to read null-terminated ASCII version string, err: " + err.Error(), + } + } + + // If we skip the connection id and auth-plugin-data-part-1 fields the spec says + // there is a filler byte that should always be zero at this position + fillerPos := position + 13 + if position >= len(response) { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "buffer is too small to be a valid initial handshake packet", + } + } + + // According to the specification this should always be zero since it is a filler byte + if response[fillerPos] != 0x00 { + return "", &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: fmt.Sprintf( + "expected filler byte at ths position to be zero got: %d", + response[fillerPos], + ), + } + } + + return mysqlVersionStr, nil +} + +// readNullTerminatedASCIIString is responsible for reading a null terminated +// ASCII string from a buffer and returns it as a string type +func readNullTerminatedASCIIString(buffer []byte, startPosition int) (string, int, error) { + characters := []byte{} + success := false + endPosition := 0 + + for position := startPosition; position < len(buffer); position++ { + if buffer[position] >= 0x20 && buffer[position] <= 0x7E { + characters = append(characters, buffer[position]) + } else if buffer[position] == 0x00 { + success = true + endPosition = position + break + } else { + return "", 0, &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: "encountered invalid ASCII character"} + } + } + + if !success { + return "", 0, &utils.InvalidResponseErrorInfo{ + Service: MYSQL, + Info: "hit the end of the buffer without encountering a null terminator", + } + } + + return string(characters), endPosition, nil +} + +// readEOFTerminatedASCIIString is responsible for reading an ASCII string +// that is terminated by the end of the message +func readEOFTerminatedASCIIString(buffer []byte, startPosition int) (string, error) { + characters := []byte{} + + for position := startPosition; position < len(buffer); position++ { + if buffer[position] >= 0x20 && buffer[position] <= 0x7E { + characters = append(characters, buffer[position]) + } else { + return "", &utils.InvalidResponseErrorInfo{Service: MYSQL, Info: "encountered invalid ASCII character"} + } + } + + return string(characters), nil +} diff --git a/scanner/pkg/fingerprint/plugins/services/mysql/mysql_test.go b/scanner/pkg/fingerprint/plugins/services/mysql/mysql_test.go new file mode 100644 index 0000000..aee436b --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/mysql/mysql_test.go @@ -0,0 +1,42 @@ +package mysql + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestMySQL(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "mysql", + Port: 3306, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "mysql", + Tag: "5.7.39", + Env: []string{ + "MYSQL_ROOT_PASSWORD=my-secret-pw", + }, + }, + }, + } + + p := &MYSQLPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns.go b/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns.go new file mode 100644 index 0000000..602fc47 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns.go @@ -0,0 +1,72 @@ +package netbios + +import ( + "crypto/rand" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const NETBIOS = "netbios-ns" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + transactionID := make([]byte, 2) + _, err := rand.Read(transactionID) + if err != nil { + return nil, &utils.RandomizeError{Message: "Transaction ID"} + } + InitialConnectionPackage := append(transactionID, []byte{ //nolint:gocritic + // Transaction ID + 0x00, 0x10, // Flag: Broadcast + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Queries + 0x20, 0x43, 0x4b, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, + 0x00, 0x21, + 0x00, 0x01, + }...) + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + stringBegin := strings.Index(string(response), "\x00\x00\x00\x00\x00") + 7 + stringEnd := strings.Index(string(response), "\x20\x20\x20") + if stringBegin == -1 || stringEnd == -1 || stringEnd < stringBegin || + stringBegin >= len(response) || stringEnd >= len(response) { + return nil, nil + } + payload := plugins.ServiceNetbios{ + NetBIOSName: string(response[stringBegin:stringEnd]), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 137 +} + +func (p *Plugin) Name() string { + return NETBIOS +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 700 +} diff --git a/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns_test.go b/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns_test.go new file mode 100644 index 0000000..4ebc407 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/netbios/netbiosns_test.go @@ -0,0 +1,40 @@ +package netbios + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestNetBIOS(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "netbios-ns", + Port: 137, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "dperson/samba", + Cmd: []string{"-n"}, + Privileged: true, + }, + }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ntp/ntp.go b/scanner/pkg/fingerprint/plugins/services/ntp/ntp.go new file mode 100644 index 0000000..1570b19 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ntp/ntp.go @@ -0,0 +1,67 @@ +package ntp + +import ( + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const NTP = "ntp" + +type Plugin struct{} + +var ModeServer uint8 = 4 + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + // reference: https://datatracker.ietf.org/doc/html/rfc5905#section-7.3 + InitialConnectionPackage := []byte{ + 0xe3, 0x00, 0x0a, 0xf8, // LI/VN/Mode | Stratum | Poll | Precision + 0x00, 0x00, 0x00, 0x00, // Root Delay + 0x00, 0x00, 0x00, 0x00, // Root Dispersion + 0x00, 0x00, 0x00, 0x00, // Reference Identifier + 0x00, 0x00, 0x00, 0x00, // Reference Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Origin Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Receive Timestamp + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // Transmit Timestamp + 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check if response is valid NTP packet + if response[0]&0x07 == ModeServer && len(response) == len(InitialConnectionPackage) { + return plugins.CreateServiceFrom(target, plugins.ServiceNTP{}, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 123 +} + +func (p *Plugin) Name() string { + return NTP +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 800 +} diff --git a/scanner/pkg/fingerprint/plugins/services/ntp/ntp_test.go b/scanner/pkg/fingerprint/plugins/services/ntp/ntp_test.go new file mode 100644 index 0000000..b6f1067 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ntp/ntp_test.go @@ -0,0 +1,37 @@ +package ntp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestNTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ntp", + Port: 123, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "cturra/ntp", + }, + }, + } + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn.go b/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn.go new file mode 100644 index 0000000..405bb29 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn.go @@ -0,0 +1,85 @@ +package openvpn + +import ( + "crypto/rand" + "net" + "reflect" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const OPENVPN = "OpenVPN" + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /** + * https://build.openvpn.net/doxygen/ssl__pkt_8h_source.html + * https://openvpn.net/community-resources/openvpn-protocol/ + * + * Send CLIENT_RESET control message, expect back valid SERVER_RESET message from server + * Checks if SERVER_RESET opcode is received, along with whether remote session ID is contained in response + * NOTE: Does not work if tls-auth is enabled in OpenVPN config (drops connection due to HMAC error) + */ + + var POpcodeShift uint8 = 3 + var PControlHardResetClientV2 uint8 = 7 + var PControlHardResetServerV2 uint8 = 8 + var SessionIDLength = 8 + + InitialConnectionPackage := []byte{ + PControlHardResetClientV2 << POpcodeShift, // opcode/key_id + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // Session ID (64-bit), + 0x0, // Message Packet-ID Array Length + 0x0, 0x0, 0x0, 0x0, // Message Packet-ID + } + _, err := rand.Read( + InitialConnectionPackage[1 : 1+SessionIDLength], + ) // generate random session ID + if err != nil { + return nil, &utils.RandomizeError{Message: "session ID"} + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check if response is valid OpenVPN packet + if (response[0] >> POpcodeShift) == PControlHardResetServerV2 { + for i := 0; i < len(response)-SessionIDLength; i++ { + if reflect.DeepEqual( + response[i:i+SessionIDLength], + InitialConnectionPackage[1:1+SessionIDLength], + ) { + return plugins.CreateServiceFrom(target, plugins.ServiceOpenVPN{}, false, "", plugins.UDP), nil + } + } + } + return nil, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 1194 +} + +func (p *Plugin) Name() string { + return OPENVPN +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 708 +} diff --git a/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn_test.go b/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn_test.go new file mode 100644 index 0000000..f03339b --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/openvpn/openvpn_test.go @@ -0,0 +1,38 @@ +package openvpn + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/test" +) + +func TestOpenVPN(t *testing.T) { + // the Privileged container does not run on Github actions -- but this test passes locally + testcases := []test.Testcase{ + // { + // Description: "openvpn", + // Port: 1194, + // Protocol: plugins.UDP, + // Expected: func(res *plugins.PluginResults) bool { + // return res != nil + // }, + // RunConfig: dockertest.RunOptions{ + // Repository: "jpetazzo/dockvpn", + // Privileged: true, + // }, + // }, + } + + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/oracledb/oracle.go b/scanner/pkg/fingerprint/plugins/services/oracledb/oracle.go new file mode 100644 index 0000000..aabb8eb --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/oracledb/oracle.go @@ -0,0 +1,253 @@ +package oracledb + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "net" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type ORACLEPlugin struct{} + +const ORACLE = "oracle" + +func init() { + plugins.RegisterPlugin(&ORACLEPlugin{}) +} + +/* +Transparent Network Substrate Protocol + + Packet Length: 270 + Packet Checksum: 0x0000 + Packet Type: Connect (1) + Reserved Byte: 00 + Header Checksum: 0x0000 + Connect + Version: 318 + Version (Compatible): 300 + Service Options: 0x0c41, Header Checksum, Full Duplex + Session Data Unit Size: 8192 + Maximum Transmission Data Unit Size: 65535 + NT Protocol Characteristics: 0x7f08, Confirmed release, TDU based IO, Spawner running, Data test, + Callback IO supported, ASync IO Supported, Packet oriented IO, + Generate SIGURG signal + Line Turnaround Value: 0 + Value of 1 in Hardware: 0100 + Length of Connect Data: 196 + Offset to Connect Data: 74 + Maximum Receivable Connect Data: 5120 + Connect Flags 0: 0x41, NA services wanted + ...0 .... = NA services required: False + .... 0... = NA services linked in: False + .... .0.. = NA services enabled: False + .... ..0. = Interchange is involved: False + .... ...1 = NA services wanted: True + Connect Flags 1: 0x41, NA services wanted + ...0 .... = NA services required: False + .... 0... = NA services linked in: False + .... .0.. = NA services enabled: False + .... ..0. = Interchange is involved: False + .... ...1 = NA services wanted: True + Trace Cross Facility Item 1: 0xd8870000 + Trace Cross Facility Item 2: 0x00000000 + Trace Unique Connection ID: 0x0000000000000000 + Connect Data: (DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=XE)(CID=(PROGRAM=sqlplus) + (HOST=a68e91558f29)(USER=oracle))(CONNECTION_ID=3krKWwBDEZ/gUwQAEaydrQ==)) + (ADDRESS=(PROTOCOL=tcp)(HOST=192.168.1.116)(PORT=1521))) + + + Expected Rejection Response: + Transparent Network Substrate Protocol + Packet Length: 103 + Packet Checksum: 0x0000 + Packet Type: Refuse (4) + Reserved Byte: 00 + Header Checksum: 0x0000 + Refuse + Refuse Reason (User): 0x22 + Refuse Reason (System): 0x00 + Refuse Data Length: 91 + Refuse Data: (DESCRIPTION=(TMP=)(VSNNUM=352321536)(ERR=12514)(ERROR_STACK=(ERROR=(CODE=12514)(EMFI=4)))) + + + Transparent Network Substrate Protocol + Packet Length: 8 + Packet Checksum: 0x0000 + Packet Type: Resend (11) + Reserved Byte: 00 + Header Checksum: 0x0000 + + + TESTED AGAINST: Oracle DB XE 21c; however, I could not find an 11g download link which is required + to build the container for testing. Also please note the heuristic-ness of the response verifications. + The request settings may need to be tinkered with (areas of tinkering have been noted with comments) + + Oracle Database 10.2, 11.x, 12.x, and 18c are available as a media or FTP request + for those customers who own a valid Oracle Database product license for any edition. + To request access to these releases, follow the instructions in Oracle Support Document + 1071023.1 (Requesting Physical Shipment or Download URL for Software Media) from My Oracle Support. + NOTE: for Oracle Database 10.2, you should request 10.2.0.1 even if you want to install a later + patch set. Once you install 10.2.0.1 you can then apply any 10.2 patch set. Similarly, for + 11.1 request 11.1.0.6 which must be applied before installing 11.1.0.7. Patch sets can be + downloaded from the Patches and Updates tab on My Oracle Support. +*/ +func checkForOracle(host string, port string) []byte { + // This string varies widely for several scripts from metasploit and nmap, + // probably needs to be adapted over time as we get feedback on how effective it is + connectData := []byte( + "(DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=non-abc-existent-ser-vice-123-a-a-bc-asdf)(CID=(PROGRAM=sqlplus)" + + "(HOST=__jdbc__)(USER=)))(ADDRESS=(PROTOCOL=tcp)(HOST=" + host + ")(PORT=" + port + ")))", + ) + tnsHeaderPktLen := [2]byte{} + binary.BigEndian.PutUint16(tnsHeaderPktLen[:], uint16(len(connectData)+58)) + tnsHeaderPktCkSm := [2]byte{0x00, 0x00} // This is left empty + tnsHeaderPktType := [1]byte{0x01} // Connect type + tnsHeaderReservedByte := [1]byte{0x00} + tnsHeaderChecksum := [2]byte{0x00, 0x00} + + connectVersion := [2]byte{0x01, 0x3c} + connectVersionCompat := [2]byte{0x01, 0x2c} + connectServiceOpts := [2]byte{0x00, 0x00} + sessionDUS := [2]byte{0x80, 0x00} // Data Unit Size + maxSessionDUS := [2]byte{0x7F, 0xFF} + ntPrtoChar := [2]byte{0x7F, 0x08} + lineTurnaroundVal := [2]byte{0x00, 0x00} + valOneInHardware := [2]byte{0x00, 0x01} + lenConnectData := [2]byte{} + binary.BigEndian.PutUint16(lenConnectData[:], uint16(len(connectData))) + offsetConnectData := [2]byte{0x00, 0x3a} + MaxDataRecv := [4]byte{0x00, 0x00, 0x04, 0x00} + connectFlags := [2]byte{0x00, 0x00} + traceCrossFacilityItems := [8]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + traceUnqConnID := [8]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0} + magicBytes := [8]byte{ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + } // These bytes are undocumented, and I believe + // Are dependent on the version of TNS used and some connect and NT flags. + // This may be a focus for reliability and accuracy + combine := [][]byte{ + tnsHeaderPktLen[:], + tnsHeaderPktCkSm[:], + tnsHeaderPktType[:], + tnsHeaderReservedByte[:], + tnsHeaderChecksum[:], + connectVersion[:], + connectVersionCompat[:], + connectServiceOpts[:], + sessionDUS[:], + maxSessionDUS[:], + ntPrtoChar[:], + lineTurnaroundVal[:], + valOneInHardware[:], + lenConnectData[:], + offsetConnectData[:], + MaxDataRecv[:], + connectFlags[:], + traceCrossFacilityItems[:], + traceUnqConnID[:], + magicBytes[:], + connectData, + } + + fullRequest := make([]byte, len(connectData)+58) + index := 0 + for _, s := range combine { + index += copy(fullRequest[index:], s) + } + + return fullRequest +} + +func isOracleDBRunning(response []byte) bool { + beginPattern := []byte{ + 0x28, 0x44, 0x45, 0x53, 0x43, 0x52, 0x49, 0x50, + 0x54, 0x49, 0x4f, 0x4e, 0x3d, 0x28, 0x54, 0x4d, + 0x50, 0x3d, 0x29, 0x28, 0x56, 0x53, 0x4e, 0x4e, + 0x55, 0x4d, 0x3d, + } + + if len(response) < 27 { + return false + } + + responseCode := int(response[4]) + + // This should always be a response code of 4 (rejection), + // however I have included resend and accept response codes as well + if responseCode != 4 && responseCode != 2 && responseCode != 11 { + return false + } + + // When making a request with the function above, every oracle version should return a variation of: + // (DESCRIPTION=(TMP=)(VSNNUM=318767104)(ERR=1189)(ERROR_STACK=(ERROR=(CODE=1189)(EMFI=4)))) + // VSNUM and ERR will change based on the version of oracle used + // Instead, key off (DESCRIPTION=(TMP=)(VSNNUM= to determine if the server is running oracle + return bytes.Index(response, beginPattern) > 0 +} + +func parseInfo(response []byte) map[string]any { + refuseData := response[12:] + code := regexp.MustCompile(`[0-9]+`).FindAllStringSubmatch(string(refuseData), 2) + VSNNum := code[0][0] + ErrCode := code[1][0] + VsNum, _ := strconv.Atoi(VSNNum) + version := big.NewInt(int64(VsNum)).Bytes() + split := strconv.FormatInt(int64(version[1]), 16) + versionStr := fmt.Sprintf("%d.%c.%c.%d.%d", version[0], split[0], split[1], version[2], version[3]) + return map[string]any{"Oracle TNS Listener Version": versionStr, "VSNNUM": VSNNum, "ERROR_CODE": ErrCode} +} + +func (p *ORACLEPlugin) PortPriority(port uint16) bool { + return port == 1521 +} + +func (p *ORACLEPlugin) Name() string { + return ORACLE +} + +func (p *ORACLEPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *ORACLEPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + addr := strings.Split(conn.RemoteAddr().String(), ":") + ip, port := addr[0], addr[1] + request := checkForOracle(ip, port) + + response, err := utils.SendRecv(conn, request, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if isOracleDBRunning(response) { + oracleInfo := fmt.Sprintf("%s", parseInfo(response)) + payload := plugins.ServiceOracle{ + Info: oracleInfo, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, nil +} + +func (p *ORACLEPlugin) Priority() int { + return 900 +} diff --git a/scanner/pkg/fingerprint/plugins/services/oracledb/oracle_test.go b/scanner/pkg/fingerprint/plugins/services/oracledb/oracle_test.go new file mode 100644 index 0000000..fe99d2a --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/oracledb/oracle_test.go @@ -0,0 +1,39 @@ +package oracledb + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestOracleDB(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "oracledb", + Port: 1521, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "babim/oracledatabase", + Tag: "11.2.0.4", + }, + }, + } + + p := &ORACLEPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/pop3/pop3.go b/scanner/pkg/fingerprint/plugins/services/pop3/pop3.go new file mode 100644 index 0000000..6eda8bd --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/pop3/pop3.go @@ -0,0 +1,127 @@ +package pop3 + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type POP3Plugin struct{} // POP3 +type TLSPlugin struct{} // POP3S + +const POP3 = "pop3" +const POP3S = "pop3s" + +func init() { + plugins.RegisterPlugin(&POP3Plugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *POP3Plugin) PortPriority(port uint16) bool { + return port == 110 +} + +func DetectPOP3(conn net.Conn, timeout time.Duration, tls bool) (string, bool, error) { + // read initial response from server + initialResponse, err := utils.Recv(conn, timeout) + if err != nil { + return "", false, err + } + if len(initialResponse) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + // send a bogus command and read error response + errResponse, err := utils.SendRecv(conn, []byte("Not a command \r\n"), timeout) + if err != nil { + return "", false, err + } + if len(errResponse) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + isPOP3 := false + if strings.HasPrefix(string(initialResponse), "+OK") && + strings.HasPrefix(string(errResponse), "-ERR") { + isPOP3 = true + } + + if !isPOP3 { + // no ? :( + if tls { + return "", true, &utils.InvalidResponseErrorInfo{ + Service: POP3S, + Info: "did not get expected banner for POP3S", + } + } + return "", true, &utils.InvalidResponseErrorInfo{ + Service: POP3, + Info: "did not get expected banner for POP3", + } + } + + return string(initialResponse[4:]), true, nil +} + +func (p *POP3Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectPOP3(conn, timeout, false) + + if check && err != nil { // service is not running POP3 + return nil, nil + } else if !check && err != nil { // plugin error + return nil, err + } + + // service is running POP3 + payload := plugins.ServicePOP3{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 995 +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + result, check, err := DetectPOP3(conn, timeout, true) + + if check && err != nil { // service is not running POP3S + return nil, nil + } else if !check && err != nil { // plugin error + return nil, err + } + + // service is running POP3S + payload := plugins.ServicePOP3{ + Banner: result, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil +} + +func (p *POP3Plugin) Name() string { + return POP3 +} + +func (p *POP3Plugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return POP3S +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *POP3Plugin) Priority() int { + return 120 +} + +func (p *TLSPlugin) Priority() int { + return 122 +} diff --git a/scanner/pkg/fingerprint/plugins/services/pop3/pop3_test.go b/scanner/pkg/fingerprint/plugins/services/pop3/pop3_test.go new file mode 100644 index 0000000..154cda5 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/pop3/pop3_test.go @@ -0,0 +1,39 @@ +package pop3 + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestPOP3(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "pop3", + Port: 110, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "instrumentisto/dovecot", + Name: "dovecot-pop3-test", + }, + }, + } + + p := &POP3Plugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql.go b/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql.go new file mode 100644 index 0000000..066a62d --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql.go @@ -0,0 +1,133 @@ +package postgres + +import ( + "encoding/binary" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type POSTGRESPlugin struct{} + +const POSTGRES = "postgres" + +// https://www.postgresql.org/docs/current/protocol-flow.html +// the following three values are the only three valid responses +// from a server for the first byte +const ErrorResponse byte = 0x45 + +// all of the following messages start with R (0x52) +// AuthenticationOk +// AuthenticationKerberosV5 +// AuthenticationCleartextPassword +// AuthenticationMD5Password +// AuthenticationSCMCredential +// AuthenticationGSS +// AuthenticationSSPI +// AuthenticationGSSContinue +// AuthenticationSASL +// AuthenticationSASLContinue +// AuthenticationSASLFinal +// NegotiateProtocolVersion +const AuthReq byte = 0x52 + +const NegotiateProtocolVersion = 0x76 + +func verifyPSQL(data []byte) bool { + msgLength := len(data) + if msgLength < 6 { + // from reading (https://www.postgresql.org/docs/14/protocol-message-formats.html) + // no valid server response from the startup packet can be less than 6 bytes + return false + } + + // (heuristic) Check if length of error or authentication method is reasonable + // (assume length is less than 16 bits) + if data[1] != 0 || data[2] != 0 { + return false + } + + // ErrorResponse or NegotiateProtocolVersion status codes are probably a PSQL server + if data[0] == ErrorResponse || data[0] == NegotiateProtocolVersion { + return true + } + + // A message starting with AuthReq is likely a PSQL server + if data[0] == AuthReq { + return true + } + + // Anything else is not a valid server response + return false +} + +// parse the message from the PSQL server to see if it requires AUTH +// a valid AUTH_OK message is: +// [AuthReq UINT32(8) UINT32(0)] +func successfulAuth(data []byte) bool { + msgLength := len(data) + // the AUTH_OK message is 9 bytes + if msgLength < 9 { + return false + } + if data[0] != AuthReq { + return false + } + length := binary.BigEndian.Uint32(data[1:5]) + if length != 8 { + return false + } + msg := binary.BigEndian.Uint32(data[5:9]) + return msg == 0 +} + +func init() { + plugins.RegisterPlugin(&POSTGRESPlugin{}) +} + +func (p *POSTGRESPlugin) PortPriority(port uint16) bool { + return port == 5432 +} + +func (p *POSTGRESPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + startupPacket := []byte{ + 0x00, 0x00, 0x00, 0x54, 0x00, 0x03, 0x00, 0x00, 0x75, 0x73, 0x65, 0x72, 0x00, 0x70, 0x6f, 0x73, + 0x74, 0x67, 0x72, 0x65, 0x73, 0x00, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x00, 0x70, + 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x00, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x70, 0x73, 0x71, 0x6c, 0x00, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, 0x55, 0x54, + 0x46, 0x38, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, startupPacket, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + isPSQL := verifyPSQL(response) + if !isPSQL { + return nil, nil + } + + payload := plugins.ServicePostgreSQL{ + AuthRequired: !successfulAuth(response), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *POSTGRESPlugin) Name() string { + return POSTGRES +} + +func (p *POSTGRESPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *POSTGRESPlugin) Priority() int { + return 1000 +} diff --git a/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql_test.go b/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql_test.go new file mode 100644 index 0000000..a05bf69 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/postgresql/postgresql_test.go @@ -0,0 +1,44 @@ +package postgres + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestPostgreSQL(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "postgresql", + Port: 5432, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "postgres", + Env: []string{ + "POSTGRES_PASSWORD=secret", + "POSTGRES_USER=user_name", + "POSTGRES_DB=dbname", + "listen_addresses = '*'", + }, + }, + }, + } + + p := &POSTGRESPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/rdp/rdp.go b/scanner/pkg/fingerprint/plugins/services/rdp/rdp.go new file mode 100644 index 0000000..dfa33c3 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rdp/rdp.go @@ -0,0 +1,372 @@ +package rdp + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "reflect" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type RDPPlugin struct{} +type TLSPlugin struct{} + +const RDP = "rdp" + +func init() { + plugins.RegisterPlugin(&RDPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +// checkSignature checks if a given response matches the expected signature for +// the response +func checkSignature(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func (p *RDPPlugin) PortPriority(port uint16) bool { + return port == 3389 +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 3389 +} + +// getOperatingSystemSignatures returns operating system specific signatures +// for the RDP service. +func getOperatingSystemSignatures() map[string][]byte { + Windows2000 := []byte{ + 0x03, 0x00, 0x00, 0x0b, 0x06, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + } + + WindowsServer2003 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + 0x03, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2008 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + Windows7OrServer2008R2 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x09, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2008R2DC := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x01, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + Windows10 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x1f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2012Or8 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x0f, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, + } + + WindowsServer2016or2019 := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, 0x02, + 0x1f, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, + } + + signatures := map[string][]byte{ + "Windows 2000": Windows2000, + "Windows Server 2003": WindowsServer2003, + "Windows Server 2008": WindowsServer2008, + "Windows 7 or Server 2008 R2": Windows7OrServer2008R2, + "Windows Server 2008 R2 DC": WindowsServer2008R2DC, + "Windows 10": Windows10, + "Windows 8 or Server 2012": WindowsServer2012Or8, + "Windows Server 2016 or 2019": WindowsServer2016or2019, + } + + return signatures +} + +// checkIsRDPGeneric leverages a generic RDP signature to identify if the +// target port is running the RDP service. +func checkRDP(response []byte) bool { + GenericRDPSignature := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, + } + + signature := GenericRDPSignature + signatureLength := len(GenericRDPSignature) + + if len(response) < signatureLength { + return false + } + + responseSlice := response[:signatureLength] + tof := checkSignature(responseSlice, signature) + return tof +} + +// guessOS tries to leverage operating system specific signatures to identify +// if the target port is running a specific operating system. +func guessOS(response []byte) (bool, string) { + signatures := getOperatingSystemSignatures() + for fingerprint, signature := range signatures { + signatureLength := len(signature) + + if len(response) < signatureLength { + continue + } + + responseSlice := response[:signatureLength] + tof := checkSignature(responseSlice, signature) + if tof { + return true, fingerprint + } + } + + return false, "" +} + +func DetectRDP(conn net.Conn, timeout time.Duration) (string, bool, error) { + InitialConnectionPacket := []byte{ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x0b, + 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, InitialConnectionPacket, timeout) + if err != nil { + return "", false, err + } + if len(response) == 0 { + return "", true, &utils.ServerNotEnable{} + } + + isRDP := checkRDP(response) + fingerprint := "" + if isRDP { + success, osFingerprint := guessOS(response) + if success { + fingerprint = osFingerprint + } + + return fingerprint, true, nil + } + return "", true, &utils.InvalidResponseError{Service: RDP} +} + +func DetectRDPAuth(conn net.Conn, timeout time.Duration) (*plugins.ServiceRDP, bool, error) { + info := plugins.ServiceRDP{} + + // CredSSP protocol - NTLM authentication + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp + // http://davenport.sourceforge.net/ntlm.html + + NegotiatePacket := []byte{ + 0x30, 0x37, 0xA0, 0x03, 0x02, 0x01, 0x60, 0xA1, 0x30, 0x30, 0x2E, 0x30, 0x2C, 0xA0, 0x2A, 0x04, 0x28, + // Signature + 'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00, + // Message Type + 0x01, 0x00, 0x00, 0x00, + // Negotiate Flags + 0xF7, 0xBA, 0xDB, 0xE2, + // Domain Name Fields + 0x00, 0x00, // DomainNameLen + 0x00, 0x00, // DomainNameMaxLen + 0x00, 0x00, 0x00, 0x00, // DomainNameBufferOffset + // Workstation Fields + 0x00, 0x00, // WorkstationLen + 0x00, 0x00, // WorkstationMaxLen + 0x00, 0x00, 0x00, 0x00, // WorkstationBufferOffset + // Version + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + response, err := utils.SendRecv(conn, NegotiatePacket, timeout) + if err != nil { + return nil, false, err + } + + type NTLMChallenge struct { + Signature [8]byte + MessageType uint32 + TargetNameLen uint16 + TargetNameMaxLen uint16 + TargetNameBufferOffset uint32 + NegotiateFlags uint32 + ServerChallenge uint64 + Reserved uint64 + TargetInfoLen uint16 + TargetInfoMaxLen uint16 + TargetInfoBufferOffset uint32 + Version [8]byte + // Payload (variable) + } + var challengeLen = 56 + + challengeStartOffset := bytes.Index(response, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}) + if challengeStartOffset == -1 { + return nil, false, nil + } + if len(response) < challengeStartOffset+challengeLen { + return nil, false, nil + } + var responseData NTLMChallenge + response = response[challengeStartOffset:] + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &responseData) + if err != nil { + return nil, false, err + } + + // Check if valid NTLM challenge response message structure + if responseData.MessageType != 0x00000002 || + responseData.Reserved != 0 || + !reflect.DeepEqual(responseData.Version[4:], []byte{0, 0, 0, 0xF}) { + return nil, false, nil + } + + // Parse: Version + type version struct { + MajorVersion byte + MinorVersion byte + BuildNumber uint16 + } + var versionData version + versionBuf := bytes.NewBuffer(responseData.Version[:4]) + err = binary.Read(versionBuf, binary.LittleEndian, &versionData) + if err != nil { + return nil, true, err + } + info.OSVersion = fmt.Sprintf("%d.%d.%d", versionData.MajorVersion, + versionData.MinorVersion, + versionData.BuildNumber) + + // Parse: TargetName + targetNameLen := int(responseData.TargetNameLen) + if targetNameLen > 0 { + startIdx := int(responseData.TargetNameBufferOffset) + endIdx := startIdx + targetNameLen + targetName := strings.ReplaceAll(string(response[startIdx:endIdx]), "\x00", "") + info.TargetName = targetName + } + + // Parse: TargetInfo + AvIDMap := map[uint16]string{ + 1: "NetBIOSComputerName", + 2: "NetBIOSDomainName", + 3: "FQDN", // DNS Computer Name + 4: "DNSDomainName", + 5: "DNSTreeName", + } + + type AVPair struct { + AvID uint16 + AvLen uint16 + } + var avPairLen = 4 + targetInfoLen := int(responseData.TargetInfoLen) + if targetInfoLen > 0 { + startIdx := int(responseData.TargetInfoBufferOffset) + if startIdx+targetInfoLen > len(response) { + return &info, true, fmt.Errorf("Invalid TargetInfoLen value") + } + var avPair AVPair + avPairBuf := bytes.NewBuffer(response[startIdx : startIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, true, err + } + currIdx := startIdx + for avPair.AvID != 0 { + if field, exists := AvIDMap[avPair.AvID]; exists { + value := strings.ReplaceAll(string(response[currIdx+avPairLen:currIdx+avPairLen+int(avPair.AvLen)]), "\x00", "") + switch field { + case "netbiosComputerName": + info.NetBIOSComputerName = value + case "netbiosDomainName": + info.NetBIOSDomainName = value + case "dnsComputerName": + info.DNSComputerName = value + case "dnsDomainName": + info.DNSDomainName = value + case "forestName": // MsvAvDnsTreeName + info.ForestName = value + } + } + currIdx += avPairLen + int(avPair.AvLen) + if currIdx+avPairLen > startIdx+targetInfoLen { + return &info, true, fmt.Errorf("Invalid AV_PAIR list") + } + avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, true, err + } + } + } + + return &info, true, nil +} + +func (p *RDPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + fingerprint, check, err := DetectRDP(conn, timeout) + if check && err != nil { + return nil, nil + } else if check && err == nil { + payload := plugins.ServiceRDP{ + OSFingerprint: fingerprint, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + return nil, err +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + info, check, err := DetectRDPAuth(conn, timeout) + if check && err != nil { + return nil, nil + } else if check && info != nil && err == nil { + return plugins.CreateServiceFrom(target, *info, true, "", plugins.TCP), nil + } + return nil, err +} + +func (p *RDPPlugin) Name() string { + return RDP +} + +func (p *RDPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return RDP +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *RDPPlugin) Priority() int { + return 89 +} + +func (p *TLSPlugin) Priority() int { + return 89 +} diff --git a/scanner/pkg/fingerprint/plugins/services/rdp/rdp_test.go b/scanner/pkg/fingerprint/plugins/services/rdp/rdp_test.go new file mode 100644 index 0000000..dd33fa4 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rdp/rdp_test.go @@ -0,0 +1,38 @@ +package rdp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestRDP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "rdp", + Port: 3389, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "scottyhardy/docker-remote-desktop", + }, + }, + } + + p := &RDPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/redis/redis.go b/scanner/pkg/fingerprint/plugins/services/redis/redis.go new file mode 100644 index 0000000..b4e64e9 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/redis/redis.go @@ -0,0 +1,144 @@ +package redis + +import ( + "bytes" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type REDISPlugin struct{} +type REDISTLSPlugin struct{} + +type Info struct { + AuthRequired bool +} + +const REDIS = "redis" +const REDISTLS = "redis" + +// Check if the response is from a Redis server +// returns an error if it's not validated as a Redis server +// and a Info struct with AuthRequired if it is +func checkRedis(data []byte) (Info, error) { + // a valid pong response will be the 7 bytes [+PONG(CR)(NL)] + pong := [7]byte{0x2b, 0x50, 0x4f, 0x4e, 0x47, 0x0d, 0x0a} + // an auth error will start with the 7 bytes: [-NOAUTH] + noauth := [7]byte{0x2d, 0x4e, 0x4f, 0x41, 0x55, 0x54, 0x48} + + msgLength := len(data) + if msgLength < 7 { + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "too short of a response", + } + } + + if msgLength == 7 { + if bytes.Equal(data, pong[:]) { + // Valid PONG response means redis server and no auth + return Info{AuthRequired: false}, nil + } + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "invalid PONG response", + } + } + if !bytes.Equal(data[:7], noauth[:]) { + return Info{}, &utils.InvalidResponseErrorInfo{ + Service: REDIS, + Info: "invalid Error response", + } + } + + return Info{AuthRequired: true}, nil +} + +func init() { + plugins.RegisterPlugin(&REDISPlugin{}) + plugins.RegisterPlugin(&REDISTLSPlugin{}) +} + +func (p *REDISPlugin) PortPriority(port uint16) bool { + return port == 6379 +} + +func (p *REDISTLSPlugin) PortPriority(port uint16) bool { + return port == 6380 +} + +func (p *REDISTLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return DetectRedis(conn, target, timeout, true) +} + +func DetectRedis(conn net.Conn, target plugins.Target, timeout time.Duration, tls bool) (*plugins.Service, error) { + //https://redis.io/commands/ping/ + // PING is a supported command since 1.0.0 + // [*1(CR)(NL)$4(CR)(NL)PING(CR)(NL)] + ping := []byte{ + 0x2a, + 0x31, + 0x0d, + 0x0a, + 0x24, + 0x34, + 0x0d, + 0x0a, + 0x50, + 0x49, + 0x4e, + 0x47, + 0x0d, + 0x0a, + } + + response, err := utils.SendRecv(conn, ping, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + result, err := checkRedis(response) + if err != nil { + return nil, nil + } + payload := plugins.ServiceRedis{ + AuthRequired: result.AuthRequired, + } + if tls { + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCPTLS), nil + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *REDISPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + return DetectRedis(conn, target, timeout, false) +} + +func (p *REDISPlugin) Name() string { + return REDIS +} + +func (p *REDISTLSPlugin) Name() string { + return REDISTLS +} + +func (p *REDISPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *REDISTLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *REDISPlugin) Priority() int { + return 413 +} + +func (p *REDISTLSPlugin) Priority() int { + return 414 +} diff --git a/scanner/pkg/fingerprint/plugins/services/redis/redis_test.go b/scanner/pkg/fingerprint/plugins/services/redis/redis_test.go new file mode 100644 index 0000000..2f773c9 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/redis/redis_test.go @@ -0,0 +1,38 @@ +package redis + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestRedis(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "redis", + Port: 6379, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "redis", + }, + }, + } + + p := &REDISPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/rsync/rsync.go b/scanner/pkg/fingerprint/plugins/services/rsync/rsync.go new file mode 100644 index 0000000..6e0e9ba --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rsync/rsync.go @@ -0,0 +1,77 @@ +package rsync + +import ( + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type RSYNCPlugin struct{} + +const ( + RsyncMagicHeaderLength = 8 + RSYNC = "rsync" +) + +func init() { + plugins.RegisterPlugin(&RSYNCPlugin{}) +} + +func (p *RSYNCPlugin) PortPriority(port uint16) bool { + return port == 873 +} + +// Run +/* + rsync is a file synchronization protocol that can run over a number of protocols. Once + a communication stream is set up between the sender and receiver processes, the protocol is the same, regardless + of whether that stream is a unix pipe, an SSH connection, or a raw TCP socket. This program detects the + presence of an rsync daemon, which detects incoming connections and forks to use a raw TCP socket. The + rsync daemon uses no transport encryption. + + The rsync protocol is not standardized, but all implementations use a magic header "@RSYNCD:" during synchronization. + + This program was tested with docker run -p 873:873 vimagick/rsyncd + The default port for rsyncd is 873 +*/ +func (p *RSYNCPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + requestBytes := []byte{ + // ascii "@RSYNCD:" magic header + 0x40, 0x52, 0x53, 0x59, 0x54, 0x43, 0x44, 0x3a, + // space + 0x20, + // ascii "29" client version + 0x32, 0x39, + // newline + 0x0a, + } + + response, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + if string(response[:RsyncMagicHeaderLength]) == "@RSYNCD:" { + version := strings.Split(string(response[RsyncMagicHeaderLength+1:]), "\n")[0] + return plugins.CreateServiceFrom(target, plugins.ServiceRsync{}, false, version, plugins.TCP), nil + } + + return nil, nil +} + +func (p *RSYNCPlugin) Name() string { + return RSYNC +} + +func (p *RSYNCPlugin) Type() plugins.Protocol { + return plugins.TCP +} +func (p *RSYNCPlugin) Priority() int { + return 578 +} diff --git a/scanner/pkg/fingerprint/plugins/services/rsync/rsync_test.go b/scanner/pkg/fingerprint/plugins/services/rsync/rsync_test.go new file mode 100644 index 0000000..a053e03 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rsync/rsync_test.go @@ -0,0 +1,38 @@ +package rsync + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestRsync(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "rsync", + Port: 873, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "vimagick/rsyncd", + }, + }, + } + + p := &RSYNCPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp.go b/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp.go new file mode 100644 index 0000000..3accfbe --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp.go @@ -0,0 +1,113 @@ +package rtsp + +import ( + "math/rand" + "net" + "strconv" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const ( + RtspMagicHeader = "RTSP/1.0" + RtspMagicHeaderLength = 8 + RtspCseqHeader = "CSeq: " + RtspCseqHeaderLength = 6 + RtspServerHeader = "Server: " + RtspServerHeaderLength = 8 + RtspNewlineLength = 2 + RTSP = "rtsp" +) + +type RTSPPlugin struct{} + +func init() { + rand.Seed(time.Now().UnixNano()) + plugins.RegisterPlugin(&RTSPPlugin{}) +} + +func (p *RTSPPlugin) PortPriority(port uint16) bool { + return port == 554 +} + +/* + rtsp is a media control protocol used to control the flow of data from a real time + data streaming protocol. rtsp itself does not transport any data. The structure of rtsp + requests is very similar to that of http requests. + + To detect the presence of RTSP, this program sends an OPTIONS request, and then validates + the returned header and cseq value. + + This program was tested with docker run -p 554:8554 aler9/rtsp-simple-server. + The default port for rtsp is 554. +*/ + +func (p *RTSPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + cseq := strconv.Itoa(rand.Intn(10000)) //nolint:gosec + + requestString := strings.Join([]string{ + "OPTIONS rtsp://example.com RTSP/1.0\r\n", + "Cseq: ", cseq, "\r\n", + "\r\n", + }, "") + + requestBytes := []byte(requestString) + + responseBytes, err := utils.SendRecv(conn, requestBytes, timeout) + if err != nil { + return nil, err + } + if len(responseBytes) == 0 { + return nil, nil + } + response := string(responseBytes) + + if len(response) < RtspMagicHeaderLength { + return nil, nil + } + if string(response[:RtspMagicHeaderLength]) == RtspMagicHeader { + cseqStart := strings.Index(response, RtspCseqHeader) + if cseqStart == -1 { + return nil, nil + } + + cseqValueStart := cseqStart + RtspCseqHeaderLength + if response[cseqValueStart:cseqValueStart+len(cseq)+RtspNewlineLength] != cseq+"\r\n" { + return nil, nil + } + + serverStart := strings.Index(response, RtspServerHeader) + if serverStart == -1 { + return nil, nil + } + + serverValueStart := serverStart + RtspServerHeaderLength + serverValueEnd := strings.Index(response[serverValueStart:], "\r\n") + if serverValueStart+serverValueEnd >= len(response) { + return nil, nil + } + + serverinfo := response[serverValueStart : serverValueStart+serverValueEnd] + payload := plugins.ServiceRtsp{ + ServerInfo: serverinfo, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + return nil, nil +} + +func (p *RTSPPlugin) Name() string { + return RTSP +} + +func (p *RTSPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *RTSPPlugin) Priority() int { + return 1001 +} diff --git a/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp_test.go b/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp_test.go new file mode 100644 index 0000000..c6fbe34 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/rtsp/rtsp_test.go @@ -0,0 +1,39 @@ +package rtsp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestRtsp(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "rtsp", + Port: 8554, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "aler9/rtsp-simple-server", + ExposedPorts: []string{"8554"}, + }, + }, + } + + p := &RTSPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/smb/smb.go b/scanner/pkg/fingerprint/plugins/services/smb/smb.go new file mode 100644 index 0000000..afd513a --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/smb/smb.go @@ -0,0 +1,361 @@ +package smb + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "net" + "reflect" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type SMBPlugin struct{} + +const SMB = "smb" + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5cd64522-60b3-4f3e-a157-fe66f1228052 +type SMB2PacketHeader struct { + ProtocolID [4]byte + StructureSize uint16 + CreditCharge uint16 + Status uint32 // In SMB 3.x dialect, used as ChannelSequence & Reserved fields + Command uint16 + CreditRequest uint16 + Flags uint32 + NextCommand uint32 + MessageID uint64 + Reserved uint32 + TreeID uint32 + SessionID uint64 + Signature [16]byte +} + +// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/63abf97c-0d09-47e2-88d6-6bfa552949a5 +type NegotiateResponse struct { + SessionMsgPrefix [4]byte + PacketHeader SMB2PacketHeader + // Negotiate Response + StructureSize uint16 + SecurityMode uint16 + DialectRevision uint16 + Reserved uint16 // if DialectRevision is 0x0311, used as NegotiateContextCount field + ServerGUID [16]byte + Capabilities uint32 + MaxTransactSize uint32 + MaxReadSize uint32 + MaxWriteSize uint32 + SystemTime uint64 + ServerStartTime uint64 + SecurityBufferOffset uint16 + SecurityBufferLength uint16 + Reserved2 uint32 // if DialectRevision is 0x0311, used as NegotiateContextOffset field + // Variable (Buffer, Padding, NegotiateContextList, etc.) +} + +type NTLMChallenge struct { + Signature [8]byte + MessageType uint32 + TargetNameLen uint16 + TargetNameMaxLen uint16 + TargetNameBufferOffset uint32 + NegotiateFlags uint32 + ServerChallenge uint64 + Reserved uint64 + TargetInfoLen uint16 + TargetInfoMaxLen uint16 + TargetInfoBufferOffset uint32 + Version [8]byte + // Payload (variable) +} + +func init() { + plugins.RegisterPlugin(&SMBPlugin{}) +} + +func (p *SMBPlugin) PortPriority(port uint16) bool { + return port == 445 +} + +func DetectSMBv2(conn net.Conn, timeout time.Duration) (*plugins.ServiceSMB, error) { + info := plugins.ServiceSMB{} + + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5 + negotiateReqPacket := []byte{ + // NetBios Session Service + 0x00, // Message Type + 0x00, 0x00, 0x66, // Length + + // SMBv2 Packet Header + 0xFE, 0x53, 0x4D, 0x42, // ProtocolId + 0x40, 0x00, // StructureSize + 0x00, 0x00, // CreditCharge + 0x00, 0x00, 0x00, 0x00, // ChannelSequence/Reserved/Status + 0x00, 0x00, // Command (Negotiate) + 0x00, 0x1F, // CreditRequest + 0x00, 0x00, 0x00, 0x00, // Flags + 0x00, 0x00, 0x00, 0x00, // NextCommand + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MessageID + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // TreeID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SessionID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature (continued) + + // SMBv2 Negotiate Request + 0x24, 0x00, // StructureSize + 0x01, 0x00, // DialectCount + 0x01, 0x00, // SecurityMode (Signing Enabled) + 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // Capabilities + 0x13, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, // ClientGuid + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x37, // ClientGuid (continued) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ClientStartTime + 0x02, 0x02, // Dialects (SMB 2.0.2) + } + sessionPrefixLen := 4 + packetHeaderLen := 64 + minNegoResponseLen := 64 + + response, err := utils.SendRecv(conn, negotiateReqPacket, timeout) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return nil, nil + } + return nil, err + } + + // Check the length of the response to see if it is lower than the minimum + // packet size for SMB2 NEGOTIATE Response Packet + if len(response) < sessionPrefixLen+packetHeaderLen+minNegoResponseLen { + return nil, nil + } + + var negotiateResponseData NegotiateResponse + responseBuf := bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &negotiateResponseData) + if err != nil { + return nil, err + } + + if !reflect.DeepEqual(negotiateResponseData.PacketHeader.ProtocolID[:], []byte{0xFE, 'S', 'M', 'B'}) { + return nil, nil + } + + if negotiateResponseData.PacketHeader.StructureSize != 0x40 { + return nil, nil + } + + if negotiateResponseData.PacketHeader.Command != 0x0000 { // SMB2 NEGOTIATE (0x0000) + return nil, nil + } + + if negotiateResponseData.StructureSize != 0x41 { + return nil, nil + } + + signingEnabled := false + signingRequired := false + if negotiateResponseData.SecurityMode&1 == 1 { + signingEnabled = true + } + if negotiateResponseData.SecurityMode&2 == 2 { + signingRequired = true + } + info.SigningEnabled = signingEnabled + info.SigningRequired = signingRequired + + /** + * At this point, we know SMBv2 is detected. + * Below, we try to obtain more metadata via session setup request w/ NTLM auth + */ + + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-authsod/9a20f8ac-612a-4e0a-baab-30e922e7e1f5 + // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a3c2c28-d6b0-48ed-b917-a86b2ca4575f + sessionSetupReqPacket := []byte{ + // NetBios Session Service + 0x00, // Message Type + 0x00, 0x00, 0xA2, // Length + + // SMBv2 Packet Header + 0xFE, 0x53, 0x4D, 0x42, // ProtocolId + 0x40, 0x00, // StructureSize + 0x00, 0x00, // CreditCharge + 0x00, 0x00, 0x00, 0x00, // ChannelSequence/Reserved/Status + 0x01, 0x00, // Command (SESSION_SETUP) + 0x00, 0x20, // CreditRequest + 0x00, 0x00, 0x00, 0x00, // Flags + 0x00, 0x00, 0x00, 0x00, // NextCommand + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MessageID + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // TreeID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // SessionID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Signature (continued) + + // SMBv2 Session Setup Request + 0x19, 0x00, // Structure Size + 0x00, // Flags + 0x01, // SecurityMode + 0x01, 0x00, 0x00, 0x00, // Capabilities + 0x00, 0x00, 0x00, 0x00, // Channel + 0x58, 0x00, // SecurityBufferOffset + 0x4A, 0x00, // SecurityBufferLength + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // PreviousSessionId + // Security Buffer + 0x60, 0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, + 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, + 0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, + 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04, 0x28, + // Signature + 'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00, + // Message Type + 0x01, 0x00, 0x00, 0x00, + // Negotiate Flags + 0xF7, 0xBA, 0xDB, 0xE2, + // Domain Name Fields + 0x00, 0x00, // DomainNameLen + 0x00, 0x00, // DomainNameMaxLen + 0x00, 0x00, 0x00, 0x00, // DomainNameBufferOffset + // Workstation Fields + 0x00, 0x00, // WorkstationLen + 0x00, 0x00, // WorkstationMaxLen + 0x00, 0x00, 0x00, 0x00, // WorkstationBufferOffset + // Version + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + response, err = utils.SendRecv(conn, sessionSetupReqPacket, timeout) + if err != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return &info, nil + } + return &info, err + } + + challengeLen := 56 + challengeStartOffset := bytes.Index(response, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}) + if challengeStartOffset == -1 { + return &info, nil + } + if len(response) < challengeStartOffset+challengeLen { + return &info, nil + } + var sessionResponseData NTLMChallenge + response = response[challengeStartOffset:] + responseBuf = bytes.NewBuffer(response) + err = binary.Read(responseBuf, binary.LittleEndian, &sessionResponseData) + if err != nil { + return &info, err + } + + // Check if valid NTLM challenge response message structure + if sessionResponseData.MessageType != 0x00000002 || + sessionResponseData.Reserved != 0 || + !reflect.DeepEqual(sessionResponseData.Version[4:], []byte{0, 0, 0, 0xF}) { + return &info, nil + } + + // Parse: Version + type version struct { + MajorVersion byte + MinorVersion byte + BuildNumber uint16 + } + var versionData version + versionBuf := bytes.NewBuffer(sessionResponseData.Version[:4]) + err = binary.Read(versionBuf, binary.LittleEndian, &versionData) + if err != nil { + return &info, err + } + info.OSVersion = fmt.Sprintf("%d.%d.%d", versionData.MajorVersion, + versionData.MinorVersion, + versionData.BuildNumber) + + // Parse: TargetInfo + AvIDMap := map[uint16]string{ + 1: "netbiosComputerName", + 2: "netbiosDomainName", + 3: "dnsComputerName", + 4: "dnsDomainName", + 5: "forestName", // MsvAvDnsTreeName + } + type AVPair struct { + AvID uint16 + AvLen uint16 + // Value (variable) + } + var avPairLen = 4 + targetInfoLen := int(sessionResponseData.TargetInfoLen) + if targetInfoLen > 0 { + startIdx := int(sessionResponseData.TargetInfoBufferOffset) + if startIdx+targetInfoLen > len(response) { + return &info, nil + } + var avPair AVPair + avPairBuf := bytes.NewBuffer(response[startIdx : startIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, err + } + currIdx := startIdx + for avPair.AvID != 0 { + if field, exists := AvIDMap[avPair.AvID]; exists { + value := strings.ReplaceAll(string(response[currIdx+avPairLen:currIdx+avPairLen+int(avPair.AvLen)]), "\x00", "") + switch field { + case "netbiosComputerName": + info.NetBIOSComputerName = value + case "netbiosDomainName": + info.NetBIOSDomainName = value + case "dnsComputerName": + info.DNSComputerName = value + case "dnsDomainName": + info.DNSDomainName = value + case "forestName": // MsvAvDnsTreeName + info.ForestName = value + } + } + currIdx += avPairLen + int(avPair.AvLen) + if currIdx+avPairLen > startIdx+targetInfoLen { + return &info, nil + } + avPairBuf = bytes.NewBuffer(response[currIdx : currIdx+avPairLen]) + err = binary.Read(avPairBuf, binary.LittleEndian, &avPair) + if err != nil { + return &info, nil + } + } + } + + return &info, nil +} + +func (p *SMBPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + info, err := DetectSMBv2(conn, timeout) + if err != nil { + return nil, err + } + if info == nil { + return nil, nil + } + + return plugins.CreateServiceFrom(target, info, false, info.OSVersion, plugins.TCP), nil +} + +func (p *SMBPlugin) Name() string { + return SMB +} + +func (p *SMBPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *SMBPlugin) Priority() int { + return 320 +} diff --git a/scanner/pkg/fingerprint/plugins/services/smb/smb_test.go b/scanner/pkg/fingerprint/plugins/services/smb/smb_test.go new file mode 100644 index 0000000..695c07c --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/smb/smb_test.go @@ -0,0 +1,39 @@ +package smb + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestSMB(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "smb", + Port: 445, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "dperson/samba", + Cmd: []string{"-S"}, + }, + }, + } + + p := &SMBPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/smtp/smtp.go b/scanner/pkg/fingerprint/plugins/services/smtp/smtp.go new file mode 100644 index 0000000..17bd0a7 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/smtp/smtp.go @@ -0,0 +1,188 @@ +package smtp + +import ( + "bytes" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type SMTPPlugin struct{} +type TLSPlugin struct{} + +const SMTP = "smtp" +const SMTPS = "smtps" + +type Data struct { + Banner string + AuthMethods []string +} + +func init() { + plugins.RegisterPlugin(&SMTPPlugin{}) + plugins.RegisterPlugin(&TLSPlugin{}) +} + +func (p *SMTPPlugin) PortPriority(port uint16) bool { + return port == 25 || port == 587 || port == 465 || port == 2525 +} + +func handleSMTPConn(response []byte) (bool, bool) { + // Checks for an expected response on CONNECTION ESTABLISHMENT + // RFC 5321 Section 4.3.2 + validResponses := []string{"220", "421", "500", "501", "554"} + isSMTP := false + isSMTPErr := false + for i := 0; i < len(validResponses); i++ { + if bytes.Equal(response[0:3], []byte(validResponses[i])) { + // Received a valid response code on connection + isSMTP = true + if bytes.Equal(response[0:1], []byte("4")) || bytes.Equal(response[0:1], []byte("5")) { + // Received a valid error response code on connection + isSMTPErr = true + } + break + } + } + return isSMTP, isSMTPErr +} + +func handleSMTPHelo(response []byte) (bool, bool) { + // Checks for an expected response from the HELO command + // RFC 5321 Section 4.3.2 + validResponses := []string{"250", "421", "500", "501", "502", "504", "550"} + isSMTP := false + isSMTPErr := false + for i := 0; i < len(validResponses); i++ { + if bytes.Equal(response[0:3], []byte(validResponses[i])) { + // HELO command received a valid response code + isSMTP = true + if bytes.Equal(response[0:1], []byte("4")) || bytes.Equal(response[0:1], []byte("5")) { + // HELO command received a valid error response code + isSMTPErr = true + } + break + } + } + return isSMTP, isSMTPErr +} + +func (p *TLSPlugin) PortPriority(port uint16) bool { + return port == 465 +} + +func DetectSMTP(conn net.Conn, tls bool, timeout time.Duration) (Data, bool, error) { + protocol := SMTP + if tls { + protocol = SMTPS + } + + response, err := utils.Recv(conn, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + isSMTP, smtpError := handleSMTPConn(response) + if !isSMTP && !smtpError { + return Data{}, true, &utils.InvalidResponseError{Service: protocol} + } + + banner := make([]byte, len(response)) + copy(banner, response) + + // Send the EHLO message + smtpEhloCommand := []byte("EHLO example.com\r\n") + response, err = utils.SendRecv(conn, smtpEhloCommand, timeout) + if err != nil { + return Data{}, false, err + } + if len(response) == 0 { + return Data{}, true, &utils.ServerNotEnable{} + } + + isSMTP, smtpError = handleSMTPHelo(response) + if !isSMTP { + return Data{}, true, &utils.InvalidResponseErrorInfo{ + Service: protocol, + Info: "invalid SMTP Helo response", + } + } + + // a valid smtperror means it is smtp + if smtpError { + data := Data{ + Banner: string(banner), + } + + return data, true, nil + } + + if isSMTP { + data := Data{ + Banner: string(banner), + AuthMethods: strings.Split(strings.ReplaceAll(string(response), "-", " "), " "), + } + + return data, true, nil + } + + return Data{}, true, &utils.InvalidResponseError{Service: protocol} +} + +func (p *SMTPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectSMTP(conn, false, timeout) + if err == nil && check { + payload := plugins.ServiceSMTP{ + Banner: data.Banner, + AuthMethods: data.AuthMethods, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } else if err != nil && check { + return nil, nil + } + return nil, err +} + +func (p *TLSPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + data, check, err := DetectSMTP(conn, false, timeout) + if err == nil && check { + payload := plugins.ServiceSMTP{ + Banner: data.Banner, + AuthMethods: data.AuthMethods, + } + return plugins.CreateServiceFrom(target, payload, true, "", plugins.TCP), nil + } else if err != nil && check { + return nil, nil + } + return nil, err +} + +func (p *SMTPPlugin) Name() string { + return SMTP +} + +func (p *SMTPPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *TLSPlugin) Name() string { + return SMTPS +} + +func (p *TLSPlugin) Type() plugins.Protocol { + return plugins.TCPTLS +} + +func (p *SMTPPlugin) Priority() int { + return 60 +} + +func (p *TLSPlugin) Priority() int { + return 61 +} diff --git a/scanner/pkg/fingerprint/plugins/services/smtp/smtp_test.go b/scanner/pkg/fingerprint/plugins/services/smtp/smtp_test.go new file mode 100644 index 0000000..3319ad4 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/smtp/smtp_test.go @@ -0,0 +1,38 @@ +package smtp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestSMTP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "smtp", + Port: 25, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "bytemark/smtp", + }, + }, + } + + p := &SMTPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/snmp/snmp.go b/scanner/pkg/fingerprint/plugins/services/snmp/snmp.go new file mode 100644 index 0000000..89a957c --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/snmp/snmp.go @@ -0,0 +1,75 @@ +package snmp + +import ( + "bytes" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const SNMP = "SNMP" + +type SNMPPlugin struct{} + +func init() { + plugins.RegisterPlugin(&SNMPPlugin{}) +} + +func (f *SNMPPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + RequestID := []byte{0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00} + InitialConnectionPackage := []byte{ + 0x30, 0x29, // package length + 0x02, 0x01, 0x00, // Version: 1 + 0x04, 0x06, // Community + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // method: "public" + 0xa0, // PDU type: GET + 0x1c, + 0x02, 0x04, 0xff, 0xff, 0xff, 0xff, // Request ID: -1 + 0x02, 0x01, 0x00, // Error status: no error + 0x02, 0x01, 0x00, // Error index + 0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08, 0x2b, 0x06, // Object ID + 0x01, 0x02, 0x01, 0x01, 0x01, 0x00, 0x05, 0x00, + } + InfoOffset := 33 + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + idx := strings.Index(string(response), "public") + if idx == -1 { + return nil, nil + } + stringBegin := idx + InfoOffset + if bytes.Contains(response, RequestID) { + if stringBegin < len(response) { + return plugins.CreateServiceFrom(target, plugins.ServiceSNMP{}, false, + string(response[stringBegin:]), plugins.UDP), nil + } + return plugins.CreateServiceFrom(target, plugins.ServiceSNMP{}, false, "", plugins.UDP), nil + } + return nil, nil +} + +func (f *SNMPPlugin) Name() string { + return SNMP +} + +func (f *SNMPPlugin) PortPriority(i uint16) bool { + return i == 161 +} + +func (f *SNMPPlugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (f *SNMPPlugin) Priority() int { + return 81 +} diff --git a/scanner/pkg/fingerprint/plugins/services/snmp/snmp_test.go b/scanner/pkg/fingerprint/plugins/services/snmp/snmp_test.go new file mode 100644 index 0000000..a6d7742 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/snmp/snmp_test.go @@ -0,0 +1,39 @@ +package snmp + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestSNMP(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "snmp", + Port: 161, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "polinux/snmpd", + ExposedPorts: []string{"161/udp"}, + }, + }, + } + + p := &SNMPPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/ssh/ssh.go b/scanner/pkg/fingerprint/plugins/services/ssh/ssh.go new file mode 100644 index 0000000..1c1c2f9 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ssh/ssh.go @@ -0,0 +1,392 @@ +package ssh + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "math/big" + "net" + "strings" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" + "github.com/ctrlsam/rigour/third_party/cryptolib/ssh" +) + +type SSHPlugin struct{} + +const SSH = "ssh" + +func init() { + plugins.RegisterPlugin(&SSHPlugin{}) +} + +func (p *SSHPlugin) PortPriority(port uint16) bool { + return port == 22 || port == 2222 +} + +// https://www.rfc-editor.org/rfc/rfc4253.html#section-4 +// from the RFC, two things: +// When the connection has been established, both sides MUST send an +// identification string. This identification string MUST be +// +// SSH-protoversion-softwareversion SP comments CR LF +// +// The server MAY send other lines of data before sending the version +// +// string. Each line SHOULD be terminated by a Carriage Return and Line +// Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded +// in ISO-10646 UTF-8 [RFC3629] (language is not specified). +func checkSSH(data []byte) (string, error) { + msgLength := len(data) + if msgLength < 4 { + return "", &utils.InvalidResponseErrorInfo{Service: SSH, Info: "response too short"} + } + sshID := []byte("SSH-") + if bytes.Equal(data[:4], sshID) { + return string(data), nil + } + + for _, line := range strings.Split(string(data), "\r\n") { + if len(line) >= 4 && line[:4] == "SSH-" { + return line, nil + } + } + + return "", &utils.InvalidResponseErrorInfo{Service: SSH, Info: "invalid banner prefix"} +} + +func checkAlgo(data []byte) (map[string]string, error) { + length := len(data) + if length < 26 { + return nil, fmt.Errorf("invalid response length") + } + cookie := hex.EncodeToString(data[6:22]) + + kexAlgorithmsLength := int(big.NewInt(0).SetBytes(data[22:26]).Uint64()) + if length < 26+kexAlgorithmsLength { + return nil, fmt.Errorf("invalid response length") + } + kexAlgos := string(data[26 : 26+kexAlgorithmsLength]) + + sHKAlgoBegin := 26 + kexAlgorithmsLength + if length < 4+sHKAlgoBegin { + return nil, fmt.Errorf("invalid response length") + } + sHKAlgoLength := int(big.NewInt(0).SetBytes(data[sHKAlgoBegin : 4+sHKAlgoBegin]).Uint64()) + if length < 4+sHKAlgoBegin+sHKAlgoLength { + return nil, fmt.Errorf("invalid response length") + } + serverHostKeyAlgos := string(data[4+sHKAlgoBegin : 4+sHKAlgoBegin+sHKAlgoLength]) + + encryptAlgoCToSBegin := 4 + sHKAlgoBegin + sHKAlgoLength + if length < 4+encryptAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + encryptAlgoCToSLength := int(big.NewInt(0).SetBytes(data[encryptAlgoCToSBegin : 4+encryptAlgoCToSBegin]).Uint64()) + if length < 4+encryptAlgoCToSBegin+encryptAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + ciphersClientServer := string(data[4+encryptAlgoCToSBegin : 4+encryptAlgoCToSBegin+encryptAlgoCToSLength]) + + encryptAlgoSToCBegin := 4 + encryptAlgoCToSBegin + encryptAlgoCToSLength + if length < 4+encryptAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + encryptAlgoSToCLength := int(big.NewInt(0).SetBytes(data[encryptAlgoSToCBegin : 4+encryptAlgoSToCBegin]).Uint64()) + if length < 4+encryptAlgoCToSBegin+encryptAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + ciphersServerClient := string(data[4+encryptAlgoSToCBegin : 4+encryptAlgoSToCBegin+encryptAlgoSToCLength]) + + macAlgoCToSBegin := 4 + encryptAlgoSToCBegin + encryptAlgoSToCLength + if length < 4+macAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + macAlgoCToSLength := int(big.NewInt(0).SetBytes(data[macAlgoCToSBegin : 4+macAlgoCToSBegin]).Uint64()) + if length < 4+macAlgoCToSBegin+macAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + macClientServer := string(data[4+macAlgoCToSBegin : 4+macAlgoCToSBegin+macAlgoCToSLength]) + + macAlgoSToCBegin := 4 + macAlgoCToSBegin + macAlgoCToSLength + if length < 4+macAlgoSToCBegin { + return nil, fmt.Errorf("invalid response length") + } + macAlgoSToCLength := int(big.NewInt(0).SetBytes(data[macAlgoSToCBegin : 4+macAlgoSToCBegin]).Uint64()) + if length < 4+macAlgoSToCBegin+macAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + macServerClient := string(data[4+macAlgoSToCBegin : 4+macAlgoSToCBegin+macAlgoSToCLength]) + + compAlgoCToSBegin := 4 + macAlgoSToCBegin + macAlgoSToCLength + if length < 4+compAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + compAlgoCToSLength := int(big.NewInt(0).SetBytes(data[compAlgoCToSBegin : 4+compAlgoCToSBegin]).Uint64()) + if length < 4+compAlgoCToSBegin+compAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + compressionClientServer := string(data[4+compAlgoCToSBegin : 4+compAlgoCToSBegin+compAlgoCToSLength]) + + compAlgoSToCBegin := 4 + compAlgoCToSBegin + compAlgoCToSLength + if length < 4+compAlgoSToCBegin { + return nil, fmt.Errorf("invalid response length") + } + compAlgoSToCLength := int(big.NewInt(0).SetBytes(data[compAlgoSToCBegin : 4+compAlgoSToCBegin]).Uint64()) + if length < 4+compAlgoSToCBegin+compAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + compressionServerClient := string(data[4+compAlgoSToCBegin : 4+compAlgoSToCBegin+compAlgoSToCLength]) + + langAlgoCToSBegin := 4 + compAlgoSToCBegin + compAlgoSToCLength + if length < 4+langAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + langAlgoCToSLength := int(big.NewInt(0).SetBytes(data[langAlgoCToSBegin : 4+langAlgoCToSBegin]).Uint64()) + if length < 4+langAlgoCToSBegin+langAlgoCToSLength { + return nil, fmt.Errorf("invalid response length") + } + languagesClientServer := string(data[4+langAlgoCToSBegin : 4+langAlgoCToSBegin+langAlgoCToSLength]) + + langAlgoSToCBegin := 4 + langAlgoCToSBegin + langAlgoCToSLength + if length < 4+langAlgoCToSBegin { + return nil, fmt.Errorf("invalid response length") + } + langAlgoSToCLength := int(big.NewInt(0).SetBytes(data[langAlgoSToCBegin : 4+langAlgoSToCBegin]).Uint64()) + if length < 4+langAlgoCToSBegin+langAlgoSToCLength { + return nil, fmt.Errorf("invalid response length") + } + languagesServerClient := string(data[4+langAlgoSToCBegin : 4+langAlgoSToCBegin+langAlgoSToCLength]) + + info := map[string]string{ + "Cookie": cookie, + "KexAlgos": kexAlgos, + "ServerHostKeyAlgos": serverHostKeyAlgos, + "CiphersClientServer": ciphersClientServer, + "CiphersServerClient": ciphersServerClient, + "MACsClientServer": macClientServer, + "MACsServerClient": macServerClient, + "CompressionClientServer": compressionClientServer, + "CompressionServerClient": compressionServerClient, + "LanguagesClientServer": languagesClientServer, + "LanguagesServerClient": languagesServerClient, + } + + return info, nil +} + +func (p *SSHPlugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + response, err := utils.Recv(conn, timeout) + passwordAuth := false + + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + banner, err := checkSSH(response) + if err != nil { + return nil, err + } + + msg := []byte("SSH-2.0-Rigour-SSH2\r\n") + + response, err = utils.SendRecv(conn, msg, timeout) + if err != nil { + return nil, err + } + + algo, err := checkAlgo(response) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + // check auth methods + conf := ssh.ClientConfig{} + conf.Timeout = timeout + conf.Auth = nil + conf.Auth = append(conf.Auth, ssh.Password("admin")) + conf.Auth = append(conf.Auth, + ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range answers { + answers[i] = "password" + } + return answers, nil + }), + ) + + conf.User = "admin" + conf.HostKeyCallback = ssh.InsecureIgnoreHostKey() + // use all the ciphers supported by the go crypto ssh library + conf.KeyExchanges = append(conf.KeyExchanges, + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group14-sha1", + "diffie-hellman-group14-sha256", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "curve25519-sha256@libssh.org", + "curve25519-sha256", + ) + conf.Ciphers = append(conf.Ciphers, + "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", + "chacha20-poly1305@openssh.com", + "arcfour256", "arcfour128", "arcfour", + "aes128-cbc", + "3des-cbc", + ) + + authClient, err := ssh.Dial("tcp", target.Address.String(), &conf) + + if err != nil { + passwordAuth = strings.Contains(err.Error(), "password") || strings.Contains(err.Error(), "keyboard-interactive") + } + + if authClient != nil { + authClient.Close() + } + + sshConfig := &ssh.ClientConfig{} + fullConf := *sshConfig + fullConf.SetDefaults() + + c := ssh.NewTransport(conn, fullConf.Rand, true) + t := ssh.NewHandshakeTransport(c, &fullConf.Config, msg, []byte(banner)) + sendMsg := ssh.KexInitMsg{ + KexAlgos: t.Config.KeyExchanges, + CiphersClientServer: t.Config.Ciphers, + CiphersServerClient: t.Config.Ciphers, + MACsClientServer: t.Config.MACs, + MACsServerClient: t.Config.MACs, + ServerHostKeyAlgos: ssh.SupportedHostKeyAlgos, + CompressionClientServer: []string{"none"}, + CompressionServerClient: []string{"none"}, + } + _, err = io.ReadFull(rand.Reader, sendMsg.Cookie[:]) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + if firstKeyExchange := t.SessionID == nil; firstKeyExchange { + sendMsg.KexAlgos = make([]string, 0, len(t.Config.KeyExchanges)+1) + sendMsg.KexAlgos = append(sendMsg.KexAlgos, t.Config.KeyExchanges...) + sendMsg.KexAlgos = append(sendMsg.KexAlgos, "ext-info-c") + } + packet := ssh.Marshal(sendMsg) + packetCopy := make([]byte, len(packet)) + copy(packetCopy, packet) + + err = ssh.PushPacket(t.HandshakeTransport, packetCopy) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + + cookie, err := hex.DecodeString(algo["cookie"]) + var ret [16]byte + copy(ret[:], cookie) + + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + otherInit := &ssh.KexInitMsg{ + KexAlgos: strings.Split(algo["KexAlgos"], ","), + Cookie: ret, + ServerHostKeyAlgos: strings.Split(algo["ServerHostKeyAlgos"], ","), + CiphersClientServer: strings.Split(algo["CiphersClientServer"], ","), + CiphersServerClient: strings.Split(algo["CiphersServerClient"], ","), + MACsClientServer: strings.Split(algo["MACsClientServer"], ","), + MACsServerClient: strings.Split(algo["MACsServerClient"], ","), + CompressionClientServer: strings.Split(algo["CompressionClientServer"], ","), + CompressionServerClient: strings.Split(algo["CompressionServerClient"], ","), + FirstKexFollows: false, + Reserved: 0, + } + + t.Algorithms, err = ssh.FindAgreedAlgorithms(false, &sendMsg, otherInit) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + magics := ssh.HandshakeMagics{ + ClientVersion: t.ClientVersion, + ServerVersion: t.ServerVersion, + ClientKexInit: packet, + ServerKexInit: response[5 : len(response)-10], + } + + kex := ssh.GetKex(t.Algorithms.Kex) + + result, err := ssh.Clients(t, kex, &magics) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + hostKey, err := ssh.ParsePublicKey(result.HostKey) + if err != nil { + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil + } + fingerprint := ssh.FingerprintSHA256(hostKey) + base64HostKey := base64.StdEncoding.EncodeToString(result.HostKey) + + payload := plugins.ServiceSSH{ + Banner: banner, + PasswordAuthEnabled: passwordAuth, + Algo: fmt.Sprintf("%s", algo), + HostKey: base64HostKey, + HostKeyType: hostKey.Type(), + HostKeyFingerprint: fingerprint, + } + return plugins.CreateServiceFrom(target, payload, false, "", plugins.TCP), nil +} + +func (p *SSHPlugin) Name() string { + return SSH +} + +func (p *SSHPlugin) Type() plugins.Protocol { + return plugins.TCP +} + +func (p *SSHPlugin) Priority() int { + return 2 +} diff --git a/scanner/pkg/fingerprint/plugins/services/ssh/ssh_test.go b/scanner/pkg/fingerprint/plugins/services/ssh/ssh_test.go new file mode 100644 index 0000000..edb1d1c --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/ssh/ssh_test.go @@ -0,0 +1,38 @@ +package ssh + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestSSH(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "ssh", + Port: 22, + Protocol: plugins.TCP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "sickp/alpine-sshd", + }, + }, + } + + p := &SSHPlugin{} + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/stun/stun.go b/scanner/pkg/fingerprint/plugins/services/stun/stun.go new file mode 100644 index 0000000..297ba8f --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/stun/stun.go @@ -0,0 +1,182 @@ +package stun + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "hash/crc32" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +const STUN = "stun" + +type Plugin struct{} + +var MessageHeaderLength = 20 +var FingerprintAttrLength = 8 +var BindingResponse = "0101" +var MagicCookie = "2112a442" +var ATTRIBUTES = map[uint32]string{ + 0x0001: "MappedAddress", + 0x0006: "Username", + 0x0008: "MessageIntegrity", + 0x0009: "ErrorCode", + 0x000a: "UnknownAttributes", + 0x0014: "Realm", + 0x0015: "Nonce", + 0x0020: "XORMappedAddress", + 0x8022: "Software", + 0x8023: "AlternateServer", + 0x8028: "Fingerprint", +} +var RigourXor uint32 = 0x5354554e + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (p *Plugin) Run(conn net.Conn, timeout time.Duration, target plugins.Target) (*plugins.Service, error) { + /** + * https://datatracker.ietf.org/doc/html/rfc8489 + * + * Sends binding request with FINGERPRINT attribute + * Checks if response contains valid message type, magic cookie, and transaction ID + */ + + InitialConnectionPackage := []byte{ + 0x00, 0x01, // Message Type (class: Request, method: Binding) + 0x00, 0x0c, // Message Length + 0x21, 0x12, 0xA4, 0x42, // Magic Cookie + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Transaction ID + + // Attribute: SOFTWARE + 0x80, 0x22, // attribute type + 0x0, 0x0, // attribute length + + // Attribute: FINGERPRINT + 0x80, 0x28, // attribute type + 0x0, 0x4, // attribute length + 0x0, 0x0, 0x0, 0x0, // CRC-32 checksum + } + _, err := rand.Read(InitialConnectionPackage[8:20]) // generate random transaction ID + if err != nil { + return nil, &utils.RandomizeError{Message: "transaction ID"} + } + TransactionID := hex.EncodeToString(InitialConnectionPackage[8:20]) + + fingerprintValue := crc32.ChecksumIEEE( + InitialConnectionPackage[:len(InitialConnectionPackage)-FingerprintAttrLength], + ) ^ RigourXor + for i := 1; i <= 4; i++ { + InitialConnectionPackage[len(InitialConnectionPackage)-i] = byte(fingerprintValue & 0xFF) + fingerprintValue >>= 8 + } + + response, err := utils.SendRecv(conn, InitialConnectionPackage, timeout) + if err != nil { + return nil, err + } + if len(response) == 0 { + return nil, nil + } + + // check response + if len(response) < MessageHeaderLength { + return nil, nil + } + rmsgType, rmagicCookie, rtransID := hex.EncodeToString(response[:2]), + hex.EncodeToString(response[4:8]), + hex.EncodeToString(response[8:20]) + if rmsgType != BindingResponse { + return nil, nil + } + if rmagicCookie != MagicCookie { + return nil, nil + } + if rtransID != TransactionID { + return nil, nil + } + + // parse attributes (possibly optional) + infoMap, err := parseResponse(response) + if err != nil { + return nil, nil + } + payload := plugins.ServiceStun{ + Info: fmt.Sprintf("%s", infoMap), + } + + return plugins.CreateServiceFrom(target, payload, false, "", plugins.UDP), nil +} + +func parseResponse(response []byte) (map[string]any, error) { + attrInfo := make(map[string]any) + idx := MessageHeaderLength + length := len(response) + for idx < length { + // parse attribute type, length + if idx+4 > length { + return nil, &utils.InvalidResponseErrorInfo{ + Service: "OpenVPN", + Info: "invalid attribute T/L header", + } + } + attrType, attrLen := (int(response[idx])<<8)+int(response[idx+1]), + (int(response[idx+2])<<8)+int(response[idx+3]) + idx += 4 + if attrLen == 0 { + continue + } + + // parse attribute value + if idx+attrLen > length { + return nil, &utils.InvalidResponseErrorInfo{ + Service: "OpenVPN", + Info: "invalid attribute length", + } + } + attrValue := response[idx : idx+attrLen] + idx += attrLen + var attrValueStr string + attrName, exists := ATTRIBUTES[uint32(attrType)] + if exists { + if attrName == "Software" { + attrValueStr = string(attrValue) + } else { + attrValueStr = hex.EncodeToString(attrValue) + } + } else { + attrName = fmt.Sprintf("%04x", attrType) + attrValueStr = hex.EncodeToString(attrValue) + } + + // update attribute info map + // if attribute appeared more than once, only process first occurence + _, exists = attrInfo[attrName] + if !exists { + attrInfo[attrName] = attrValueStr + } + } + + return attrInfo, nil +} + +func (p *Plugin) PortPriority(i uint16) bool { + return i == 3478 +} + +func (p *Plugin) Name() string { + return STUN +} + +func (p *Plugin) Type() plugins.Protocol { + return plugins.UDP +} + +func (p *Plugin) Priority() int { + return 2000 +} diff --git a/scanner/pkg/fingerprint/plugins/services/stun/stun_test.go b/scanner/pkg/fingerprint/plugins/services/stun/stun_test.go new file mode 100644 index 0000000..0bfa11d --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/stun/stun_test.go @@ -0,0 +1,38 @@ +package stun + +import ( + "testing" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + "github.com/ctrlsam/rigour/pkg/test" + "github.com/ory/dockertest/v3" +) + +func TestSTUN(t *testing.T) { + testcases := []test.Testcase{ + { + Description: "stun", + Port: 3478, + Protocol: plugins.UDP, + Expected: func(res *plugins.Service) bool { + return res != nil + }, + RunConfig: dockertest.RunOptions{ + Repository: "zenosmosis/docker-coturn", + ExposedPorts: []string{"3478/udp"}, + }, + }, + } + var p *Plugin + + for _, tc := range testcases { + tc := tc + t.Run(tc.Description, func(t *testing.T) { + t.Parallel() + err := test.RunTest(t, tc, p) + if err != nil { + t.Errorf(err.Error()) + } + }) + } +} diff --git a/scanner/pkg/fingerprint/plugins/services/telnet/telnet.go b/scanner/pkg/fingerprint/plugins/services/telnet/telnet.go new file mode 100644 index 0000000..89ea626 --- /dev/null +++ b/scanner/pkg/fingerprint/plugins/services/telnet/telnet.go @@ -0,0 +1,285 @@ +package telnet + +import ( + "encoding/hex" + "net" + "time" + + "github.com/ctrlsam/rigour/pkg/fingerprint/plugins" + utils "github.com/ctrlsam/rigour/pkg/fingerprint/plugins/pluginutils" +) + +type TELNETPlugin struct{} + +const TELNET = "telnet" + +// https://www.rfc-editor.org/rfc/rfc854 +const IAC byte = 255 +const DONT byte = 254 +const DO byte = 253 +const WONT byte = 252 +const WILL byte = 251 +const SE byte = 240 +const NOP byte = 241 +const DM byte = 242 +const BRK byte = 243 +const IP byte = 244 +const AO byte = 245 +const AYT byte = 246 +const EC byte = 247 +const EL byte = 248 +const GA byte = 249 +const SB byte = 250 + +// https://users.cs.cf.ac.uk/Dave.Marshall/Internet/node141.html +const ECHO byte = 1 +const SUPPRESSGOAHEAD byte = 3 +const STATUS byte = 5 +const TIMINGMARK byte = 6 +const TERMTYPE byte = 24 +const WINDOWSIZE byte = 31 +const TERMSPEED byte = 32 +const REMOTEFLOWCTRL byte = 33 +const LINEMODE byte = 34 +const ENVVAR byte = 36 + +// https://www.iana.org/assignments/telnet-options/telnet-options.xhtml +// Binary Transmission [RFC856] +// Reconnection [NIC 15391 of 1973] +// Approx Message Size Negotiation [NIC 15393 of 1973] +// Remote Controlled Trans and Echo [RFC726] +// Output Line Width [NIC 20196 of August 1978] +// Output Page Size [NIC 20197 of August 1978] +// Output Carriage-Return Disposition [RFC652] +// Output Horizontal Tab Stops [RFC653] +// Output Horizontal Tab Disposition [RFC654] +// Output Formfeed Disposition [RFC655] +// Output Vertical Tabstops [RFC656] +// Output Vertical Tab Disposition [RFC657] +// Output Linefeed Disposition [RFC658] +// Extended ASCII [RFC698] +// Logout [RFC727] +// Byte Macro [RFC735] +// Data Entry Terminal [RFC1043][RFC732] +// SUPDUP [RFC736][RFC734] +// SUPDUP Output [RFC749] +// Send Location [RFC779] +// End of Record [RFC885] +// TACACS User Identification [RFC927] +// Output Marking [RFC933] +// Terminal Location Number [RFC946] +// Telnet 3270 Regime [RFC1041] +// X.3 PAD [RFC1053] +// X Display Location [RFC1096] +// Authentication Option [RFC2941] +// Encryption Option [RFC2946] +// New Environment Option [RFC1572] +// TN3270E [RFC2355] +// XAUTH [Rob_Earhart] +// CHARSET [RFC2066] +// Telnet Remote Serial Port (RSP) [Robert_Barnes] +// Com Port Control Option [RFC2217] +// Telnet Suppress Local Echo [Wirt_Atmar] +// Telnet Start TLS [Michael_Boe] +// KERMIT [RFC2840] +// SEND-URL [David_Croft] +// FORWARD_X [Jeffrey_Altman] +// TELOPT PRAGMA LOGON [Steve_McGregory] +// TELOPT SSPI LOGON [Steve_McGregory] +// TELOPT PRAGMA HEARTBEAT [Steve_McGregory] +const BinTransmission byte = 0 +const RECON byte = 2 +const ApproxMsgSizeNeg byte = 4 +const RemoteCtrlTE byte = 7 +const OUTLINEWIDTH byte = 8 +const OUTPAGESIZE byte = 9 +const OUTCRD byte = 10 +const OUTHTS byte = 11 +const OUTHTD byte = 12 +const OUTFFD byte = 13 +const OUTVT byte = 14 +const OUTVTD byte = 15 +const OUTLD byte = 16 +const EXTASCII byte = 17 +const LOGOUT byte = 18 +const BYTEMACRO byte = 19 +const DataEntryTerm byte = 20 +const SUPDUP byte = 21 +const SupdupOut byte = 22 +const SendLoc byte = 23 +const EOR byte = 25 +const TACAS byte = 26 +const OM byte = 27 +const TERMLOCN byte = 28 +const T3270 byte = 29 +const X3PAD byte = 30 +const XDISP byte = 35 +const AUTHOPT byte = 37 +const ENCOPT byte = 38 +const NEWENVOPT byte = 39 +const TN327 byte = 40 +const XAUTH byte = 41 +const CHARSET byte = 42 +const TRSP byte = 43 +const COMPORT byte = 44 +const TSLE byte = 45 +const TSTLS byte = 46 +const KERMIT byte = 47 +const SENDURL byte = 48 +const ForX byte = 49 +const TELPL byte = 138 +const TELSSPI byte = 139 +const TELPRAGMA byte = 140 + +var TelnetCommandMap = map[byte]bool{ + IAC: true, + DONT: true, + DO: true, + WONT: true, + WILL: true, + SE: true, + NOP: true, + DM: true, + BRK: true, + IP: true, + AO: true, + AYT: true, + EC: true, + EL: true, + GA: true, + SB: true, +} + +// https://users.cs.cf.ac.uk/Dave.Marshall/Internet/node141.html +// https://www.iana.org/assignments/telnet-options/telnet-options.xhtml +var TelnetOptionsMap = map[byte]bool{ + ECHO: true, + SUPPRESSGOAHEAD: true, + STATUS: true, + TIMINGMARK: true, + TERMTYPE: true, + WINDOWSIZE: true, + TERMSPEED: true, + REMOTEFLOWCTRL: true, + LINEMODE: true, + ENVVAR: true, + BinTransmission: true, + RECON: true, + ApproxMsgSizeNeg: true, + RemoteCtrlTE: true, + OUTLINEWIDTH: true, + OUTPAGESIZE: true, + OUTCRD: true, + OUTHTS: true, + OUTHTD: true, + OUTFFD: true, + OUTVT: true, + OUTVTD: true, + OUTLD: true, + EXTASCII: true, + LOGOUT: true, + BYTEMACRO: true, + DataEntryTerm: true, + SUPDUP: true, + SupdupOut: true, + SendLoc: true, + EOR: true, + TACAS: true, + OM: true, + TERMLOCN: true, + T3270: true, + X3PAD: true, + XDISP: true, + AUTHOPT: true, + ENCOPT: true, + NEWENVOPT: true, + TN327: true, + XAUTH: true, + CHARSET: true, + TRSP: true, + COMPORT: true, + TSLE: true, + TSTLS: true, + KERMIT: true, + SENDURL: true, + ForX: true, + TELPL: true, + TELSSPI: true, + TELPRAGMA: true, +} + +func isTelnet(telnet []byte) error { + msgLength := len(telnet) + matchError := &utils.InvalidResponseError{Service: TELNET} + + if msgLength == 0 || msgLength == 1 { + // a 0 or 1 byte response is probably not a telnet server + // matchError.Msg = "invalid message length" + return matchError + } + + // the first byte must be IAC + if telnet[0] != IAC { + // matchError.Msg = "missing IAC first byte" + return matchError + } + // if the next code isn't a valid telnet command probably not a telnet server + if _, ok := TelnetCommandMap[telnet[1]]; !ok { + // matchError.Msg = "invalid telnet command" + return matchError + } + + if msgLength == 2 { + // the first two bytes were valid telnet speak and only two bytes were sent, good chance this is a telnet server + return nil + } + + // msgLength is not 0, 1, or 2 (so it's 3 or greater) + // check if the 3rd byte is a valid telnet option, if it is this is likely a real telnet server sending: + // IAC,,