{"slug": "cve-2026-106173-fortisiem-grpc-path-traversal-rce", "title": "CVE-2026-106173: FortiSIEM gRPC Path Traversal RCE", "summary": "Fortinet published advisory FG-IR-26-180 on 13 October 2026 for CVE-2026-106173, a critical path traversal in FortiSIEM's phQueryMaster gRPC service on tcp/27918 that allows unauthenticated attackers to write arbitrary files and achieve remote code execution. Horizon3 researchers found the service accepts connections without client certificates and that the SendQueryResult handler builds a file path from an unvalidated query-id, so a value like ../../../../../tmp/x escapes the query directory, with the streamed bytes becoming the file's contents. The flaw sits in a component Horizon3 had not previously examined, distinct from the phMonitor service on tcp/7900 behind its earlier FortiSIEM disclosures CVE-2023-34992, CVE-2024-23108 and CVE-2025-64155.", "body_md": "On 13 October 2026, Fortinet published a security [advisory](https://fortiguard.fortinet.com/psirt/FG-IR-26-180) for a critical vulnerability affecting their FortiSIEM product. The vulnerability, CVE-2026-106173, allows for an arbitrary file write by abusing an issue in a gRPC service.\n\nHorizon3 is no stranger to the FortiSIEM. We’ve disclosed several critical vulnerabilities through the years that you can catch up on:\n\n1. [CVE-2023-34992: Fortinet FortiSIEM Command Injection Deep-Dive](https://horizon3.ai/attack-research/disclosures/cve-2023-34992-fortinet-fortisiem-command-injection-deep-dive/)\n2. [CVE-2024-23108: Fortinet FortiSIEM 2nd Order Command Injection Deep-Dive](https://horizon3.ai/attack-research/disclosures/cve-2024-23108-fortinet-fortisiem-2nd-order-command-injection-deep-dive/)\n3. [CVE-2025-64155: Three Years of Remotely Rooting the Fortinet FortiSIEM](https://horizon3.ai/attack-research/disclosures/cve-2025-64155-three-years-of-remotely-rooting-the-fortinet-fortisiem/)\n\nAll of our prior research has involved the same component, the phMonitor service which listens on port tcp/7900. This latest advisory details a new service that we had never looked at, a gRPC service on port tcp/27918. Given our familiarity and the intrigue of a new service to look at we dove in.\n\n## **Reaching the Vulnerable Service**\n\nLooking at the listening services on the FortiSIEM, we find that phQueryMaster is bound to tcp/27918. The phQueryMaster service stands the gRPC server up in `grpcNotificationServer::run()`. The listen port comes from the command_port config value (7918), which is passed through `phSecureUtils::getGrpcPort()` and mapped to tcp/27918; the address is built as `[::]:27918`, so the service binds every interface.\n\n```\n// grpcNotificationServer::run()  (libphGrpcServices.so)\nunsigned short port = mServerPort;                                 // command_port = 7918\nstd::string addr = \"[::]:\" + phSecureUtils::getGrpcPort(port);     // [::]:27918\ngrpc::SslServerCredentialsOptions sslOps;\n// sslOps.client_certificate_request stays 0 == GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE\nauto creds = grpc::SslServerCredentials(sslOps);               \nbuilder.AddListeningPort(addr, creds, 0);                    \nbuilder.RegisterService(&svc);\nbuilder.BuildAndStart();\n```\n\nSometimes, gRPC servers can require client certificates for authentication, but here there server sets that option to false. This means any remote client can connect to the gRPC service on port tcp/27918 unauthenticated and without presenting a client certificate to reach the vulnerable gRPC service.\n\n## **The Path to Arbitrary File Write**\n\nNow that we’ve found the vulnerable gRPC service, we inspect the available RPC methods and find `SendQueryResult`.\n\n```\nmessage QueryResultReq { bytes file_content = 1; }\nservice phoenixSvc { rpc SendQueryResult(stream QueryResultReq) returns (PhoenixReply); }\n```\n\n`SendQueryResult`’s handler parses four values from the received message: `query-id`, `src-ip`, `period-start`, and `period-end`. The handler then constructs a file path with the lightly formatted `src-ip` and query-id.\n\n```\n// grpcNotificationService::SendQueryResult  (grpcNotificationService.cpp)\nreplaceString(srcIp, \".\", \"_\");                         \nstd::string fileDir = \"/query/result/\" + queryId;       \nif (!phMiscUtils::DirExists(fileDir.c_str()))           \n    phMiscUtils::mkdir(fileDir.c_str());\n```\n\nBug #1 is that `fileDir` is built by string concatenation with an attacker controlled value, without any input validation. A `query-id` of `../../../../../tmp/x`  allows an attacker to traverse outside of the query directory. So an attacker has arbitrary directory control.\n\nThe filename itself is also partially attacker controlled, `result-<srcIp>-0-0-<id>.dat`, and the streamed bytes that follow will be the files contents.\n\n```\nstd::stringstream path;\npath << fileDir << \"/result-\" << srcIp << \"-\"\n     << periodStart << \"-\" << periodEnd << \"-\"\n     << mGetQueryResultUniqId() << \".dat\";              \nstd::ofstream output;\noutput.open(path.str().c_str(), ios::binary|ios::out|ios::trunc);   \nwhile (reader->Read(&req))                              \n    output.write(req.file_content().data(), req.file_content().size());\n```\n\nSo ultimately, an unauthenticated attacker can write arbitrary data into an arbitrary directory, but with a limited filename. The minimal request is one gRPC `SendQueryResult` with this metadata:\n\n```\nquery-id:     ../../../../../tmp/fortisiem-cve-2026-106173\nsrc-ip:       127.0.0.1\nperiod-start: 0\nperiod-end:   0\n```\n\nwhich creates `/tmp/fortisiem-cve-2026-106173/result-127_0_0_1-0-0-<id>.dat` containing the streamed bytes.\n\n## **Exact Filename Control**\n\nThe path traversal lets an attacker choose the directory a file lands in, but not its exact name: the server always tacks on its own ending, `result-<src>-<start>-<end>-<id>.dat`, where `<id>` is a counter the server controls and the request can’t set. On its own, your stuck with a limited file write that isn’t so useful.\n\nThat turns out not to matter, because of how the server copies the header values. It copies each value using the exact length the client declares, which lets an attacker embed a null byte (`\\0`) in the middle of the query-id. For example: `.../some/target\\0ignored`. When the server later hands that path to the operating system to open the file, the OS treats the null byte as the end of the string and silently discards everything after it, including the server’s own result-…-.dat ending. The write lands on the exact path that preceded the null byte. Pointed at an existing file this overwrites it in place, keeping the original owner and permissions; a second variant uses two requests to create a brand-new file with an exact name. Because normal tools like curl and standard gRPC libraries restrict putting null bytes in headers, we must manually assemble the gRPC message to include our null byte.\n\nOverwriting an existing file in place takes a single request whose metadata is:\n\n```\nquery-id:     ../../../../../opt/charting/redishb.sh\\0ignored\nsrc-ip:       127.0.0.1\nperiod-start: 0\nperiod-end:   0\n```\n\n## **The Path to Code Execution**\n\nAs with previous vulnerabilities we’ve reported that enable arbitrary file write, we’re looking for files on the filesystem that allow us to gain code execution by overwriting them. There’s a multitude of services that ingest files through cron jobs on the FortiSIEM. One file that is recurrently executed to perform health checks on the redis service is `/opt/charting/redishb.sh`. This script is invoked every 60 seconds by a cron job and is writable by the admin user context we have.\n\nPutting it all together, to gain code execution we:\n\n1. Traverse the directory path\n2. Control the filename\n3. Overwrite a recurringly executed script\n\nOur proof of concept can be found on our [GitHub](https://github.com/horizon3ai/CVE-2026-106173).\n\n## **Indicators of Compromise**\n\nWe did not find any application logs that would explicitly reveal that this vulnerability was exploited. However, if the vulnerability is used to overwrite an existing script, one should carefully inspect the file write or modified timestamps, and inspect any that appear to not match the surrounding file timestamps.\n\nFiles of particular interest are:\n\n1. `/opt/charting/redishb.sh`\n2. `/opt/phoenix/bin/phLicenseTool`", "url": "https://wpnews.pro/news/cve-2026-106173-fortisiem-grpc-path-traversal-rce", "canonical_source": "https://horizon3.ai/attack-research/cve-2026-106173-deep-dive-fortisiem-grpc-path-traversal-file-write-rce/", "published_at": "2026-10-21 07:09:18+00:00", "updated_at": "2026-10-21 07:41:14.382228+00:00", "lang": "en", "topics": ["ai-safety"], "entities": ["Fortinet", "FortiSIEM", "Horizon3", "CVE-2026-106173", "phQueryMaster", "phMonitor", "FG-IR-26-180"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/cve-2026-106173-fortisiem-grpc-path-traversal-rce", "markdown": "https://wpnews.pro/news/cve-2026-106173-fortisiem-grpc-path-traversal-rce.md", "text": "https://wpnews.pro/news/cve-2026-106173-fortisiem-grpc-path-traversal-rce.txt", "jsonld": "https://wpnews.pro/news/cve-2026-106173-fortisiem-grpc-path-traversal-rce.jsonld"}}