RCE in Slanger, a Ruby implementation of Pusher

While researching a web application last February, I learned about Slanger, an open source server implementation of Pusher. In this post I describe the discovery of a critical RCE vulnerability in Slanger 0.6.0, and the efforts that followed to responsibly disclose the vulnerability.

SECURITY NOTICE – If you are making use of Slanger in your products, stop reading and get your security fix first! A patch is available on GitHub or as part of the Ruby gem in version 0.6.1.

Pusher vs. Slanger

Pusher is a product that provides a number of libraries to enable the use of WebSockets in a variety of programming languages. WebSockets, according to their website, “represent a standard for bi-directional realtime communication between servers and clients.”

Some of the functionalities offered by Pusher are subscribing and unsubscribing to public and private channels. For example, the following WebSocket request would result in a user subscribing to the channel “presence-example-channel”:

  "event": "pusher:subscribe",
  "data": {
    "channel": "presence-example-channel",
    "auth": "<APP_KEY>:<server_generated_signature>",
    "channel_data": "{
      \"user_id\": \"<unique_user_id>\",
      \"user_info\": {
        \"name\": \"Phil Leggetter\",
        \"twitter\": \"@leggetter\",

While researching a web application, I learned about Slanger, an open-source Ruby implementation that “speaks” the Pusher protocol. In other words, it is a free alternative to Pusher that can be spun up as a stand-alone server in order to accept and process messages like the example above. At the time of writing, the vulnerable library has over 45,000 downloads on Rubygems.org.

I was able to determine what library was being used when the following error message was returned in response to my invalid input:


socket = new WebSocket("wss://push.example.com/app/<app-id>?");


{"event":"pusher:error","data":"{\"code\":500,\"message\":\"expected true at line 1, column 2 [parse.c:148]\\n /usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/handler.rb:28:in `load'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/handler.rb:28:in `onmessage'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/lib/slanger/web_socket_server.rb:30:in `block (3 levels) in run'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/connection.rb:18:in `trigger_on_message'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/message_processor_06.rb:52:in `message'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/framing07.rb:118:in `process_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/handler.rb:47:in `receive_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/em-websocket-0.5.1/lib/em-websocket/connection.rb:76:in `receive_data'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/eventmachine- `run_machine'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/eventmachine- `run'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/slanger-0.6.0/bin/slanger:106:in `<main>'\"}"}

Note the occurrence of gem slanger-0.6.0, which appears to point to this open source Ruby project on GitHub. Having all the source code at my disposition, my attention was drawn to the file handler.rb that contained the following function responsible for handling incoming WebSocket messages:

def onmessage(msg)
   msg = Oj.load(msg)
   msg['data'] = Oj.load(msg['data']) if msg['data'].is_a? String
   event = msg['event'].gsub(/\Apusher:/, 'pusher_')
   if event =~ /\Aclient-/
      msg['socket_id'] = connection.socket_id
      Channel.send_client_message msg
   elsif respond_to? event, true
      send event, msg

Since msg is the string that comes from the user’s end of the two-way communication, that gives us considerable control over this function. In particular, when trying to understand what Oj.load is supposed to do, this code started to look promising — from an attacker’s perspective.

Ruby unmarshalling

As it turns out, Oj (short for “Optimized JSON”) is a “fast JSON parser and Object marshaller as a Ruby gem.” Now that is interesting. From earlier experience, I knew that Ruby marshalling can lead to remote code execution vulnerabilities.

From online documentation, I learned that Oj allows serialization and deserialization of Ruby objects by default. For example, the following string is the result of an object of the class Sample::Doc being serialized with Oj.dump(sample).

{"^o":"Sample::Doc","title":"Sample","user":"ohler","create_time":{"^t":1371361533.272099000},"layers":{},"change_history":[{"^o":"Sample::Change","user":"ohler","time":{"^t":1371361533.272106000},"comment":"Changed at t+0."}],"props":{":purpose":"an example"}}

So presumably, passing a similarly serialized object via socket.send(...) should result in our input being “unmarshalled” by the underlying code in Slanger. All that now stands between an attacker’s input and remote code execution, is the availability of classes and objects that can be manipulated into executing system commands.

Since the Ruby gem appears to have a dependency on the Rails environment, I could build on earlier work by Charlie Somerville to construct a working payload that would lead to remote command execution in two different ways, one of which did not require knowing the app key. See the exploit in action in the video below.

Continue reading on the next page for an account of how I tried to responsibly disclose this bug.

A lot of coffee went into the writing of this article. If it helped you stay secure, please consider buying me a coffee, or invite me to your bug bounty program. :)

Buy me a coffeeBuy me a coffee

From blind XXE to root-level file read access

Polyphemus, by Johann Heinrich Wilhelm Tischbein, 1802 (Landesmuseum Oldenburg)

On a recent bug bounty adventure, I came across an XML endpoint that responded interestingly to attempted XXE exploitation. The endpoint was largely undocumented, and the only reference to it that I could find was an early 2016 post from a distraught developer in difficulties.

