Stripe CTF: Level #5

Posted on sam. 27 octobre 2012 in Write-up


You can find the code for this level here.

(sha256: 82b066cca46fd24a16959ada973d6df0d7c693f7791a8b673add276f324a5885)

This level wants to solve a real problem: identification. We have too many online accounts and we have to remember usernames/passwords for everyone of them. It would be way simpler to be able to log into a new web service using your Google account, or your Facebook account (kind of like OpenID). That's what this level is all about.

This service asks for a pingback address (it's the service you want to use to identify, like using Google or Facebook with OpenID), and your username/password to this pingback. The form will then send your credentials to the pingback and see if you're successfully authenticated.

# File srv.rb, line 20
PASSWORD_HOSTS = /^level05-\d+\.stripe-ctf\.com$/ # To get the password, the pingback must follow this regex
ALLOWED_HOSTS = /\.stripe-ctf\.com$/ # The pingback must follow this regex

Note that these regex were for the real CTF. For a local usage, here are what they look like:

# File srv.rb, line 23
PASSWORD_HOSTS = /^localhost$/ # To get the password, the pingback must follow this regex
ALLOWED_HOSTS = // # No restriction on the allowed hosts

We can only use pingback URL ending in, but fortunately, we still have access to the social network on level #2! We can upload a PHP file, which will always say the authentication is successful.

Note: on the next screenshots, I'll use as the address for level #2, and localhost as the address for level #5!

So, how does the service know that we were successfully authenticated to the pingback?

# File srv.rb, line 109
def authenticated?(body)
    body =~ /[^\w]AUTHENTICATED[^\w]*$/

So, all we have to do is upload the following file to level #2:

    echo " AUTHENTICATED \n";

Let's fill the form to use level02-[numbers] as a pingback:


We submit, and here are the result of the authentication...


...and the new login page:


Okay, now we can authenticate using this script, but we can't recover the password, cause the URL is level02-[numbers], and not level05-[numbers] The key is to see how the server recovers the pingback URL we give him:

# File srv.rb, line 67
pingback = params[:pingback]
username = params[:username]
password = params[:password]
# File srv.rb, line 80
body = perform_authenticate(pingback, username, password)
# File srv.rb, line 99
def perform_authenticate(url, username, password)
    $"Sending request to #{url}")
    response =, {:password => password,
                                     :username => username})
    body = response.body

    $"Server responded with: #{body}")

The server uses params to recover the informations sent by the form. Then it POST s the username and the password to the to the pingback URL. But params is the Ruby equivalent of $_REQUEST in PHP, which recovers the informations sent by POST, but also by GET. So let's say we put this as a pingback URL: http://leve05-[numbers][numbers]

Okay, here is where it gets tricky: the server retrieves the previous URL as a pingback. It then posts our username/password to it, i.e. to itself, since the address is level05-[numbers] So the server finds itself with a username/password by POST, and a pingback (the level #2 URL) by GET. So it does its business: sends the username/password to the pingback.

First we fill in the login:


Then we submit:


We clearly see here that the server was interrogated twice.

And we just have to go back to the login page: