JS-Recon detailed. Analizying the internal network with a XSS
JS-Recon is a network reconnaissance tool written in JavaScript by @lavakumark, which makes use of HTML5 features like Cross Origin Requests(CORs) and WebSockets.
JS-Recon can perform:
- Port Scans
- Network Scans
- Detecting private IP address
And… what is the impact here? Well, with a browser, we can try to determine the status of an internal website, trying to avoid firewall restrictions with a XSS or tricking our victim to visit out site with the javascript code.
How does it work?
Definitions
CORS (Cross-Origin Resource Sharing): mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin.
WebSockets: The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user’s browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.
Description of JS-Recon funcionallity
CORS XMLHttpRequest has five possible readystate status and WebSocket has four possible readystate status.
But, what is a readystate status?
ReadyState property returns the state an XMLHttpRequest client is in. An XHR client exists in one of the following states:
- 0 request not initialized
- 1 server connection established
- 2 request received
- 3 processing request
- 4 request finished and response is ready
When a new connection is made to any service the status of the readystate property changes based on the state of the connection. This transition between different states can be used to determine if the remote port to which the connection is being made is either open, closed or filtered.
- Port Scanning:
When a WebSocket or CORS connection is made to a specific port of an IP address in the internal network the initial state of WebSocket is readystate 0 and for CORS its readystate 1. Depending on the status of the remote port, these initial readystate status change sooner or later. The below table shows the relation between the status of the remote port and the duration of the initial readystate status. By observing how soon the initial readystate status changes we can identify the status of the remote port.
Port Status | WebSocket (ReadyState 0) | WebSocket (ReadyState 1) |
---|---|---|
Open (applications type 1 & 2) | < 100ms | < 100ms |
Closed | ~ 1000ms | ~ 1000ms |
Filtered | > 3000ms | > 3000ms |
There are some limitations to performing port scans this way. The major limitation is that all browser’s block connections to well known ports and so they cannot be scanned. The other limitation is that these are application level scans unlike the socket level scans performed by tools like nmap. This means that based on the nature of the application listening on a particular port the response and interpretation might vary.
There are four types of responses expected from applications:
- 1 Close on connect: Application terminates the connection as soon as the connection is established due to protocol mismatch
- 2 Respond & close on connect: Similar to type-1 but before closing the connection it sends some default response
- 3 Open with no response: Application keeps the connection open expecting more data or data that would match its protocol specification
- 4 Open with response: Similar to type-3 but sends some default response on connection, like a banner or welcome message
The behavior of WebSockets and COR for each of these types is shown in the table below.
Application Type | WebSocket (ReadyState 0)/CORS (ReadyState 1) |
---|---|
Closed on connect | < 100ms |
Response & close on connect | < 100ms |
Open with no response | > 3000ms |
Open with response | < 100ms (FF & Safari) | > 300ms (Chrome) |
- Network Scanning:
The port scanning technique can be applied to perform horizontal network scans of internal networks. Since both an open port and a closed port can be accurately identified, horizontal scans can be made for specific ports that would be allowed through the personal firewalls of most corporate systems.
Identification of an open or closed port would indicate that a particular IP address is up.
Ports like 445 or 3389 are ideal for such purpose as these are usually allowed across personal firewalls of desktop systems. It has been found that port 445 is of Application type-1 on Windows 7 and can be detected whether it is open or closed. However port 445 on Windows XP and port 3389 are of application type-3 and the host can only be detected if these ports are closed on such systems.
- Detecting Private IP Address:
Most home user’s connected to WiFi routers are given IP addresses in the 192.168.x.x range. And the IP address of the router is often 192.168.x.1 and they almost always have their administrative web interfaces running on port 80 or 443.
These two trends can be exploited to guess the private IP address of the user in two steps:
Step 1: Identify the user’s subnet This can be done by scanning port 80 and/or 443 on the IP addresses from 192.168.0.1 to 192.168.255.1. If the user is on the 192.168.3.x subnet then we would get a response for 192.168.3.1 which would be his router and thus the subnet can be identified.
Step 2: Identify the IP address Once the subnet is identified we scan the entire subnet for a port that would be filtered by personal firewalls, port 30000 for example. So we iterate from 192.169.x.2 to 192.168.x.254, when we reach the IP address of the user we would get a response (open/closed) because the request is generated from the user’s browser from within his system and so his personal firewall does not block the request.
Analyzing Detecting IP Address
The basis of the rest of the functionality are the same. However, to not extend this article, only a funcionatility would be analyzed:
The first function called is find_private_ip()
:
function find_private_ip()
{
scan_type=3;
network_address = [192,168,0,1];
reset_scan_out();
document.getElementById('result').innerHTML = "Detection started<br>";
find_network();
}
It sets variables and cleans the output. Then calls the function find_network()
:
function find_network()
{
if(network_address[2] > 255)
{
network_address[2] == 0;
document.getElementById('result').innerHTML = "The local network could not be identified...detection stopped";
}
else
{
document.getElementById('result').innerHTML += "Currently checking - " + network_address.join(".");
network_address[2]++;
is_dest_up(1);
}
}
Find_network()
checks if the 192.168.X.X value, that corresponds with the subnet of the victim is over 255. If this value is over, the subnet could not be verified and the detections tops.
If subnet values is not over 255, adds 1 to the subnet mask and calls is_dest_up(1)
:
function is_dest_up(pis_code)
{
var pis_port = 80;
...
start_time = (new Date).getTime();
try
{
ws = new WebSocket("ws://" + network_address.join(".") + ":" + pis_port);
...
setTimeout("check_idp(1)",100);
}
catch(err)
{
document.getElementById('result').innerHTML += "<b>Scan stopped. Exception: " + err + "</b>";
return;
}
}
The function set the port of the router to 80. Then starts a timmer and creates a websocket trying to connect to the port 80 in the IP that iteracts. Therefore, if the there is a response in the 80 of the router IP and a delay in the response, the script could determine the subnet IP.
function check_idp(pis_code)
{
var interval = (new Date).getTime() - start_time;
if(ws.readyState == 0)
{
if(interval > closed_port_max)
{
...
setTimeout("find_network()",1);
}
else
{
setTimeout("check_idp(" + pis_code + ")",100);
}
}
else
{
...
document.getElementById('result').innerHTML = "Network found -- " + network_address.join(".") + "..checking for IP<br>";
setTimeout("find_ip()",1);
...
}
}
Check_idp()
checks the readyState of the socket, if is equal to 0 and the time of the interval is over the closed_port_max seconds, the port is closed and continues enumerating the next subnet. If not, calls the function again to check if the readyState has changed and if it has changed, it calls find_ip()
doing a similar process to guess the IP of the user.
Taking advantage of Cross-Site Scripting to get the Internal IP of a Victim
After analyzing the potential of JS-Recon, an attacker can think about:
- Infect the victim with a XSS
- Detect the internal IP of the victim
- Detect the open services of the victim
- Detect the desired open services of the IPs in the victim LAN
Therefore, editing the javascript and invoking find_network()
first, and when the victim IP is detected, call scan_ports()
passing the victim IP as parameter and ending calling scan_network()
specifying the IP range of the victim LAN and the desired port/s, we could do a complete local scan of a victim network using a script.
In this POC only the internal IP is detected and retrieved:
If we inject the malicious javascript in the XSS or call to a hosted javascript in our machine:
<script src="http://localhost/1.js" />
Automatically, the script will try to find our router interface and detect the IP Range:
After finding the subnet, the script will try to find the internal IP of the victim:
Finally, an assyncronous request to the attacker site will be done passing the internal IP as parameter:
Limitations
Blocked Ports: To avoid Cross Protocol exploitation almost all popular browsers block connections to certains well known ports. Due to this the status of these ports cannot be determined.
Linear Scanning: The determination of port status is based on timing of the readyState status changes. Opening multiple simultaneous connections interferes with this timing leading to unreliable results. Hence to avoid such situations all scans are performed one port at a time.
Internal Networks Only: As stated above, timing is critical to identification of port status. Depending on the location of the target device this timing could vary. JSRecon has been tuned to scan internal networks with very low turn around time. Scanning external networks would require only two minor changes - values of the variables open_port_max and closed_port_max must be suitable updated.