Below, I will outline the thought process that helped me make sense of what I encountered, and that in the end allowed me to elevate what seemed to be a medium-criticality vulnerability into a critical finding.

I will put deliberate emphasis on the various error messages that I encountered in the hope that it can point others in the right direction in the future.

Note that I have anonymised all endpoints and other identifiable information, as the vulnerability was reported as part of a private disclosure program, and the affected company does not want any information regarding their environment or this finding to be published.

Continue reading

Punicoder – discover domains that are phishing you

So we’re seeing homograph attacks again. Examples show how ‘apple.com’ and ‘epic.com’ can be mimicked by the use of Internationalized Domain Names (IDN) consisting entirely of unicode characters, i.e. xn--80ak6aa92e.com and xn--e1awd7f.com respectively.

As I found myself looking for ways to discover domain names that could be used for phishing attempts, I created a Python script called Punicoder to do the hard work for me. See the screenshot below for example output, and try it out for yourself here.

Punicoder output

Pro tip: use the following series of commands to find out if any of these domains resolve:

pieter@ubuntu:~$ python punicoder.py google.com | cut -d' ' -f2 | nslookup | grep -Pzo '(?s)Name:\s(.*?)Address: (.*?).Server'
Name: xn--oogle-qmc.com
Name: xn--gogl-0nd52e.com
Name: xn--gogl-1nd42e.com
Name: xn--oole-z7bc.com
Name: xn--goole-tmc.com
Name: xn--ggle-55da.com

Hack.lu 2015: Creative Cheating

Write-up of Hack.lu 2015’s Creative Cheating challenge.

The first challenge I solved on Hack.lu 2015, hosted by FluxFingers, was Creative Cheating.

The challenge

Mr. Miller suspects that some of his students are cheating in an automated computer test. He captured some traffic between crypto nerds Alice and Bob. It looks mostly like garbage but maybe you can figure something out. He knows that Alice’s RSA key is (n, e) = (0x53a121a11e36d7a84dde3f5d73cf, 0x10001) ( and Bob’s is (n, e) = (0x99122e61dc7bede74711185598c7, 0x10001) (

The solution

Upon inspection of the packet capture, we notice every packet from Alice ( to Bob ( contains a base64-encoded payload. E.g.


Continue reading

How to set up a Wifi captive portal


The objective of this Wifi captive portal is to mimic the behaviour of a legitimate access point protected by a portal login page for demonstrational purposes. That includes the following:

  • Broadcast a rogue access point
  • Mimic captive portal behaviour:
    • User gets to see a login page when trying to connect;
    • After logging in, the user can continue to access the network and surf freely.

Continue reading

CSRF Discoverer – A Chrome extension

Continue reading

HTTP Auth Phishing

HTTP Auth offers attackers easy phishing. This post describes how it is done and how the attacker could circumvent the constant reappearing of the authentication prompt.


An attacker could force an HTTP authentication pop-up window in the victim’s browser and log the input to a file on his own server. This post is the write-up of an idea I had a while ago. While investigating, I found out the idea had been coined before. Find some related posts below my findings.

This attack doesn’t require much work from the attacker. Let’s have a look at some of the configuration he needs.

Continue reading

Impressie van een Reis (6)

Derwent Hunter

Vijf grijze balkjes en een wit kruisje informeren me dat ik niet bereikbaar ben. Geladen met tientallen reizigers raast de bus over verlaten wegen naar het noorden. Links van me kleurt de ondergaande zon de hemel roze als was ze een artiest die aan het einde van haar act om nog een laatste applaus vraagt. Welverdiend, ze heeft de hele dag haar best gedaan. Mijn applaus schuilt in de glimlach die ik weinig gericht de wereld instuur.

Kilometers lange vers geplante bomen volgen elkaar netjes op in rijen en bieden een schouwspel van licht en schaduw aan wie het zien wil. Mijn stemming wordt bepaald door Dire Straits in mijn oren.

Uitkijkend over een golvend tapijt van diepblauw water leun ik tegen de metalen balustrade van de Derwent Hunter die zachtjes heen en weer wiegt. Het zilte water spat uiteen tegen de boeg en bereikt zo af en toe mijn gezicht dat de verkoeling verwelkomt. De zon is verhuld achter een versleten wolkengordijn dat door de gaten af en toe een opklaring toelaat.

Wanneer de hemelsluizen opengaan is het water overal en plakt mijn lichaam aan mijn tshirt. “Ain’t no sunshine when she’s gone,” zing ik tegen de wind die me niet kan horen. Ik kan ook mezelf niet horen maar ga ervan uit dat ik goed bezig ben en vervolg met een leugenachtige “it’s not warm when she’s away,” hoewel de verhulde zon nog steeds een aardige temperatuur garandeert.

In bed gelegen, twee weken voor mijn aankomst op Europese bodem, sta ik kort of lang stil bij de relativiteit van tijd. Vijf weken vlogen alvast om, die twee laatste zullen weldra ook weer haast verstreken zijn.