@mi-sec/port-scanner
Version:
simple port scanner utility
428 lines (377 loc) • 10.2 kB
JavaScript
/** ****************************************************************************************************
* File: port-scanner.js
* Project: @mi-sec/port-scanner
* @author MI-SEC
*******************************************************************************************************/
'use strict';
const
net = require( 'net' ),
{ EventEmitter } = require( 'events' ),
LightMap = require( '@mi-sec/lightmap' ),
NetworkCidr = require( '@mi-sec/network-cidr' );
const commonPorts = new LightMap( [
[ 7, 'echo' ],
[ 9, 'discard' ],
[ 13, 'daytime' ],
[ 21, 'ftp' ],
[ 22, 'ssh' ],
[ 23, 'telnet' ],
[ 25, 'smtp' ],
[ 26, 'rsftp' ],
[ 37, 'time' ],
[ 53, 'domain' ],
[ 79, 'finger' ],
[ 80, 'http' ],
[ 81, 'hosts2-ns' ],
[ 88, 'kerberos-sec' ],
[ 106, 'pop3pw' ],
[ 110, 'pop3' ],
[ 111, 'rpcbind' ],
[ 113, 'ident' ],
[ 119, 'nntp' ],
[ 135, 'msrpc' ],
[ 139, 'netbios-ssn' ],
[ 143, 'imap' ],
[ 144, 'news' ],
[ 179, 'bgp' ],
[ 199, 'smux' ],
[ 389, 'ldap' ],
[ 427, 'svrloc' ],
[ 443, 'https' ],
[ 444, 'snpp' ],
[ 445, 'microsoft-ds' ],
[ 465, 'smtps' ],
[ 513, 'login' ],
[ 514, 'shell' ],
[ 515, 'printer' ],
[ 543, 'klogin' ],
[ 544, 'kshell' ],
[ 548, 'afp' ],
[ 554, 'rtsp' ],
[ 587, 'submission' ],
[ 631, 'ipp' ],
[ 646, 'ldp' ],
[ 873, 'rsync' ],
[ 990, 'ftps' ],
[ 993, 'imaps' ],
[ 995, 'pop3s' ],
[ 1025, 'NFS-or-IIS' ],
[ 1026, 'LSA-or-nterm' ],
[ 1027, 'IIS' ],
[ 1028, 'unknown' ],
[ 1029, 'ms-lsa' ],
[ 1110, 'nfsd-status' ],
[ 1433, 'ms-sql-s' ],
[ 1720, 'h323q931' ],
[ 1723, 'pptp' ],
[ 1755, 'wms' ],
[ 1900, 'upnp' ],
[ 2000, 'cisco-sccp' ],
[ 2001, 'dc' ],
[ 2049, 'nfs' ],
[ 2121, 'ccproxy-ftp' ],
[ 2717, 'pn-requester' ],
[ 3000, 'ppp' ],
[ 3128, 'squid-http' ],
[ 3306, 'mysql' ],
[ 3389, 'ms-wbt-server' ],
[ 3986, 'unknown' ],
[ 4899, 'radmin' ],
[ 5000, 'upnp' ],
[ 5009, 'airport-admin' ],
[ 5051, 'ida-agent' ],
[ 5060, 'sip' ],
[ 5101, 'admdog' ],
[ 5190, 'aol' ],
[ 5357, 'wsdapi' ],
[ 5432, 'postgresql' ],
[ 5631, 'unknown' ],
[ 5666, 'nrpe' ],
[ 5800, 'vnc-http' ],
[ 5900, 'vnc' ],
[ 6000, 'X11' ],
[ 6001, 'X11:1' ],
[ 6646, 'unknown' ],
[ 7070, 'realserver' ],
[ 8000, 'http-alt' ],
[ 8008, 'http' ],
[ 8009, 'ajp13' ],
[ 8080, 'http-proxy' ],
[ 8081, 'blackice-icecap' ],
[ 8443, 'https-alt' ],
[ 8888, 'unknown' ],
[ 9100, 'jetdirect' ],
[ 9999, 'abyss' ],
[ 10000, 'unknown' ],
[ 32768, 'filenet-tms' ],
[ 49152, 'unknown' ],
[ 49153, 'unknown' ],
[ 49154, 'unknown' ],
[ 49155, 'unknown' ],
[ 49156, 'unknown' ],
[ 49157, 'unknown' ]
] );
function convertHighResolutionTime( t ) {
return ( ( t[ 0 ] * 1e9 ) + t[ 1 ] ) / 1e6;
}
function deepCopy( n ) {
return JSON.parse( JSON.stringify( n ) );
}
/**
* TCPConnect
* @description
* make a socket connection to a single host/port
* @param {object} opts - socket options
* @param {string} opts.host - ip address
* @param {number} opts.port - port to connect to
* @param {number} [opts.timeout=1000] - socket timeout
* @param {number} [opts.bannerlen=512] - maximum banner length to gather
* @param {object} [opts.connectionOpts={}] - connection options
* [ref](https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener)
*
* @example
* new TCPConnect( {
* host: '192.168.1.30',
* port: 22
* } ).scan();
*
* {
* host: '192.168.1.30',
* port: 22,
* status: 'open',
* banner: 'SSH-2.0-OpenSSH_7.9\r\n',
* time: 6.081522,
* service: 'ssh'
* }
*/
class TCPConnect
{
constructor( opts = {} )
{
if ( !opts.hasOwnProperty( 'host' ) ) {
throw new Error( 'TCPConnect opts.host is required' );
}
else if ( !opts.hasOwnProperty( 'port' ) ) {
throw new Error( 'TCPConnect opts.port is required' );
}
this.opts = deepCopy( opts );
this.debug( 'preparing' );
this.opts.timeout = this.opts.timeout || 2000;
this.opts.bannerlen = this.opts.bannerlen || 512;
this.opts.connectionOpts = this.opts.connectionOpts || {};
this.data = {
host: this.opts.host,
port: this.opts.port,
banner: '',
error: false,
status: null,
opened: false,
time: 0
};
}
startTime()
{
this.timeStopped = false;
this.data.time = process.hrtime();
}
stopTime()
{
if ( !this.timeStopped ) {
const end = process.hrtime( this.data.time );
this.data.time = convertHighResolutionTime( end );
}
this.timeStopped = true;
}
onConnect()
{
this.stopTime();
this.data.opened = true;
this.debug( `connected in ${ this.data.time }ms` );
}
onData( buf )
{
if ( this.opts.bannerGrab ) {
// ref if IAC negotiation is needed
// Ref: http://www.iana.org/assignments/telnet-options/telnet-options.xhtml
if ( this.data.banner.length < this.opts.bannerlen ) {
return this.data.banner += buf.toString( 'ascii' );
}
}
this.socket && this.socket.destroy();
}
onTimeout()
{
this.debug( `socket timeout (opened ${ this.data.opened })` );
if ( !this.data.opened ) {
this.data.status = 'closed (timeout)';
}
else {
this.data.status = 'open';
}
this.socket && this.socket.destroy();
}
onError( e )
{
this.data.error = true;
this.data.status = /ECONNREFUSED/.test( e.message ) ? 'closed (refused)' :
/EHOSTUNREACH/.test( e.message ) ? 'closed (unreachable)' :
e.message;
}
scan()
{
this.debug( 'scanning' );
this.startTime();
return new Promise(
( res, rej ) => {
this.socket = net.createConnection( {
host: this.opts.host,
port: this.opts.port,
...this.opts.connectionOpts
} );
this.socket.removeAllListeners( 'timeout' );
this.socket.setTimeout( this.opts.timeout );
this.socket.on( 'connect', this.onConnect.bind( this ) );
this.socket.on( 'data', this.onData.bind( this ) );
this.socket.on( 'timeout', this.onTimeout.bind( this ) );
this.socket.on( 'error', this.onError.bind( this ) );
this.socket.on( 'close', () => {
// if ( !this.data.banner ) {
// this.data.opened = false;
// }
this.stopTime();
this.debug( 'scan complete' );
if ( !this.data.status ) {
this.data.status = this.data.opened ? 'open' : 'closed';
}
if ( this.opts.identifyService ) {
this.data.service = commonPorts.get( this.opts.port ) || 'unknown';
}
if ( this.socket ) {
this.socket.destroy();
delete this.socket;
}
if ( this.data.error ) {
rej( this.data );
}
else {
res( this.data );
}
} );
}
);
}
debug( ...msg )
{
if ( this.opts.debug ) {
console.log( `[TCPConnect] ${ this.opts.host }:${ this.opts.port }`, ...msg );
}
}
}
class PortScanner extends EventEmitter
{
constructor( opts = {} )
{
super();
this.opts = opts;
this.opts.cidr = new NetworkCidr( this.opts.host || '127.0.0.1' );
if ( this.opts.ports ) {
if ( !Array.isArray( this.opts.ports ) ) {
this.opts.ports = [ this.opts.ports ];
}
}
else {
this.opts.ports = [ ...commonPorts.keys() ];
}
this.opts.timeout = this.opts.timeout || 1000;
this.opts.debug = this.opts.hasOwnProperty( 'debug' ) ? this.opts.debug : false;
this.opts.onlyReportOpen = this.opts.hasOwnProperty( 'onlyReportOpen' ) ? this.opts.onlyReportOpen : true;
this.opts.bannerGrab = this.opts.hasOwnProperty( 'bannerGrab' ) ? this.opts.bannerGrab : true;
this.opts.identifyService = this.opts.hasOwnProperty( 'identifyService' ) ? this.opts.identifyService : true;
this.debug( 'starting scan with options' );
this.debug( ` host: ${ this.opts.host }` );
this.debug( ` cidr: ${ this.opts.cidr }` );
this.debug( ` ports: ${ this.opts.ports }` );
this.debug( ` timeout: ${ this.opts.timeout }` );
this.debug( ` debug: ${ this.opts.debug }` );
this.debug( ` onlyReportOpen: ${ this.opts.onlyReportOpen }` );
this.debug( ` bannerGrab: ${ this.opts.bannerGrab }` );
this.debug( ` identifyService: ${ this.opts.identifyService }` );
this.result = new LightMap();
setImmediate( () => this.emit( 'ready', this.opts ) );
}
static isRange( n )
{
return !!n && n === '' + n && /\d+-\d+/.test( n );
}
static * portRangeIterator( ports )
{
for ( let i = 0; i < ports.length; i++ ) {
const p = ports[ i ];
if ( PortScanner.isRange( p ) ) {
let [ x, y ] = p.split( '-' );
x = +x;
y = +y;
while ( x <= y ) {
yield x++;
}
}
else {
yield p;
}
}
}
async scan()
{
const
hosts = this.opts.cidr.hosts(),
total = this.opts.cidr.size * this.opts.ports.length,
jobs = [];
function worker( host, port ) {
const
connect = new TCPConnect( { ...this.opts, host, port } );
return connect.scan()
.then( ( data ) => {
if ( !data.opened && this.opts.onlyReportOpen ) {
return;
}
this.result.set( `${ host }:${ port }`, data );
this.emit( 'data', data );
} )
.catch( ( data ) => {
if ( !data.opened && this.opts.onlyReportOpen ) {
return;
}
this.result.set( `${ host }:${ port }`, data );
this.emit( 'data', data );
} )
.finally( () => {
this.emit( 'progress', ++progress / total );
} );
}
let progress = 0;
for ( const host of hosts ) {
for ( const port of PortScanner.portRangeIterator( this.opts.ports ) ) {
// TODO::: implement https://github.com/mcollina/fastq
// jobs are completed with the original timeout and must be multithreaded to complete
// within set timeout value
jobs.push( worker.call( this, host, port ) );
}
}
await Promise.all( jobs );
if ( this.opts.onlyReportOpen ) {
this.result = this.result.filter( ( v ) => v && v.status === 'open' );
}
this.emit( 'done', this.result );
}
debug( ...msg )
{
if ( this.opts.debug ) {
console.log( ...msg );
}
}
}
module.exports = PortScanner;
module.exports.PortScanner = PortScanner;
module.exports.TCPConnect = TCPConnect;
module.exports.commonPorts = commonPorts;
module.exports.convertHighResolutionTime = convertHighResolutionTime;