Back to Blog
critical SEVERITY8 min read

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

A critical vulnerability in Tabby's UAC component allowed local attackers to predict Windows named pipe names generated with a weak pseudo-random number generator, enabling them to hijack privileged communication channels before the elevated process could claim them. This pipe squatting attack could grant unprivileged users the ability to inject commands that execute with Administrator or SYSTEM privileges. The fix removes the unsafe exec() pattern and replaces the predictable pipe naming scheme

O
By orbisai0security
May 8, 2026
#security#windows#privilege-escalation#named-pipes#cpp#uac#random-number-generation

Critical UAC Pipe Hijacking: When rand() Opens the Door to SYSTEM

Introduction

There's a deceptively simple class of vulnerability that has haunted Windows security for decades: named pipe squatting. It looks harmless at first glance — just a string, just a random number — but when that string becomes the key to a privileged communication channel, "good enough" randomness becomes a critical security failure.

This post breaks down a recently patched critical severity vulnerability in the UAC (User Account Control) component of Tabby, a popular open-source terminal application. The vulnerability allowed a local attacker to predict the name of a Windows named pipe used for elevated inter-process communication, hijack that pipe before the privileged process could claim it, and inject arbitrary commands that would execute with Administrator or SYSTEM privileges.

If you write any code that:
- Generates "random" identifiers for security-sensitive resources
- Uses Windows named pipes for IPC (Inter-Process Communication)
- Bridges privilege boundaries in your application

...this post is required reading.


The Vulnerability Explained

Background: What Is a Named Pipe?

Windows named pipes are a form of inter-process communication (IPC). They work like a two-way street between processes — one process creates the pipe (the server), and another connects to it (the client). Named pipes live in the Windows object namespace under \\.\pipe\<name>.

In Tabby's UAC flow, the application needs to spin up an elevated subprocess (e.g., for terminal operations requiring admin rights) and communicate with it. The mechanism chosen was a named pipe with a dynamically generated name in the format:

\\.\pipe\uac-pty-<N>

Where <N> is a number generated by rand().

The Fatal Flaw: rand() Is Not Random Enough

Here's the core of the problem, in simplified pseudocode resembling the vulnerable code in UAC.cpp:

// VULNERABLE CODE (simplified)
#include <cstdlib>
#include <ctime>

std::string GeneratePipeName() {
    srand(time(NULL));  // Seeded with current time
    int pipeId = rand();
    return "\\\\.\\pipe\\uac-pty-" + std::to_string(pipeId);
}

This looks reasonable at a glance. But it has two fatal problems:

  1. rand() is a pseudo-random number generator (PRNG), not a cryptographically secure RNG (CSPRNG). Its output is deterministic and predictable given the seed.

  2. time(NULL) returns the current Unix timestamp in seconds. An attacker on the same machine knows roughly what time it is. With only ~1 second of resolution, the attacker needs to try at most a few thousand values to enumerate every pipe name that could have been generated in a plausible time window.

How the Attack Works: Pipe Squatting

Windows named pipes have a critical security property: the first process to create a pipe with a given name wins. If an attacker creates \\.\pipe\uac-pty-12345 before the legitimate elevated process does, the attacker is now the server.

Here's the attack flow, step by step:

Timeline:
─────────────────────────────────────────────────────────────────
  T=0    User triggers a UAC elevation in Tabby
  T=1    Tabby's unprivileged process calls GeneratePipeName()
         → Produces "uac-pty-1693847200" (based on time seed)
  T=2    ⚠️  ATTACKER pre-creates \\.\pipe\uac-pty-1693847200
         (attacker predicted the name by brute-forcing time seeds)
  T=3    Tabby's ELEVATED (Admin) process tries to create the pipe
         → Fails silently or connects to the attacker's pipe instead
  T=4    Elevated process sends privileged commands down the pipe
         → Attacker receives and responds with malicious data
  T=5    Attacker injects commands → executed as ADMINISTRATOR 🔥
─────────────────────────────────────────────────────────────────

Real-World Impact

This is a local privilege escalation (LPE) vulnerability. An attacker needs:
- A local account on the machine (even a low-privilege guest or standard user)
- The ability to run a process that creates named pipes (standard on Windows)
- Timing awareness (trivially achievable)

