hack.lu CTF 2014 write-up: Objection

Posted on So, 2014-10-26 in ctf

So we got the source for something that looked like CoffeeScript, but had really strange string literals. After some investigation I found out, that this was in fact coco source code. Coco in turn is a fork of CoffeeScript. Both compile to JavaScript. The service allows you to login with the admin password that is read from a file. If the user is authenticated as admin, he can read the secret token using the get_token function. The following source file was provided.

const net = require \net
const BufferStream = require \bufferstream

admin_password = (require \fs).readFileSync \admin_password, \utf8

server = net.createServer (con) ->
  console.log 'client connected'
  con.write 'hello!\n'
  client_context =
    is_admin: false
    token: (require \fs).readFileSync \secret_token, \utf8
    login: ([password], cb) ->
      if password == admin_password
        cb "Authentication successful  " + password
        @is_admin = true
        cb "Authentication failed  " + password
    get_token: ([], cb) ->
      if not @is_admin then return cb "You are not authorized to perform this action."
      cb "The current token is #{@token}"
  console.log client_context
  in_stream = new BufferStream {encoding:\utf8, size:\flexible}
  con.pipe in_stream
  <- in_stream.split \\n
  it .= toString \utf8
  console.log "got line: #{it}"
  [funcname, ...args] = it.split ' '
  if typeof client_context[funcname] != \function
    return con.write "error: unknown function #funcname\n"
  client_context[funcname] args, ->
    con.write "#it\n"

server.listen 1408, ->
  console.log 'server bound'

The whole thing is a kind of sandbox escape, since the command are implemented by calling methods on the 'client_context' object. The input string is split on the space character and the first token is the function name. So we can actually call an arbitrary function on the object. All further tokens are passed as a string array. So we cannot inject code or something like that. But wait, let's look at how the the methods of the object are called exactly.

client_context[funcname] args, ->
  con.write "#it\n"

First the function is accessed via the name as a string. args is an array of strings. Then a callback function is passed as the last argument. The '->' arrow is CoffeeScript/coco shorthand for defining a function. The '#it' in the string is syntax for string expansion using the 'it' variable. Because this variable isn't declared anywhere in the function body coco infers that it must be an argument to the function. As you can see in this example:

$ coco -bce '-> console.log "#it\n"'
  return console.log(it + "\n");

Now to the interesting part: how to get the flag. Let's have a look what functions a plain object in JavaScript has. So let's fire up a node.js interpreter and poke around a little bit by hitting tab.

> x = {}
> x.
x.__defineGetter__      x.__defineSetter__      x.__lookupGetter__      x.__lookupSetter__
x.constructor           x.hasOwnProperty        x.isPrototypeOf         x.propertyIsEnumerable
x.toLocaleString        x.toString              x.valueOf

__defineGetter__ seems useful, since the properties 'is_admin' and 'token' are accessed (you can spot properties in CoffeeScript and coco by the @ prefix). It expects the following arguments

obj.__defineGetter__(sprop, fun)

Where sprop is the property name and fun is the getter function. This matches exactly the call of the function in the source file we got. So we can pass the anonymous callback function as a getter. Since this function takes an argument 'in' as parameter, calling the function without any argument results in 'it' just being undefined. Concatenating an undefined variable with a string results in the string "undefined" + the other string. So let's define the getter for 'is_admin' by sending the following string to the service.

__defineGetter__ is_admin

Now if is_admin is accessed, instead the anonymous callback function is called. Remember it looks like this in CoffeeScript

-> con.write "#it\n"

In CoffeeScript/coco the last expression is also the return value of a function. The anonymous function compiled to JavaScript looks like this:

    return con.write(it + "\n");

This means this anonymous function will return the return value of the 'write' method of the con object, which is an socket object. The socket.write function will return true if the entire data was flushed to the kernel buffer. Since we only write the string "undefinedn", chances are pretty good this will happen. So accessing the 'is_admin' property will in fact call this anonymous callback function and return 'true'. On the way it will write "undefinedn", but this doesn't matter since we are suddenly admin :)

__defineGetter__ is_admin
The current token is flag{real_cowboys_dont_use_object_create_null}

Hooray :)