novnc / websockify

Websockify is a WebSocket to TCP proxy/bridge. This allows a browser to connect to any application/server/service.
GNU Lesser General Public License v3.0
3.94k stars 779 forks source link

Multiple targets support (pending: AJAX query API) #3

Closed kanaka closed 9 years ago

kanaka commented 13 years ago

Instead of just having a single target host:port, it would be useful to be able to specify multiple host:port targets on the command line. This would address security issues related to allowing the client to select an arbitrary host:port, but it would allow a single instance of the proxy to serve multiple VNC servers.

This would require a mechanism to communicate to the client the list of valid targets and it would require a method for the client to select a target on connect. For backwards compatibility the first target host:port on the command line would be the default if the client doesn't specify one.

Update:

There have been several proposals to add arbitrary target support to websockify (i.e. by passing the host and port in the path). This model is a HUGE security risk because allows a malicious piece of Javascript to connect to arbitrary socket ports inside the network. Even if the websockify server only allows connections from browsers inside the network it still wouldn't help.

Imagine a malicious piece of Javascript (perhaps delivered via a hacked ad or via a SQL injection attack against some public website) running on a browser inside the network. This Javascript can now connect back out to a control server (using WebSockets, or AJAX or whatever) which can then give commands to connect to arbitrary ports inside the network. But connections via websockify are plain sockets which means that any socket service inside the network is at risk. Normal WebSocket connections are not an issue because they have a HTTP-like handshake that must complete before the Javascript can send any data to the server. But websockify strips off the WebSocket handshake.

In other words, the list of available targets MUST be controlled/managed by the server side (where websockify is running), NOT by Javascript running in the browser.

hsanjuan commented 12 years ago

Hi,

how hard would it be to implement this feature? Could you give some hints on your idea of how it could be done?

kanaka commented 12 years ago

This isn't too difficult but there are several parts. Here are my thoughts on the design of how this would work.

The multiple targets would be specified on the command line or in a file (whitespace separate). The command line would then look something like this:

./websockify --web ./ :6080 host1:5901 host2:5900 host3:5902

Or the file version:

./websockify --web ./ --target-list target_list.cfg :6080

Additionally, websockify would serve a JSON response to requests to the /__websockify__ path (e.g. localhost:6080/__websockify__) that contains the target list. Something like:

{"target_list": ["host1:5901", "host2:5900", "host3:5902"]}

Using a JSON map with a key allows other metadata to be served to websockify clients in the future.