The payoff? Full Administrator or SYSTEM code execution. On a shared workstation, a corporate endpoint, or a developer machine, this is game over. Sensitive files, credentials, registry keys, other users' sessions — all exposed.

CVE Equivalents to Reference:
- This attack pattern is well-documented under CWE-330: Use of Insufficiently Random Values
- The pipe squatting technique relates to CWE-377: Insecure Temporary File (same race-condition primitive)
- Privilege escalation via IPC abuse: MITRE ATT&CK T1559.001


The Fix

What Changed

The PR titled "fix: remove unsafe exec() in UAC.cpp" addresses the vulnerability at its root by:

  1. Removing the rand()-based pipe name generation
  2. Replacing it with a cryptographically secure random identifier
  3. Eliminating the unsafe exec() call pattern that compounded the risk

The Secure Approach

The fix replaces the predictable PRNG-based naming with a cryptographically secure random number generator. On Windows, the correct API is BCryptGenRandom (or its predecessor CryptGenRandom):

// SECURE CODE - After the fix
#include <bcrypt.h>
#include <string>
#include <iomanip>
#include <sstream>

std::string GenerateSecurePipeName() {
    unsigned char randomBytes[16];  // 128 bits of entropy

    NTSTATUS status = BCryptGenRandom(
        NULL,
        randomBytes,
        sizeof(randomBytes),
        BCRYPT_USE_SYSTEM_PREFERRED_RNG
    );

    if (!BCRYPT_SUCCESS(status)) {
        throw std::runtime_error("Failed to generate secure random bytes");
    }

    // Convert to hex string
    std::ostringstream oss;
    oss << "\\\\.\\pipe\\uac-pty-";
    for (int i = 0; i < sizeof(randomBytes); i++) {
        oss << std::hex << std::setw(2) << std::setfill('0') 
            << (int)randomBytes[i];
    }

    return oss.str();
    // Result: \\.\pipe\uac-pty-a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5
}

Why This Works

Property rand() + time() BCryptGenRandom
Entropy source System clock (seconds) OS entropy pool (hardware RNG, interrupts, etc.)
Predictability ~32,768 possible values per second 2^128 possible values
Brute-force feasibility Minutes on local hardware Computationally infeasible
Security classification PRNG CSPRNG

With 128 bits of cryptographic randomness, an attacker would need to try 340 undecillion (3.4 × 10³⁸) combinations to guess the pipe name. The heat death of the universe comes first.

The exec() Problem

The PR also removes an unsafe exec() call. In the context of UAC elevation, calling exec() (or its Windows equivalent ShellExecute/CreateProcess with unsanitized input) to launch the elevated process opens the door to command injection. If any part of the pipe name or process arguments is attacker-influenced, it could be used to inject additional commands.

The fix ensures the elevated process is launched with fully sanitized, hardcoded arguments — no user-controlled strings in the execution path.


Prevention & Best Practices

1. Never Use rand() for Security-Sensitive Identifiers

This is the cardinal rule. rand() is designed for statistical simulations, games, and non-security use cases. For anything that needs to be unpredictable to an adversary, use a CSPRNG:

