HCS (Heroes Cyber Security) as official cybersecurity team of Institut Teknologi Sepuluh Nopember has participated on SwampCTF 2024.
We managed to 3rd place out of 362 teams, thank you for @daffainfo, @kerupuksambel, and @iktaS who's had participate with me to solve the challenges.
My potions would kill you, traveler. You cannot handle my potions.
Broken logic at the source code, whatever ammount when repay the debt make our full dept is gone and able to get the flag.
Source code.
const express = require('express');
const session = require('express-session');
require('dotenv').config();
const app = express();
const port = 3000;
const FLAG = process.env.FLAG;
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
}));
// Middleware to check if user session exists
const checkUserSession = (req, res, next) => {
if (!req.session.user) {
req.session.user = {
debtAmount: 0,
gold: 0,
swampShade: false,
loanPending: false
};
}
next();
};
app.use(checkUserSession);
function verifyAmount(gold) {
gold = parseInt(gold);
if (isNaN(gold) || gold < 1) {
return false;
}
return true;
}
// Map Potion ID to Name and Price
const potions = [
{ name: "Essence of the Abyss ⚗️", price: 10 },
{ name: "Potion of Astral Alignment ⚗️", price: 2 },
{ name: "Elixir of the Enigma ⚗️", price: 34 },
{ name: "Stardust Elixir 🧪", price: 11 },
{ name: "Swampshade Serum ⚗️", price: 100 },
{ name: "Phoenix Tears Potion ⚗️", price: 65 },
];
app.get('/stats', (req, res) => {
// Show current stats
res.json({
debtAmount: req.session.user.debtAmount,
gold: req.session.user.gold,
swampShade: req.session.user.swampShade,
loanPending: req.session.user.loanPending
});
});
app.get('/checkout', (req, res) => {
// Check if user has a pending loan
if (req.session.user.loanPending) {
return res.json({ message: "Ermm you still have a debt 🤓" });
}
// Set the loan to pending
if (req.session.user.swampShade) {
return res.json({ message: "You are worthy: " + FLAG });
}
else {
return res.json({ message: "You don't possess the SwampShade potion!" });
}
});
// Example request: http://localhost:3000/borrow?amount=1000
app.get('/borrow', (req, res) => {
let amount = req.query.amount;
// Check if request is a number
if (!verifyAmount(amount)) {
return res.json({ message: "Invalid amount" });
}
if (req.session.user.loanPending) {
return res.json({ message: "Repay your loan first!" });
} else {
// Set the loan to pending
req.session.user.loanPending = true;
req.session.user.debtAmount = Number(amount);
req.session.user.gold = Number(amount);
return res.json({ message: "You have successfully borrowed gold 🪙" });
}
});
// Example request: http://localhost:3000/buy?id=1
app.get('/buy', (req, res) => {
potionID = req.query.id;
// Check if potion ID is valid
if (!potionID || !potions[potionID]) {
return res.json({ message: "Invalid potion ID" });
}
// Buy the potion
if (req.session.user.gold < potions[potionID].price) {
return res.json({ message: "Not enough gold" });
}
// Update user's stats
req.session.user.gold -= potions[potionID].price;
// SwampShade Serum
if (potionID == 4) {
req.session.user.swampShade = true;
}
return res.json({ message: "Potion acquired" });
});
// Example request: http://localhost:3000/repay?amount=1000
app.get('/repay', checkUserSession, (req, res) => {
// Get the amount to be repayed
let amount = req.query.amount;
// Check if user has a pending loan
if (!req.session.user.loanPending) {
return res.json({ message: "You do not have any debts" });
}
// Check if request is a number
if (!verifyAmount(amount)) {
return res.json({ message: "Invalid amount" });
}
// If the amount is a number, check if it's enough to repay the loan
if (req.session.user.gold < Number(amount)) {
return res.json({ message: "You don't have that much money" });
}
// Check if the amount is enough to repay the loan
if (req.session.user.debtAmount <= Number(amount)) {
return res.json({ message: "This is not enough to repay the debt" });
}
// Repay the loan
req.session.user.gold = 0;
req.session.user.debtAmount = 0;
req.session.user.loanPending = false;
return res.json({ message: "✨ Debt Repaid ✨" });
});
app.get('/', (req, res) => {
res.json({ message: "My potions are too expensive for you, traveler! 🧙" });
});
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
To get the flag we need to set req.session.user.swampShade
to true
But before that because our account is doesn't have balance, we can borrow according the function at line 75
.
After borrowed some balance, you can buy the item that can make set req.session.user.swampShade
to true by according at the line 110
.
if (potionID == 4) {
req.session.user.swampShade = true;
}
Now, this time we need make out debt balance to zero.
According to the source code at the line 144
, we can repay debt by any value so it's will make our debt to zero.
req.session.user.gold = 0;
req.session.user.debtAmount = 0;
req.session.user.loanPending = false;
Now we can checkout the flag.
swampCTF{Y0u_c4nt_h4ndl3_my_str0ng3st_p0t10ns!}
In order to help our university comply with The Americans with Disabilities Act we made an ASCII to braille webservice, now we just hope the faculty doesn't print out their braille on flat pieces of paper…
SQL Injection at the search column for get the flag.
Visit the website.
Try to inject the search column.
It's look vulnerable to SQL Injection, so we try inject more but it's only had a 1
order.
Then we find the table that contains flag with this payload.
' union select (select table_name from information_schema.tables limit 1 offset 2)-- -
Get the table name, after that we try to extract column name but the result is flag
which is we can extract it with SELECT flag from flag
.
Then our final payload is like this.
' union (select flag from flag)-- -
swampCTF{Un10n_A11_Th3_W4yyy!}
All the existing meme generators leave stupid watermarks so we wrote our own!
SSRF and bypass the blacklist filter to read the flag.
Simply this web can generate a image with a text, but after several test the generate form it's vulnerable to anything.
And we concern there anothe form, it's a Load Image.
Injected the form.
Well according to my conclusion this can be a SSRF, and since the debug mode
of website is ON.
We can leak the source by change the value of imageURL to true
.
Bypassing the filter using hex, a -> %61
.
Then final payload like this
http://loc%61lhost:5000/%61dmin
.
swampCTF{SSRF_15_n0_Jok3!!1}
Under Construction
Chaining SQLi into oufile with LFI for get the flag.
Get a website like this.
Since the website was using parameter ?page=
, this make me immediately to test LFI.
Look that is vulnerable to LFI, but we found another bug at the login page.
The login page was vulnerable to SQLi, at my conclusion we need to chaining the SQLi with LFI.
How? there a method name SQLi into oufile
which is we can spawn a backdoor, then we try payload like this.
admin' union select 1,"<?php system(\"ls\"); ?>",3 into outfile "/tmp/bbb.txt"-- -&password=admin
After that access it with LFI method.
We found the flag file, and read it.
swampCTF{ch4ining_Vu1ns_I5_Pr3tty_C0Ol}
The university told us that our last ASCII to Braille converter was vulnerable. No worries, we fixed it and now it is unhackable!
SQLi on the feedback form.
With same environment we try to inject the search column again.
Look's like it has been fixed, after that we notice there a one more form.
Since the form was only accepted Braille
encoding, so we need to convert it first.
The output.
Then we try some postgre SQLi
from PayloadAllTheThings.
We managed find the correct payload.
a'),(cast((SELECT table_name FROM information_schema.tables LIMIT 1 OFFSET 2) as int));-- -
Table name is flag
, had a feeling this was same pattern with BrailleDB-1
.
So, we created the final payload like this.
a'),(cast((SELECT flag FROM flag) as int));-- -
swampCTF{S33ing_0utput_1s_0v3rrat3d}