When noVNC (or another websockify client) loads, the Websock constructor, would issue an async AJAX request to the __websockify__ target. When it returns it would populate an attribute on the Websock object and also fire an event: 'onmetadata' (which would also send the target list/array as the first argument.

For the noVNC UI, the way this would work is that the connect dialog would have an additional drop-down list (that starts hidden until the AJAX request comes back successfully). When the onmetadata event fires, a handler in noVNC which check the target list and if there is a list of one or more targets, it would add these to the target drop-down list and make it visible (the first one in the list should also be marked '(default)' and be the one selected.

When the user requests a connection and a non-default target was selected in the drop-down, then Websock adds a target-list query parameter to the path used in the WebSocket request. Note that this will have to be combined with any query parameters/path that the user may have already specified.

For example: The second item in the target drop-down is host2:5900 and the user selects it. Also, the user enters the following path: /mypath/websockify?mytoken=ABCD1234

And then the user clicks on connect. Websock will modify the path to become:

/mypath/websockify?websockify_target=host2%3A5900&mytoken=ABCD1234

Note the encoding of the ":" symbol.

On the websockify side the path in the WebSocket connection request is parsed. If the path query params contain a websockify_target value then this value is checked against the known list from the command line (or file). If there is a match, then this is the target that is used for connections. If there is a websockify_target value but it does not match, then the client connection is closed with an error code (not sure which is most appropriate). If there is no websockify_target value, then the first target specified on the command line (or file) is used.

To maintain backwards compatibility, if there is no response (or an error) from the AJAX request to __websockify__ then no modification is made to the path.

If the target list is specified using a file, then this file should be re-read every time there is a request to the __websockify__ path to allow dynamic updates (it should be relatively infrequent so performance is not an issue).

Unanswered questions:

Update:

The host:port should not be communicated in the websockify AJAX communication, just the tokens (communicating the host:port reveals information about the internal network that is unnecessary). I.e. the JSON response would look more like this:

 {"targets": ["token1", "token2", "token3"]}

Likewise, the path should just take a token without a host entry.

hsanjuan commented 12 years ago

Thanks for the insight. Actually, what I needed was to be able to launch connections with arbitrary targets using one instance of websockify, and not launching one whole proxy everytime.

It turns out that it was fairly easy to modify the code to reach that behaviour:

https://github.com/hsanjuan/websockify/commit/c73640b41e274faa09a9cdc98e2495646ef1ab88

In my use-case I don't know a priori in what address or port machines are going to be, I only know that at the moment of launching noVNC. So this way I can launch the proxy as usual (and would behave as usual), and without specifying a target, in which case it will extract the target address and port from the http request.

I dont speak fluent python, and im not sure this fits your idea, but let me know if it would qualify for a pull request.

kanaka commented 12 years ago

Small update to my original proposal. If the config file on the command line refers to a directory, then every file in that directory should be read as the configuration. This is a common pattern for configuration that allows the configuration to be easily and programmatically updated without requiring config file parsing.

hsanjuan commented 12 years ago

Hello @kanaka, can you have a look to

https://github.com/hsanjuan/websockify/commit/e17e1158d85b5955ca740f4b97643192e1d67eb0

and tell me if this would be enough to be pulled? I have implemented the file-based related strategies.

kanaka commented 12 years ago

@hsanjuan, yeah, that looks good. I'll try and pull that in and test in in the next couple of days.

hsanjuan commented 12 years ago

good! ill open a pull request then :)

kanaka commented 12 years ago

I've pulled in the target configuration changes in websockify (thanks @hsanjuan). If anybody wants to tackle the AJAX part of it I made an addendum to my original suggested design above that addresses a small security issue and matches more closely with the websockify changes that I just pulled in.

hsanjuan commented 12 years ago

Thanks! Looks great. I wonder if we can pull this to the websockify that comes as utils with kanaka/noVNC?

AricStewart commented 12 years ago

Hi there, New to the project but this work is exactly what I need for a websockify setup with one exception. We need the token to be able to be secure. So placing it on the URL does not work for us as it is sent in plain text.

I have been working out a patch which sends the token as the first message on the web socket "token: " and then after reading that first message websockify setups the redirection.

Would work like this be of interest to the broader project and if so is this a procedure that would be acceptable or would there be additional suggestions.

kanaka commented 12 years ago

@AricStewart, sending it after the handshake does change the security of the token. It obfuscates it very slightly due to client to server masking, but masking was NOT designed for security but to workaround a theoretical intermediary (e.g. proxy, load balancer) vulnerability. Each frame contains the mask at the beginning of the frame and it is trivial to use the mask to decode the frame.

If you want to secure the data then you need to use an encrypted websocket connection (wss://), in which case the URL/path is encrypted just like everything else. If you have some other compelling reason besides security why you don't want to use the path, then I might be willing to accept a path that puts the token in a cookie (which get included in the handshake from the client). I don't want to clutter up data channel itself with configuration metadata.

AricStewart commented 12 years ago

Ah and here is where my ignorance is showing. :) I was not aware that the URL path was encrypted using ssl. I thought it was only the data after the initial connection. Thus my thought of having the token exchanged at that point. Thanks for pointing this out to me.

kanaka commented 12 years ago

@AricStewart it's a common misconception actually (I myself thought that until a few years ago). However, both https and wss connections are encrypted using TLS for all bytes of the connection (i.e. TLS negotiation is the very first thing that happens for each connection).

kanaka commented 12 years ago

Just to update. The biggest chunk of this feature is complete. The missing piece is functionality to allow the list of valid target token to be query-able from the client using some sort of AJAX call.

verwilst commented 11 years ago

I have the latest version of master, but can't seem to open multiple connections? Is there documentation somewhere?

SeyZ commented 11 years ago

Is it already merged? I don't think. Can anybody confirm?

kanaka commented 11 years ago

@verwilst, @SeyZ to use multiple target support you use the --target-config TARGET_FILE option. The TARGET_FILE is a configuration file that should contain the following format:

token1: target_host_1:target_port_1
token2: target_host_2:target_port_2
...

To select the one of the targets, you must pass token=TOKEN in the query string part of the WebSocket request. For example, to connect to the second target from the client, you would connect using the following:

ws = new WebSocket('ws://wshost:wsport/?token=token2');

Or if using websock.js it is similar:

ws = new Websock();
ws.open('ws://wshost:wsport/?path=token%3Dtoken2');  // Note the second = is URI encoded

If you are using noVNC, then you need to set the WebSocket request path via the RFB.connect call. If you are using vnc_auto.html, then you can pass the WebSocket request path in the initial noVNC page query string:

.../vnc_auto.html?path=token=token2

If you are using vnc.html, the WebSocket request path can be set via the settings menu to ?token=token2.

rickparrish commented 10 years ago

I was about to submit a feature request, but I think this may be similar to what I was going to ask for so I thought I would comment here instead.

What I want isn't actually multiple port forwarding, it's multiple host forwarding. I wrote a telnet (soon rlogin as well) client, but unfortunately only a small set of BBSes are capable of accepting WebSocket connections natively, which means the majority of connections need to be made via a proxy like websockify. Many SysOps setup my client and websockify on their own server, and for this purpose it works perfect. But there are also many SysOps who aren't capable of running websockify (either can't figure it out, or don't run a supported OS), so I want to run a public relay for them.

So what I would like is to be able to start websockify with --relay-ports=23,513, and then when the client connects it'll pass the requested host:port like GET /some.host.name/23, at which point websockify will say "ok, 23 is allowed, so I'll go connect to some.host.name:23 now". If the client sends GET /some.host.name/80 then websockify will say "oops 80 isn't allowed" and send an error to the user saying relaying for 80 is not allowed.

Since I am whitelisting the ports on the server, I don't see this as a huge security hole (I'm not saying that it's not a security hole, just that it's not as huge as allowing all ports). I realize someone could still relay via my server to attack another server on 23 or 513, but it's a smallish risk due to the uncommon use of these ports nowadays.

