CVE-2024-33828

a validation check that can never be true: command injection in Zope's mkwsgiinstance

Zope 5.9's mkwsgiinstance takes a -p path for the Python interpreter and runs it through subprocess. The check meant to reject a bad interpreter is written so it can never fire, so the path is never validated and any executable you name runs as the user. Local. Found with Ilyase Dehy.

not the obvious objection

mkwsgiinstance is the local setup script that scaffolds a Zope WSGI instance. The lazy read is “of course you can run commands, you already control its arguments.” Set that aside. The actual bug is a safety check that the author wrote, believed was running, and that has never run a single time. The interesting part of this CVE is a dead guard, not the shell.

Found with Ilyase Dehy.


the guard that is always false

src/Zope2/utilities/mkwsgiinstance.py, the -p / --python handler:

if opt in ("-p", "--python"):
    python = os.path.abspath(os.path.expanduser(arg))
    if not os.path.exists(python) and os.path.isfile(python):
        usage(sys.stderr, "The Python interpreter does not exist.")
        sys.exit(2)

Read the condition out loud. It fires only when the path not os.path.exists(python) and os.path.isfile(python). A path that does not exist cannot also be a file. The two halves are mutually exclusive, so the and is always false, the error branch is dead, and sys.exit(2) is never reached. The interpreter is never validated. The author almost certainly meant if not (os.path.exists(python) and os.path.isfile(python)), reject when it is not an existing file. The stray placement inverted it into a no-op.


where the path runs

python carries straight through to get_zope2path, which executes it:

output = subprocess.check_output(
    [python, '-c', 'import Zope2; print(Zope2.__file__)'],
    text=True,
    stderr=subprocess.PIPE)

This is not a shell string, there is no shell=True. It is a direct exec of the binary at python with fixed arguments. That is enough: whatever executable you hand to -p is launched as the user running the script. Point it at a binary, point it at a script you dropped, and it runs. The “validate the interpreter” check that was supposed to stop exactly this does nothing.


proof

(env) root@lab:/opt/Zope# mkwsgiinstance -p "/usr/bin/mkdir" -d "/tmp/temp;"
Please choose a username and password for the initial user.
These will be the credentials you use to initially manage
your new Zope instance.

Username: d
Password:
Verify password:
(env) root@lab:/opt/Zope# ls /tmp
'temp;'

-p /usr/bin/mkdir puts mkdir where a Python interpreter is expected. The dead guard waves it through and the script executes the attacker-chosen binary instead of an interpreter. Swap mkdir for a script that does real work and it runs with the script user’s privileges.

CVE-2024-33828 mkwsgiinstance running an attacker-supplied binary through the -p flag


honest severity

This is local. The advisory scores it AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, and the realistic reading is exactly that: an attacker who can invoke mkwsgiinstance with their own arguments runs an arbitrary executable as that user. It is not remote and it is not unauthenticated. What makes it worth writing up is the failure mode, a guard that looks like input validation, passes review, and is logically incapable of ever rejecting anything. As of writing, the same condition is still present upstream, so treat any “fixed version” claim as unconfirmed and read the code before trusting the check.


the fix that should be there

Two changes. Correct the boolean so the check can actually reject a non-file:

if not os.path.isfile(python):
    usage(sys.stderr, "The Python interpreter does not exist.")
    sys.exit(2)

And better, do not execute an arbitrary path at all. Resolve the interpreter against a known set (sys.executable, a configured allowlist) instead of running whatever string arrives on the command line. A check that exists is not the same as a check that runs.


references