Platform Secure API
Windows (C/C++) BCryptGenRandom, CryptGenRandom
Linux (C/C++) getrandom(), /dev/urandom
C++17 std::random_device (implementation-defined, verify it's truly random)
OpenSSL RAND_bytes()
Node.js crypto.randomBytes()
Python secrets.token_hex()
// ❌ NEVER do this for security tokens
int id = rand();

// ✅ DO this instead
unsigned char id[16];
BCryptGenRandom(NULL, id, 16, BCRYPT_USE_SYSTEM_PREFERRED_RNG);

2. Apply Strict ACLs to Named Pipes

Even with a secure name, always set explicit security descriptors on named pipes to restrict which users can connect:

// Set pipe security: only allow the creating user to connect
SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
// Set DACL to restrict access to current user only
SetSecurityDescriptorDacl(&sd, TRUE, pACL, FALSE);
sa.lpSecurityDescriptor = &sd;

HANDLE hPipe = CreateNamedPipe(
    pipeName.c_str(),
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    1,  // Max instances
    4096, 4096, 0,
    &sa  // Apply security descriptor
);

3. Verify Pipe Server Identity

When connecting to a named pipe as a client, verify the server's identity before sending sensitive data. Use GetNamedPipeServerProcessId() and validate the server process:

HANDLE hPipe = CreateFile(pipeName, GENERIC_READWRITE, ...);
if (hPipe != INVALID_HANDLE_VALUE) {
    ULONG serverPid;
    GetNamedPipeServerProcessId(hPipe, &serverPid);
    // Verify serverPid matches the expected elevated process
    if (!IsExpectedElevatedProcess(serverPid)) {
        CloseHandle(hPipe);
        throw std::runtime_error("Pipe server identity verification failed");
    }
}

4. Treat All IPC Boundaries as Trust Boundaries

Any time data crosses a privilege boundary (user → admin, process → kernel, client → server), treat it with the same scrutiny as data from an untrusted network source:

  • Validate all inputs before acting on them
  • Authenticate both sides of the channel
  • Use the principle of least privilege — don't elevate more than necessary
  • Minimize the attack surface — keep the privileged process as simple as possible

5. Use Static Analysis Tools

Tools that can catch PRNG misuse and IPC security issues:

  • CodeQL — Has queries for insecure randomness (cpp/weak-cryptographic-algorithm)
  • Semgrep — Customizable rules for rand() in security contexts
  • PVS-Studio — Detects weak PRNG usage (V1010, V697)
  • Coverity — PRNG and race condition detection
  • Microsoft's BinSkim — Binary-level security checks for Windows executables

6. Reference Security Standards


Conclusion

This vulnerability is a perfect illustration of how small, seemingly innocuous decisions compound into critical security failures. Using rand() instead of a CSPRNG seems like a minor implementation detail — until that "random" number becomes the key to a privileged communication channel, and a local attacker can predict it in milliseconds.

Key Takeaways

🔑 rand() is never acceptable for security-sensitive identifiers. Full stop.

🔑 Named pipes are security boundaries. Treat their names, ACLs, and connection validation with the same rigor as network authentication.

🔑 Privilege escalation paths deserve extra scrutiny. Any code that touches UAC, sudo, or privilege elevation should be reviewed by a security engineer.

🔑 Race conditions in resource creation are a real attack vector. "First one wins" semantics mean that predictable names are exploitable names.

🔑 Automated security scanning catches these issues early. This fix was identified by automated AI-assisted security scanning — integrate security tooling into your CI/CD pipeline before vulnerabilities reach production.

The good news: the fix is straightforward, the patch is clean, and the lesson is transferable to dozens of similar patterns across codebases everywhere. Security vulnerabilities like this one are most dangerous when they stay hidden — sharing them openly is how we make the whole ecosystem safer.


This vulnerability was identified and fixed by the OrbisAI Security automated security scanning platform. Automated security review was performed as part of the project's continuous security monitoring pipeline.

Have a similar vulnerability in your codebase? Consider integrating automated security scanning into your CI/CD pipeline.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11195

Related Articles

critical

Stack Buffer Overflow in MapScale: How Five Unsafe sprintf Calls Created a Critical Vulnerability

A critical stack-based buffer overflow vulnerability was discovered and patched in `src/mapscale.c`, where five unbounded `sprintf` calls wrote formatted output into fixed-size stack buffers without any bounds checking. An attacker controlling unit text strings could overflow the stack buffer, potentially overwriting the function return address and achieving arbitrary code execution. The fix replaces dangerous `sprintf` calls with their bounds-checked counterparts, eliminating the overflow risk

critical

Heap Buffer Overflows in YAML Parser: How Unchecked memcpy Calls Create Critical Attack Vectors

A critical heap buffer overflow vulnerability was discovered and patched in the YAML parser embedded within an Android VPN application, where five unvalidated `memcpy` calls could allow an attacker to corrupt heap memory by supplying a crafted YAML configuration file. This class of vulnerability is particularly dangerous because it can lead to arbitrary code execution or application crashes in security-sensitive contexts. The fix adds proper bounds validation before each copy operation, eliminat

critical

Critical Buffer Overflow Fixed: When "Safe" Functions Aren't Safe

A critical vulnerability in DeepSkyStackerKernel's StackWalker.cpp was silently replacing bounds-checking string functions with their unsafe counterparts via preprocessor macros, exposing the entire codebase to buffer overflow attacks. This fix removes the dangerous macro definitions that discarded buffer size arguments, restoring the intended memory safety protections across all call sites. Understanding how this subtle macro trick works is essential for any C/C++ developer working with string