I've actually been running such a setup for a couple years now, and have not had any problems. It was discovered surprisingly quickly, and requests to relay for port 80 and 25 come in on a regular basis, but of course don't get very far because I haven't whitelisted those ports. The only reason I don't want to continue with my current setup is because it's running on my home PC (.NET binary, doesn't run under Mono unfortunately), and I'd like to move to a Linux VPS that will hopefully have better uptime and bandwidth.

I'd love to implement this myself and contribute the change (if you want it), but I have no experience with Python so have no idea where to begin. Maybe you can provide a gentle push in the right direction?

Thanks, Rick

kousu commented 10 years ago

@rickparrish , what you're asking for sounds verrrry similar to what is already done by HTTP proxies. HTTP proxies work like this when forwarding TCP:

CONNECT host:port
Otherhttpheader: ...
Otherhttpheader2: ...

[tcp stream here]

Better yet, use SOCKS which can tunnel any IP protocol.

My understanding of your problem is that you do not have a proxy which speaks websocket natively. Instead of bloating websockify, can you achieve what you want if you run a SOCKS5 server and stick websockify in front of that?

Firefox/Chrome --[websocket]--> (( websockify --[tcp]--> ssh )) --[tcp]--> BBS

* (( )) = your physical server

These command lines should get you going:

$ ssh -D 2322 localhost  #ssh is the cheapest and dirtiest SOCKS proxy you'll find; there's also http://www.inet.no/dante/
$ websockify 80 localhost:2322

I suppose you will need to put in some effort to teach javascript SOCKS, but the basic SOCKS protocol is very very short and gets out of your way after some headers--much like websocket, and it sounds like you were ready to implement something almost identical on top of WebSocket anyway. Actually, I would love to have a handy SOCKS library, since a quick google didn't bring any up and WebSockify + SOCKS opens up a lot of interesting territory.

So then in the browser, you'd do something like

ws = new WebSocket("ws://you.com/");
proxy = new SOCKS5(ws, "renegade-bbs.info.za:8889");
for(;;) { 
 proxy.read(...);
 proxy.write(...);
 }

