oss-sec mailing list archives

cloud-init follows symlinks for ssh authorized_keys


From: "Jason A. Donenfeld" <Jason () zx2c4 com>
Date: Mon, 15 Feb 2016 13:32:13 +0100

Hi folks,

Cloud-init is a service run on "cloud" distributions that takes data
provided by the VM administrator and uses it to configure the system.
It can be used to add or update users and related SSH keys.

FWIW, the SSH key handling code (and possibly other places too,
haven't checked) appears to follow symlinks. This means that a local
user could "ln -s somewhere/else ~/.ssh/authorized_keys" or perhaps
even "ln -s somewhere/else ~/.ssh" (for the auto-chown/chmod). Then,
sometime later, say the administrator updates this malicious user's
SSH key using the metadata available to cloud-init, and reboots. Uh oh
speghettio. I didn't really check these findings in practice, so you
might want to take a closer look, but it doesn't appear pretty.

http://bazaar.launchpad.net/~cloud-init-dev/cloud-init/trunk/view/head:/cloudinit/ssh_util.py

Here are a few places where this happens:

def parse_authorized_keys(fname):
    lines = []
    try:
        if os.path.isfile(fname):
            lines = util.load_file(fname).splitlines()
    except (IOError, OSError):
        util.logexc(LOG, "Error reading lines from %s", fname)
        lines = []

    parser = AuthKeyLineParser()
    contents = []
    for line in lines:
        contents.append(parser.parse(line))
    return contents

According to the python documentation:
    os.path.isfile(path)
        Return True if path is an existing regular file. This follows
symbolic links, so both islink() and isfile() can be true for the same
path.


def setup_user_keys(keys, username, options=None):
    # Make sure the users .ssh dir is setup accordingly
    (ssh_dir, pwent) = users_ssh_info(username)
    if not os.path.isdir(ssh_dir):
        util.ensure_dir(ssh_dir, mode=0o700)
        util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)

    # Turn the 'update' keys given into actual entries
    parser = AuthKeyLineParser()
    key_entries = []
    for k in keys:
        key_entries.append(parser.parse(str(k), options=options))

    # Extract the old and make the new
    (auth_key_fn, auth_key_entries) = extract_authorized_keys(username)
    with util.SeLinuxGuard(ssh_dir, recursive=True):
        content = update_authorized_keys(auth_key_entries, key_entries)
        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700)
        util.write_file(auth_key_fn, content, mode=0o600)
        util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)


Again, os.path.isdir follows symlinks, and so do chown and chmod, and
also the functions underlying write_file. By the way there are some
more race condition situations happening in the latter function, among
others, in which directories can be removed or changed around after
the "ensure" check. Whether or not that constitutes a security issue
remains to be seen.

Anyway, make of this what you will. Is this a vector? Is this not a
vector? It's certainly not very robust code in any case.

Regards,
Jason


Current thread: