Title: Exasol Database 7.1.30 - command execution Author: 0day Rubbish Research Team Contact: disclosure@0day-rubbish.com Type: remote Platform: Linux Advisory: https://0day-rubbish.com/blog/exasol-database-default-credential-python-udf-os-popen-rce CVE: pending Description: Exasol Database 7.1.30 from Exasol AG (Germany), an in-memory MPP analytical database examined in its docker-db single-container deployment form, accepts the credential pair sys / exasol on the binary SQL protocol served by the exacts process on port 8563 over TLS, and that pair is the vendor's own published text rather than a leak: the upstream docker-db README instructs the reader, under the heading "Connecting to the Database", to authenticate with username sys and password exasol, and two image-internal CI test scripts authenticate a freshly initialised database with the same pair on the same port, basic_test.sh:112 carrying $X -c n11:8563 -u sys -P exasol -jdbcparam validateservercertificate=0 and basic_sc_test.sh:98 using -u sys -P exasol as well, both cited as artifacts observed inside the 7.1.30 image at /usr/opt/EXASuite-7/EXAClusterOS-7.1.30/docker/test/ because the current upstream repository now carries documentation only. Nothing rotates the constant, and that was traced rather than assumed: the running controller process command line carries no -defaultSysPasswordHash argument; the [DB:DB1] stanza of EXAConf carries no DefaultSysPasswdHash key; db_jobs.py:104 sets self.sys_passwd_text = secrets.token_hex(20), which generates an additional random password rather than replacing the built-in one; database.py:57 passes -defaultSysPasswordHash only when EXAConf configures it explicitly; the container's init-sc path does not configure it; and no forced first-login change-password gate, must-change flag or service refusal exists anywhere in the container's start path, while the README carries guidance about persistent volumes, hugepages, resource limits and license mounting with no instruction to change the SYS password. Both the built-in default and the generated per-deployment random are accepted, so the presence of random generation in the provisioning code is not a defence. The contrast the product itself supplies is what makes this a defect rather than a design choice: Exasol generated per-deployment randoms for two of its three service credentials, the ConfD AuthenticationToken and the BucketFS WritePasswd, and did not do so for the most privileged one, and an attacker must read EXAConf to recover either random while sys / exasol is a constant every installation shares and the documentation prints. SYS is the database administrator, so the constant buys the whole warehouse over the SQL channel on its own, and it also buys the right to create script objects, which converts that administrative access into operating-system command execution (CWE-1392 primary, CWE-94 co-equal, with CWE-798, CWE-78, CWE-1188 and CWE-250 recorded alongside and CWE-284 and CWE-862 reported separately for the absent source-address restriction on 8563 and the absent authorisation distinction for script creation, neither of which raises a metric). The sink is a PYTHON3 scalar UDF body created with CREATE OR REPLACE ... AS whose run(ctx) calls os.popen("").read() and returns the result behind a marker string, after which the caller issues SELECT poc_udf() and the ordinary result set carries the command's stdout back. That construction is a sink rather than a curiosity for three reasons: the os module is importable with no declaration, allow-list or capability request; the command string is an ordinary Python literal built from what the caller supplied, so shell metacharacters, pipelines and quoting all work, and the measured round used cat /etc/passwd | head -3 whose pipeline was honoured by the shell os.popen starts; and the return value crosses the trust boundary back to the caller, which removes the need for any DNS, HTTP or ICMP exfiltration channel and means egress filtering on the host does not defeat it. Sanitisation over what the body does is absent, there being no module allow-list or deny-list, no policy forbidding os or subprocess, no capability gate, no per-statement approval, no audit prompt and no privilege required beyond SYS itself. What does exist is well-formedness checking rather than a security control: the engine validates the language alias, submitting PYTHON instead of PYTHON3 yields its own diagnostic "no definition found for language: PYTHON", and it enforces SQL syntax and the forward-slash script terminator, both of which the payload satisfied without any evasion. That alias diagnostic is itself useful evidence, because it shows script languages are resolved through a named alias table inside the engine, that the table carries the standard aliases PYTHON3, LUA, JAVA and R, and that EXAConf contains no [ScriptLanguages] stanza customising it, so reachability of the sink is not conditional on an operator having installed or enabled a language pack. Three independent detonations returned real system data through the result set, and the identity reported by id was uid=500(exadefusr) gid=500(exausers) groups=500(exausers),0(root): a dedicated script-execution account that carries root only as a supplementary group. That identity is the whole of what was measured. It is not uid=0, root is not claimed, no escalation was attempted, and this submission is not a root compromise. Three of the container's four authentication surfaces were characterised and ruled out as doors, ConfD on 443 requiring a Bearer token that is the per-deployment AuthenticationToken from EXAConf, BucketFS on 2580 returning 403 to a write attempt because the bucket WritePasswd is likewise a per-deployment random and 400 to a path traversal attempt, and OpenSSH on 22 performing ordinary host authentication with nothing product-specific observed, which is what makes this a credential-hygiene defect rather than an authentication-bypass defect. A negative control establishes that the acceptance is specific to the shipped value rather than to a broken check: five incorrect passwords, admin, password, EXASOL, exasol123 and 123456, were tried against the same transport and the same client invocation and all five returned [28000] authentication failed. The chain involves no memory corruption, no deserialisation gadget, no file upload, no protocol bypass, no local foothold and no second vulnerability; leg one is a documented product constant and leg two is a documented product feature, and what makes the pair a vulnerability is that the product neither rotates the constant nor restricts what a holder of it may execute. The vendor's own repository states that this docker distribution is an open source project that is not officially supported by Exasol, that the base image is an end-of-life CentOS 7 release, that official support for docker-db 7 is no longer available and that users should move to a newer release line, that the repository now only contains documentation with the binaries on Docker Hub, that issues are disabled, and that the project is mainly intended for automatic testing of Exasol deployments. None of that reduces the mechanism, because the credential is a property of the shipping database and the script engine is a supported core feature of the product rather than a container add-on, but it means that on the vendor's own statements no fix is planned for this deployment form, which is a reason the research was published rather than held for a patch and which raises operator exposure rather than lowering it. Only version 7.1.30 in this deployment form was executed: other 7.1.x releases, the supported non-container installation and the vendor's newer release line share the mechanism and are plausibly affected, but none was tested, that is stated as an explicitly labelled inference, and no version range is claimed. Type rationale, stated precisely because EDB's type taxonomy is coarser than CVSS: the exploit is delivered over the network to a networked database service, which is why the type is remote rather than local, and the proof of concept connects to port 8563 as a client would. The published Attack Vector is nonetheless Adjacent rather than Network, for the evidential reason given in the CVSS section below, and no reader should infer an AV:N rating from the type field. CVSS: 8.8 High (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) is the single score published for this finding, and it is not 9.8. Derivation, shown so a reader can reproduce it with the CVSS 3.1 base algorithm and its round-up-to-the-tenth rule: ISS = 1 - ((1-0.56) x (1-0.56) x (1-0.56)) = 1 - 0.44^3 = 0.914816; Impact at Scope Unchanged = 6.42 x 0.914816 = 5.873119; Exploitability = 8.22 x AV(A: 0.62) x AC(L: 0.77) x PR(N: 0.85) x UI(N: 0.85) = 2.835255; Base = 8.708373 rounded up to the nearest tenth, which is 8.8. Attack Vector is Adjacent because the listener binds all interfaces inside the container and no source-address restriction of the product's own was observed in its configuration or listener behaviour, but no shipped artifact establishes that a single-container deployment binds 8563 on 0.0.0.0 by default: the vendor's documented stand-alone run command publishes -p 127.0.0.1:9563:8563 and the page explains that port 8563 within the container is exposed on local port 9563 to be used for database connections, with the persistent-volume variant on the same page using the same loopback-bound publication, while the documented multi-node run uses --network=host under which the vendor states that there is no need to expose ports in that mode because they are all accessible on the host, and the image's FAQ adds that checking from outside the Docker network requires port forwarding, confirming host-side reachability is an explicit operator action; under default bridge networking with no publication, which several of the README's own example commands use including the license-mount and resource-limit examples, the port is reachable from the host and from every other container attached to the same bridge, a logically adjacent topology. An earlier research note asserted that the production docker deployment exposes 8563 on all interfaces by default and scored the chain as remotely reachable on that basis; that assertion rested on the state of the researchers' own harness and on a general expectation about database deployments rather than on any product artifact, it does not survive contact with the vendor's documentation, it is therefore withdrawn in that form and replaced by the narrower artifact-backed statements above, and the withdrawal lowered the published score from 9.8 to 8.8 rather than raising it, so a reader who disagrees about the deployment posture ends up with the larger figure and not the smaller one. Attack Complexity is Low because the constant is published, the self-signed certificate carries no subjectAltName so the client needs a SHA256 fingerprint in the connection string as host/FINGERPRINT:port but that fingerprint is readable from an unauthenticated TLS handshake, the PYTHON3 alias is registered by default with no [ScriptLanguages] configuration required, and there is no race, no window and no operator action to wait for, all three rounds having succeeded on their first attempt. Privileges Required is None, argued rather than inherited: CVSS 3.1 reads PR:N as the attacker being unauthorized prior to attack and not requiring any access to settings or files of the vulnerable system, and both halves hold, since no account was ever provisioned to the attacker and no trust decision by the defender admitted them, and the value is obtained from vendor documentation rather than from the target's own EXAConf or logs. User Interaction is None. Impact is High on all three axes, grounded in the SQL-layer authority of SYS so that it does not depend on the cross-namespace reach that was not established. Scope is Unchanged because the SQL service, its script engine and the database it administers are one product on one host and executing a script is what the engine is built to do, so reaching the operating system from inside it does not cross an authority boundary the product itself maintains. Conditional reading A, where 8563 is reachable from another host, is 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), Exploitability = 8.22 x 0.85 x 0.77 x 0.85 x 0.85 = 3.887043, Base = 9.760161 rounded up, and it applies to the documented multi-host --network=host form, to any operator publication of the port without a loopback restriction, to a deployment reachable across a management network or NAT, and to the supported non-container installation whose entire purpose is to answer client connections on 8563; it is labelled conditional everywhere it appears because the deployment form actually executed published loopback-only and the cross-host leg was argued from vendor artifacts rather than detonated. Conditional reading B, loopback-only publication exactly as the stand-alone quickstart documents, is 8.4 High (CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), Exploitability = 8.22 x 0.55 x 0.77 x 0.85 x 0.85 = 2.515145, Base = 8.388264 rounded up, applying where the attacker therefore needs local access to the host, a realistic position for anything else already running there including a sibling container. Three further readings are published as rejected with their arithmetic: Privileges Required Low, 8.0 High (CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H), Exploitability 2.068068, Base 7.941187 rounded up, which treats the constant as a credential the attacker supplies and is rejected for charging the attacker a privilege the defender never granted, and is also the honest reachable reading where the factory credential has been rotated but a provisioned non-administrative account still holds script-creation rights; Privileges Required High, 6.8 Medium (CVSS:3.1/AV:A/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H), Exploitability 0.900610, Base 6.773729 rounded up, reading the metric as the authority level the successful attack needs since SYS is the database's most privileged account, rejected more strongly than PR:L because it would rate a constant requiring no grant as though it were a personal administrator account, and computing to 7.2 Medium under a Network vector; and Scope Changed, 9.7 High (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), impact term 7.52 x (0.914816 - 0.029) - 3.25 x (0.914816 x 0.9731 - 0.02)^13 = 6.128026, Base = 1.08 x (6.128026 + 2.835255) = 9.680344 rounded up, with the same reading under a Network vector capped by the formula at 10.0 Critical, rejected as the headline because the namespace boundary measured as a boundary in one direction and not the other, a file written inside it not appearing outside, and because no cross-boundary effect was demonstrated. A fourth state deserves its own sentence because an operator can enter it deliberately: with the factory credential rotated and no other account holding script-creation privilege, there is no path from the network to the sink and this finding does not arise, the script engine then being a documented feature used by authorised administrators, which is why rotation moves the finding from 8.8 to not applicable and heads the operator recommendations. No score is published for the sibling sinks os.system, subprocess and the LUA, JAVA and R aliases because none was detonated, none for the BucketFS anonymous read of the default bucket because it is separate and smaller with its own root cause, none for other 7.1.x releases or the non-container installation because neither was executed, and no figure is quoted for escalation to uid=0 because it was not attempted. Impact: full administrative takeover of the database by anyone who can reach port 8563, and then operating-system command execution as uid=500(exadefusr) with root present only as a supplementary group. The SQL layer does not depend on the sink at all and is the larger half: SYS reads and writes every schema, table, view, user and privilege in an analytical platform, which typically holds whatever the organisation consolidated into it, and can stop the database, so the complete warehouse is exposed with no further exploit step and no unusual protocol activity. Integrity control extends to the account catalogue, so further accounts can be created with chosen privileges, giving an attacker a durable and legitimate-looking second entry point that survives rotation of the factory credential. Availability follows the same authority: the database can be stopped, schemas dropped and resources consumed by the same session, and the deployment runs privileged as the vendor's README requires for permissions management, UDF support and environment configuration, which widens what such consumption inside the container can reach beyond it; that widening was not tested and is not scored. The sink adds command execution with stdout returned through the ordinary SELECT result set, so the attack is non-blind and needs no exfiltration channel, and file reads inside the script namespace. Persistence is cheap and quiet: the payload uses CREATE OR REPLACE, so re-running the attack rewrites one script object rather than creating a new one per command, and the object is ordinary product state that survives the session and is visible only to someone who enumerates script definitions. Detection is weak by construction because every step is legitimate documented product usage, a TLS handshake, a SYS login, script creation and a SELECT, so a SYS session from an unexpected address is indistinguishable from a real administrator and an operator who never rotated the constant has no means of telling them apart; the highest-value signal is script-object creation with the creating user and source address, which the default deployment does not surface, and the second-best is a trial sequence of [28000] authentication failed responses against the sys user on 8563, no rate limit and no lockout having been observed on SQL authentication. The root supplementary group under the vendor-documented privileged container mode is a recognised escalation position on Linux, since gid 0 governs access to files and devices whose group is root, and it is reported as an untested position: the conditional sentence is the whole of what the evidence supports, namely that if an attacker there could write a setuid binary or reach a device node exposed by privileged mode an effective root identity would follow, and neither was tested. A separate smaller finding is reported alongside and deliberately not folded into any metric: the BucketFS default bucket is Public=True, which permits unauthenticated read listing, while a write attempt returned 403 because the bucket WritePasswd is a per-deployment random and a path traversal attempt returned 400. Verification boundary: live verification against one container built from the vendor's docker-db image at 7.1.30, pulled onto an isolated cloud sandbox, started with the privileged flag the vendor's README requires, with a host directory mounted at /exa and the SQL port published to the host loopback only. Researcher-side commands ran under an ordinary unprivileged account and nothing in the chain depended on elevated privileges on the researchers' host. The three detonations drove the vendor's own console client inside the container against the container's loopback on 8563, so this is a real product executing real commands rather than an emulation or a protocol replica, and no step of the chain was driven from a separate network host. The cross-host legs, that a client on another machine can complete the same TLS handshake, retrieve the fingerprint, authenticate and submit the same statements over 8563, follow from the protocol being a network protocol and from the in-container listener binding all interfaces, and they are argued from the vendor's documented deployment forms and the observed bind rather than demonstrated between two machines; that is why the headline Attack Vector is Adjacent and why the 9.8 Network reading is labelled conditional. Measured, with the returned values reproduced in the advisory: authentication with the shipped constant; rejection of five wrong passwords with [28000] authentication failed; creation of a PYTHON3 scalar UDF by SYS; execution of os.popen inside it; return of the command's stdout through the result set; the executing identity uid=500(exadefusr) gid=500(exausers) groups=500(exausers),0(root); visibility of the host kernel release 5.10.134-19.2.al8.x86_64 from inside the UDF; and readability of /etc/passwd from the UDF process, whose output began root:x:0:0:root:/root:/bin/bash. Three rounds, each an independent create-and-select cycle with a different command, each returning real system data, each succeeding on its first attempt; there was no fourth round and no round was repeated merely to demonstrate determinism, three successes with three different commands being evidence of repeatability on their own. Not verified and not claimed: cross-host remote detonation; escalation to uid=0; any write or execution crossing from the UDF namespace into the container's or the host's filesystem view, a separate write-marker test having shown that a file written by the UDF did not appear in the container's own view, so the two views are distinct mount namespaces and the account names in the id output come from the namespace's own view, which is also why even the /etc/passwd read is not asserted to have been the container's file or the host's; persistence; the effect of the chain on a non-container installation or on another 7.1.x release; and the sibling sinks os.system, subprocess and the LUA, JAVA and R aliases. Read from source or documentation rather than detonated: the absence of -defaultSysPasswordHash on the controller command line, the absence of DefaultSysPasswdHash in EXAConf, the behaviour of db_jobs.py:104 and database.py:57, the contents of the image's test scripts, the ConfD token generation scheme, and every documented deployment command quoted above. One inconsistency is disclosed rather than smoothed over: the notes record the host-side port publication as 8562 in one place and 8563 in another, and because every measured leg ran through the container loopback that discrepancy is not load-bearing for anything claimed here. Exactly three commands were executed as payloads, an identity capture, a file read and a kernel-version read; no product file was modified or deleted and no data was destroyed. Prior public coverage was checked before publication: a keyword search of the NVD for the vendor name returned exactly one record, concerning a client-side JDBC driver component rather than the database server, separate searches for the database product name and for the docker deployment name returned no records at all, no published advisory was found whose root cause is either leg of this chain, the single unrelated record is deliberately not cited in the advisory, and no neighbouring identifier should be inferred as related. Exploit notes: the four statements are CREATE SCHEMA, OPEN SCHEMA, CREATE OR REPLACE PYTHON3 SCALAR SCRIPT with the body and its forward-slash terminator, and SELECT. Because the certificate has no subjectAltName the connection string must carry the fingerprint in the form host/FINGERPRINT:port, obtainable from an unauthenticated handshake. The return type is declared as a wide VARCHAR so command output is not truncated by the declared width, and the marker prefix distinguishes the returned value from the echoed statement text. The command is embedded as a Python double-quoted string literal, so backslashes and double quotes are escaped and nothing else is. Two client behaviours had to be engineered around, both recorded because they were the difference between a hanging tool and a working one: the vendor client's single-statement mode rejects a multi-statement payload containing the slash terminator and reports an unfinished command, so the payload is fed on stdin, and the client does not exit at end of file on stdin, so the tool starts it with a pipe, waits a bounded interval, kills it and keeps the partial stdout, by which point the result set has already been printed; an early transcript parser matched the marker inside the echoed CREATE SCRIPT source, and the fix filters matches followed by a quote, a space or a plus sign, which is how echoed source differs from a real result. Proof-of-concept and full analysis, published unconditionally: https://0day-rubbish.com/blog/exasol-database-default-credential-python-udf-os-popen-rce PoC location in the public repository: product/exasol/database-7.1.30/default-credential-python-udf-os-popen-rce/exploit/exasol_database_default_credential_python_udf_os_popen_rce.py Repository: https://github.com/Exploit-Garbage/0day-Rubbish The shipped proof of concept is Python 3 using only the standard library, with no third-party dependency and no hardcoded target. It refuses a non-loopback target unless --allow-remote is passed; acquires the certificate fingerprint with an unauthenticated TLS handshake and prints only the first and last few hex characters so that a per-deployment value is not written into a research log, with --fingerprint to supply the value instead and --no-encryption for a deployment that disabled TLS; asserts the credential gate with a trivial query before doing anything else; and creates and selects the UDF once per requested command. --rounds 3 reproduces the verification method by cycling through three different commands, --control runs the negative control alone and creates nothing, --gate probes only the credential, and --cleanup drops only the schema the run created. Nothing destructive is performed: the tool establishes no listener, opens no reverse shell, writes no file, installs no persistence, touches no other container or host service, does not attempt to reach uid=0 and does not attempt to cross the UDF namespace boundary. The default command is id. Those omissions are deliberate, the finding being that command execution is reachable at all from a documented constant, and demonstrating more would add risk without adding evidence. Exasol AG was notified through the vulnerability-reporting mailbox published in its RFC 9116 security.txt, which is the only intake the vendor offers and which welcomes responsible disclosure of vulnerabilities in its products and services. Nothing was submitted to any pre-publication intake form, none being published, and no individual was addressed by name. The notification was sent unconditionally and in clear text, the security.txt publishing no inline key field: the research is already public, no permission was asked for, no embargo was requested and nothing is withheld pending a reply. The vendor states that this docker deployment form is not officially supported and that official support for it has ended, so no fix is planned for it. -- 0day Rubbish Research Team disclosure@0day-rubbish.com https://0day-rubbish.com