(except you can't write the calls quite in that order; it would be nice if the WebSocket API came with an explicit connect() method so callable after the event handlers are set up...)

By the way, whitelisting ports is a very strong and good first step. You also need to watch out for: limiting network traffic per client, and passive scanning attacks (like nmap's FTP bounce scan) and being part of a DDoS--where your proxy is used to open 10 000 empty connections..

rickparrish commented 10 years ago

Thanks @kousu, it didn't even occur to me to use something like a SOCKS proxy in between websockify and the remote host -- at quick glance I can't see a reason for this not to work. My original plan would have only required modification to websockify with no changes on the client side, but as you mentioned SOCKS is pretty simple so shouldn't be a big deal to implement.

My only reservation is that I plan on using this on fairly low-end VPSes, and so a slight modification to websockify will require less resources than a whole new process (or multiple processes from what the Dante FAQ says). Definitely something to look into though, maybe Dante will turn out to be fairly lightweight, so thanks again for the suggestion.

kanaka commented 10 years ago

@rickparrish does --target-config not work for you? You will have to whitelist host and port combinations in the config. However, the --target-config option makes it really easy to maintain because the --target-config option can point to a directory of files which contain snippits of the configuration and it re-reads the config every time a new client connects. This makes it really easy to create and maintain your configuration. Also, you could use the host:port as the token used for connections so that the users of the service don't have to remember a token for each host:port target (although you may need to use a different character than ":" for the delimiter in the token).

I'm not going to pull a feature to add arbitrarily selection of host or port (i.e. not full whitelist) to websockify because that could be far too easily abused if the administrator is not very careful and diligent.

rickparrish commented 10 years ago

Unfortunately I don't think it does since it requires me knowing ahead of time every last host a user might want to telnet to. I want the user to be able to punch in any arbitrary host in my client, and as long as they want to connect to port 23 on that host, then websockify should make the connection.

I totally understand not wanting to pull the feature in. The issues @kousu mentioned concern me a little, and there are probably other things to watch out for as well. I'm probably the only one that would ever want such a feature anyway!

Do you mind looking at my modification though? As I mentioned I've never looked at Python before, so hopefully I didn't make any stupid mistakes. https://github.com/rickparrish/websockify/commit/3526e09570de9199aa990f8d4e6d01b81f9ab0a3

Thanks, Rick

kanaka commented 10 years ago

@rickparrish I added a comment to your code. Otherwise, looks fine (apart from remaining security concerns)

rickparrish commented 10 years ago

Thanks for the code comment, I'll incorporate that in later. I've thought a little more about the security concerns:

passive scanning attacks (like nmap's FTP bounce scan) -- sounds like that allowed an attacker to scan all ports on a victim machine, but websockify would restrict the attacker to scanning only the relayed port(s) on the victim machine. So a real problem, but a limited one.

being part of a DDoS where your proxy is used to open 10 000 empty connections -- similar to limiting network traffic, what does the attacker gain? It would require the attacker to open 10,000 connections to websockify in order to have websockify open 10,000 connections to a victim machine, so why wouldn't they do it directly?

kousu commented 10 years ago

On August 27, 2014 3:06:59 PM EDT, rickparrish notifications@github.com wrote:

Thanks for the code comment, I'll incorporate that in later. I've thought a little more about the security concerns:

  • limiting network traffic per client -- what does an attacker gain from going through the relay? There is no amplification taking place (slightly the opposite), so websockify will only send bytes to the victim as fast as the attacker sends to websockify, so the attacker might as well connect to the victim directly. They may think they gain anonymity by going through the relay, but all IPs are logged so they would be wrong. Am I missing some other attack vector here?

This concern isn't so much an attack as an easy way to eat up your bandwidth if you're not careful. If you proxy five BBSes each with a couple hundred users going through your server, your bandwidth costs will add up. Being able to limit per-user bandwidth is one way to control your costs while ensuring fairness for clients.

passive scanning attacks (like nmap's FTP bounce scan) -- sounds like that allowed an attacker to scan all ports on a victim machine, but websockify would restrict the attacker to scanning only the relayed port(s) on the victim machine. So a real problem, but a limited one.

Yet HTTP has become a transport protocol. Maybe you can't scan for smb with websockify, but a clever attacker could scan for

being part of a DDoS would require the attacker to open 10,000 connections to websockify

Good point. My intuition said there was an attack here. Maybe I'm wrong.

Speaking generally, I am concerned about adding proxying to websockify because accidentally constructing open proxies is too easy in general and shoots unsuspecting users in the foot. There are preexisting proxies out there, which I would gladly use in conjunction with websockify so long as all their long-tested security practices are followed, whereas we don't know all the pitfalls that might be involved in proxy code. (I also feel the same way about websockify's execve() option; there's already nc/socat if you want to stuff a program behind a socket)

kanaka commented 10 years ago

I think @kousu addressed things pretty well.

Just a couple more points:

If you run a service on the Internet it either needs to be hardened (which takes a lot of expertise and effort) or well hidden (e.g. port knock) otherwise I guarantee that it will eventually be exploited in some way that is going to be unpleasant for you. I kept security issues in mind while writing websockify but it's not fully hardened so I recommend either hiding it or using aggressive white-listing to drop unauthorized connections as early as possible.

Just because you log IP addresses for requests, doesn't mean an attacker can't make your life miserable. If he/she controls both ends of the traffic he/she can send data back and forth via the relay that is flagged as illegal/suspicious data by your ISP or the authorities. You'll then be shutdown or subpoena'd and have to jump through various hoops (or worse) to get your service restored. The attacker's systems will by that time be long gone (or more likely, the end-points were just other hijacked systems).

Regarding denial of service, a determined attacker is not going to open connection the normal way. He is going to generate just enough of the TCP exchange to cause websockify to open a remote connection and then promptly "forget" about that connection from his perspective (but not sending an actual close). He could easily exhaust resources (e.g. open files) on your proxy host or the target host without breaking a sweat himself.

Can you whitelist your clients using a firewall (a variation of hiding the service)?

rickparrish commented 10 years ago

Thanks for all the feedback.

The VPS would be suspended if it exceeded the bandwidth limit, so someone sending large amounts of data wouldn't cost me anything, but it could end up taking the server offline until the next billing month.

And I hadn't considered that someone controlling both ends might intentionally send illegal data back and forth. The handful of proxies I'm running are all with providers that I don't use for any other purpose, so no great harm if my services with them are terminated, but after what happened to the Austrian guy who ran a Tor exit node, I am a little worried about the potential legal hassles.

I really was hoping to make it a totally open system where anybody could connect to any BBS without my intervention (ie whitelisting ahead of time), but maybe I do need to re-think that plan and implement a system where SysOps and/or users request to have host:port combinations whitelisted (at which point the previously mentioned --target-config would work perfectly).

kanaka commented 10 years ago

@rickparrish it's definitely more hassle, but I would definitely recommend full host:port whitelisting if you can do it. When I originally built websockify, we whitelisted both targets and clients (automatically from a secure web UI). But at least fully whitelisting the targets will protect you from the most common and serious issues.

cnelson711 commented 10 years ago

I took the node.js version of Websockify and added the multiple host functionality one night this summer; though I took a different approach than whitelisting and use the term channel_id instead of token.

There's a control web server accessed over HTTPS with HTTP Basic Auth. From the control web server, you can create a new channel with a POST request, or list all the channel's with a GET request. Channels timeout and disappear after not being used. This approach relies on a third party system (i.e. a secured web application) to reserve channels and pass the channel_id's to both the viewer and server ends of the connection.

This is good if you want to use VNC as a support feature and you already have a separate program running on the remote computer that maintains a connection to the webserver and sets up the VNC session. The remote user can "Ask for help" to allow a tech support person in. Or the remote user can send a link to a friend (that expires); when the friend connects, it asks the remote user permission before sharing their screen. You just pass the channel_id along with the command that was being used to setup the VNC connection.

Basic usage (more details): A secured web application POSTs to the control webserver over HTTPS with a long secret username/password for Basic Auth. The response is something like: {"channel_id": "b8b58e55-572c-4810-9536-cb8e16027ecf"}

The secured web application sends the channel_id to both the viewer and the server. The NoVNC viewer passes the channel_id as the path for the secured websocket. The VNC server (./x11vnc -rfbport 0 -coe pre=b8b58e55-572c-4810-9536-cb8e16027ecf in my case) makes a reverse connection to the proxy with the channel_id as a pre string (i.e. the first 36 bytes sent are the channel_id).

It's a little over 200 LOC. Haven't tested for resiliency or for figuring out the max load yet.

Edit: After reading all the posts above, sounds like --target-config and using a whitelisted directory pretty much amounts to the same thing.

kousu commented 10 years ago

@rickparrish , I took my own idea and finished it. Maybe you will find it useful, and if you use it, you will help me find bugs in it! I present: socks5.js

If you actually deploy this, remember that SOCKS implements precisely the feature you were asking for ans so is subject to all the sorts of firewall-bypassing attacks kanaka outlined. However, it looks like dante lets you write whitelists pretty trivially, though, so change my diagram to

Firefox/Chrome --> [socks5.js --[WebSocket]-- > ] --> (( websockify --[tcp]--> dante )) --[tcp]--> BBS

* (( )) = your physical server

and you should be safe. Plus, dante does bandwidth throttling!