// HackTricks · Network Services

Perl Command-Execution Sinks

Perl Command-Execution Sinks

Perl backticks/qx// sinks in Apache mod_perl handlers (reachability and exploitation)

One real-world pattern is Perl code building a shell command and executing it with backticks (qx//). In a mod_perl access handler, request data such as $r->uri() can reach that string before authentication completes. RCE requires both reachable attacker input and shell interpretation; neither mod_perl nor URI access alone creates command injection.[1]

Risky Perl execution primitives include:[4]

  • Backticks / qx//: my $out = `cmd ...`;.
  • One-argument system STRING when the string contains shell metacharacters; list form (system PROGRAM, LIST) avoids shell parsing.
  • Two-argument open with a pipe expression, such as open my $fh, "cmd |" or "| cmd". Prefer three-argument open and explicit argument lists.
  • IPC::Open3 with one command scalar can inherit Perl’s single-string shell handling. Pass the program and arguments as separate list elements when no shell is required.[5]
  • An explicit shell such as system('/bin/sh', '-c', $attacker_string), which is dangerous even though it uses list form.

Minimal vulnerable shape observed in the wild:[1]

sub getCASURL {
  ...
  my $exec_cmd = "...";
  if ($type eq 'login') {
    $exec_cmd .= $uri;        # $uri from $r->uri() → attacker-controlled
    my $out = `$exec_cmd`;    # backticks = shell
  }
}

Key reachability considerations in mod_perl:[1]

  • Handler registration: httpd.conf must route the request into the Perl module, for example with PerlModule MOD_SEC_EMC::AccessHandler and an applicable access-handler configuration.
  • Vulnerable branch: The Dell chain required the unauthenticated login flow (type eq "login"), reached by omitting the expected authentication cookie.
  • Resolvable path: The requested URI must fall within a scope processed by the handler. Otherwise the sink is never reached.

Exploitation workflow [1]

  1. Inspect httpd.conf for PerlModule/MOD_PERL handler scopes to find a resolvable path processed by the handler.
  2. Send an unauthenticated request so the login redirect path is taken (type == “login”).
  3. Place shell metacharacters in the request-URI path so $r->uri() carries your payload into the command string.

Example HTTP PoC (path injection via ’;’)

GET /ui/health;id HTTP/1.1
Host: target
Connection: close

Payload notes

  • Try separators such as ;, &&, |, backticks, $(), and encoded newlines (%0A) according to the surrounding quote context.
  • If other arguments are quoted but the URI remains raw in one branch, an end-of-string payload such as ;id# or &&/usr/bin/id# may terminate the intended command and comment the remainder.

Hardening

  • Do not build shell strings. Prefer argument-vector execution: system('/usr/bin/curl', '--silent', '--', $safe_url).[4]
  • If a shell is unavoidable, escape strictly and consistently across all branches; treat $r->uri() as hostile. Consider URI::Escape for paths/queries and strong allowlists.
  • Avoid backticks/qx// for command execution; capture output via open3/list form if truly needed without invoking a shell.
  • In mod_perl handlers, keep auth/redirect code paths free of command execution or ensure identical sanitization across branches to avoid “fixed everywhere but one branch” regressions.

Vulnerability hunting

  • Patch-diff modules that assemble shell commands; look for inconsistent quoting between branches (e.g., if ($type eq ‘login’) left unescaped).
  • Grep for backticks, qx//, open\s*(|||, and system\s*(\s*” to find string-based shells. Build a call graph from sink to request entry ($r) to verify pre-auth reachability.

Real-world case: Dell UnityVSA pre-auth RCE (CVE-2025-36604)[1][2][3]

  • Pre-auth command injection via backticks in AccessTool.pm:getCASURL when type == “login” concatenated raw $uri ($r->uri()).
  • Reachable through MOD_SEC_EMC::AccessHandler → make_return_address($r) → getCASLoginURL(…, type=“login”) → getCASURL(…, $uri, ‘login’).
  • Practical nuance: use a resolvable path covered by the handler; otherwise the module won’t execute and the sink won’t be hit.

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.