in websec

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\",
        \"blogUrl\":\"http://blog.pusher.com\"
      }
    }"
  }
}

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:

Request

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

Response

{"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-1.0.9.1/lib/eventmachine.rb:193:in `run_machine'\\n/usr/local/rvm/gems/ruby-2.3.0/gems/eventmachine-1.0.9.1/lib/eventmachine.rb:193:in `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
end

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

Leave a Reply for Pieter Cancel Reply

Write a Comment

Comment