Sploitus

Exploit for CVE-2026-47630

githubexploit Β· 2026-08-22

Exploit Code

README202 lines
## https://sploitus.com/exploit?id=50E23042-8B80-509D-AEFB-6AB23B62C158
# CVE-2026-47630 β€” NVIDIA Triton Inference Server: arbitrary `dlopen` via `TRITON_BATCH_STRATEGY_PATH`

Absolute path traversal in the custom batching-strategy loader of
`triton-inference-server/core`. A model configuration parameter is passed
unvalidated to `dlopen(RTLD_NOW | RTLD_LOCAL)`, so anyone able to influence a
model's `config.pbtxt` obtains native code execution inside the Triton server
process at model-load time.

| | |
|---|---|
| CVE | [CVE-2026-47630](https://nvd.nist.gov/vuln/detail/CVE-2026-47630) |
| NVIDIA bulletin | [5865](https://github.com/NVIDIA/product-security/blob/main/2026/5865/5865.md) (2026-08-18) |
| CWE | CWE-36 (Absolute Path Traversal); reported as CWE-114 / CWE-829 |
| CVSS v3.1 (NVIDIA) | 5.5 MEDIUM β€” `AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N` |
| Affected | Triton Inference Server 0.0 – 26.05 (Linux) |
| Fixed in | 26.06 |
| Reported by | s1ko ([github.com/s1ko](https://github.com/s1ko), `s1ko@riseup.net`) |
| Vendor tracking | NVIDIA PSIRT ticket 6139742 |

NVIDIA's acknowledgement in bulletin 5865 reads verbatim: `CVE-2026-47630: s1ko`.

## Summary

Triton accepts a per-model `TRITON_BATCH_STRATEGY_PATH` parameter from the
model's `config.pbtxt`. Through 26.05 the value was treated as an arbitrary
filesystem path and handed unmodified to `dlopen`. There was no containment
check, no allowlist, and no signature or hash verification. Absolute paths,
traversal sequences and symlinks were all accepted.

`RTLD_NOW` resolves every symbol immediately and runs the object's
`__attribute__((constructor))` / `.init_array` routines *before* `dlopen`
returns. Execution is therefore unconditional on a successful load β€” none of
the `TRITONBACKEND_ModelBatch*` entry points need to exist for the payload to
run, and it runs with the privileges of the Triton process (frequently `root`
in NGC containers).

## Execution

### Code path (as of 26.05)

`src/backend_model.cc` β€” attacker input reaches `batch_libpath`, validated only
for existence:

```cpp
if (model_config.parameters().contains("TRITON_BATCH_STRATEGY_PATH")) {
  batch_libpath = model_config.parameters()
                      .at("TRITON_BATCH_STRATEGY_PATH")
                      .string_value();
  bool exists = false;
  RETURN_IF_ERROR(FileExists(batch_libpath, &exists));
  if (!exists) {
    return Status(
        triton::common::Error::Code::NOT_FOUND,
        ("Batching library path not found: " + batch_libpath).c_str());
  }
}
```

Notably absent is any call to `IsChildPathEscapingParentPath`, which Triton
already applied to label paths and backend library paths elsewhere in the same
file.

`src/backend_model.cc` β€” `SetBatchingStrategy` forwards it:

```cpp
Status TritonModel::SetBatchingStrategy(const std::string& batch_libpath)
{
  std::unique_ptr slib;
  RETURN_IF_ERROR(SharedLibrary::Acquire(&slib));
  RETURN_IF_ERROR(slib->OpenLibraryHandle(batch_libpath, &batch_dlhandle_));
```

`src/shared_library.cc` β€” the sink:

```cpp
*handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
```

### Reachability

Three practical routes to controlling `model_config.parameters()`:

1. **Multi-tenant model repository.** A tenant who owns their model's
   `config.pbtxt` plants a `.so` in their own directory and points the
   parameter at it. The payload runs with the server's privileges, giving
   access to every other tenant's weights, secrets and GPU.
2. **File-override API.** `TRITONSERVER_ServerLoadModelWithParameters`
   combined with the documented `file:` parameter prefix lets a
   caller with model-load rights submit `config.pbtxt` and the `.so` in a
   single request and trigger the load immediately. No filesystem access
   outside the API is required.
3. **Compromised model-store mirror.** S3, GCS, Azure Blob or NGC mirrors
   treated as authoritative for model artifacts. Whoever controls the mirror
   delivers both files; Triton fetches and `dlopen`s on demand.

### Proof of concept

[`poc/`](poc/) contains the primitive, reduced to what `OpenLibraryHandle`
does. Sources only β€” build them yourself:

```
$ cd poc && ./build.sh
$ gcc -O0 -o test_dlopen test_dlopen.c -ldl
$ ./test_dlopen ./evil.so
dlopen OK, handle=0x55d1779652c0
$ cat /tmp/triton_dlopen_rce_proof.log
[triton-dlopen-rce] constructor fired @ Fri May  1 09:56:56 2026
  pid=240157  uid=1000  euid=1000  cwd=/opt/poc
```

The payload is inert: it appends one line recording pid, uid, euid and cwd.
Verified on Debian 13, GCC 14.2, x86_64.

For the Triton-side trigger, place the built `evil.so` next to
[`poc/config.pbtxt`](poc/config.pbtxt) in the model repository and load the
model β€” auto-load, `--load-model`, or
`POST /v2/repository/models/evil_model/load`. On an affected version the
constructor fires during `SetBatchingStrategy`, before any backend symbol
lookup.

## Detection

The load is logged by Triton itself at `INFO`:

```
Loading custom batching strategy library  for model 
```

On a fixed build, a rejected attempt surfaces as
`Batching library path escapes model repository.`

Any `TRITON_BATCH_STRATEGY_PATH` value that is absolute, contains `..`, or
resolves outside the model directory is worth alerting on regardless of
version. A Sigma rule covering both the log line and the config parameter is
in [`detection/`](detection/).

Complementary signals:

- `config.pbtxt` files carrying a `TRITON_BATCH_STRATEGY_PATH` parameter at
  all β€” the feature is rare in practice, so presence alone is a useful filter.
- Shared objects appearing inside a model repository that are not backend
  artifacts.
- `openat`/`mmap` of a `.so` outside the model root by the `tritonserver`
  process (auditd, eBPF, or Falco).

## Mitigation

**Upgrade to Triton Inference Server 26.06 or later.** The fix adds the
containment check the loader was missing:

```cpp
bool escapes{true};
RETURN_IF_ERROR(IsChildPathEscapingParentPath(
    batch_libpath, localized_model_dir->Path(), &escapes));
if (escapes) {
  return Status(
      Status::Code::INVALID_ARG,
      "Batching library path escapes model repository.");
}
```

Where upgrading is not immediately possible, compensating controls:

- Reject or strip `TRITON_BATCH_STRATEGY_PATH` from every `config.pbtxt`
  before it reaches the model repository.
- Treat the model repository as a trust boundary: no untrusted party writes
  to it, and remote mirrors are integrity-verified before sync.
- Disable the file-override load path (`--model-control-mode` other than
  `explicit`, or authorization on the repository endpoints) unless it is
  required.
- Run `tritonserver` as an unprivileged user with a read-only model
  repository mount, so a successful load has the smallest possible blast
  radius.
- Verify a hash or signature over model artifacts prior to load.

Mapping: MITRE ATT&CK [T1574.006 Hijack Execution Flow: Dynamic Linker
Hijacking](https://attack.mitre.org/techniques/T1574/006/) and
[T1129 Shared Modules](https://attack.mitre.org/techniques/T1129/);
NIST SP 800-53r5 `SI-7`, `CM-5`, `SC-18`; CIS Controls v8 Β§2, Β§4.

## Timeline

| Date | Event |
|---|---|
| 2026-04-25 | Primary Triton path-handling finding reported; NVIDIA PSIRT opens ticket 6128799 |
| 2026-05-01 | This secondary finding confirmed and reported; PSIRT opens ticket 6139742 |
| 2026-05-28 | Coordinated-disclosure cadence agreed with PSIRT |
| 2026-06 | Fix ships in Triton Inference Server 26.06 |
| 2026-08-18 | NVIDIA publishes bulletin 5865, assigning CVE-2026-47630 and crediting s1ko |
| 2026-08-22 | This write-up published |

## References

- NVIDIA security bulletin 5865 β€” https://github.com/NVIDIA/product-security/blob/main/2026/5865/5865.md
- CVE record β€” https://github.com/NVIDIA/product-security/blob/main/2026/5865/CVE-2026-47630.json
- `triton-inference-server/core` β€” https://github.com/triton-inference-server/core
- NVIDIA Product Security β€” https://www.nvidia.com/security/

## License

MIT β€” see [LICENSE](LICENSE).